├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ ├── Check.yml
│ ├── Publish.yml
│ └── TES3Merge.yml
├── .gitignore
├── .gitmodules
├── LICENSE
├── NuGet.Config
├── TES3Merge.Tests
├── FileLoader.cs
├── Installation
│ ├── Morrowind.cs
│ └── OpenMW.cs
├── Merger
│ ├── ALCH.cs
│ ├── CREA.cs
│ ├── LEVC.cs
│ └── NPC_.cs
├── Parser
│ └── CELL.cs
├── Plugins
│ ├── AOF Potions Recolored.esp
│ ├── BTB's Game Improvements (Necro Edit) Tweaked.esp
│ ├── Bob's Diverse Blood.ESP
│ ├── F&F_NoSitters.ESP
│ ├── F&F_base.esm
│ ├── F&F_scarce.ESP
│ ├── Morrowind.esm
│ ├── Patch for Purists - Book Typos.ESP
│ ├── Patch for Purists - Semi-Purist Fixes.ESP
│ ├── Patch for Purists.esm
│ ├── ST_Alchemy_Balance_Sri_1.4.esp
│ ├── Wares-base.esm
│ ├── merge_add_effects.esp
│ ├── merge_base.esp
│ ├── merge_edit_all.esp
│ ├── merge_minor_tweaks.esp
│ ├── tes3conv.exe
│ ├── to_esp.bat
│ └── to_json.bat
├── Properties
│ ├── Resources.Designer.cs
│ └── Resources.resx
├── RecordTest.cs
├── TES3Merge.Tests.csproj
└── Utility.cs
├── TES3Merge.sln
├── TES3Merge
├── AssemblyInfo.cs
├── Commands
│ ├── MergeCommand.cs
│ ├── MultipatchCommand.cs
│ └── VerifyCommand.cs
├── Extensions
│ ├── GenericObjectExtensions.cs
│ └── StreamExtensions.cs
├── Merger
│ ├── CELL.cs
│ ├── CLAS.cs
│ ├── CREA.cs
│ ├── FACT.cs
│ ├── LEVC.cs
│ ├── LEVI.cs
│ ├── NPC_.cs
│ └── Shared.cs
├── Program.cs
├── RecordMerger.cs
├── TES3Merge.csproj
├── TES3Merge.ini
├── Util
│ ├── Bsa.cs
│ ├── DataFile.cs
│ ├── Installation.cs
│ ├── ScopedStopwatch.cs
│ └── Util.cs
└── tes3merge_icon_by_markel.ico
├── changelog.md
├── readme.md
├── tes3merge_icon_by_markel.ico
└── tes3merge_icon_by_markel.png
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories
2 | root = true
3 |
4 | # C# files
5 | [*.cs]
6 |
7 | #### Core EditorConfig Options ####
8 |
9 | # Indentation and spacing
10 | indent_size = 4
11 | indent_style = space
12 | tab_width = 4
13 |
14 | # New line preferences
15 | end_of_line = crlf
16 | insert_final_newline = false
17 |
18 | #### .NET Coding Conventions ####
19 |
20 | # Organize usings
21 | dotnet_separate_import_directive_groups = false
22 | dotnet_sort_system_directives_first = false
23 | file_header_template = unset
24 |
25 | # this. and Me. preferences
26 | dotnet_style_qualification_for_event = false
27 | dotnet_style_qualification_for_field = false
28 | dotnet_style_qualification_for_method = false
29 | dotnet_style_qualification_for_property = false
30 |
31 | # Language keywords vs BCL types preferences
32 | dotnet_style_predefined_type_for_locals_parameters_members = true
33 | dotnet_style_predefined_type_for_member_access = true
34 |
35 | # Parentheses preferences
36 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
37 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
38 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary
39 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
40 |
41 | # Modifier preferences
42 | dotnet_style_require_accessibility_modifiers = for_non_interface_members
43 |
44 | # Expression-level preferences
45 | dotnet_style_coalesce_expression = true
46 | dotnet_style_collection_initializer = true
47 | dotnet_style_explicit_tuple_names = true
48 | dotnet_style_namespace_match_folder = true
49 | dotnet_style_null_propagation = true
50 | dotnet_style_object_initializer = true
51 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
52 | dotnet_style_prefer_auto_properties = true
53 | dotnet_style_prefer_compound_assignment = true
54 | dotnet_style_prefer_conditional_expression_over_assignment = true
55 | dotnet_style_prefer_conditional_expression_over_return = true
56 | dotnet_style_prefer_inferred_anonymous_type_member_names = true
57 | dotnet_style_prefer_inferred_tuple_names = true
58 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true
59 | dotnet_style_prefer_simplified_boolean_expressions = true
60 | dotnet_style_prefer_simplified_interpolation = true
61 |
62 | # Field preferences
63 | dotnet_style_readonly_field = true
64 |
65 | # Parameter preferences
66 | dotnet_code_quality_unused_parameters = all
67 |
68 | # Suppression preferences
69 | dotnet_remove_unnecessary_suppression_exclusions = none
70 |
71 | # New line preferences
72 | dotnet_style_allow_multiple_blank_lines_experimental = true
73 | dotnet_style_allow_statement_immediately_after_block_experimental = true
74 |
75 | #### C# Coding Conventions ####
76 |
77 | # var preferences
78 | csharp_style_var_elsewhere = true:silent
79 | csharp_style_var_for_built_in_types = true:silent
80 | csharp_style_var_when_type_is_apparent = true:silent
81 |
82 | # Expression-bodied members
83 | csharp_style_expression_bodied_accessors = true:silent
84 | csharp_style_expression_bodied_constructors = false:silent
85 | csharp_style_expression_bodied_indexers = true:silent
86 | csharp_style_expression_bodied_lambdas = true:silent
87 | csharp_style_expression_bodied_local_functions = false:silent
88 | csharp_style_expression_bodied_methods = false:silent
89 | csharp_style_expression_bodied_operators = false:silent
90 | csharp_style_expression_bodied_properties = true:silent
91 |
92 | # Pattern matching preferences
93 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
94 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
95 | csharp_style_prefer_extended_property_pattern = true:suggestion
96 | csharp_style_prefer_not_pattern = true:suggestion
97 | csharp_style_prefer_pattern_matching = true:silent
98 | csharp_style_prefer_switch_expression = true:suggestion
99 |
100 | # Null-checking preferences
101 | csharp_style_conditional_delegate_call = true:suggestion
102 | csharp_style_prefer_parameter_null_checking = true:suggestion
103 |
104 | # Modifier preferences
105 | csharp_prefer_static_local_function = true:suggestion
106 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
107 |
108 | # Code-block preferences
109 | csharp_prefer_braces = true:silent
110 | csharp_prefer_simple_using_statement = true:suggestion
111 | csharp_style_namespace_declarations = block_scoped:silent
112 | csharp_style_prefer_method_group_conversion = true:silent
113 |
114 | # Expression-level preferences
115 | csharp_prefer_simple_default_expression = true:suggestion
116 | csharp_style_deconstructed_variable_declaration = true:suggestion
117 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
118 | csharp_style_inlined_variable_declaration = true:suggestion
119 | csharp_style_prefer_index_operator = true:suggestion
120 | csharp_style_prefer_local_over_anonymous_function = true:suggestion
121 | csharp_style_prefer_null_check_over_type_check = true:suggestion
122 | csharp_style_prefer_range_operator = true:suggestion
123 | csharp_style_prefer_tuple_swap = true:suggestion
124 | csharp_style_throw_expression = true:suggestion
125 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion
126 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent
127 |
128 | # 'using' directive preferences
129 | csharp_using_directive_placement = outside_namespace:silent
130 |
131 | # New line preferences
132 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
133 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
134 | csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
135 |
136 | #### C# Formatting Rules ####
137 |
138 | # New line preferences
139 | csharp_new_line_before_catch = true
140 | csharp_new_line_before_else = true
141 | csharp_new_line_before_finally = true
142 | csharp_new_line_before_members_in_anonymous_types = true
143 | csharp_new_line_before_members_in_object_initializers = true
144 | csharp_new_line_before_open_brace = all
145 | csharp_new_line_between_query_expression_clauses = true
146 |
147 | # Indentation preferences
148 | csharp_indent_block_contents = true
149 | csharp_indent_braces = false
150 | csharp_indent_case_contents = true
151 | csharp_indent_case_contents_when_block = true
152 | csharp_indent_labels = one_less_than_current
153 | csharp_indent_switch_labels = true
154 |
155 | # Space preferences
156 | csharp_space_after_cast = false
157 | csharp_space_after_colon_in_inheritance_clause = true
158 | csharp_space_after_comma = true
159 | csharp_space_after_dot = false
160 | csharp_space_after_keywords_in_control_flow_statements = true
161 | csharp_space_after_semicolon_in_for_statement = true
162 | csharp_space_around_binary_operators = before_and_after
163 | csharp_space_around_declaration_statements = false
164 | csharp_space_before_colon_in_inheritance_clause = true
165 | csharp_space_before_comma = false
166 | csharp_space_before_dot = false
167 | csharp_space_before_open_square_brackets = false
168 | csharp_space_before_semicolon_in_for_statement = false
169 | csharp_space_between_empty_square_brackets = false
170 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
171 | csharp_space_between_method_call_name_and_opening_parenthesis = false
172 | csharp_space_between_method_call_parameter_list_parentheses = false
173 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
174 | csharp_space_between_method_declaration_name_and_open_parenthesis = false
175 | csharp_space_between_method_declaration_parameter_list_parentheses = false
176 | csharp_space_between_parentheses = false
177 | csharp_space_between_square_brackets = false
178 |
179 | # Wrapping preferences
180 | csharp_preserve_single_line_blocks = true
181 | csharp_preserve_single_line_statements = true
182 |
183 | #### Naming styles ####
184 |
185 | # Naming rules
186 |
187 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
188 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
189 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
190 |
191 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
192 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types
193 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
194 |
195 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
196 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
197 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
198 |
199 | # Symbol specifications
200 |
201 | dotnet_naming_symbols.interface.applicable_kinds = interface
202 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
203 | dotnet_naming_symbols.interface.required_modifiers =
204 |
205 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
206 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
207 | dotnet_naming_symbols.types.required_modifiers =
208 |
209 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
210 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
211 | dotnet_naming_symbols.non_field_members.required_modifiers =
212 |
213 | # Naming styles
214 |
215 | dotnet_naming_style.pascal_case.required_prefix =
216 | dotnet_naming_style.pascal_case.required_suffix =
217 | dotnet_naming_style.pascal_case.word_separator =
218 | dotnet_naming_style.pascal_case.capitalization = pascal_case
219 |
220 | dotnet_naming_style.begins_with_i.required_prefix = I
221 | dotnet_naming_style.begins_with_i.required_suffix =
222 | dotnet_naming_style.begins_with_i.word_separator =
223 | dotnet_naming_style.begins_with_i.capitalization = pascal_case
224 |
225 | [*.{cs,vb}]
226 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
227 | tab_width = 4
228 | indent_size = 4
229 | end_of_line = crlf
230 | dotnet_style_coalesce_expression = true:suggestion
231 | dotnet_style_null_propagation = true:suggestion
232 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
233 | dotnet_style_prefer_auto_properties = true:silent
234 | dotnet_style_object_initializer = true:suggestion
235 | dotnet_style_collection_initializer = true:suggestion
236 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
237 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
238 | dotnet_style_prefer_conditional_expression_over_return = true:silent
239 | dotnet_style_explicit_tuple_names = true:suggestion
240 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
241 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
242 | dotnet_style_prefer_compound_assignment = true:suggestion
243 | dotnet_style_prefer_simplified_interpolation = true:suggestion
244 | dotnet_style_namespace_match_folder = true:suggestion
245 | dotnet_style_readonly_field = true:suggestion
246 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent
247 | dotnet_style_predefined_type_for_member_access = true:silent
248 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
249 | dotnet_style_allow_multiple_blank_lines_experimental = true:silent
250 | dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
251 | dotnet_code_quality_unused_parameters = all:suggestion
252 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
253 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
254 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
255 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
256 | dotnet_style_qualification_for_field = false:silent
257 | dotnet_style_qualification_for_property = false:silent
258 | dotnet_style_qualification_for_event = false:silent
259 | dotnet_style_qualification_for_method = false:silent
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto
3 |
4 | # Explicitly declare text files you want to always be normalized and converted
5 | # to native line endings on checkout.
6 |
7 | # Declare files that will always have CRLF line endings on checkout.
8 | *.sln text eol=lf
9 | *.cs text eol=lf
10 |
11 | # Denote all files that are truly binary and should not be modified.
12 | *.png binary
13 | *.jpg binary
--------------------------------------------------------------------------------
/.github/workflows/Check.yml:
--------------------------------------------------------------------------------
1 | name: FastCheck
2 |
3 | on:
4 | pull_request:
5 | branches: [ master, main ]
6 | push:
7 | branches: [ dev* ]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | with:
16 | submodules: true
17 | - name: Setup .NET
18 | uses: actions/setup-dotnet@v1
19 | with:
20 | dotnet-version: '6.x'
21 |
22 | - name: Restore dependencies
23 | run: dotnet restore
24 | - name: Build
25 | run: dotnet build --no-restore
26 |
27 | - name: Test
28 | run: dotnet test --no-build --verbosity normal --filter TestCategory!=Installation
29 |
--------------------------------------------------------------------------------
/.github/workflows/Publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 |
10 | publish-linux:
11 | runs-on: ubuntu-latest
12 | permissions:
13 | contents: write
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | with:
18 | submodules: true
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v1
21 | with:
22 | dotnet-version: '6.x'
23 |
24 | - name: Publish
25 | run: dotnet publish TES3Merge/TES3Merge.csproj -o publish/linux -c Release -r linux-x64 -p:PublishSingleFile=true --no-self-contained
26 |
27 | # create zip
28 | - run: ls -R
29 | - run: zip TES3Merge-linux.zip publish/linux/TES3Merge publish/linux/TES3Merge.ini
30 |
31 | # RELEASE
32 | - name: Release
33 | uses: ncipollo/release-action@v1
34 | with:
35 | draft: true
36 | generateReleaseNotes: true
37 | artifacts: "TES3Merge-linux.zip"
38 | token: ${{ secrets.GITHUB_TOKEN }}
39 |
40 |
41 | publish-win:
42 | runs-on: windows-latest
43 | needs: publish-linux
44 | permissions:
45 | contents: write
46 |
47 | steps:
48 | - uses: actions/checkout@v2
49 | with:
50 | submodules: true
51 | - name: Setup .NET
52 | uses: actions/setup-dotnet@v1
53 | with:
54 | dotnet-version: '6.x'
55 |
56 | - name: Publish
57 | run: dotnet publish TES3Merge/TES3Merge.csproj -o publish/win -c Release -r win-x64 -p:PublishSingleFile=true --no-self-contained
58 |
59 | # create zip
60 | - run: dir
61 | - run: Compress-Archive -LiteralPath "publish\win\TES3Merge.exe","publish\win\TES3Merge.ini" -DestinationPath "TES3Merge-win.zip"
62 |
63 |
64 | # RELEASE
65 | - name: Release
66 | uses: ncipollo/release-action@v1
67 | with:
68 | allowUpdates: true
69 | draft: true
70 | generateReleaseNotes: true
71 | artifacts: "TES3Merge-win.zip"
72 | token: ${{ secrets.GITHUB_TOKEN }}
73 |
74 |
75 | publish-osx:
76 | needs: publish-linux
77 | runs-on: macos-latest
78 | permissions:
79 | contents: write
80 |
81 | steps:
82 | - uses: actions/checkout@v2
83 | with:
84 | submodules: true
85 | - name: Setup .NET
86 | uses: actions/setup-dotnet@v1
87 | with:
88 | dotnet-version: '6.x'
89 |
90 | - name: Publish
91 | run: dotnet publish TES3Merge/TES3Merge.csproj -o publish/osx -c Release -r osx-x64 -p:PublishSingleFile=true --no-self-contained
92 |
93 | # create zip
94 | - run: ls -R
95 | - run: zip TES3Merge-osx.zip publish/osx/TES3Merge publish/osx/TES3Merge.ini
96 |
97 |
98 | # RELEASE
99 | - name: Release
100 | uses: ncipollo/release-action@v1
101 | with:
102 | allowUpdates: true
103 | draft: true
104 | generateReleaseNotes: true
105 | artifacts: "TES3Merge-osx.zip"
106 | token: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/TES3Merge.yml:
--------------------------------------------------------------------------------
1 | name: Check_Build
2 |
3 | on:
4 | push:
5 | branches: [ master, main ]
6 |
7 | jobs:
8 | test:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 | with:
14 | submodules: true
15 | - name: Setup .NET
16 | uses: actions/setup-dotnet@v1
17 | with:
18 | dotnet-version: '6.x'
19 |
20 | - name: Restore dependencies
21 | run: dotnet restore
22 | - name: Build
23 | run: dotnet build --no-restore
24 |
25 | - name: Test
26 | run: dotnet test --no-build --verbosity normal --filter TestCategory!=Installation
27 |
28 | build-win:
29 | needs: [test]
30 | runs-on: windows-latest
31 |
32 | steps:
33 | - uses: actions/checkout@v2
34 | with:
35 | submodules: true
36 | - name: Setup .NET
37 | uses: actions/setup-dotnet@v1
38 | with:
39 | dotnet-version: '6.x'
40 |
41 | - name: Publish
42 | run: dotnet publish TES3Merge/TES3Merge.csproj -o publish/win -c Release -r win-x64 -p:PublishSingleFile=true --no-self-contained
43 |
44 | - uses: actions/upload-artifact@v4
45 | with:
46 | name: tes3merge-win
47 | path: ./publish/win/TES3Merge.exe
48 |
49 | build-linux:
50 | needs: [test]
51 | runs-on: ubuntu-latest
52 |
53 | steps:
54 | - uses: actions/checkout@v2
55 | with:
56 | submodules: true
57 | - name: Setup .NET
58 | uses: actions/setup-dotnet@v1
59 | with:
60 | dotnet-version: '6.x'
61 |
62 | - name: Publish
63 | run: dotnet publish TES3Merge/TES3Merge.csproj -o publish/linux -c Release -r linux-x64 -p:PublishSingleFile=true --no-self-contained
64 |
65 | - uses: actions/upload-artifact@v4
66 | with:
67 | name: tes3merge-linux
68 | path: ./publish/linux/TES3Merge
69 |
70 | build-osx:
71 | needs: [test]
72 | runs-on: macos-latest
73 |
74 | steps:
75 | - uses: actions/checkout@v2
76 | with:
77 | submodules: true
78 | - name: Setup .NET
79 | uses: actions/setup-dotnet@v1
80 | with:
81 | dotnet-version: '6.x'
82 |
83 | - name: Publish
84 | run: dotnet publish TES3Merge/TES3Merge.csproj -o publish/osx -c Release -r osx-x64 -p:PublishSingleFile=true --no-self-contained
85 |
86 | - uses: actions/upload-artifact@v4
87 | with:
88 | name: tes3merge-osx
89 | path: ./publish/osx/TES3Merge
90 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 |
33 | # Visual Studio 2015/2017 cache/options directory
34 | .vs/
35 | # Uncomment if you have tasks that create the project's static files in wwwroot
36 | #wwwroot/
37 |
38 | # Visual Studio 2017 auto generated files
39 | Generated\ Files/
40 |
41 | # MSTest test Results
42 | [Tt]est[Rr]esult*/
43 | [Bb]uild[Ll]og.*
44 |
45 | # NUnit
46 | *.VisualState.xml
47 | TestResult.xml
48 | nunit-*.xml
49 |
50 | # Build Results of an ATL Project
51 | [Dd]ebugPS/
52 | [Rr]eleasePS/
53 | dlldata.c
54 |
55 | # Benchmark Results
56 | BenchmarkDotNet.Artifacts/
57 |
58 | # .NET Core
59 | project.lock.json
60 | project.fragment.lock.json
61 | artifacts/
62 |
63 | # StyleCop
64 | StyleCopReport.xml
65 |
66 | # Files built by Visual Studio
67 | *_i.c
68 | *_p.c
69 | *_h.h
70 | *.ilk
71 | *.meta
72 | *.obj
73 | *.iobj
74 | *.pch
75 | *.pdb
76 | *.ipdb
77 | *.pgc
78 | *.pgd
79 | *.rsp
80 | *.sbr
81 | *.tlb
82 | *.tli
83 | *.tlh
84 | *.tmp
85 | *.tmp_proj
86 | *_wpftmp.csproj
87 | *.log
88 | *.vspscc
89 | *.vssscc
90 | .builds
91 | *.pidb
92 | *.svclog
93 | *.scc
94 |
95 | # Chutzpah Test files
96 | _Chutzpah*
97 |
98 | # Visual C++ cache files
99 | ipch/
100 | *.aps
101 | *.ncb
102 | *.opendb
103 | *.opensdf
104 | *.sdf
105 | *.cachefile
106 | *.VC.db
107 | *.VC.VC.opendb
108 |
109 | # Visual Studio profiler
110 | *.psess
111 | *.vsp
112 | *.vspx
113 | *.sap
114 |
115 | # Visual Studio Trace Files
116 | *.e2e
117 |
118 | # TFS 2012 Local Workspace
119 | $tf/
120 |
121 | # Guidance Automation Toolkit
122 | *.gpState
123 |
124 | # ReSharper is a .NET coding add-in
125 | _ReSharper*/
126 | *.[Rr]e[Ss]harper
127 | *.DotSettings.user
128 |
129 | # JustCode is a .NET coding add-in
130 | .JustCode
131 |
132 | # TeamCity is a build add-in
133 | _TeamCity*
134 |
135 | # DotCover is a Code Coverage Tool
136 | *.dotCover
137 |
138 | # AxoCover is a Code Coverage Tool
139 | .axoCover/*
140 | !.axoCover/settings.json
141 |
142 | # Visual Studio code coverage results
143 | *.coverage
144 | *.coveragexml
145 |
146 | # NCrunch
147 | _NCrunch_*
148 | .*crunch*.local.xml
149 | nCrunchTemp_*
150 |
151 | # MightyMoose
152 | *.mm.*
153 | AutoTest.Net/
154 |
155 | # Web workbench (sass)
156 | .sass-cache/
157 |
158 | # Installshield output folder
159 | [Ee]xpress/
160 |
161 | # DocProject is a documentation generator add-in
162 | DocProject/buildhelp/
163 | DocProject/Help/*.HxT
164 | DocProject/Help/*.HxC
165 | DocProject/Help/*.hhc
166 | DocProject/Help/*.hhk
167 | DocProject/Help/*.hhp
168 | DocProject/Help/Html2
169 | DocProject/Help/html
170 |
171 | # Click-Once directory
172 | publish/
173 |
174 | # Publish Web Output
175 | *.[Pp]ublish.xml
176 | *.azurePubxml
177 | # Note: Comment the next line if you want to checkin your web deploy settings,
178 | # but database connection strings (with potential passwords) will be unencrypted
179 | *.pubxml
180 | *.publishproj
181 |
182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
183 | # checkin your Azure Web App publish settings, but sensitive information contained
184 | # in these scripts will be unencrypted
185 | PublishScripts/
186 |
187 | # NuGet Packages
188 | *.nupkg
189 | # NuGet Symbol Packages
190 | *.snupkg
191 | # The packages folder can be ignored because of Package Restore
192 | **/[Pp]ackages/*
193 | # except build/, which is used as an MSBuild target.
194 | !**/[Pp]ackages/build/
195 | # Uncomment if necessary however generally it will be regenerated when needed
196 | #!**/[Pp]ackages/repositories.config
197 | # NuGet v3's project.json files produces more ignorable files
198 | *.nuget.props
199 | *.nuget.targets
200 |
201 | # Microsoft Azure Build Output
202 | csx/
203 | *.build.csdef
204 |
205 | # Microsoft Azure Emulator
206 | ecf/
207 | rcf/
208 |
209 | # Windows Store app package directories and files
210 | AppPackages/
211 | BundleArtifacts/
212 | Package.StoreAssociation.xml
213 | _pkginfo.txt
214 | *.appx
215 | *.appxbundle
216 | *.appxupload
217 |
218 | # Visual Studio cache files
219 | # files ending in .cache can be ignored
220 | *.[Cc]ache
221 | # but keep track of directories ending in .cache
222 | !?*.[Cc]ache/
223 |
224 | # Others
225 | ClientBin/
226 | ~$*
227 | *~
228 | *.dbmdl
229 | *.dbproj.schemaview
230 | *.jfm
231 | *.pfx
232 | *.publishsettings
233 | orleans.codegen.cs
234 |
235 | # Including strong name files can present a security risk
236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
237 | #*.snk
238 |
239 | # Since there are multiple workflows, uncomment next line to ignore bower_components
240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
241 | #bower_components/
242 |
243 | # RIA/Silverlight projects
244 | Generated_Code/
245 |
246 | # Backup & report files from converting an old project file
247 | # to a newer Visual Studio version. Backup files are not needed,
248 | # because we have git ;-)
249 | _UpgradeReport_Files/
250 | Backup*/
251 | UpgradeLog*.XML
252 | UpgradeLog*.htm
253 | ServiceFabricBackup/
254 | *.rptproj.bak
255 |
256 | # SQL Server files
257 | *.mdf
258 | *.ldf
259 | *.ndf
260 |
261 | # Business Intelligence projects
262 | *.rdl.data
263 | *.bim.layout
264 | *.bim_*.settings
265 | *.rptproj.rsuser
266 | *- [Bb]ackup.rdl
267 | *- [Bb]ackup ([0-9]).rdl
268 | *- [Bb]ackup ([0-9][0-9]).rdl
269 |
270 | # Microsoft Fakes
271 | FakesAssemblies/
272 |
273 | # GhostDoc plugin setting file
274 | *.GhostDoc.xml
275 |
276 | # Node.js Tools for Visual Studio
277 | .ntvs_analysis.dat
278 | node_modules/
279 |
280 | # Visual Studio 6 build log
281 | *.plg
282 |
283 | # Visual Studio 6 workspace options file
284 | *.opt
285 |
286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
287 | *.vbw
288 |
289 | # Visual Studio LightSwitch build output
290 | **/*.HTMLClient/GeneratedArtifacts
291 | **/*.DesktopClient/GeneratedArtifacts
292 | **/*.DesktopClient/ModelManifest.xml
293 | **/*.Server/GeneratedArtifacts
294 | **/*.Server/ModelManifest.xml
295 | _Pvt_Extensions
296 |
297 | # Paket dependency manager
298 | .paket/paket.exe
299 | paket-files/
300 |
301 | # FAKE - F# Make
302 | .fake/
303 |
304 | # CodeRush personal settings
305 | .cr/personal
306 |
307 | # Python Tools for Visual Studio (PTVS)
308 | __pycache__/
309 | *.pyc
310 |
311 | # Cake - Uncomment if you are using it
312 | # tools/**
313 | # !tools/packages.config
314 |
315 | # Tabs Studio
316 | *.tss
317 |
318 | # Telerik's JustMock configuration file
319 | *.jmconfig
320 |
321 | # BizTalk build output
322 | *.btp.cs
323 | *.btm.cs
324 | *.odx.cs
325 | *.xsd.cs
326 |
327 | # OpenCover UI analysis results
328 | OpenCover/
329 |
330 | # Azure Stream Analytics local run output
331 | ASALocalRun/
332 |
333 | # MSBuild Binary and Structured Log
334 | *.binlog
335 |
336 | # NVidia Nsight GPU debugger configuration file
337 | *.nvuser
338 |
339 | # MFractors (Xamarin productivity tool) working folder
340 | .mfractor/
341 |
342 | # Local History for Visual Studio
343 | .localhistory/
344 |
345 | # BeatPulse healthcheck temp database
346 | healthchecksdb
347 |
348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
349 | MigrationBackup/
350 | /TES3Merge.Tests/Plugins/*.json
351 | TES3Merge/Properties/launchSettings.json
352 |
353 | # VS Code and Rider
354 | .vscode/
355 | .idea/
356 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "TES3Tool"]
2 | path = TES3Tool
3 | url = git@github.com:NullCascade/TES3Tool.git
4 | branch = master
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2019 NullCascade
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/NuGet.Config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/FileLoader.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 |
4 | namespace TES3Merge.Tests;
5 |
6 | internal static class FileLoader
7 | {
8 | ///
9 | /// A map of loaded plugins. This is lazy-filled as requested.
10 | ///
11 | private static readonly Dictionary LoadedPlugins = new();
12 |
13 | ///
14 | /// A filter for all the types we will load. This optimizes loading so we don't load records we will never test.
15 | ///
16 | private static readonly List testedRecords = new(new string[] {
17 | "ALCH",
18 | "CREA",
19 | "LEVC",
20 | "NPC_",
21 | });
22 |
23 | ///
24 | /// Lazy-loads a plugin in the Plugins folder. Ensure that the plugin is set to copy over to the output folder.
25 | ///
26 | /// The name of the plugin file, including the file extension, relative to the plugins folder.
27 | ///
28 | internal static TES3Lib.TES3? GetPlugin(string name)
29 | {
30 | if (!LoadedPlugins.ContainsKey(name))
31 | {
32 | var loadedPlugin = TES3Lib.TES3.TES3Load(Path.Combine("Plugins", name), testedRecords);
33 | loadedPlugin.Path = name; // Override path to remove prefix.
34 | LoadedPlugins[name] = loadedPlugin;
35 | return loadedPlugin;
36 | }
37 | return LoadedPlugins[name];
38 | }
39 |
40 | ///
41 | /// Lazy-loads a plugin through , and returns a record from it with the given .
42 | ///
43 | /// The full file name of the plugin, including file extension, relative to the plugins folder.
44 | /// The id of the record to find. It does not need to manually specify a null terminator.
45 | /// The found record, or null if the plugin could not be loaded or if the record does not exist.
46 | internal static TES3Lib.Base.Record? FindRecord(string pluginName, string id)
47 | {
48 | TES3Lib.TES3? plugin = GetPlugin(pluginName);
49 | return plugin is null ? null : plugin.FindRecord(id);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/Installation/Morrowind.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Hosting;
3 | using Microsoft.Extensions.Logging;
4 | using Microsoft.VisualStudio.TestTools.UnitTesting;
5 | using Serilog;
6 | using Serilog.Events;
7 | using System;
8 | using System.IO;
9 | using TES3Merge.Util;
10 | using static TES3Merge.Tests.Utility;
11 |
12 | namespace TES3Merge.Tests.Installation;
13 |
14 | [TestClass, TestCategory("Installation")]
15 | public class Morrowind
16 | {
17 | protected IHost _host;
18 | protected Microsoft.Extensions.Logging.ILogger _logger;
19 |
20 | public MorrowindInstallation? Install;
21 |
22 | public Morrowind()
23 | {
24 | // Setup logging.
25 | Log.Logger = new LoggerConfiguration()
26 | .MinimumLevel.Debug()
27 | .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
28 | .Enrich.FromLogContext()
29 | .WriteTo.Console(outputTemplate: "[{Level:u3}] {Message:lj}{NewLine}{Exception}")
30 | .CreateLogger();
31 |
32 | var hostBuilder = Host.CreateDefaultBuilder().UseSerilog();
33 | _host = hostBuilder.Build();
34 | _logger = _host.Services.GetRequiredService>();
35 |
36 | if (Directory.Exists(Properties.Resources.OpenMWInstallDirectory))
37 | {
38 | Install = new MorrowindInstallation(Properties.Resources.MorrowindInstallDirectory);
39 | }
40 | }
41 |
42 | [TestMethod]
43 | public void InstallationFound()
44 | {
45 | if (Directory.Exists(Properties.Resources.MorrowindInstallDirectory))
46 | {
47 | Assert.IsNotNull(Install);
48 | }
49 | }
50 |
51 | [TestMethod]
52 | public void InstallationPathValid()
53 | {
54 | if (Install is null)
55 | {
56 | Assert.Inconclusive();
57 | return;
58 | }
59 |
60 | _logger.LogInformation("Installation Directory: {path}", Install.RootDirectory);
61 | var exePath = Path.Combine(Install.RootDirectory, "Morrowind.exe");
62 | Assert.IsTrue(File.Exists(exePath));
63 | }
64 |
65 | [TestMethod]
66 | public void ArchivesFound()
67 | {
68 | if (Install is null)
69 | {
70 | Assert.Inconclusive();
71 | return;
72 | }
73 |
74 | _logger.LogInformation("Archives: {list}", Install.Archives);
75 | Assert.IsTrue(Install.Archives.Contains("Morrowind.bsa"));
76 | Assert.IsTrue(Install.Archives.Contains("Tribunal.bsa"));
77 | Assert.IsTrue(Install.Archives.Contains("Bloodmoon.bsa"));
78 | }
79 |
80 | [TestMethod]
81 | public void ArchivesAreInOrder()
82 | {
83 | if (Install is null)
84 | {
85 | Assert.Inconclusive();
86 | return;
87 | }
88 |
89 | _logger.LogInformation("Archives: {list}", Install.Archives);
90 | Assert.AreEqual(Install.Archives[0], "Morrowind.bsa");
91 | Assert.AreEqual(Install.Archives[1], "Tribunal.bsa");
92 | Assert.AreEqual(Install.Archives[2], "Bloodmoon.bsa");
93 | }
94 |
95 | [TestMethod]
96 | public void GameFilesFound()
97 | {
98 | if (Install is null)
99 | {
100 | Assert.Inconclusive();
101 | return;
102 | }
103 |
104 | _logger.LogInformation("Game Files: {list}", Install.GameFiles);
105 | Assert.IsTrue(Install.GameFiles.Contains("Morrowind.esm"));
106 | Assert.IsTrue(Install.GameFiles.Contains("Tribunal.esm"));
107 | Assert.IsTrue(Install.GameFiles.Contains("Bloodmoon.esm"));
108 | }
109 |
110 | [TestMethod]
111 | public void GameFilesFoundAreInOrder()
112 | {
113 | if (Install is null)
114 | {
115 | Assert.Inconclusive();
116 | return;
117 | }
118 |
119 | _logger.LogInformation("Game Files: {list}", Install.GameFiles);
120 | Assert.AreEqual(Install.GameFiles[0], "Morrowind.esm");
121 | Assert.AreEqual(Install.GameFiles[1], "Tribunal.esm");
122 | Assert.AreEqual(Install.GameFiles[2], "Bloodmoon.esm");
123 | }
124 |
125 | [TestMethod]
126 | public void DataFilesFound()
127 | {
128 | if (Install is null)
129 | {
130 | Assert.Inconclusive();
131 | return;
132 | }
133 |
134 | Assert.IsNotNull(Install.GetDataFile("Morrowind.esm"));
135 | Assert.IsNotNull(Install.GetDataFile("Morrowind.bsa"));
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/Installation/OpenMW.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Hosting;
3 | using Microsoft.Extensions.Logging;
4 | using Microsoft.VisualStudio.TestTools.UnitTesting;
5 | using Serilog;
6 | using Serilog.Events;
7 | using System;
8 | using System.IO;
9 | using TES3Merge.Util;
10 | using static TES3Merge.Tests.Utility;
11 |
12 | namespace TES3Merge.Tests.Installation;
13 |
14 | [TestClass, TestCategory("Installation")]
15 | public class OpenMW
16 | {
17 | protected IHost _host;
18 | protected Microsoft.Extensions.Logging.ILogger _logger;
19 |
20 | public OpenMWInstallation? Install;
21 |
22 | public OpenMW()
23 | {
24 | // Setup logging.
25 | Log.Logger = new LoggerConfiguration()
26 | .MinimumLevel.Debug()
27 | .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
28 | .Enrich.FromLogContext()
29 | .WriteTo.Console(outputTemplate: "[{Level:u3}] {Message:lj}{NewLine}{Exception}")
30 | .CreateLogger();
31 |
32 | var hostBuilder = Host.CreateDefaultBuilder().UseSerilog();
33 | _host = hostBuilder.Build();
34 | _logger = _host.Services.GetRequiredService>();
35 |
36 | if (Directory.Exists(Properties.Resources.OpenMWInstallDirectory))
37 | {
38 | Install = new OpenMWInstallation(Properties.Resources.OpenMWInstallDirectory);
39 | }
40 | }
41 |
42 | [TestMethod]
43 | public void InstallationFound()
44 | {
45 | if (Directory.Exists(Properties.Resources.OpenMWInstallDirectory))
46 | {
47 | Assert.IsNotNull(Install);
48 | }
49 | }
50 |
51 | [TestMethod]
52 | public void InstallationPathValid()
53 | {
54 | if (Install is null)
55 | {
56 | Assert.Inconclusive();
57 | return;
58 | }
59 |
60 | _logger.LogInformation("Installation Directory: {path}", Install.RootDirectory);
61 | var cfgPath = Path.Combine(Install.RootDirectory, "openmw.cfg");
62 | Assert.IsTrue(File.Exists(cfgPath));
63 | }
64 |
65 | [TestMethod]
66 | public void ArchivesFound()
67 | {
68 | if (Install is null)
69 | {
70 | Assert.Inconclusive();
71 | return;
72 | }
73 |
74 | _logger.LogInformation("Archives: {list}", Install.Archives);
75 | Assert.IsTrue(Install.Archives.Contains("Morrowind.bsa"));
76 | Assert.IsTrue(Install.Archives.Contains("Tribunal.bsa"));
77 | Assert.IsTrue(Install.Archives.Contains("Bloodmoon.bsa"));
78 | }
79 |
80 | [TestMethod]
81 | public void ArchivesAreInOrder()
82 | {
83 | if (Install is null)
84 | {
85 | Assert.Inconclusive();
86 | return;
87 | }
88 |
89 | _logger.LogInformation("Archives: {list}", Install.Archives);
90 | Assert.AreEqual(Install.Archives[0], "Morrowind.bsa");
91 | Assert.AreEqual(Install.Archives[1], "Tribunal.bsa");
92 | Assert.AreEqual(Install.Archives[2], "Bloodmoon.bsa");
93 | }
94 |
95 | [TestMethod]
96 | public void GameFilesFound()
97 | {
98 | if (Install is null)
99 | {
100 | Assert.Inconclusive();
101 | return;
102 | }
103 |
104 | _logger.LogInformation("Game Files: {list}", Install.GameFiles);
105 | Assert.IsTrue(Install.GameFiles.Contains("Morrowind.esm"));
106 | Assert.IsTrue(Install.GameFiles.Contains("Tribunal.esm"));
107 | Assert.IsTrue(Install.GameFiles.Contains("Bloodmoon.esm"));
108 | }
109 |
110 | [TestMethod]
111 | public void GameFilesFoundAreInOrder()
112 | {
113 | if (Install is null)
114 | {
115 | Assert.Inconclusive();
116 | return;
117 | }
118 |
119 | _logger.LogInformation("Game Files: {list}", Install.GameFiles);
120 | Assert.AreEqual(Install.GameFiles[0], "Morrowind.esm");
121 | Assert.AreEqual(Install.GameFiles[1], "Tribunal.esm");
122 | Assert.AreEqual(Install.GameFiles[2], "Bloodmoon.esm");
123 | }
124 |
125 | [TestMethod]
126 | public void DataFilesFound()
127 | {
128 | if (Install is null)
129 | {
130 | Assert.Inconclusive();
131 | return;
132 | }
133 |
134 | Assert.IsNotNull(Install.GetDataFile("Morrowind.esm"));
135 | Assert.IsNotNull(Install.GetDataFile("Morrowind.bsa"));
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/Merger/ALCH.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Logging;
3 | using Microsoft.VisualStudio.TestTools.UnitTesting;
4 | using static TES3Merge.Tests.Utility;
5 |
6 | namespace TES3Merge.Tests.Merger;
7 |
8 | ///
9 | /// Special cases to consider for this record:
10 | /// - Effects can be added and removed.
11 | /// - Effects can be changed, and effect data can be made strange if merged dumbly.
12 | ///
13 | [TestClass]
14 | public class ALCH : RecordTest
15 | {
16 | internal TES3Lib.Records.ALCH MergedDefault;
17 | internal TES3Lib.Records.ALCH p_fortify_intelligence_c;
18 | private static readonly string[] BasicMergeMasters = new string[] { "merge_base.esp", "merge_edit_all.esp", "merge_minor_tweaks.esp" };
19 | private static readonly string[] AddedEffectsMergeMasters = new string[] { "merge_base.esp", "merge_edit_all.esp", "merge_add_effects.esp", "merge_minor_tweaks.esp" };
20 | private static readonly string[] RealWorldTestMasters = new string[] { "Morrowind.esm", "BTB's Game Improvements (Necro Edit) Tweaked.esp", "AOF Potions Recolored.esp", "ST_Alchemy_Balance_Sri_1.4.esp" };
21 |
22 | public ALCH()
23 | {
24 | MergedDefault = CreateMergedRecord("merge_alchemy", AddedEffectsMergeMasters);
25 | p_fortify_intelligence_c = CreateMergedRecord("p_fortify_intelligence_c", RealWorldTestMasters);
26 |
27 | _logger = _host.Services.GetRequiredService>();
28 | }
29 |
30 | [TestMethod]
31 | public void EditorId()
32 | {
33 | LogRecords("NAME.EditorId", MergedDefault, BasicMergeMasters);
34 |
35 | Assert.AreEqual(MergedDefault.NAME.EditorId, GetCached("merge_minor_tweaks.esp").NAME.EditorId);
36 | }
37 |
38 | [TestMethod]
39 | public void ModelPath()
40 | {
41 | LogRecords("MODL.ModelPath", MergedDefault, BasicMergeMasters);
42 |
43 | Assert.AreEqual(MergedDefault.MODL.ModelPath, GetCached("merge_edit_all.esp").MODL.ModelPath);
44 | }
45 |
46 | [TestMethod]
47 | public void IconPath()
48 | {
49 | LogRecords("TEXT.IconPath", MergedDefault, BasicMergeMasters);
50 |
51 | Assert.AreEqual(MergedDefault.TEXT.IconPath, GetCached("merge_edit_all.esp").TEXT.IconPath);
52 | }
53 |
54 | [TestMethod]
55 | public void DisplayName()
56 | {
57 | LogRecords("FNAM.FileName", MergedDefault, BasicMergeMasters);
58 |
59 | Assert.AreEqual(MergedDefault.FNAM.FileName, GetCached("merge_minor_tweaks.esp").FNAM.FileName);
60 | }
61 |
62 | [TestMethod]
63 | public void Value()
64 | {
65 | LogRecords("ALDT.Value", MergedDefault, BasicMergeMasters);
66 |
67 | Assert.AreEqual(MergedDefault.ALDT.Value, GetCached("merge_edit_all.esp").ALDT.Value);
68 | }
69 |
70 | [TestMethod]
71 | public void Weight()
72 | {
73 | LogRecords("ALDT.Weight", MergedDefault, BasicMergeMasters);
74 |
75 | Assert.AreEqual(MergedDefault.ALDT.Weight, GetCached("merge_edit_all.esp").ALDT.Weight);
76 | }
77 |
78 | [TestMethod]
79 | public void ScriptName()
80 | {
81 | LogRecords("SCRI.ScriptName", MergedDefault, BasicMergeMasters);
82 |
83 | Assert.AreEqual(MergedDefault.SCRI?.ScriptName, GetCached("merge_edit_all.esp").SCRI?.ScriptName);
84 | }
85 |
86 | [TestMethod]
87 | public void Effects()
88 | {
89 | LogRecordsEffects(MergedDefault, AddedEffectsMergeMasters);
90 |
91 | // Ensure we have the right number of effects.
92 | Assert.IsNotNull(MergedDefault.ENAM);
93 | Assert.IsNotNull(GetCached("merge_edit_all.esp").ENAM);
94 | Assert.IsNotNull(GetCached("merge_add_effects.esp").ENAM);
95 |
96 | Assert.AreEqual(MergedDefault.ENAM.Count, GetCached("merge_add_effects.esp").ENAM.Count);
97 |
98 | // Make sure we ended up with the right first effect.
99 | Assert.AreEqual(MergedDefault.ENAM[0].MagicEffect, TES3Lib.Enums.MagicEffect.BoundCuirass);
100 |
101 | // Make sure all the properties were respected from the changed effect.
102 | // We don't want a changed effect to end up with a bunch of invalid properties.
103 | Assert.AreEqual(MergedDefault.ENAM[0].Skill, GetCached("merge_edit_all.esp").ENAM[0].Skill);
104 | Assert.AreEqual(MergedDefault.ENAM[0].Attribute, GetCached("merge_edit_all.esp").ENAM[0].Attribute);
105 | Assert.AreEqual(MergedDefault.ENAM[0].Magnitude, GetCached("merge_edit_all.esp").ENAM[0].Magnitude);
106 | Assert.AreEqual(MergedDefault.ENAM[0].Duration, GetCached("merge_edit_all.esp").ENAM[0].Duration);
107 |
108 | // Ensure that we carried over the right second effect.
109 | Assert.AreEqual(MergedDefault.ENAM[1], GetCached("merge_add_effects.esp").ENAM[1]);
110 |
111 | void LogRecordsEffects(TES3Lib.Records.ALCH merged, params string[] plugins)
112 | {
113 | foreach (var parent in plugins)
114 | {
115 | var plugin = RecordCache[parent];
116 | _logger.LogInformation("{Plugin} : {Count} ({Parent})", plugin, plugin.ENAM?.Count, parent);
117 | LogRecordsEnumerable(plugin.ENAM);
118 | }
119 | _logger.LogInformation("{MergedObjectsPluginName} : {Count}", MergedObjectsPluginName, merged.ENAM?.Count);
120 | LogRecordsEnumerable(merged.ENAM);
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/Merger/CREA.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Logging;
3 | using Microsoft.VisualStudio.TestTools.UnitTesting;
4 | using System.Linq;
5 | using TES3Lib.Subrecords.Shared;
6 | using static TES3Merge.Tests.Utility;
7 |
8 | namespace TES3Merge.Tests.Merger;
9 |
10 | ///
11 | /// Special cases to consider for this record:
12 | ///
13 | [TestClass]
14 | public class CREA : RecordTest
15 | {
16 | internal TES3Lib.Records.CREA MergedDefault;
17 |
18 | private static readonly string[] BasicMergeMasters = new string[] { "merge_base.esp", "merge_edit_all.esp", "merge_minor_tweaks.esp" };
19 | private static readonly string[] AddedMergeMasters = new string[] { "merge_base.esp", "merge_edit_all.esp", "merge_add_effects.esp", "merge_minor_tweaks.esp" };
20 |
21 | public CREA()
22 | {
23 | MergedDefault = CreateMergedRecord("alit", AddedMergeMasters);
24 |
25 | _logger = _host.Services.GetRequiredService>();
26 | }
27 |
28 | [TestMethod]
29 | public void EditorId()
30 | {
31 | LogRecords("NAME.EditorId", MergedDefault, BasicMergeMasters);
32 |
33 | Assert.AreEqual(MergedDefault.NAME.EditorId, GetCached("merge_minor_tweaks.esp").NAME.EditorId);
34 | }
35 |
36 | [TestMethod]
37 | public void DisplayName()
38 | {
39 | LogRecords("FNAM.FileName", MergedDefault, BasicMergeMasters);
40 |
41 | Assert.AreEqual(MergedDefault.FNAM.FileName, GetCached("merge_minor_tweaks.esp").FNAM.FileName);
42 | }
43 |
44 | [TestMethod]
45 | public void ScriptName()
46 | {
47 | LogRecords("SCRI.ScriptName", MergedDefault, BasicMergeMasters);
48 |
49 | Assert.AreEqual(MergedDefault.SCRI?.ScriptName, GetCached("merge_edit_all.esp").SCRI?.ScriptName);
50 | }
51 |
52 | // TODO
53 |
54 | //"race": "Dark Elf",
55 | //"class": "Barbarian",
56 | //"faction": "",
57 | //"head": "b_n_dark elf_m_head_05",
58 | //"hair": "b_n_dark elf_m_hair_08",
59 | //"npc_flags": 24,
60 | //"data":
61 | //"spells": [],
62 | //"ai_data":
63 | //"travel_destinations"
64 |
65 | [TestMethod]
66 | public void Inventory()
67 | {
68 | LogRecordsInventory(MergedDefault, AddedMergeMasters);
69 |
70 | // this is the load order
71 | var merge_base = GetCached("merge_base.esp").NPCO;
72 | var merge_edit_all = GetCached("merge_edit_all.esp").NPCO;
73 | var merge_add_effects = GetCached("merge_add_effects.esp").NPCO;
74 | var merge_minor_tweaks = GetCached("merge_minor_tweaks.esp").NPCO;
75 |
76 | // Ensure not null
77 | Assert.IsNotNull(merge_base);
78 | Assert.IsNotNull(merge_edit_all);
79 | Assert.IsNotNull(merge_add_effects);
80 | Assert.IsNotNull(merge_minor_tweaks);
81 | Assert.IsNotNull(MergedDefault.AIPackages);
82 |
83 | // TODO
84 | // make sure all the rest is inclusively merged
85 |
86 | // make sure all the rest is non-inclusively merged
87 |
88 |
89 | void LogRecordsInventory(TES3Lib.Records.CREA merged, params string[] plugins)
90 | {
91 | foreach (var parent in plugins)
92 | {
93 | var plugin = RecordCache[parent];
94 | _logger.LogInformation("{Plugin} : {Count} ({Parent})", plugin, plugin.NPCO?.Count, parent);
95 | LogRecordsEnumerable(plugin.NPCO);
96 | }
97 | _logger.LogInformation("{MergedObjectsPluginName} : {Count}", MergedObjectsPluginName, merged.NPCO?.Count);
98 | LogRecordsEnumerable(merged.NPCO);
99 | }
100 | }
101 |
102 | [TestMethod]
103 | public void AIPackages()
104 | {
105 | LogRecordsAIPackages(MergedDefault, AddedMergeMasters);
106 |
107 | // this is the load order
108 | var merge_base = GetCached("merge_base.esp").AIPackages;
109 | var merge_edit_all = GetCached("merge_edit_all.esp").AIPackages;
110 | var merge_add_effects = GetCached("merge_add_effects.esp").AIPackages;
111 | var merge_minor_tweaks = GetCached("merge_minor_tweaks.esp").AIPackages;
112 |
113 | // Ensure not null
114 | Assert.IsNotNull(merge_base);
115 | Assert.IsNotNull(merge_edit_all);
116 | Assert.IsNotNull(merge_add_effects);
117 | Assert.IsNotNull(merge_minor_tweaks);
118 | Assert.IsNotNull(MergedDefault.AIPackages);
119 |
120 | // wander packages are merged
121 | // distance is taken from merge_add_effects
122 | var distanceMerged = (MergedDefault.AIPackages.First().AIPackage as AI_W)?.Distance;
123 | var distanceCorrect = (merge_add_effects.First().AIPackage as AI_W)?.Distance;
124 | Assert.AreEqual(distanceMerged, distanceCorrect);
125 |
126 | // duration is taken from merge_minor_tweaks
127 | var durationMerged = (MergedDefault.AIPackages.First().AIPackage as AI_W)?.Duration;
128 | var durationCorrect = (merge_minor_tweaks.First().AIPackage as AI_W)?.Duration;
129 | Assert.AreEqual(durationMerged, durationCorrect);
130 |
131 | // other packages are taken by load order
132 | // last esp has the correct amount of packages since no merging is done
133 | Assert.AreEqual(MergedDefault.AIPackages.Count, merge_minor_tweaks.Count);
134 |
135 | // or inclusively merged
136 | // TODO tests
137 |
138 | void LogRecordsAIPackages(TES3Lib.Records.CREA merged, params string[] plugins)
139 | {
140 | foreach (var parent in plugins)
141 | {
142 | var plugin = RecordCache[parent];
143 | _logger.LogInformation("{Plugin} : {Count} ({Parent})", plugin, plugin.AIPackages?.Count, parent);
144 | LogRecordsEnumerable(plugin.AIPackages?.Select(x => x.AIPackage));
145 | }
146 | _logger.LogInformation("{MergedObjectsPluginName} : {Count}", MergedObjectsPluginName, merged.AIPackages?.Count);
147 | LogRecordsEnumerable(merged.AIPackages?.Select(x => x.AIPackage));
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/Merger/LEVC.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Logging;
3 | using Microsoft.VisualStudio.TestTools.UnitTesting;
4 | using System.Linq;
5 | using TES3Lib.Subrecords.Shared;
6 | using static TES3Merge.Tests.Utility;
7 |
8 | namespace TES3Merge.Tests.Merger;
9 |
10 | ///
11 | /// Special cases to consider for this record:
12 | ///
13 | [TestClass]
14 | public class LEVC : RecordTest
15 | {
16 | internal TES3Lib.Records.LEVC Merged__aa_sitters_bardrink_telmora;
17 |
18 | private static readonly string[] FriendsAndFoesMasters = new string[] { "F&F_base.esm", "F&F_NoSitters.ESP", "F&F_scarce.ESP" };
19 |
20 | public LEVC()
21 | {
22 | Merged__aa_sitters_bardrink_telmora = CreateMergedRecord("_aa_sitters_bardrink_telmora", FriendsAndFoesMasters);
23 |
24 | _logger = _host.Services.GetRequiredService>();
25 | }
26 |
27 | [TestMethod]
28 | public void EditorId()
29 | {
30 | LogRecords("NAME.EditorId", Merged__aa_sitters_bardrink_telmora, FriendsAndFoesMasters);
31 |
32 | Assert.AreEqual("_aa_sitters_bardrink_telmora\0", Merged__aa_sitters_bardrink_telmora.NAME.EditorId);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/Merger/NPC_.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Logging;
3 | using Microsoft.VisualStudio.TestTools.UnitTesting;
4 | using System.Linq;
5 | using TES3Lib.Subrecords.Shared;
6 | using static TES3Merge.Tests.Utility;
7 |
8 | namespace TES3Merge.Tests.Merger;
9 |
10 | ///
11 | /// Special cases to consider for this record:
12 | ///
13 | [TestClass]
14 | public class NPC_ : RecordTest
15 | {
16 | internal TES3Lib.Records.NPC_ MergedDefault;
17 |
18 | private static readonly string[] BasicMergeMasters = new string[] { "merge_base.esp", "merge_edit_all.esp", "merge_minor_tweaks.esp" };
19 | private static readonly string[] AddedMergeMasters = new string[] { "merge_base.esp", "merge_edit_all.esp", "merge_add_effects.esp", "merge_minor_tweaks.esp" };
20 |
21 | public NPC_()
22 | {
23 | MergedDefault = CreateMergedRecord("zennammu urshumusa", AddedMergeMasters);
24 |
25 | _logger = _host.Services.GetRequiredService>();
26 | }
27 |
28 | [TestMethod]
29 | public void EditorId()
30 | {
31 | LogRecords("NAME.EditorId", MergedDefault, BasicMergeMasters);
32 |
33 | Assert.AreEqual(MergedDefault.NAME.EditorId, GetCached("merge_minor_tweaks.esp").NAME.EditorId);
34 | }
35 |
36 | [TestMethod]
37 | public void DisplayName()
38 | {
39 | LogRecords("FNAM.FileName", MergedDefault, BasicMergeMasters);
40 |
41 | Assert.AreEqual(MergedDefault.FNAM.FileName, GetCached("merge_minor_tweaks.esp").FNAM.FileName);
42 | }
43 |
44 | [TestMethod]
45 | public void ScriptName()
46 | {
47 | LogRecords("SCRI.ScriptName", MergedDefault, BasicMergeMasters);
48 |
49 | Assert.AreEqual(MergedDefault.SCRI?.ScriptName, GetCached("merge_edit_all.esp").SCRI?.ScriptName);
50 | }
51 |
52 | // TODO
53 |
54 | //"race": "Dark Elf",
55 | //"class": "Barbarian",
56 | //"faction": "",
57 | //"head": "b_n_dark elf_m_head_05",
58 | //"hair": "b_n_dark elf_m_hair_08",
59 | //"npc_flags": 24,
60 | //"data":
61 | //"spells": [],
62 | //"ai_data":
63 | //"travel_destinations"
64 |
65 | [TestMethod]
66 | public void Inventory()
67 | {
68 | LogRecordsInventory(MergedDefault, AddedMergeMasters);
69 |
70 | // this is the load order
71 | var merge_base = GetCached("merge_base.esp").NPCO;
72 | var merge_edit_all = GetCached("merge_edit_all.esp").NPCO;
73 | var merge_add_effects = GetCached("merge_add_effects.esp").NPCO;
74 | var merge_minor_tweaks = GetCached("merge_minor_tweaks.esp").NPCO;
75 |
76 | // Ensure not null
77 | Assert.IsNotNull(merge_base);
78 | Assert.IsNotNull(merge_edit_all);
79 | Assert.IsNotNull(merge_add_effects);
80 | Assert.IsNotNull(merge_minor_tweaks);
81 | Assert.IsNotNull(MergedDefault.AIPackages);
82 |
83 | // TODO
84 | // make sure all the rest is inclusively merged
85 |
86 | // make sure all the rest is non-inclusively merged
87 |
88 |
89 | void LogRecordsInventory(TES3Lib.Records.NPC_ merged, params string[] plugins)
90 | {
91 | foreach (var parent in plugins)
92 | {
93 | var plugin = RecordCache[parent];
94 | _logger.LogInformation("{Plugin} : {Count} ({Parent})", plugin, plugin.NPCO?.Count, parent);
95 | LogRecordsEnumerable(plugin.NPCO);
96 | }
97 | _logger.LogInformation("{MergedObjectsPluginName} : {Count}", MergedObjectsPluginName, merged.NPCO?.Count);
98 | LogRecordsEnumerable(merged.NPCO);
99 | }
100 | }
101 |
102 | [TestMethod]
103 | public void AIPackages()
104 | {
105 | LogRecordsAIPackages(MergedDefault, AddedMergeMasters);
106 |
107 | // this is the load order
108 | var merge_base = GetCached("merge_base.esp").AIPackages;
109 | var merge_edit_all = GetCached("merge_edit_all.esp").AIPackages;
110 | var merge_add_effects = GetCached("merge_add_effects.esp").AIPackages;
111 | var merge_minor_tweaks = GetCached("merge_minor_tweaks.esp").AIPackages;
112 |
113 | // Ensure not null
114 | Assert.IsNotNull(merge_base);
115 | Assert.IsNotNull(merge_edit_all);
116 | Assert.IsNotNull(merge_add_effects);
117 | Assert.IsNotNull(merge_minor_tweaks);
118 | Assert.IsNotNull(MergedDefault.AIPackages);
119 |
120 | // wander packages are merged
121 | // distance is taken from merge_add_effects
122 | var distanceMerged = (MergedDefault.AIPackages.First().AIPackage as AI_W)?.Distance;
123 | var distanceCorrect = (merge_add_effects.First().AIPackage as AI_W)?.Distance;
124 | Assert.AreEqual(distanceMerged, distanceCorrect);
125 |
126 | // duration is taken from merge_minor_tweaks
127 | var durationMerged = (MergedDefault.AIPackages.First().AIPackage as AI_W)?.Duration;
128 | var durationCorrect = (merge_minor_tweaks.First().AIPackage as AI_W)?.Duration;
129 | Assert.AreEqual(durationMerged, durationCorrect);
130 |
131 | // other packages are taken by load order
132 | // last esp has the correct amount of packages since no merging is done
133 | Assert.AreEqual(MergedDefault.AIPackages.Count, merge_minor_tweaks.Count);
134 |
135 | // or inclusively merged
136 | // TODO tests
137 |
138 | void LogRecordsAIPackages(TES3Lib.Records.NPC_ merged, params string[] plugins)
139 | {
140 | foreach (var parent in plugins)
141 | {
142 | var plugin = RecordCache[parent];
143 | _logger.LogInformation("{Plugin} : {Count} ({Parent})", plugin, plugin.AIPackages?.Count, parent);
144 | LogRecordsEnumerable(plugin.AIPackages?.Select(x => x.AIPackage));
145 | }
146 | _logger.LogInformation("{MergedObjectsPluginName} : {Count}", MergedObjectsPluginName, merged.AIPackages?.Count);
147 | LogRecordsEnumerable(merged.AIPackages?.Select(x => x.AIPackage));
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/Parser/CELL.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Logging;
3 | using Microsoft.VisualStudio.TestTools.UnitTesting;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Reflection;
9 | using System.Text;
10 | using System.Threading.Tasks;
11 | using TES3Lib;
12 | using TES3Lib.Base;
13 | using static TES3Merge.Tests.Utility;
14 |
15 | namespace TES3Merge.Tests.Parser
16 | {
17 | [TestClass]
18 | public class CELL
19 | {
20 | [TestMethod]
21 | public void Parse()
22 | {
23 | // load esp
24 | var path = Path.Combine("Plugins", "F&F_base.esm");
25 | var file = TES3.TES3LoadSync(path, new() { "CELL" });
26 |
27 | // serialize CELL to bytes
28 | var errored = 0;
29 | foreach (var r in file.Records)
30 | {
31 | if (r is null)
32 | {
33 | continue;
34 | }
35 | if (!r.Name.Equals("CELL"))
36 | {
37 | continue;
38 | }
39 |
40 | var newRecord = Activator.CreateInstance(r.GetType(), new object[] { r.GetRawLoadedBytes() }) as TES3Lib.Records.CELL ?? throw new Exception("Could not create activator instance.");
41 |
42 | var newSerialized = newRecord.SerializeRecordForMerge();
43 | var lastSerialized = (r as TES3Lib.Records.CELL)!.SerializeRecordForMerge();
44 |
45 | var result = lastSerialized.SequenceEqual(newSerialized);
46 | if (!result)
47 | {
48 | //var outdir = new FileInfo(path).Directory?.FullName;
49 | //File.WriteAllBytes(Path.Combine(outdir!, "file1.bin"), lastSerialized);
50 | //File.WriteAllBytes(Path.Combine(outdir!, "file2.bin"), newSerialized);
51 |
52 | errored++;
53 | }
54 | }
55 |
56 | Assert.IsTrue(errored == 0);
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/AOF Potions Recolored.esp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/AOF Potions Recolored.esp
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/BTB's Game Improvements (Necro Edit) Tweaked.esp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/BTB's Game Improvements (Necro Edit) Tweaked.esp
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/Bob's Diverse Blood.ESP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/Bob's Diverse Blood.ESP
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/F&F_NoSitters.ESP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/F&F_NoSitters.ESP
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/F&F_base.esm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/F&F_base.esm
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/F&F_scarce.ESP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/F&F_scarce.ESP
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/Morrowind.esm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/Morrowind.esm
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/Patch for Purists - Book Typos.ESP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/Patch for Purists - Book Typos.ESP
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/Patch for Purists - Semi-Purist Fixes.ESP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/Patch for Purists - Semi-Purist Fixes.ESP
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/Patch for Purists.esm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/Patch for Purists.esm
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/ST_Alchemy_Balance_Sri_1.4.esp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/ST_Alchemy_Balance_Sri_1.4.esp
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/Wares-base.esm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/Wares-base.esm
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/merge_add_effects.esp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/merge_add_effects.esp
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/merge_base.esp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/merge_base.esp
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/merge_edit_all.esp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/merge_edit_all.esp
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/merge_minor_tweaks.esp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/merge_minor_tweaks.esp
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/tes3conv.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullCascade/TES3Merge/1058246a1a94ab9f045e2574e5022de6516db4d2/TES3Merge.Tests/Plugins/tes3conv.exe
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/to_esp.bat:
--------------------------------------------------------------------------------
1 | tes3conv.exe -o "merge_add_effects.json" "merge_add_effects.esp"
2 | tes3conv.exe -o "merge_base.json" "merge_base.esp"
3 | tes3conv.exe -o "merge_edit_all.json" "merge_edit_all.esp"
4 | tes3conv.exe -o "merge_minor_tweaks.json" "merge_minor_tweaks.esp"
5 | tes3conv.exe -o "Morrowind.json" "Morrowind.esm"
6 | tes3conv.exe -o "Bob's Diverse Blood.json" "Bob's Diverse Blood.esp"
7 | tes3conv.exe -o "Patch for Purists.json" "Patch for Purists.esm"
--------------------------------------------------------------------------------
/TES3Merge.Tests/Plugins/to_json.bat:
--------------------------------------------------------------------------------
1 | tes3conv.exe -o "merge_add_effects.esp" "merge_add_effects.json"
2 | tes3conv.exe -o "merge_base.esp" "merge_base.json"
3 | tes3conv.exe -o "merge_edit_all.esp" "merge_edit_all.json"
4 | tes3conv.exe -o "merge_minor_tweaks.esp" "merge_minor_tweaks.json"
5 | tes3conv.exe -o "Morrowind.esm" "Morrowind.json"
6 | tes3conv.exe -o "Bob's Diverse Blood.esp" "Bob's Diverse Blood.json"
7 | tes3conv.exe -o "Patch for Purists.esm" "Patch for Purists.json"
--------------------------------------------------------------------------------
/TES3Merge.Tests/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace TES3Merge.Tests.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TES3Merge.Tests.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 |
63 | ///
64 | /// Looks up a localized string similar to D:\Games\Morrowind.
65 | ///
66 | internal static string MorrowindInstallDirectory {
67 | get {
68 | return ResourceManager.GetString("MorrowindInstallDirectory", resourceCulture);
69 | }
70 | }
71 |
72 | ///
73 | /// Looks up a localized string similar to D:\Games\OpenMW.
74 | ///
75 | internal static string OpenMWInstallDirectory {
76 | get {
77 | return ResourceManager.GetString("OpenMWInstallDirectory", resourceCulture);
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | D:\Games\Morrowind
122 |
123 |
124 | D:\Games\OpenMW
125 |
126 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/RecordTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Hosting;
3 | using Microsoft.Extensions.Logging;
4 | using Serilog;
5 | using Serilog.Events;
6 | using System;
7 | using System.Collections;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 | using static TES3Merge.Tests.FileLoader;
11 |
12 | namespace TES3Merge.Tests.Merger;
13 |
14 | public abstract class RecordTest where T : TES3Lib.Base.Record
15 | {
16 | protected Microsoft.Extensions.Logging.ILogger _logger;
17 | protected IHost _host;
18 |
19 | public RecordTest()
20 | {
21 | Log.Logger = new LoggerConfiguration()
22 | .MinimumLevel.Debug()
23 | .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
24 | .Enrich.FromLogContext()
25 | .WriteTo.Console(outputTemplate: "[{Level:u3}] {Message:lj}{NewLine}{Exception}")
26 | .CreateLogger();
27 |
28 | var hostBuilder = Host.CreateDefaultBuilder().UseSerilog();
29 |
30 | _host = hostBuilder.Build();
31 | _logger = _host.Services.GetRequiredService>>();
32 | }
33 |
34 | #region Record Management
35 | internal static Dictionary RecordCache = new();
36 |
37 | internal static T GetCached(string plugin)
38 | {
39 | return RecordCache[plugin];
40 | }
41 |
42 | internal static T CreateMergedRecord(string objectId, params string[] parentFiles)
43 | {
44 | // Load files.
45 | List parents = new();
46 | foreach (var file in parentFiles)
47 | {
48 | var parent = GetPlugin(file) ?? throw new Exception($"Parent file {file} could not be loaded.");
49 | parents.Add(parent);
50 | }
51 |
52 | // Find records.
53 | List records = new();
54 | foreach (var parent in parents)
55 | {
56 | var record = RecordCache.ContainsKey(parent.Path)
57 | ? RecordCache[parent.Path]
58 | : parent.FindRecord(objectId) as T ?? throw new Exception($"Parent file {parent.Path} does not have record {objectId}.");
59 | records.Add(record);
60 | RecordCache[parent.Path] = record;
61 | }
62 |
63 | // Create merge.
64 | var first = records.First();
65 | var last = records.Last();
66 | var merged = Activator.CreateInstance(last.GetType(), new object[] { last.SerializeRecord() }) as T ?? throw new Exception("Could not create record.");
67 | for (var i = records.Count - 2; i > 0; i--)
68 | {
69 | RecordMerger.Merge(merged, first, records[i]);
70 | }
71 | return merged;
72 | }
73 | #endregion
74 |
75 | #region Logging
76 | internal void LogRecordValue(string property, string plugin)
77 | {
78 | LogRecordValue(GetCached(plugin), property, plugin);
79 | }
80 |
81 | internal void LogRecordValue(T record, string property, string plugin = Utility.MergedObjectsPluginName)
82 | {
83 | _logger.LogInformation("{plugin} : {PropertyValue}", plugin, Utility.GetPropertyValue(record, property));
84 | }
85 |
86 | internal void LogRecords(string property, T merged, params string[] plugins)
87 | {
88 | foreach (var plugin in plugins)
89 | {
90 | LogRecordValue(property, plugin);
91 | }
92 | LogRecordValue(merged, property);
93 | }
94 |
95 | internal void LogRecordsEnumerable(IEnumerable? items)
96 | {
97 | if (items is null)
98 | {
99 | return;
100 | }
101 |
102 | foreach (var item in items)
103 | {
104 | _logger.LogInformation(" - {Name}: {@Item}", item.GetType().Name, item);
105 | }
106 | }
107 |
108 | #endregion
109 | }
110 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/TES3Merge.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | true
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | True
46 | True
47 | Resources.resx
48 |
49 |
50 |
51 |
52 |
53 | ResXFileCodeGenerator
54 | Resources.Designer.cs
55 |
56 |
57 |
58 |
59 |
60 | PreserveNewest
61 |
62 |
63 | PreserveNewest
64 |
65 |
66 | PreserveNewest
67 |
68 |
69 | PreserveNewest
70 |
71 |
72 | PreserveNewest
73 |
74 |
75 | PreserveNewest
76 |
77 |
78 | PreserveNewest
79 |
80 |
81 | PreserveNewest
82 |
83 |
84 | PreserveNewest
85 |
86 |
87 | PreserveNewest
88 |
89 |
90 | PreserveNewest
91 |
92 |
93 | PreserveNewest
94 |
95 |
96 | PreserveNewest
97 |
98 |
99 | PreserveNewest
100 |
101 |
102 | PreserveNewest
103 |
104 |
105 | PreserveNewest
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/TES3Merge.Tests/Utility.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace TES3Merge.Tests;
6 |
7 | internal static class Utility
8 | {
9 | internal const string MergedObjectsPluginName = "Merged Objects.esp";
10 |
11 | internal static TES3Lib.Base.Record? FindRecord(this TES3Lib.TES3 plugin, string id)
12 | {
13 | return plugin.Records.FirstOrDefault(r => r.GetEditorId() == $"{id}\0");
14 | }
15 |
16 | internal static object? GetPropertyValue(object src, string property)
17 | {
18 | if (src is null) throw new ArgumentException("Value cannot be null.", nameof(src));
19 | if (property is null) throw new ArgumentException("Value cannot be null.", nameof(property));
20 |
21 | if (property.Contains('.')) //complex type nested
22 | {
23 | var temp = property.Split(new char[] { '.' }, 2);
24 | var value = GetPropertyValue(src, temp[0]);
25 | if (value is null) return null;
26 | return GetPropertyValue(value, temp[1]);
27 | }
28 | else
29 | {
30 | return src.GetType().GetProperty(property)?.GetValue(src, null);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/TES3Merge.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.32014.148
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TES3Lib", "TES3Tool\TES3Lib\TES3Lib.csproj", "{EB6C834A-B58E-4BBF-8BC5-4DEE6A925B4B}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Utility", "TES3Tool\Utility\Utility.csproj", "{1BC5CBBB-E155-4D9A-BC55-6FE75109C1B9}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TES3Merge", "TES3Merge\TES3Merge.csproj", "{5E35B4F8-4150-41D8-B78C-64ED0EF8871E}"
11 | ProjectSection(ProjectDependencies) = postProject
12 | {EB6C834A-B58E-4BBF-8BC5-4DEE6A925B4B} = {EB6C834A-B58E-4BBF-8BC5-4DEE6A925B4B}
13 | {1BC5CBBB-E155-4D9A-BC55-6FE75109C1B9} = {1BC5CBBB-E155-4D9A-BC55-6FE75109C1B9}
14 | EndProjectSection
15 | EndProject
16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TES3Merge.Tests", "TES3Merge.Tests\TES3Merge.Tests.csproj", "{5F6DDA09-33B6-4FAC-AC34-5FABBFA6D0F6}"
17 | ProjectSection(ProjectDependencies) = postProject
18 | {5E35B4F8-4150-41D8-B78C-64ED0EF8871E} = {5E35B4F8-4150-41D8-B78C-64ED0EF8871E}
19 | EndProjectSection
20 | EndProject
21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{64D6AD8F-D337-411B-A5D9-0A2F2FDA811E}"
22 | ProjectSection(SolutionItems) = preProject
23 | .editorconfig = .editorconfig
24 | EndProjectSection
25 | EndProject
26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{95C57EC3-AAEB-493D-8FAC-D54AD18C5BFF}"
27 | EndProject
28 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{491D16BA-EA9E-42AB-A2AC-455F22A9B4B2}"
29 | EndProject
30 | Global
31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
32 | Debug|Any CPU = Debug|Any CPU
33 | Release|Any CPU = Release|Any CPU
34 | EndGlobalSection
35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
36 | {EB6C834A-B58E-4BBF-8BC5-4DEE6A925B4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37 | {EB6C834A-B58E-4BBF-8BC5-4DEE6A925B4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
38 | {EB6C834A-B58E-4BBF-8BC5-4DEE6A925B4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 | {EB6C834A-B58E-4BBF-8BC5-4DEE6A925B4B}.Release|Any CPU.Build.0 = Release|Any CPU
40 | {1BC5CBBB-E155-4D9A-BC55-6FE75109C1B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {1BC5CBBB-E155-4D9A-BC55-6FE75109C1B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {1BC5CBBB-E155-4D9A-BC55-6FE75109C1B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
43 | {1BC5CBBB-E155-4D9A-BC55-6FE75109C1B9}.Release|Any CPU.Build.0 = Release|Any CPU
44 | {5E35B4F8-4150-41D8-B78C-64ED0EF8871E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
45 | {5E35B4F8-4150-41D8-B78C-64ED0EF8871E}.Debug|Any CPU.Build.0 = Debug|Any CPU
46 | {5E35B4F8-4150-41D8-B78C-64ED0EF8871E}.Release|Any CPU.ActiveCfg = Release|Any CPU
47 | {5E35B4F8-4150-41D8-B78C-64ED0EF8871E}.Release|Any CPU.Build.0 = Release|Any CPU
48 | {5F6DDA09-33B6-4FAC-AC34-5FABBFA6D0F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
49 | {5F6DDA09-33B6-4FAC-AC34-5FABBFA6D0F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
50 | {5F6DDA09-33B6-4FAC-AC34-5FABBFA6D0F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
51 | {5F6DDA09-33B6-4FAC-AC34-5FABBFA6D0F6}.Release|Any CPU.Build.0 = Release|Any CPU
52 | EndGlobalSection
53 | GlobalSection(SolutionProperties) = preSolution
54 | HideSolutionNode = FALSE
55 | EndGlobalSection
56 | GlobalSection(NestedProjects) = preSolution
57 | {EB6C834A-B58E-4BBF-8BC5-4DEE6A925B4B} = {491D16BA-EA9E-42AB-A2AC-455F22A9B4B2}
58 | {1BC5CBBB-E155-4D9A-BC55-6FE75109C1B9} = {491D16BA-EA9E-42AB-A2AC-455F22A9B4B2}
59 | {5F6DDA09-33B6-4FAC-AC34-5FABBFA6D0F6} = {95C57EC3-AAEB-493D-8FAC-D54AD18C5BFF}
60 | EndGlobalSection
61 | GlobalSection(ExtensibilityGlobals) = postSolution
62 | SolutionGuid = {3535F6FB-C6C9-4C94-8B77-21E8B5E90365}
63 | EndGlobalSection
64 | EndGlobal
65 |
--------------------------------------------------------------------------------
/TES3Merge/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 | using System.Runtime.InteropServices;
3 |
4 | // In SDK-style projects such as this one, several assembly attributes that were historically
5 | // defined in this file are now automatically added during build and populated with
6 | // values defined in project properties. For details of which attributes are included
7 | // and how to customise this process see: https://aka.ms/assembly-info-properties
8 |
9 |
10 | // Setting ComVisible to false makes the types in this assembly not visible to COM
11 | // components. If you need to access a type in this assembly from COM, set the ComVisible
12 | // attribute to true on that type.
13 |
14 | [assembly: ComVisible(false)]
15 |
16 | // The following GUID is for the ID of the typelib if this project is exposed to COM.
17 |
18 | [assembly: Guid("07e58b19-a0f4-4b20-831a-8478e9546029")]
19 |
20 | [assembly: InternalsVisibleTo("TES3Merge.Tests")]
21 |
--------------------------------------------------------------------------------
/TES3Merge/Commands/MultipatchCommand.cs:
--------------------------------------------------------------------------------
1 | using System.CommandLine;
2 | using static TES3Merge.Util.Util;
3 |
4 | namespace TES3Merge.Commands;
5 |
6 | public class MultipatchCommand : Command
7 | {
8 | private new const string Description = "Create a multipatch that merges levelled lists and fixes various other bugs";
9 | private new const string Name = "multipatch";
10 |
11 | public MultipatchCommand() : base(Name, Description)
12 | {
13 | this.SetHandler(() => MultipatchAction.Run());
14 | }
15 | }
16 |
17 | internal static class MultipatchAction
18 | {
19 | ///
20 | /// Main command wrapper
21 | ///
22 | internal static void Run()
23 | {
24 | #if DEBUG == false
25 | try
26 | #else
27 | //Console.WriteLine("Press any button to continue...");
28 | //Console.ReadLine();
29 | #endif
30 | {
31 | Multipatch();
32 | }
33 |
34 | #if DEBUG == false
35 | catch (Exception e)
36 | {
37 | Console.WriteLine("A serious error has occurred. Please post the TES3Merge.log file to GitHub: https://github.com/NullCascade/TES3Merge/issues");
38 | Logger.WriteLine("An unhandled exception has occurred. Traceback:");
39 | Logger.WriteLine(e.Message);
40 | Logger.WriteLine(e.StackTrace);
41 | }
42 | #endif
43 |
44 | ShowCompletionPrompt();
45 | }
46 |
47 | ///
48 | /// tes3cmd multipatch
49 | /// Merge LEVI and LEVC
50 | ///
51 | ///
52 | private static void Multipatch()
53 | {
54 | using var ssw = new ScopedStopwatch();
55 |
56 | MergeAction.Merge(
57 | new MergeAction.Settings(
58 | true,
59 | new List() { "LEVI", "LEVC", "CREA", "CELL" },
60 | null,
61 | Util.EPatch.All,
62 | false,
63 | true,
64 | "multipatch.esp"));
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/TES3Merge/Commands/VerifyCommand.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * TODO
3 | *
4 | * check NIF paths for each esp
5 | *
6 | */
7 |
8 | using System.Collections.Concurrent;
9 | using System.CommandLine;
10 | using System.Reflection;
11 | using System.Text.Json;
12 | using System.Text.RegularExpressions;
13 | using TES3Lib;
14 | using TES3Lib.Base;
15 | using static TES3Merge.Util.Util;
16 |
17 | namespace TES3Merge.Commands;
18 |
19 | public class VerifyCommand : Command
20 | {
21 | private new const string Description = "Checks esps for missing file paths";
22 | private new const string Name = "verify";
23 |
24 | public VerifyCommand() : base(Name, Description)
25 | {
26 | this.SetHandler(() => VerifyAction.Run());
27 | }
28 | }
29 |
30 | internal static class VerifyAction
31 | {
32 | ///
33 | /// Main command wrapper
34 | ///
35 | internal static void Run()
36 | {
37 | #if DEBUG == false
38 | try
39 | #endif
40 | {
41 | Verify();
42 | }
43 |
44 | #if DEBUG == false
45 | catch (Exception e)
46 | {
47 | Console.WriteLine("A serious error has occurred. Please post the TES3Merge.log file to GitHub: https://github.com/NullCascade/TES3Merge/issues");
48 | Logger.WriteLine("An unhandled exception has occurred. Traceback:");
49 | Logger.WriteLine(e.Message);
50 | Logger.WriteLine(e.StackTrace);
51 | }
52 | #endif
53 |
54 | ShowCompletionPrompt();
55 | }
56 |
57 | ///
58 | /// Verifies all active esps in the current Morrowind directory
59 | /// Parses all enabled records of the plugin and checks paths if the file exists
60 | ///
61 | ///
62 | private static void Verify()
63 | {
64 | ArgumentNullException.ThrowIfNull(CurrentInstallation);
65 |
66 | using var ssw = new ScopedStopwatch();
67 | LoadConfig();
68 | ArgumentNullException.ThrowIfNull(Configuration);
69 |
70 | // get merge tags
71 | var (supportedMergeTags, objectIdFilters) = GetMergeTags();
72 |
73 | // Shorthand install access.
74 | var sortedMasters = CurrentInstallation.GameFiles;
75 |
76 | // Go through and build a record list.
77 | var reportDict = new ConcurrentDictionary>>();
78 | WriteToLogAndConsole($"Parsing plugins ... ");
79 | //foreach (var sortedMaster in sortedMasters)
80 | Parallel.ForEach(sortedMasters, sortedMaster =>
81 | {
82 | // this can be enabled actually
83 | if (Path.GetExtension(sortedMaster) == ".esm")
84 | {
85 | //continue;
86 | return;
87 | }
88 |
89 | var map = new Dictionary>();
90 |
91 | // go through all records
92 | WriteToLogAndConsole($"Parsing input file: {sortedMaster}");
93 | var fullGameFilePath = Path.Combine(CurrentInstallation.RootDirectory, "Data Files", $"{sortedMaster}");
94 | var file = TES3.TES3Load(fullGameFilePath, supportedMergeTags);
95 | foreach (var record in file.Records)
96 | {
97 | #region checks
98 |
99 | if (record is null)
100 | {
101 | continue;
102 | }
103 | if (record.GetType().Equals(typeof(TES3Lib.Records.TES3)))
104 | {
105 | continue;
106 | }
107 | var editorId = record.GetEditorId().Replace("\0", string.Empty);
108 | if (string.IsNullOrEmpty(editorId))
109 | {
110 | continue;
111 | }
112 |
113 | // Check against object filters.
114 | var allow = true;
115 | var lowerId = editorId.ToLower();
116 | foreach (var kv in objectIdFilters)
117 | {
118 | try
119 | {
120 | if (Regex.Match(lowerId, kv.Key).Success)
121 | {
122 | allow = kv.Value;
123 | }
124 | }
125 | catch (Exception)
126 | {
127 |
128 | }
129 | }
130 | if (!allow)
131 | {
132 | continue;
133 | }
134 |
135 | #endregion
136 |
137 | // verify here
138 | GetPathsInRecord(record, map);
139 | }
140 |
141 | if (map.Count > 0)
142 | {
143 | reportDict.AddOrUpdate(sortedMaster, map, (key, oldValue) => map);
144 | }
145 | }
146 | );
147 |
148 | // pretty print
149 | WriteToLogAndConsole($"\n------------------------------------");
150 | WriteToLogAndConsole($"Results:\n");
151 | foreach (var (plugin, val) in reportDict)
152 | {
153 | WriteToLogAndConsole($"\n{plugin} ({val.Count})");
154 | foreach (var (recordID, list) in val)
155 | {
156 | foreach (var item in list)
157 | {
158 | //Console.WriteLine("{0,-20} {1,5}\n", "Name", "Hours");
159 | WriteToLogAndConsole(string.Format("\t{0,-40} {1,5}", recordID, item));
160 | }
161 | }
162 | }
163 | // serialize to file
164 | WriteToLogAndConsole($"\n");
165 | var reportPath = Path.Combine(CurrentInstallation.RootDirectory, "Data Files", "report.json");
166 | WriteToLogAndConsole($"Writing report to: {reportPath}");
167 | {
168 | using var fs = new FileStream(reportPath, FileMode.Create);
169 | JsonSerializer.Serialize(fs, reportDict, new JsonSerializerOptions() { WriteIndented = true });
170 | }
171 |
172 | }
173 |
174 | ///
175 | /// loop through all subrecords of a record
176 | ///
177 | ///
178 | ///
179 | ///
180 | private static void GetPathsInRecord(
181 | Record record,
182 | Dictionary> map)
183 | {
184 | var recordDict = new List();
185 | var properties = record
186 | .GetType()
187 | .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
188 | .OrderBy(x => x.MetadataToken)
189 | .ToList();
190 |
191 | foreach (var property in properties)
192 | {
193 | var val = record is not null ? property.GetValue(record) : null;
194 | if (val is Subrecord subrecord)
195 | {
196 | GetPathsInSubRecordRecursive(subrecord, recordDict);
197 | }
198 | }
199 |
200 | if (recordDict.Count > 0 && record is not null)
201 | {
202 | var id = record.GetEditorId().TrimEnd('\0');
203 | map.Add(id, recordDict);
204 | }
205 | }
206 |
207 | ///
208 | /// Loop through all properties of a subrecord
209 | /// and check if a property is a file path
210 | /// then checks if that file exists in the filemap
211 | ///
212 | ///
213 | ///
214 | ///
215 | private static void GetPathsInSubRecordRecursive(
216 | Subrecord subRecord,
217 | List map)
218 | {
219 | ArgumentNullException.ThrowIfNull(CurrentInstallation);
220 |
221 | var recordTypeName = subRecord.Name;
222 | var properties = subRecord
223 | .GetType()
224 | .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
225 | .OrderBy(x => x.MetadataToken)
226 | .ToList()!;
227 |
228 | foreach (var property in properties)
229 | {
230 | var val = subRecord is not null ? property.GetValue(subRecord) : null;
231 | if (val is string rawstr)
232 | {
233 | var str = rawstr.TrimEnd('\0').ToLower();
234 | var file = CurrentInstallation.GetSubstitutingDataFile(str);
235 | if (file is null)
236 | {
237 | map.Add(str);
238 | }
239 | }
240 | else
241 | {
242 | if (val is Subrecord subrecord)
243 | {
244 | GetPathsInSubRecordRecursive(subrecord, map);
245 | }
246 | }
247 | }
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/TES3Merge/Extensions/GenericObjectExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using System.Reflection;
3 |
4 | namespace TES3Merge;
5 |
6 | static class GenericObjectExtensions
7 | {
8 | public static bool IsNonStringEnumerable(this PropertyInfo pi)
9 | {
10 | return pi is not null && pi.PropertyType.IsNonStringEnumerable();
11 | }
12 |
13 | public static bool IsNonStringEnumerable(this object instance)
14 | {
15 | return instance is not null && instance.GetType().IsNonStringEnumerable();
16 | }
17 |
18 | public static bool IsNonStringEnumerable(this Type type)
19 | {
20 | if (type is null || type == typeof(string))
21 | return false;
22 | return typeof(IEnumerable).IsAssignableFrom(type);
23 | }
24 |
25 | internal static bool NullableSequenceEqual(this IEnumerable? a, IEnumerable? b)
26 | {
27 | if (a is null)
28 | {
29 | return b is null;
30 | }
31 | else if (b is null)
32 | {
33 | return a is null;
34 | }
35 | return a.SequenceEqual(b);
36 | }
37 |
38 | public static bool PublicInstancePropertiesEqual(this T self, T to, params string[] ignore) where T : class
39 | {
40 | if (self is not null && to is not null)
41 | {
42 | var type = typeof(T);
43 | var ignoreList = new List(ignore);
44 | var unequalProperties =
45 | from pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
46 | where !ignoreList.Contains(pi.Name) && pi.GetUnderlyingType().IsSimpleType() && pi.GetIndexParameters().Length == 0
47 | let selfValue = type.GetProperty(pi.Name)?.GetValue(self, null)
48 | let toValue = type.GetProperty(pi.Name)?.GetValue(to, null)
49 | where selfValue != toValue && (selfValue is null || !selfValue.Equals(toValue))
50 | select selfValue;
51 | return !unequalProperties.Any();
52 | }
53 | return self == to;
54 | }
55 |
56 | ///
57 | /// A set of types that are considered to be "simple" by default.
58 | ///
59 | public static readonly HashSet SimpleTypes = new()
60 | {
61 | typeof(string),
62 | typeof(decimal),
63 | typeof(DateTime),
64 | typeof(DateTimeOffset),
65 | typeof(TimeSpan),
66 | typeof(Guid)
67 | };
68 |
69 | ///
70 | /// Determine whether a type is simple (String, Decimal, DateTime, etc)
71 | /// or complex (i.e. custom class with public properties and methods).
72 | ///
73 | ///
74 | public static bool IsSimpleType(this Type type)
75 | {
76 | return
77 | type.IsValueType ||
78 | type.IsPrimitive ||
79 | SimpleTypes.Contains(type) ||
80 | (Convert.GetTypeCode(type) != TypeCode.Object);
81 | }
82 |
83 | public static Type GetUnderlyingType(this MemberInfo member)
84 | {
85 | if (member is null)
86 | {
87 | throw new ArgumentException("Input MemberInfo must not be null.");
88 | }
89 |
90 | return member.MemberType switch
91 | {
92 | MemberTypes.Event => ((EventInfo)member).EventHandlerType ?? throw new ArgumentException("EventInfo does not have EventHandlerType."),
93 | MemberTypes.Field => ((FieldInfo)member).FieldType,
94 | MemberTypes.Method => ((MethodInfo)member).ReturnType,
95 | MemberTypes.Property => ((PropertyInfo)member).PropertyType,
96 | _ => throw new ArgumentException("Input MemberInfo must be if type EventInfo, FieldInfo, MethodInfo, or PropertyInfo"),
97 | };
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/TES3Merge/Extensions/StreamExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace TES3Merge.Extensions;
4 |
5 | public static class StreamExtensions
6 | {
7 | public static byte[] ToByteArray(this Stream input, bool keepPosition = false)
8 | {
9 | if (input is MemoryStream memoryStream)
10 | {
11 | return memoryStream.ToArray();
12 | }
13 | else
14 | {
15 | using var ms = new MemoryStream();
16 | if (!keepPosition)
17 | {
18 | input.Position = 0;
19 | }
20 | input.CopyTo(ms);
21 | return ms.ToArray();
22 |
23 | }
24 | }
25 |
26 | public static T ReadStruct(this Stream m_stream) where T : struct
27 | {
28 | var size = Marshal.SizeOf();
29 |
30 | var m_temp = new byte[size];
31 | m_stream.Read(m_temp, 0, size);
32 |
33 | var handle = GCHandle.Alloc(m_temp, GCHandleType.Pinned);
34 | T item = Marshal.PtrToStructure(handle.AddrOfPinnedObject());
35 |
36 | handle.Free();
37 |
38 | return item;
39 | }
40 |
41 | public static void WriteStruct(this Stream m_stream, T value) where T : struct
42 | {
43 | var m_temp = new byte[Marshal.SizeOf()];
44 | var handle = GCHandle.Alloc(m_temp, GCHandleType.Pinned);
45 |
46 | Marshal.StructureToPtr(value, handle.AddrOfPinnedObject(), true);
47 | m_stream.Write(m_temp, 0, m_temp.Length);
48 |
49 | handle.Free();
50 | }
51 |
52 | public static T[] ReadStructs(this Stream m_stream, uint count) where T : struct
53 | {
54 | var size = Marshal.SizeOf();
55 | var items = new T[count];
56 |
57 | var m_temp = new byte[size];
58 | for (uint i = 0; i < count; i++)
59 | {
60 | m_stream.Read(m_temp, 0, size);
61 |
62 | var handle = GCHandle.Alloc(m_temp, GCHandleType.Pinned);
63 | items[i] = Marshal.PtrToStructure(handle.AddrOfPinnedObject());
64 |
65 | handle.Free();
66 | }
67 |
68 | return items;
69 | }
70 |
71 | public static void WriteStructs(this Stream m_stream, T[] array) where T : struct
72 | {
73 | var size = Marshal.SizeOf();
74 | var m_temp = new byte[size];
75 | for (var i = 0; i < array.Length; i++)
76 | {
77 | var handle = GCHandle.Alloc(m_temp, GCHandleType.Pinned);
78 |
79 | Marshal.StructureToPtr(array[i], handle.AddrOfPinnedObject(), true);
80 | m_stream.Write(m_temp, 0, m_temp.Length);
81 |
82 | handle.Free();
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/TES3Merge/Merger/CELL.cs:
--------------------------------------------------------------------------------
1 | namespace TES3Merge.Merger;
2 |
3 | internal static class CELL
4 | {
5 | public static bool Merge(object currentParam, object firstParam, object nextParam)
6 | {
7 | // Get the values as their correct type.
8 | var current = currentParam as TES3Lib.Records.CELL ?? throw new ArgumentException("Current record is of incorrect type.");
9 | var first = firstParam as TES3Lib.Records.CELL ?? throw new ArgumentException("First record is of incorrect type.");
10 | var next = nextParam as TES3Lib.Records.CELL ?? throw new ArgumentException("Next record is of incorrect type.");
11 |
12 | // Store modified state.
13 | var modified = false;
14 |
15 | // Ensure that the record type hasn't changed.
16 | if (!first.Name.Equals(next.Name))
17 | {
18 | throw new Exception("Record types differ!");
19 | }
20 |
21 | // Cover the base record flags.
22 | if (current.Flags.SequenceEqual(first.Flags) && !next.Flags.SequenceEqual(first.Flags))
23 | {
24 | current.Flags = next.Flags;
25 | modified = true;
26 | }
27 |
28 | /*
29 |
30 | Cell Name Patch (--cellnames)
31 |
32 | Creates a patch to ensure renamed cells are not accidentally reverted to
33 | their original name.
34 |
35 | This solves the following plugin conflict that causes bugs:
36 | * Master A names external CELL (1, 1) as: "".
37 | * Plugin B renames CELL (1, 1) to: "My City".
38 | * Plugin C modifies CELL (1, 1), using the original name "", reverting
39 | renaming done by plugin B.
40 | * References in plugin B (such as in scripts) that refer to "My City" break.
41 |
42 | This option works by scanning your currently active plugin load order for
43 | cell name reversions like those in the above example, and ensures whenever
44 | possible that cell renaming is properly maintained.
45 |
46 | */
47 | if (current is TES3Lib.Records.CELL cell)
48 | {
49 | // only check exterior cells for rename reversion problem
50 | if (!cell.DATA.Flags.Contains(TES3Lib.Enums.Flags.CellFlag.IsInteriorCell))
51 | {
52 | var currentValue = current.NAME;
53 | var firstValue = first.NAME;
54 | var nextValue = next.NAME;
55 |
56 | // Handle null cases.
57 | if (firstValue is null && currentValue is null && nextValue is not null)
58 | {
59 | current.NAME = nextValue;
60 | modified = true;
61 | }
62 |
63 | var currentIsUnmodified = currentValue is not null ? currentValue.Equals(firstValue) : firstValue is null;
64 | var nextIsModified = !(nextValue is not null ? nextValue.Equals(firstValue) : firstValue is null);
65 |
66 | if (currentIsUnmodified && nextIsModified)
67 | {
68 | if (!string.IsNullOrEmpty(nextValue?.EditorId.TrimEnd('\0')))
69 | {
70 | current.NAME = nextValue;
71 | modified = true;
72 | }
73 | }
74 | }
75 | }
76 |
77 | return modified;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/TES3Merge/Merger/CLAS.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | namespace TES3Merge.Merger;
4 |
5 | internal static class CLAS
6 | {
7 | static readonly string[] ClassDataBasicProperties = { "IsPlayable", "Specialization" };
8 |
9 | public static bool CLDT(PropertyInfo property, object currentParam, object firstParam, object nextParam)
10 | {
11 | // Get the values as their correct type.
12 | var current = property.GetValue(currentParam) as TES3Lib.Subrecords.CLAS.CLDT ?? throw new ArgumentException("Current record is of incorrect type.");
13 | var first = property.GetValue(firstParam) as TES3Lib.Subrecords.CLAS.CLDT ?? throw new ArgumentException("First record is of incorrect type.");
14 | var next = property.GetValue(nextParam) as TES3Lib.Subrecords.CLAS.CLDT ?? throw new ArgumentException("Next record is of incorrect type.");
15 |
16 | bool modified = false;
17 |
18 | // Perform basic merges.
19 | if (RecordMerger.MergeNamedProperties(ClassDataBasicProperties, current, first, next))
20 | {
21 | modified = true;
22 | }
23 |
24 | return modified;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/TES3Merge/Merger/CREA.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using TES3Lib.Base;
3 | using TES3Lib.Subrecords.Shared;
4 |
5 | namespace TES3Merge.Merger;
6 |
7 | // TODO duplicated code. fix this with some interfaces or something
8 | internal static class CREA
9 | {
10 | ///
11 | /// Merger for the AIPackage list in CREA
12 | ///
13 | ///
14 | ///
15 | ///
16 | ///
17 | ///
18 | ///
19 | ///
20 | internal static bool AIPackage(PropertyInfo property, object currentParam, object firstParam, object nextParam)
21 | {
22 | // Get the values as their correct type.
23 | var current = property.GetValue(currentParam) as List<(IAIPackage AIPackage, TES3Lib.Subrecords.CREA.CNDT CNDT)>
24 | ?? throw new ArgumentException("Current record is of incorrect type.");
25 | var first = property.GetValue(firstParam) as List<(IAIPackage AIPackage, TES3Lib.Subrecords.CREA.CNDT CNDT)>
26 | ?? throw new ArgumentException("First record is of incorrect type.");
27 | var next = property.GetValue(nextParam) as List<(IAIPackage AIPackage, TES3Lib.Subrecords.CREA.CNDT CNDT)>
28 | ?? throw new ArgumentException("Next record is of incorrect type.");
29 |
30 | var modified = false;
31 |
32 | // Ensure that we have a current value.
33 | if (current == null)
34 | {
35 | if (first != null)
36 | {
37 | current = new List<(IAIPackage AIPackage, TES3Lib.Subrecords.CREA.CNDT CNDT)>(first);
38 | property.SetValue(currentParam, current);
39 | }
40 | else if (next != null)
41 | {
42 | current = new List<(IAIPackage AIPackage, TES3Lib.Subrecords.CREA.CNDT CNDT)>(next);
43 | property.SetValue(currentParam, current);
44 | }
45 | else
46 | {
47 | return false;
48 | }
49 | }
50 |
51 | if (first == null)
52 | {
53 | throw new NullReferenceException(nameof(first));
54 | }
55 |
56 | // for now we only merge the wander package
57 | if (current.Count + first.Count + next.Count > 0)
58 | {
59 | modified = MergeWanderPackage(current, first, next);
60 | }
61 |
62 | return modified;
63 |
64 | static bool MergeWanderPackage(
65 | List<(IAIPackage AIPackage, TES3Lib.Subrecords.CREA.CNDT CNDT)> current,
66 | List<(IAIPackage AIPackage, TES3Lib.Subrecords.CREA.CNDT CNDT)>? first,
67 | List<(IAIPackage AIPackage, TES3Lib.Subrecords.CREA.CNDT CNDT)>? next)
68 | {
69 | // only merge one wander package
70 | var currentValue = current.FirstOrDefault(x => x.AIPackage.GetType() == typeof(AI_W)).AIPackage as AI_W;
71 | var firstValue = first?.FirstOrDefault(x => x.AIPackage.GetType() == typeof(AI_W)).AIPackage as AI_W;
72 | var nextValue = next?.FirstOrDefault(x => x.AIPackage.GetType() == typeof(AI_W)).AIPackage as AI_W;
73 |
74 | // TODO remove multiple wander packages?
75 |
76 | // we always have a current value
77 |
78 | // If we have no first value, but do have a next value, this is a new property. Add it.
79 | if (firstValue is null && nextValue is not null && nextValue is not null)
80 | {
81 | currentValue = nextValue;
82 | return true;
83 | }
84 | // If we have values for everything...
85 | if (firstValue is not null && nextValue is not null)
86 | {
87 | var result = RecordMerger.MergeAllProperties(currentValue, firstValue, nextValue);
88 | return result;
89 | }
90 |
91 | return false;
92 | }
93 | }
94 |
95 | // list of summoned creatures for multipatch in lowercase!
96 | public static List SummonedCreatures = new()
97 | {
98 | "centurion_fire_dead",
99 | "wraith_sul_senipul",
100 | "ancestor_ghost_summon",
101 | "atronach_flame_summon",
102 | "atronach_frost_summon",
103 | "atronach_storm_summon",
104 | "bonelord_summon",
105 | "bonewalker_summon",
106 | "bonewalker_greater_summ",
107 | "centurion_sphere_summon",
108 | "clannfear_summon",
109 | "daedroth_summon",
110 | "dremora_summon",
111 | "golden saint_summon",
112 | "hunger_summon",
113 | "scamp_summon",
114 | "skeleton_summon",
115 | "ancestor_ghost_variner",
116 | "fabricant_summon",
117 | "bm_bear_black_summon",
118 | "bm_wolf_grey_summon",
119 | "bm_wolf_bone_summon"
120 | };
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/TES3Merge/Merger/FACT.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | namespace TES3Merge.Merger;
4 |
5 | internal static class FACT
6 | {
7 | static readonly string[] FactionDataBasicProperties = { "IsHiddenFromPlayer" };
8 |
9 | public static bool FADT(PropertyInfo property, object currentParam, object firstParam, object nextParam)
10 | {
11 | // Get the values as their correct type.
12 | var current = property.GetValue(currentParam) as TES3Lib.Subrecords.FACT.FADT ?? throw new ArgumentException("Current record is of incorrect type.");
13 | var first = property.GetValue(firstParam) as TES3Lib.Subrecords.FACT.FADT ?? throw new ArgumentException("First record is of incorrect type.");
14 | var next = property.GetValue(nextParam) as TES3Lib.Subrecords.FACT.FADT ?? throw new ArgumentException("Next record is of incorrect type.");
15 |
16 | bool modified = false;
17 |
18 | // Perform basic merges.
19 | if (RecordMerger.MergeNamedProperties(FactionDataBasicProperties, current, first, next))
20 | {
21 | modified = true;
22 | }
23 |
24 | return modified;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/TES3Merge/Merger/LEVC.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Reflection;
3 | using TES3Lib.Subrecords.LEVC;
4 |
5 | namespace TES3Merge.Merger;
6 |
7 | internal static class LEVC
8 | {
9 | private class CRITComparer : EqualityComparer<(CNAM CNAM, INTV INTV)>
10 | {
11 | public override bool Equals((CNAM CNAM, INTV INTV) x, (CNAM CNAM, INTV INTV) y)
12 | {
13 | return string.Equals(x.CNAM.CreatureEditorId, y.CNAM.CreatureEditorId) && x.INTV.PCLevelOfPrevious == y.INTV.PCLevelOfPrevious;
14 | }
15 |
16 | public override int GetHashCode([DisallowNull] (CNAM CNAM, INTV INTV) obj)
17 | {
18 | return base.GetHashCode();
19 | }
20 | }
21 |
22 | private class KeyValuePairComparer : EqualityComparer>
23 | {
24 | public override bool Equals(KeyValuePair<(CNAM, INTV INTV), int> x, KeyValuePair<(CNAM, INTV INTV), int> y)
25 | {
26 | return CritComparer.Equals(x.Key, y.Key) && x.Value == y.Value;
27 | }
28 |
29 | public override int GetHashCode([DisallowNull] KeyValuePair<(CNAM, INTV INTV), int> obj)
30 | {
31 | return base.GetHashCode();
32 | }
33 | }
34 |
35 | private static readonly CRITComparer CritComparer = new();
36 | private static readonly KeyValuePairComparer kvpComparer = new();
37 |
38 | internal static bool CRIT(PropertyInfo property, object currentParam, object firstParam, object nextParam)
39 | {
40 | // Get the values as their correct type.
41 | var current = property.GetValue(currentParam) as List<(CNAM CNAM, INTV INTV)>
42 | ?? throw new ArgumentException("Current record is of incorrect type.");
43 | var first = property.GetValue(firstParam) as List<(CNAM CNAM, INTV INTV)>
44 | ?? throw new ArgumentException("First record is of incorrect type.");
45 | var next = property.GetValue(nextParam) as List<(CNAM CNAM, INTV INTV)>
46 | ?? throw new ArgumentException("Next record is of incorrect type.");
47 |
48 | var modified = false;
49 |
50 | // Ensure that we have a current value.
51 | if (current == null)
52 | {
53 | if (first != null)
54 | {
55 | current = new List<(CNAM CNAM, INTV INTV)>(first);
56 | property.SetValue(currentParam, current);
57 | }
58 | else if (next != null)
59 | {
60 | current = new List<(CNAM CNAM, INTV INTV)>(next);
61 | property.SetValue(currentParam, current);
62 | }
63 | else
64 | {
65 | return false;
66 | }
67 | }
68 |
69 | if (first == null)
70 | {
71 | throw new ArgumentNullException(nameof(first));
72 | }
73 |
74 | // minimal distinct inclusive list merge
75 | // map occurences of items in each plugin
76 | var fmap = first.ToLookup(x => x, CritComparer).ToDictionary(x => x.Key, y => y.Count());
77 | var cmap = current.ToLookup(x => x, CritComparer).ToDictionary(x => x.Key, y => y.Count());
78 | var nmap = next.ToLookup(x => x, CritComparer).ToDictionary(x => x.Key, y => y.Count());
79 |
80 | // gather all
81 | var map = fmap
82 | .Union(cmap, kvpComparer)
83 | .Union(nmap, kvpComparer)
84 | .Distinct(kvpComparer)
85 | .ToLookup(x => x.Key, CritComparer)
86 | .ToDictionary(x => x.Key, y => y.Select(x => x.Value).Max());
87 |
88 | // add by minimal count
89 | var union = new List<(CNAM CNAM, INTV INTV)>();
90 | foreach (var (item, cnt) in map)
91 | {
92 | for (var i = 0; i < cnt; i++)
93 | {
94 | union.Add(item);
95 | }
96 | }
97 |
98 | // order
99 | union = union
100 | .OrderBy(x => x.INTV.PCLevelOfPrevious)
101 | .ThenBy(x => x.CNAM.CreatureEditorId)
102 | .ToList();
103 |
104 | // compare to vanilla
105 | if (!union.SequenceEqual(first))
106 | {
107 | property.SetValue(currentParam, union);
108 | modified = true;
109 | }
110 |
111 | return modified;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/TES3Merge/Merger/LEVI.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Linq;
3 | using System.Reflection;
4 | using TES3Lib.Base;
5 | using TES3Lib.Subrecords.LEVI;
6 | using TES3Lib.Subrecords.Shared;
7 | using static TES3Merge.RecordMerger;
8 |
9 | namespace TES3Merge.Merger;
10 |
11 | internal static class LEVI
12 | {
13 | private class ITEMComparer : EqualityComparer<(INAM CNAM, INTV INTV)>
14 | {
15 | public override bool Equals((INAM CNAM, INTV INTV) x, (INAM CNAM, INTV INTV) y)
16 | {
17 | return string.Equals(x.CNAM.ItemEditorId, y.CNAM.ItemEditorId) && x.INTV.PCLevelOfPrevious == y.INTV.PCLevelOfPrevious;
18 | }
19 |
20 | public override int GetHashCode([DisallowNull] (INAM CNAM, INTV INTV) obj)
21 | {
22 | return base.GetHashCode();
23 | }
24 | }
25 |
26 | private class KeyValuePairComparer : EqualityComparer>
27 | {
28 | public override bool Equals(KeyValuePair<(INAM, INTV INTV), int> x, KeyValuePair<(INAM, INTV INTV), int> y)
29 | {
30 | return ItemComparer.Equals(x.Key, y.Key) && x.Value == y.Value;
31 | }
32 |
33 | public override int GetHashCode([DisallowNull] KeyValuePair<(INAM, INTV INTV), int> obj)
34 | {
35 | return base.GetHashCode();
36 | }
37 | }
38 |
39 | private static readonly ITEMComparer ItemComparer = new();
40 | private static readonly KeyValuePairComparer kvpComparer = new();
41 |
42 | internal static bool ITEM(PropertyInfo property, object currentParam, object firstParam, object nextParam)
43 | {
44 | // Get the values as their correct type.
45 | var current = property.GetValue(currentParam) as List<(INAM INAM, INTV INTV)>
46 | ?? throw new ArgumentException("Current record is of incorrect type.");
47 | var first = property.GetValue(firstParam) as List<(INAM INAM, INTV INTV)>
48 | ?? throw new ArgumentException("First record is of incorrect type.");
49 | var next = property.GetValue(nextParam) as List<(INAM INAM, INTV INTV)>
50 | ?? throw new ArgumentException("Next record is of incorrect type.");
51 |
52 | var modified = false;
53 |
54 | // Ensure that we have a current value.
55 | if (current == null)
56 | {
57 | if (first != null)
58 | {
59 | current = new List<(INAM INAM, INTV INTV)>(first);
60 | property.SetValue(currentParam, current);
61 | }
62 | else if (next != null)
63 | {
64 | current = new List<(INAM INAM, INTV INTV)>(next);
65 | property.SetValue(currentParam, current);
66 | }
67 | else
68 | {
69 | return false;
70 | }
71 | }
72 |
73 | if (first == null)
74 | {
75 | throw new ArgumentNullException(nameof(first));
76 | }
77 |
78 | /*
79 | * some special cases:
80 | *
81 | * 1) chance
82 | * 2) list flags
83 | * mod A sets chance_none to 75 and changes the list to only one item
84 | * mod B keeps chance_none at 50 but adds more items
85 | *
86 | * naive outcome:
87 | * chance 75 but a lot of items
88 | *
89 | * desired outcome:
90 | * priority -> needs community rules
91 | * -> but retain the naive merge by default
92 | *
93 | * 2) handle duplicate items
94 | * mod A,B and C all have different lists but keep one item from vanilla
95 | * mod D adds one item 10 times
96 | *
97 | * desired outcome: minimal distinct items
98 | *
99 | */
100 |
101 | // minimal distinct inclusive list merge
102 | // map occurences of items in each plugin
103 | var fmap = first.ToLookup(x => x, ItemComparer).ToDictionary(x => x.Key, y => y.Count());
104 | var cmap = current.ToLookup(x => x, ItemComparer).ToDictionary(x => x.Key, y => y.Count());
105 | var nmap = next.ToLookup(x => x, ItemComparer).ToDictionary(x => x.Key, y => y.Count());
106 |
107 | // gather all
108 | var map = fmap
109 | .Union(cmap, kvpComparer)
110 | .Union(nmap, kvpComparer)
111 | .Distinct(kvpComparer)
112 | .ToLookup(x => x.Key, ItemComparer)
113 | .ToDictionary(x => x.Key, y => y.Select(x => x.Value).Max());
114 |
115 | // add by minimal count
116 | var union = new List<(INAM INAM, INTV INTV)>();
117 | foreach (var (item, cnt) in map)
118 | {
119 | for (var i = 0; i < cnt; i++)
120 | {
121 | union.Add(item);
122 | }
123 | }
124 |
125 | // order
126 | union = union
127 | .OrderBy(x => x.INTV.PCLevelOfPrevious)
128 | .ThenBy(x => x.INAM.ItemEditorId)
129 | .ToList();
130 |
131 | // compare to vanilla
132 | if (!union.SequenceEqual(first))
133 | {
134 | property.SetValue(currentParam, union);
135 | modified = true;
136 |
137 | // Update list count.
138 | var levi = currentParam as TES3Lib.Records.LEVI ?? throw new ArgumentException("Object is not of expected type.");
139 | levi.INDX.ItemCount = union.Count;
140 | }
141 |
142 | return modified;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/TES3Merge/Merger/NPC_.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using TES3Lib.Base;
3 | using TES3Lib.Subrecords.NPC_;
4 | using TES3Lib.Subrecords.Shared;
5 |
6 | namespace TES3Merge.Merger;
7 |
8 | internal static class NPC_
9 | {
10 | private static readonly string[] NPCDataBasicProperties = {
11 | "Agility",
12 | "Disposition",
13 | "Endurance",
14 | "Fatigue",
15 | "Gold",
16 | "Health",
17 | "Intelligence",
18 | "Level",
19 | "Luck",
20 | "Personality",
21 | "Rank",
22 | "Reputation",
23 | "Speed",
24 | "SpellPts",
25 | "Strength",
26 | "Unknown1",
27 | "Unknown2",
28 | "Unknown3",
29 | "Willpower"
30 | };
31 |
32 | public static bool NPDT(PropertyInfo property, object currentParam, object firstParam, object nextParam)
33 | {
34 | // Get the values as their correct type.
35 | var current = property.GetValue(currentParam) as NPDT
36 | ?? throw new ArgumentException("Current record is of incorrect type.");
37 | var first = property.GetValue(firstParam) as NPDT
38 | ?? throw new ArgumentException("First record is of incorrect type.");
39 | var next = property.GetValue(nextParam) as NPDT
40 | ?? throw new ArgumentException("Next record is of incorrect type.");
41 |
42 | var modified = false;
43 |
44 | // Perform basic merges.
45 | if (RecordMerger.MergeNamedProperties(NPCDataBasicProperties, current, first, next))
46 | {
47 | modified = true;
48 | }
49 |
50 | // Ensure that we always have skills, in case that we change the autocalc flag.
51 | if (current.Skills is null && next.Skills is not null)
52 | {
53 | current.Skills = next.Skills;
54 | modified = true;
55 | }
56 |
57 | // element-wise merge
58 | if (current.Skills is not null && next.Skills is not null)
59 | {
60 | if (current.Skills.SequenceEqual(next.Skills))
61 | {
62 | return modified;
63 | }
64 | if (first.Skills is null)
65 | {
66 | first.Skills = next.Skills;
67 | modified = true;
68 | }
69 | // TODO length check
70 |
71 | for (var i = 0; i < current.Skills.Length; i++)
72 | {
73 | var skill = current.Skills[i];
74 | var firstSkill = first.Skills[i];
75 | var nextSkill = next.Skills[i];
76 |
77 | var currentIsModified = firstSkill != skill;
78 | var nextIsModified = firstSkill != nextSkill;
79 |
80 | if (!currentIsModified && nextIsModified)
81 | {
82 | current.Skills[i] = nextSkill;
83 | }
84 | }
85 | }
86 | return modified;
87 | }
88 |
89 | internal static bool AIPackage(PropertyInfo property, object currentParam, object firstParam, object nextParam)
90 | {
91 | // Get the values as their correct type.
92 | var current = property.GetValue(currentParam) as List<(IAIPackage AIPackage, CNDT CNDT)>
93 | ?? throw new ArgumentException("Current record is of incorrect type.");
94 | var first = property.GetValue(firstParam) as List<(IAIPackage AIPackage, CNDT CNDT)>
95 | ?? throw new ArgumentException("First record is of incorrect type.");
96 | var next = property.GetValue(nextParam) as List<(IAIPackage AIPackage, CNDT CNDT)>
97 | ?? throw new ArgumentException("Next record is of incorrect type.");
98 |
99 | var modified = false;
100 |
101 | // Ensure that we have a current value.
102 | if (current == null)
103 | {
104 | if (first != null)
105 | {
106 | current = new List<(IAIPackage AIPackage, CNDT CNDT)>(first);
107 | property.SetValue(currentParam, current);
108 | }
109 | else if (next != null)
110 | {
111 | current = new List<(IAIPackage AIPackage, CNDT CNDT)>(next);
112 | property.SetValue(currentParam, current);
113 | }
114 | else
115 | {
116 | return false;
117 | }
118 | }
119 |
120 | if (first == null)
121 | {
122 | throw new NullReferenceException(nameof(first));
123 | }
124 |
125 | // for now we only merge the wander package
126 | if (current.Count + first.Count + next.Count > 0)
127 | {
128 | modified = MergeWanderPackage(current, first, next);
129 | }
130 |
131 | return modified;
132 |
133 | static bool MergeWanderPackage(
134 | List<(IAIPackage AIPackage, CNDT CNDT)> current,
135 | List<(IAIPackage AIPackage, CNDT CNDT)>? first,
136 | List<(IAIPackage AIPackage, CNDT CNDT)>? next)
137 | {
138 | // only merge one wander package
139 | var currentValue = current.FirstOrDefault(x => x.AIPackage.GetType() == typeof(AI_W)).AIPackage as AI_W;
140 | var firstValue = first?.FirstOrDefault(x => x.AIPackage.GetType() == typeof(AI_W)).AIPackage as AI_W;
141 | var nextValue = next?.FirstOrDefault(x => x.AIPackage.GetType() == typeof(AI_W)).AIPackage as AI_W;
142 |
143 | // TODO remove multiple wander packages?
144 |
145 | // we always have a current value
146 |
147 | // If we have no first value, but do have a next value, this is a new property. Add it.
148 | if (firstValue is null && nextValue is not null && nextValue is not null)
149 | {
150 | currentValue = nextValue;
151 | return true;
152 | }
153 | // If we have values for everything...
154 | if (firstValue is not null && nextValue is not null)
155 | {
156 | var result = RecordMerger.MergeAllProperties(currentValue, firstValue, nextValue);
157 | return result;
158 | }
159 |
160 | return false;
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/TES3Merge/Merger/Shared.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using static TES3Merge.RecordMerger;
3 |
4 | namespace TES3Merge.Merger;
5 |
6 | internal static class Shared
7 | {
8 | static readonly PublicPropertyComparer BasicComparer = new();
9 |
10 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "This function signature must match other merge functions.")]
11 | internal static bool NoMerge(PropertyInfo property, object currentParam, object firstParam, object nextParam)
12 | {
13 | return false;
14 | }
15 |
16 | internal static bool MergeEffect(List current, List? first, List? next, int index)
17 | {
18 | TES3Lib.Subrecords.Shared.Castable.ENAM? currentValue = current.ElementAtOrDefault(index);
19 | TES3Lib.Subrecords.Shared.Castable.ENAM? firstValue = first?.ElementAtOrDefault(index);
20 | TES3Lib.Subrecords.Shared.Castable.ENAM? nextValue = next?.ElementAtOrDefault(index);
21 |
22 | // If we have values for everything...
23 | if (currentValue is not null && firstValue is not null && nextValue is not null)
24 | {
25 | // If the effect has changed, override it all.
26 | if (nextValue.MagicEffect != firstValue.MagicEffect)
27 | {
28 | current[index] = nextValue;
29 | return true;
30 | }
31 | // Otherwise merge over individual properties.
32 | else
33 | {
34 | return MergeAllProperties(currentValue, firstValue, nextValue);
35 | }
36 | }
37 |
38 | // If we have no first value, but do have a next value, this is a new property. Add it.
39 | if (firstValue is null && nextValue is not null)
40 | {
41 | current.Add(nextValue);
42 | return true;
43 | }
44 |
45 | return false;
46 | }
47 |
48 | internal static bool EffectList(PropertyInfo property, object currentParam, object firstParam, object nextParam)
49 | {
50 | // Get the values as their correct type.
51 | var current = property.GetValue(currentParam) as List;
52 | var first = property.GetValue(firstParam) as List;
53 | var next = property.GetValue(nextParam) as List;
54 |
55 | var modified = false;
56 |
57 | // remove this for now until refactor
58 | // Handle null cases.
59 | //if (!current.NullableSequenceEqual(next) && next is not null)
60 | //{
61 | // current = new List(next);
62 | // property.SetValue(currentParam, current);
63 | // modified = true;
64 | //}
65 |
66 | // Ensure that we have a current value.
67 | if (current == null)
68 | {
69 | if (first != null)
70 | {
71 | current = new List(first);
72 | property.SetValue(currentParam, current);
73 | }
74 | else if (next != null)
75 | {
76 | current = new List(next);
77 | property.SetValue(currentParam, current);
78 | }
79 | else
80 | {
81 | return false;
82 | }
83 | }
84 |
85 | //
86 | for (var i = 0; i < 8; i++)
87 | {
88 | if (MergeEffect(current, first, next, i))
89 | {
90 | modified = true;
91 | }
92 | }
93 |
94 | return modified;
95 | }
96 |
97 | internal static bool ItemsList(PropertyInfo property, object currentParam, object firstParam, object nextParam)
98 | {
99 | // Get the values as their correct type.
100 | if (property.GetValue(firstParam) is not List firstAsEnumerable)
101 | {
102 | return false;
103 | }
104 | if (property.GetValue(nextParam) is not List nextAsEnumerable)
105 | {
106 | return false;
107 | }
108 | if (property.GetValue(currentParam) is not List currentAsEnumerable)
109 | {
110 | return false;
111 | }
112 | if (firstAsEnumerable == null || nextAsEnumerable == null)
113 | {
114 | return false;
115 | }
116 |
117 | var modified = false;
118 |
119 | // Ensure that we have a current value.
120 | if (currentAsEnumerable == null)
121 | {
122 | if (firstAsEnumerable != null)
123 | {
124 | currentAsEnumerable = new List(firstAsEnumerable);
125 | property.SetValue(currentParam, currentAsEnumerable);
126 | }
127 | else if (nextAsEnumerable != null)
128 | {
129 | currentAsEnumerable = new List(nextAsEnumerable);
130 | property.SetValue(currentParam, currentAsEnumerable);
131 | }
132 | else
133 | {
134 | return false;
135 | }
136 | }
137 |
138 | if (firstAsEnumerable == null)
139 | {
140 | throw new ArgumentNullException(nameof(firstAsEnumerable));
141 | }
142 |
143 | // inclusive list merge
144 | IEnumerable