├── .editorconfig
├── .gitattributes
├── .gitignore
├── ModTek.sln
├── ModTek.sln.DotSettings
├── ModTek
├── AdvancedJSONMerger.cs
├── IManifestEntry.cs
├── IModDef.cs
├── Logger.cs
├── MergeCache.cs
├── ModDef.cs
├── ModTek.cs
├── ModTek.csproj
├── ModTek.csproj.user.example
├── Patches.cs
├── ProgressPanel.cs
├── Properties
│ └── AssemblyInfo.cs
├── modtekassetbundle
└── packages.config
├── ModTekUnitTests
├── AdvancedJSONMergeInstructionTests.cs
├── ModTekUnitTests.csproj
├── Properties
│ └── AssemblyInfo.cs
└── packages.config
├── README.md
└── UNLICENSE
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | end_of_line = lf
7 | indent_style = space
8 | indent_size = 4
9 |
10 | [*.json]
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
17 |
18 | # See https://github.com/dotnet/roslyn/blob/master/src/Workspaces/CSharp/Portable/Formatting/CSharpFormattingOptions.cs
19 | # includes older, alternative and compatibility declarations (to be cleaned up) possibly used by non-roslyn editors
20 | [*.{cs,cshtml}]
21 | charset = utf-8
22 | trim_trailing_whitespace = true
23 | indent_brace_style = Allman
24 | curly_bracket_next_line = true
25 | continuation_indent_size = 4
26 |
27 | csharp_new_lines_for_braces_in_types = true
28 | csharp_new_lines_for_braces_in_methods = true
29 | csharp_new_lines_for_braces_in_properties = true
30 | csharp_new_lines_for_braces_in_accessors = true
31 | csharp_new_lines_for_braces_in_anonymous_methods = true
32 | csharp_new_lines_for_braces_in_control_blocks = true
33 | csharp_new_lines_for_braces_in_object_collection_array_initializers = true
34 | csharp_new_lines_for_braces_in_lambda_expression_body = true
35 |
36 | csharp_new_line_for_else = true
37 | csharp_new_line_before_else = true
38 |
39 | csharp_new_line_for_catch = true
40 | csharp_new_line_before_catch = true
41 |
42 | csharp_new_line_for_finally = true
43 | csharp_new_line_before_finally = true
44 |
45 | csharp_new_line_for_members_in_object_init = true
46 | csharp_new_line_before_members_in_object_init = true
47 | csharp_new_line_before_members_in_object_initializers = true
48 |
49 | csharp_new_lines_for_braces_in_anonymous_types = true
50 | csharp_new_line_for_members_in_anonymous_types = true
51 | csharp_new_line_before_members_in_anonymous_types = true
52 |
53 | csharp_new_line_for_clauses_in_query = true
54 | csharp_new_line_before_clauses_in_query = true
55 |
56 | csharp_new_line_for_open_brace = all
57 | csharp_new_line_before_open_brace = all
58 |
59 | csharp_indent_switch_labels = true
60 | csharp_indent_case_contents = true
61 | csharp_label_positioning = noIndent
62 |
63 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | #common settings that generally should always be used with your language specific settings
2 |
3 | # Auto detect text files and perform LF normalization
4 | # http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/
5 | * text=auto
6 |
7 | #
8 | # The above will handle all files NOT found below
9 | #
10 |
11 | # Documents
12 | *.doc diff=astextplain
13 | *.DOC diff=astextplain
14 | *.docx diff=astextplain
15 | *.DOCX diff=astextplain
16 | *.dot diff=astextplain
17 | *.DOT diff=astextplain
18 | *.pdf diff=astextplain
19 | *.PDF diff=astextplain
20 | *.rtf diff=astextplain
21 | *.RTF diff=astextplain
22 | *.md text
23 | *.adoc text
24 | *.textile text
25 | *.mustache text
26 | *.csv text
27 | *.tab text
28 | *.tsv text
29 | *.sql text
30 |
31 | # Graphics
32 | *.png binary
33 | *.jpg binary
34 | *.jpeg binary
35 | *.gif binary
36 | *.tif binary
37 | *.tiff binary
38 | *.ico binary
39 | *.svg binary
40 | *.eps binary
41 | # Auto detect text files and perform LF normalization
42 | # http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/
43 | * text=auto
44 |
45 | *.cs diff=csharp
46 |
47 | # Auto detect text files and perform LF normalization
48 | # http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/
49 | * text=auto
50 |
51 | # Custom for Visual Studio
52 | *.sln text eol=crlf
53 | *.csproj text eol=crlf
54 | *.vbproj text eol=crlf
55 | *.fsproj text eol=crlf
56 | *.dbproj text eol=crlf
57 |
58 | *.vcxproj text eol=crlf
59 | *.vcxitems text eol=crlf
60 | *.props text eol=crlf
61 | *.filters text eol=crlf
62 | # These settings are for any web project
63 |
64 | # Handle line endings automatically for files detected as text
65 | # and leave all files detected as binary untouched.
66 | * text=auto
67 |
68 | #
69 | # The above will handle all files NOT found below
70 | #
71 |
72 | #
73 | ## These files are text and should be normalized (Convert crlf => lf)
74 | #
75 |
76 | # source code
77 | *.php text
78 | *.css text
79 | *.sass text
80 | *.scss text
81 | *.less text
82 | *.styl text
83 | *.js text
84 | *.ts text
85 | *.coffee text
86 | *.json text
87 | *.htm text
88 | *.html text
89 | *.xml text
90 | *.svg text
91 | *.txt text
92 | *.ini text
93 | *.inc text
94 | *.pl text
95 | *.rb text
96 | *.py text
97 | *.scm text
98 | *.sql text
99 | *.sh text
100 | *.bat text
101 |
102 | # templates
103 | *.ejs text
104 | *.hbt text
105 | *.jade text
106 | *.haml text
107 | *.hbs text
108 | *.dot text
109 | *.tmpl text
110 | *.phtml text
111 | *.latte text
112 |
113 | # server config
114 | .htaccess text
115 |
116 | # git config
117 | .gitattributes text
118 | .gitignore text
119 | .gitconfig text
120 |
121 | # code analysis config
122 | .jshintrc text
123 | .jscsrc text
124 | .jshintignore text
125 | .csslintrc text
126 |
127 | # misc config
128 | *.yaml text
129 | *.yml text
130 | .editorconfig text
131 |
132 | # build config
133 | *.npmignore text
134 | *.bowerrc text
135 |
136 | # Heroku
137 | Procfile text
138 | .slugignore text
139 |
140 | # Documentation
141 | *.md text
142 | LICENSE text
143 | UNLICENSE text
144 | AUTHORS text
145 |
146 |
147 | #
148 | ## These files are binary and should be left untouched
149 | #
150 |
151 | # (binary is a macro for -text -diff)
152 | *.png binary
153 | *.jpg binary
154 | *.jpeg binary
155 | *.gif binary
156 | *.ico binary
157 | *.mov binary
158 | *.mp4 binary
159 | *.mp3 binary
160 | *.flv binary
161 | *.fla binary
162 | *.swf binary
163 | *.gz binary
164 | *.zip binary
165 | *.7z binary
166 | *.ttf binary
167 | *.eot binary
168 | *.woff binary
169 | *.woff2 binary
170 | *.pyc binary
171 | *.pdf binary
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/git,macos,linux,csharp,windows,intellij,jetbrains,aspnetcore,sublimetext,tortoisegit,visualstudio,visualstudiocode
3 |
4 | ### ASPNETCore ###
5 | ## Ignore Visual Studio temporary files, build results, and
6 | ## files generated by popular Visual Studio add-ons.
7 |
8 | # User-specific files
9 | *.suo
10 | *.user
11 | *.userosscache
12 | *.sln.docstates
13 |
14 | # User-specific files (MonoDevelop/Xamarin Studio)
15 | *.userprefs
16 |
17 | # Build results
18 | [Dd]ebug/
19 | [Dd]ebugPublic/
20 | [Rr]elease/
21 | [Rr]eleases/
22 | x64/
23 | x86/
24 | bld/
25 | [Bb]in/
26 | [Oo]bj/
27 | [Ll]og/
28 |
29 | # Visual Studio 2015 cache/options directory
30 | .vs/
31 | # Uncomment if you have tasks that create the project's static files in wwwroot
32 | #wwwroot/
33 |
34 | # MSTest test Results
35 | [Tt]est[Rr]esult*/
36 | [Bb]uild[Ll]og.*
37 |
38 | # NUNIT
39 | *.VisualState.xml
40 | TestResult.xml
41 |
42 | # Build Results of an ATL Project
43 | [Dd]ebugPS/
44 | [Rr]eleasePS/
45 | dlldata.c
46 |
47 | # DNX
48 | project.lock.json
49 | project.fragment.lock.json
50 | artifacts/
51 |
52 | *_i.c
53 | *_p.c
54 | *_i.h
55 | *.ilk
56 | *.meta
57 | *.obj
58 | *.pch
59 | *.pdb
60 | *.pgc
61 | *.pgd
62 | *.rsp
63 | *.sbr
64 | *.tlb
65 | *.tli
66 | *.tlh
67 | *.tmp
68 | *.tmp_proj
69 | *.log
70 | *.vspscc
71 | *.vssscc
72 | .builds
73 | *.pidb
74 | *.svclog
75 | *.scc
76 |
77 | # Chutzpah Test files
78 | _Chutzpah*
79 |
80 | # Visual C++ cache files
81 | ipch/
82 | *.aps
83 | *.ncb
84 | *.opendb
85 | *.opensdf
86 | *.sdf
87 | *.cachefile
88 | *.VC.db
89 | *.VC.VC.opendb
90 |
91 | # Visual Studio profiler
92 | *.psess
93 | *.vsp
94 | *.vspx
95 | *.sap
96 |
97 | # TFS 2012 Local Workspace
98 | $tf/
99 |
100 | # Guidance Automation Toolkit
101 | *.gpState
102 |
103 | # ReSharper is a .NET coding add-in
104 | _ReSharper*/
105 | *.[Rr]e[Ss]harper
106 | *.DotSettings.user
107 |
108 | # JustCode is a .NET coding add-in
109 | .JustCode
110 |
111 | # TeamCity is a build add-in
112 | _TeamCity*
113 |
114 | # DotCover is a Code Coverage Tool
115 | *.dotCover
116 |
117 | # Visual Studio code coverage results
118 | *.coverage
119 | *.coveragexml
120 |
121 | # NCrunch
122 | _NCrunch_*
123 | .*crunch*.local.xml
124 | nCrunchTemp_*
125 |
126 | # MightyMoose
127 | *.mm.*
128 | AutoTest.Net/
129 |
130 | # Web workbench (sass)
131 | .sass-cache/
132 |
133 | # Installshield output folder
134 | [Ee]xpress/
135 |
136 | # DocProject is a documentation generator add-in
137 | DocProject/buildhelp/
138 | DocProject/Help/*.HxT
139 | DocProject/Help/*.HxC
140 | DocProject/Help/*.hhc
141 | DocProject/Help/*.hhk
142 | DocProject/Help/*.hhp
143 | DocProject/Help/Html2
144 | DocProject/Help/html
145 |
146 | # Click-Once directory
147 | publish/
148 |
149 | # Publish Web Output
150 | *.[Pp]ublish.xml
151 | *.azurePubxml
152 | # TODO: Comment the next line if you want to checkin your web deploy settings
153 | # but database connection strings (with potential passwords) will be unencrypted
154 | *.pubxml
155 | *.publishproj
156 |
157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
158 | # checkin your Azure Web App publish settings, but sensitive information contained
159 | # in these scripts will be unencrypted
160 | PublishScripts/
161 |
162 | # NuGet Packages
163 | *.nupkg
164 | # The packages folder can be ignored because of Package Restore
165 | **/packages/*
166 | # except build/, which is used as an MSBuild target.
167 | !**/packages/build/
168 | # Uncomment if necessary however generally it will be regenerated when needed
169 | #!**/packages/repositories.config
170 | # NuGet v3's project.json files produces more ignoreable files
171 | *.nuget.props
172 | *.nuget.targets
173 |
174 | # Microsoft Azure Build Output
175 | csx/
176 | *.build.csdef
177 |
178 | # Microsoft Azure Emulator
179 | ecf/
180 | rcf/
181 |
182 | # Windows Store app package directories and files
183 | AppPackages/
184 | BundleArtifacts/
185 | Package.StoreAssociation.xml
186 | _pkginfo.txt
187 |
188 | # Visual Studio cache files
189 | # files ending in .cache can be ignored
190 | *.[Cc]ache
191 | # but keep track of directories ending in .cache
192 | !*.[Cc]ache/
193 |
194 | # Others
195 | ClientBin/
196 | ~$*
197 | *~
198 | *.dbmdl
199 | *.dbproj.schemaview
200 | *.jfm
201 | *.pfx
202 | *.publishsettings
203 | node_modules/
204 | orleans.codegen.cs
205 |
206 | # Since there are multiple workflows, uncomment next line to ignore bower_components
207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
208 | #bower_components/
209 |
210 | # RIA/Silverlight projects
211 | Generated_Code/
212 |
213 | # Backup & report files from converting an old project file
214 | # to a newer Visual Studio version. Backup files are not needed,
215 | # because we have git ;-)
216 | _UpgradeReport_Files/
217 | Backup*/
218 | UpgradeLog*.XML
219 | UpgradeLog*.htm
220 |
221 | # SQL Server files
222 | *.mdf
223 | *.ldf
224 |
225 | # Business Intelligence projects
226 | *.rdl.data
227 | *.bim.layout
228 | *.bim_*.settings
229 |
230 | # Microsoft Fakes
231 | FakesAssemblies/
232 |
233 | # GhostDoc plugin setting file
234 | *.GhostDoc.xml
235 |
236 | # Node.js Tools for Visual Studio
237 | .ntvs_analysis.dat
238 |
239 | # Visual Studio 6 build log
240 | *.plg
241 |
242 | # Visual Studio 6 workspace options file
243 | *.opt
244 |
245 | # Visual Studio LightSwitch build output
246 | **/*.HTMLClient/GeneratedArtifacts
247 | **/*.DesktopClient/GeneratedArtifacts
248 | **/*.DesktopClient/ModelManifest.xml
249 | **/*.Server/GeneratedArtifacts
250 | **/*.Server/ModelManifest.xml
251 | _Pvt_Extensions
252 |
253 | # Paket dependency manager
254 | .paket/paket.exe
255 | paket-files/
256 |
257 | # FAKE - F# Make
258 | .fake/
259 |
260 | # JetBrains Rider
261 | .idea/
262 | *.sln.iml
263 |
264 | # CodeRush
265 | .cr/
266 |
267 | # Python Tools for Visual Studio (PTVS)
268 | __pycache__/
269 | *.pyc
270 |
271 | # Cake - Uncomment if you are using it
272 | # tools/
273 |
274 | ### Csharp ###
275 | ## Ignore Visual Studio temporary files, build results, and
276 | ## files generated by popular Visual Studio add-ons.
277 | ##
278 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
279 |
280 | # User-specific files
281 |
282 | # User-specific files (MonoDevelop/Xamarin Studio)
283 |
284 | # Build results
285 |
286 | # Visual Studio 2015 cache/options directory
287 | # Uncomment if you have tasks that create the project's static files in wwwroot
288 | #wwwroot/
289 |
290 | # MSTest test Results
291 |
292 | # NUNIT
293 |
294 | # Build Results of an ATL Project
295 |
296 | # .NET Core
297 | **/Properties/launchSettings.json
298 |
299 |
300 | # Chutzpah Test files
301 |
302 | # Visual C++ cache files
303 |
304 | # Visual Studio profiler
305 |
306 | # TFS 2012 Local Workspace
307 |
308 | # Guidance Automation Toolkit
309 |
310 | # ReSharper is a .NET coding add-in
311 |
312 | # JustCode is a .NET coding add-in
313 |
314 | # TeamCity is a build add-in
315 |
316 | # DotCover is a Code Coverage Tool
317 |
318 | # Visual Studio code coverage results
319 |
320 | # NCrunch
321 |
322 | # MightyMoose
323 |
324 | # Web workbench (sass)
325 |
326 | # Installshield output folder
327 |
328 | # DocProject is a documentation generator add-in
329 |
330 | # Click-Once directory
331 |
332 | # Publish Web Output
333 | # TODO: Uncomment the next line to ignore your web deploy settings.
334 | # By default, sensitive information, such as encrypted password
335 | # should be stored in the .pubxml.user file.
336 | #*.pubxml
337 | *.pubxml.user
338 |
339 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
340 | # checkin your Azure Web App publish settings, but sensitive information contained
341 | # in these scripts will be unencrypted
342 |
343 | # NuGet Packages
344 | # The packages folder can be ignored because of Package Restore
345 | # except build/, which is used as an MSBuild target.
346 | # Uncomment if necessary however generally it will be regenerated when needed
347 | #!**/packages/repositories.config
348 | # NuGet v3's project.json files produces more ignorable files
349 |
350 | # Microsoft Azure Build Output
351 |
352 | # Microsoft Azure Emulator
353 |
354 | # Windows Store app package directories and files
355 |
356 | # Visual Studio cache files
357 | # files ending in .cache can be ignored
358 | # but keep track of directories ending in .cache
359 |
360 | # Others
361 |
362 | # Since there are multiple workflows, uncomment next line to ignore bower_components
363 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
364 | #bower_components/
365 |
366 | # RIA/Silverlight projects
367 |
368 | # Backup & report files from converting an old project file
369 | # to a newer Visual Studio version. Backup files are not needed,
370 | # because we have git ;-)
371 |
372 | # SQL Server files
373 | *.ndf
374 |
375 | # Business Intelligence projects
376 |
377 | # Microsoft Fakes
378 |
379 | # GhostDoc plugin setting file
380 |
381 | # Node.js Tools for Visual Studio
382 |
383 | # Typescript v1 declaration files
384 | typings/
385 |
386 | # Visual Studio 6 build log
387 |
388 | # Visual Studio 6 workspace options file
389 |
390 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
391 | *.vbw
392 |
393 | # Visual Studio LightSwitch build output
394 |
395 | # Paket dependency manager
396 |
397 | # FAKE - F# Make
398 |
399 | # JetBrains Rider
400 |
401 | # CodeRush
402 |
403 | # Python Tools for Visual Studio (PTVS)
404 |
405 | # Cake - Uncomment if you are using it
406 | # tools/**
407 | # !tools/packages.config
408 |
409 | # Telerik's JustMock configuration file
410 | *.jmconfig
411 |
412 | # BizTalk build output
413 | *.btp.cs
414 | *.btm.cs
415 | *.odx.cs
416 | *.xsd.cs
417 |
418 | ### Git ###
419 | *.orig
420 |
421 | ### Intellij ###
422 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
423 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
424 |
425 | # User-specific stuff:
426 | .idea/**/workspace.xml
427 | .idea/**/tasks.xml
428 | .idea/dictionaries
429 |
430 | # Sensitive or high-churn files:
431 | .idea/**/dataSources/
432 | .idea/**/dataSources.ids
433 | .idea/**/dataSources.xml
434 | .idea/**/dataSources.local.xml
435 | .idea/**/sqlDataSources.xml
436 | .idea/**/dynamic.xml
437 | .idea/**/uiDesigner.xml
438 |
439 | # Gradle:
440 | .idea/**/gradle.xml
441 | .idea/**/libraries
442 |
443 | # CMake
444 | cmake-build-debug/
445 |
446 | # Mongo Explorer plugin:
447 | .idea/**/mongoSettings.xml
448 |
449 | ## File-based project format:
450 | *.iws
451 |
452 | ## Plugin-specific files:
453 |
454 | # IntelliJ
455 | /out/
456 |
457 | # mpeltonen/sbt-idea plugin
458 | .idea_modules/
459 |
460 | # JIRA plugin
461 | atlassian-ide-plugin.xml
462 |
463 | # Cursive Clojure plugin
464 | .idea/replstate.xml
465 |
466 | # Ruby plugin and RubyMine
467 | /.rakeTasks
468 |
469 | # Crashlytics plugin (for Android Studio and IntelliJ)
470 | com_crashlytics_export_strings.xml
471 | crashlytics.properties
472 | crashlytics-build.properties
473 | fabric.properties
474 |
475 | ### Intellij Patch ###
476 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
477 |
478 | # *.iml
479 | # modules.xml
480 | # .idea/misc.xml
481 | # *.ipr
482 |
483 | # Sonarlint plugin
484 | .idea/sonarlint
485 |
486 | ### JetBrains ###
487 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
488 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
489 |
490 | # User-specific stuff:
491 |
492 | # Sensitive or high-churn files:
493 |
494 | # Gradle:
495 |
496 | # CMake
497 |
498 | # Mongo Explorer plugin:
499 |
500 | ## File-based project format:
501 |
502 | ## Plugin-specific files:
503 |
504 | # IntelliJ
505 |
506 | # mpeltonen/sbt-idea plugin
507 |
508 | # JIRA plugin
509 |
510 | # Cursive Clojure plugin
511 |
512 | # Ruby plugin and RubyMine
513 |
514 | # Crashlytics plugin (for Android Studio and IntelliJ)
515 |
516 | ### JetBrains Patch ###
517 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
518 |
519 | # *.iml
520 | # modules.xml
521 | # .idea/misc.xml
522 | # *.ipr
523 |
524 | # Sonarlint plugin
525 |
526 | ### Linux ###
527 |
528 | # temporary files which can be created if a process still has a handle open of a deleted file
529 | .fuse_hidden*
530 |
531 | # KDE directory preferences
532 | .directory
533 |
534 | # Linux trash folder which might appear on any partition or disk
535 | .Trash-*
536 |
537 | # .nfs files are created when an open file is removed but is still being accessed
538 | .nfs*
539 |
540 | ### macOS ###
541 | *.DS_Store
542 | .AppleDouble
543 | .LSOverride
544 |
545 | # Icon must end with two \r
546 | Icon
547 |
548 | # Thumbnails
549 | ._*
550 |
551 | # Files that might appear in the root of a volume
552 | .DocumentRevisions-V100
553 | .fseventsd
554 | .Spotlight-V100
555 | .TemporaryItems
556 | .Trashes
557 | .VolumeIcon.icns
558 | .com.apple.timemachine.donotpresent
559 |
560 | # Directories potentially created on remote AFP share
561 | .AppleDB
562 | .AppleDesktop
563 | Network Trash Folder
564 | Temporary Items
565 | .apdisk
566 |
567 | ### SublimeText ###
568 | # cache files for sublime text
569 | *.tmlanguage.cache
570 | *.tmPreferences.cache
571 | *.stTheme.cache
572 |
573 | # workspace files are user-specific
574 | *.sublime-workspace
575 |
576 | # project files should be checked into the repository, unless a significant
577 | # proportion of contributors will probably not be using SublimeText
578 | # *.sublime-project
579 |
580 | # sftp configuration file
581 | sftp-config.json
582 |
583 | # Package control specific files
584 | Package Control.last-run
585 | Package Control.ca-list
586 | Package Control.ca-bundle
587 | Package Control.system-ca-bundle
588 | Package Control.cache/
589 | Package Control.ca-certs/
590 | Package Control.merged-ca-bundle
591 | Package Control.user-ca-bundle
592 | oscrypto-ca-bundle.crt
593 | bh_unicode_properties.cache
594 |
595 | # Sublime-github package stores a github token in this file
596 | # https://packagecontrol.io/packages/sublime-github
597 | GitHub.sublime-settings
598 |
599 | ### TortoiseGit ###
600 | # Project-level settings
601 | /.tgitconfig
602 |
603 | ### VisualStudioCode ###
604 | .vscode/*
605 | !.vscode/settings.json
606 | !.vscode/tasks.json
607 | !.vscode/launch.json
608 | !.vscode/extensions.json
609 | .history
610 |
611 | ### Windows ###
612 | # Windows thumbnail cache files
613 | Thumbs.db
614 | ehthumbs.db
615 | ehthumbs_vista.db
616 |
617 | # Folder config file
618 | Desktop.ini
619 |
620 | # Recycle Bin used on file shares
621 | $RECYCLE.BIN/
622 |
623 | # Windows Installer files
624 | *.cab
625 | *.msi
626 | *.msm
627 | *.msp
628 |
629 | # Windows shortcuts
630 | *.lnk
631 |
632 | ### VisualStudio ###
633 | ## Ignore Visual Studio temporary files, build results, and
634 | ## files generated by popular Visual Studio add-ons.
635 | ##
636 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
637 |
638 | # User-specific files
639 |
640 | # User-specific files (MonoDevelop/Xamarin Studio)
641 |
642 | # Build results
643 |
644 | # Visual Studio 2015 cache/options directory
645 | # Uncomment if you have tasks that create the project's static files in wwwroot
646 | #wwwroot/
647 |
648 | # MSTest test Results
649 |
650 | # NUNIT
651 |
652 | # Build Results of an ATL Project
653 |
654 | # .NET Core
655 |
656 |
657 | # Chutzpah Test files
658 |
659 | # Visual C++ cache files
660 |
661 | # Visual Studio profiler
662 |
663 | # TFS 2012 Local Workspace
664 |
665 | # Guidance Automation Toolkit
666 |
667 | # ReSharper is a .NET coding add-in
668 |
669 | # JustCode is a .NET coding add-in
670 |
671 | # TeamCity is a build add-in
672 |
673 | # DotCover is a Code Coverage Tool
674 |
675 | # Visual Studio code coverage results
676 |
677 | # NCrunch
678 |
679 | # MightyMoose
680 |
681 | # Web workbench (sass)
682 |
683 | # Installshield output folder
684 |
685 | # DocProject is a documentation generator add-in
686 |
687 | # Click-Once directory
688 |
689 | # Publish Web Output
690 | # TODO: Uncomment the next line to ignore your web deploy settings.
691 | # By default, sensitive information, such as encrypted password
692 | # should be stored in the .pubxml.user file.
693 | #*.pubxml
694 |
695 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
696 | # checkin your Azure Web App publish settings, but sensitive information contained
697 | # in these scripts will be unencrypted
698 |
699 | # NuGet Packages
700 | # The packages folder can be ignored because of Package Restore
701 | # except build/, which is used as an MSBuild target.
702 | # Uncomment if necessary however generally it will be regenerated when needed
703 | #!**/packages/repositories.config
704 | # NuGet v3's project.json files produces more ignorable files
705 |
706 | # Microsoft Azure Build Output
707 |
708 | # Microsoft Azure Emulator
709 |
710 | # Windows Store app package directories and files
711 |
712 | # Visual Studio cache files
713 | # files ending in .cache can be ignored
714 | # but keep track of directories ending in .cache
715 |
716 | # Others
717 |
718 | # Since there are multiple workflows, uncomment next line to ignore bower_components
719 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
720 | #bower_components/
721 |
722 | # RIA/Silverlight projects
723 |
724 | # Backup & report files from converting an old project file
725 | # to a newer Visual Studio version. Backup files are not needed,
726 | # because we have git ;-)
727 |
728 | # SQL Server files
729 |
730 | # Business Intelligence projects
731 |
732 | # Microsoft Fakes
733 |
734 | # GhostDoc plugin setting file
735 |
736 | # Node.js Tools for Visual Studio
737 |
738 | # Typescript v1 declaration files
739 |
740 | # Visual Studio 6 build log
741 |
742 | # Visual Studio 6 workspace options file
743 |
744 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
745 |
746 | # Visual Studio LightSwitch build output
747 |
748 | # Paket dependency manager
749 |
750 | # FAKE - F# Make
751 |
752 | # JetBrains Rider
753 |
754 | # CodeRush
755 |
756 | # Python Tools for Visual Studio (PTVS)
757 |
758 | # Cake - Uncomment if you are using it
759 | # tools/**
760 | # !tools/packages.config
761 |
762 | # Telerik's JustMock configuration file
763 |
764 | # BizTalk build output
765 |
766 | ### VisualStudio Patch ###
767 | # By default, sensitive information, such as encrypted password
768 | # should be stored in the .pubxml.user file.
769 |
770 |
771 | # End of https://www.gitignore.io/api/git,macos,linux,csharp,windows,intellij,jetbrains,aspnetcore,sublimetext,tortoisegit,visualstudio,visualstudiocode
--------------------------------------------------------------------------------
/ModTek.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27428.2043
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModTek", "ModTek\ModTek.csproj", "{8D955C2C-D75B-453C-99D1-B337BBF82CCA}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documents", "Documents", "{07E5D5B5-E51C-4CD8-8FA9-8241FAF6440F}"
9 | ProjectSection(SolutionItems) = preProject
10 | README.md = README.md
11 | UNLICENSE = UNLICENSE
12 | EndProjectSection
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModTekUnitTests", "ModTekUnitTests\ModTekUnitTests.csproj", "{0924FD5F-434E-4278-A326-0F43697F4355}"
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Release|Any CPU = Release|Any CPU
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {8D955C2C-D75B-453C-99D1-B337BBF82CCA}.Release|Any CPU.ActiveCfg = Release|Any CPU
22 | {8D955C2C-D75B-453C-99D1-B337BBF82CCA}.Release|Any CPU.Build.0 = Release|Any CPU
23 | {0924FD5F-434E-4278-A326-0F43697F4355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {0924FD5F-434E-4278-A326-0F43697F4355}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {0924FD5F-434E-4278-A326-0F43697F4355}.Release|Any CPU.ActiveCfg = Release|Any CPU
26 | {0924FD5F-434E-4278-A326-0F43697F4355}.Release|Any CPU.Build.0 = Release|Any CPU
27 | EndGlobalSection
28 | GlobalSection(SolutionProperties) = preSolution
29 | HideSolutionNode = FALSE
30 | EndGlobalSection
31 | GlobalSection(ExtensibilityGlobals) = postSolution
32 | SolutionGuid = {D6AB82EB-4807-4845-87E8-3BD432E38E2E}
33 | EndGlobalSection
34 | GlobalSection(MonoDevelopProperties) = preSolution
35 | version = 0.4
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/ModTek.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | NEVER
3 | BT
4 | BTML
5 | DLL
6 | ID
7 | JSON
8 | True
9 | // Use the following placeholders:
10 | // $EXPR$ -- source expression
11 | // $NAME$ -- source name (string literal or 'nameof' expression)
12 | // $MESSAGE$ -- string literal in the form of "$NAME$ != null"
13 | UnityEngine.Assertions.Assert.IsNotNull($EXPR$, $MESSAGE$);
14 | 199
15 | 5000
16 | 99
17 | 100
18 | 200
19 | 1000
20 | 500
21 | 3000
22 | 50
23 | False
24 | True
25 | True
26 | 240
27 | BT
28 | BTML
29 | DLL
30 | ID
31 | JSON
32 | <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
33 | <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
34 | <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
35 | <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
36 | <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
37 | <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" />
38 | True
39 | True
40 | True
41 | True
42 | True
43 | True
44 | True
45 | True
--------------------------------------------------------------------------------
/ModTek/AdvancedJSONMerger.cs:
--------------------------------------------------------------------------------
1 |
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using Newtonsoft.Json;
6 | using Newtonsoft.Json.Converters;
7 | using Newtonsoft.Json.Linq;
8 |
9 | namespace ModTek
10 | {
11 | public static class AdvancedJSONMerger
12 | {
13 | public static string GetTargetID(string modEntryPath)
14 | {
15 | var merge = ModTek.ParseGameJSONFile(modEntryPath);
16 | return merge[nameof(MergeFile.TargetID)].ToString();
17 | }
18 |
19 | public static bool IsAdvancedJSONMerge(JObject merge)
20 | {
21 | return merge[nameof(MergeFile.TargetID)] != null;
22 | }
23 |
24 | public static void ProcessInstructionsJObject(JObject target, JObject merge)
25 | {
26 | // TODO add nice error handling (on malformed JSONPath)
27 | var instructions = merge["Instructions"].ToObject>();
28 | foreach (var instruction in instructions)
29 | {
30 | // TODO add nice error handling (which JSONPath failed)
31 | instruction.Process(target);
32 | }
33 | }
34 |
35 | // unused, this level is parsed manually
36 | private class MergeFile
37 | {
38 | [JsonProperty(Required = Required.Always)]
39 | public string TargetID;
40 |
41 | [JsonProperty(Required = Required.Always)]
42 | public List Instructions;
43 | }
44 |
45 | public enum Action
46 | {
47 | ArrayAdd, // adds a given value to the end of the target array
48 | ArrayAddAfter, // adds a given value after the target element in the array
49 | ArrayAddBefore, // adds a given value before the target element in the array
50 | ArrayConcat, // adds a given array to the end of the target array
51 | ObjectMerge, // merges a given object with the target objects
52 | Remove, // removes the target element(s)
53 | Replace, // replaces the target with a given value
54 | }
55 |
56 | public class Instruction
57 | {
58 | [JsonProperty(Required = Required.Always)]
59 | public string JSONPath;
60 |
61 | [JsonProperty(Required = Required.Always)]
62 | [JsonConverter(typeof(StringEnumConverter))]
63 | public Action Action;
64 |
65 | // TODO add external JSON support
66 | public JToken Value;
67 |
68 | public void Process(JObject root)
69 | {
70 | var tokens = root.SelectTokens(JSONPath).ToList();
71 | if (!tokens.Any())
72 | {
73 | throw new Exception("JSONPath does not point to anything");
74 | }
75 |
76 | if (ProcessTokens(tokens))
77 | {
78 | return;
79 | }
80 |
81 | if (tokens.Count > 1)
82 | {
83 | throw new Exception("JSONPath can't point to more than one token outside of the Remove action");
84 | }
85 |
86 | if (ProcessToken(tokens[0]))
87 | {
88 | return;
89 | }
90 |
91 | throw new Exception("Action is unknown");
92 | }
93 |
94 | private bool ProcessTokens(List tokens)
95 | {
96 | if (Action == Action.Remove)
97 | {
98 | foreach (var token in tokens)
99 | {
100 | if (token.Parent is JProperty)
101 | {
102 | token.Parent.Remove();
103 | }
104 | else
105 | {
106 | token.Remove();
107 | }
108 | }
109 |
110 | return true;
111 | }
112 |
113 | return false;
114 | }
115 |
116 | private bool ProcessToken(JToken token)
117 | {
118 | if (Action == Action.Replace)
119 | {
120 | token.Replace(Value);
121 | return true;
122 | }
123 |
124 | if (Action == Action.ArrayAdd)
125 | {
126 | if (!(token is JArray a))
127 | {
128 | throw new Exception("JSONPath needs to point an array");
129 | }
130 |
131 | a.Add(Value);
132 | return true;
133 | }
134 |
135 | if (Action == Action.ArrayAddAfter)
136 | {
137 | token.AddAfterSelf(Value);
138 | return true;
139 | }
140 |
141 | if (Action == Action.ArrayAddBefore)
142 | {
143 | token.AddBeforeSelf(Value);
144 | return true;
145 | }
146 |
147 | if (Action == Action.ObjectMerge)
148 | {
149 | if (!(token is JObject o1) || !(Value is JObject o2))
150 | {
151 | throw new Exception("JSONPath has to point to an object and Value has to be an object");
152 | }
153 |
154 | // same behavior as partial json merging
155 | o1.Merge(o2, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace });
156 | return true;
157 | }
158 |
159 | if (Action == Action.ArrayConcat)
160 | {
161 | if (!(token is JArray a1) || !(Value is JArray a2))
162 | {
163 | throw new Exception("JSONPath has to point to an array and Value has to be an array");
164 | }
165 |
166 | a1.Merge(a2, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat });
167 | return true;
168 | }
169 |
170 | return false;
171 | }
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/ModTek/IManifestEntry.cs:
--------------------------------------------------------------------------------
1 | using BattleTech;
2 |
3 | namespace ModTek
4 | {
5 | public interface IManifestEntry
6 | {
7 | string Type { get; set; }
8 | string Path { get; set; }
9 | string Id { get; set; }
10 | string AssetBundleName { get; set; }
11 | bool? AssetBundlePersistent { get; set; }
12 |
13 | bool AddToDB { get; set; }
14 | bool ShouldMergeJSON { get; set; }
15 | string AddToAddendum { get; set; }
16 |
17 | VersionManifestEntry GetVersionManifestEntry();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/ModTek/IModDef.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Newtonsoft.Json.Linq;
4 |
5 | namespace ModTek
6 | {
7 | public interface IModDef
8 | {
9 | string Name { get; set; }
10 | string Description { get; set; }
11 | string Author { get; set; }
12 | string Website { get; set; }
13 | string Contact { get; set; }
14 |
15 | bool Enabled { get; set; }
16 |
17 | string Version { get; set; }
18 | DateTime? PackagedOn { get; set; }
19 |
20 | HashSet DependsOn { get; set; }
21 | HashSet ConflictsWith { get; set; }
22 | HashSet OptionallyDependsOn { get; set; }
23 |
24 | string DLL { get; set; }
25 | string DLLEntryPoint { get; set; }
26 |
27 | bool LoadImplicitManifest { get; set; }
28 | List Manifest { get; set; }
29 |
30 | JObject Settings { get; set; }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ModTek/Logger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using JetBrains.Annotations;
4 |
5 | namespace ModTek
6 | {
7 | internal static class Logger
8 | {
9 | internal static string LogPath { get; set; }
10 | private static StreamWriter LogStream;
11 |
12 | private static StreamWriter GetOrCreateStream()
13 | {
14 | if (LogStream == null && !string.IsNullOrEmpty(LogPath))
15 | LogStream = File.AppendText(LogPath);
16 |
17 | return LogStream;
18 | }
19 |
20 | internal static void CloseLogStream()
21 | {
22 | if (LogStream == null)
23 | return;
24 |
25 | LogStream.Dispose();
26 | LogStream = null;
27 | }
28 |
29 | [StringFormatMethod("message")]
30 | internal static void Log(string message, params object[] formatObjects)
31 | {
32 | var stream = GetOrCreateStream();
33 | if (stream == null)
34 | return;
35 |
36 | stream.WriteLine(message, formatObjects);
37 | }
38 |
39 | [StringFormatMethod("message")]
40 | internal static void LogWithDate(string message, params object[] formatObjects)
41 | {
42 | var stream = GetOrCreateStream();
43 | if (stream == null)
44 | return;
45 |
46 | stream.WriteLine(DateTime.Now.ToLongTimeString() + " - " + message, formatObjects);
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/ModTek/MergeCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using Newtonsoft.Json;
5 | using Newtonsoft.Json.Linq;
6 |
7 | namespace ModTek
8 | {
9 | using static Logger;
10 |
11 | internal class MergeCache
12 | {
13 | public Dictionary CachedEntries { get; set; } = new Dictionary();
14 |
15 | ///
16 | /// Gets (from the cache) or creates (and adds to cache) a JSON merge
17 | ///
18 | /// The path to the original JSON file
19 | /// A list of the paths to merged in JSON
20 | /// A path to the cached JSON that contains the original JSON with the mod merges applied
21 | public string GetOrCreateCachedEntry(string absolutePath, List mergePaths)
22 | {
23 | absolutePath = Path.GetFullPath(absolutePath);
24 | var relativePath = ModTek.GetRelativePath(absolutePath, ModTek.GameDirectory);
25 |
26 | Log("");
27 |
28 | if (!CachedEntries.ContainsKey(relativePath) || !CachedEntries[relativePath].MatchesPaths(absolutePath, mergePaths))
29 | {
30 | var cachedAbsolutePath = Path.GetFullPath(Path.Combine(ModTek.CacheDirectory, relativePath));
31 | var cachedEntry = new CacheEntry(cachedAbsolutePath, absolutePath, mergePaths);
32 |
33 | if (cachedEntry.HasErrors)
34 | return null;
35 |
36 | CachedEntries[relativePath] = cachedEntry;
37 |
38 | Log($"Merge performed: {Path.GetFileName(absolutePath)}");
39 | }
40 | else
41 | {
42 | Log($"Cached merge: {Path.GetFileName(absolutePath)} ({File.GetLastWriteTime(CachedEntries[relativePath].CacheAbsolutePath).ToString("G")})");
43 | }
44 |
45 | Log($"\t{relativePath}");
46 |
47 | foreach (var contributingPath in mergePaths)
48 | Log($"\t{ModTek.GetRelativePath(contributingPath, ModTek.ModsDirectory)}");
49 |
50 | Log("");
51 |
52 | CachedEntries[relativePath].CacheHit = true;
53 | return CachedEntries[relativePath].CacheAbsolutePath;
54 | }
55 |
56 | public bool HasCachedEntry(string originalPath, List mergePaths)
57 | {
58 | var relativePath = ModTek.GetRelativePath(originalPath, ModTek.GameDirectory);
59 | return CachedEntries.ContainsKey(relativePath) && CachedEntries[relativePath].MatchesPaths(originalPath, mergePaths);
60 | }
61 |
62 | ///
63 | /// Writes the cache to disk to the path, after cleaning up old entries
64 | ///
65 | /// Where the cache should be written to
66 | public void WriteCacheToDisk(string path)
67 | {
68 | // remove all of the cache that we didn't use
69 | var unusedMergePaths = new List();
70 | foreach (var cachedEntryKVP in CachedEntries)
71 | if (!cachedEntryKVP.Value.CacheHit)
72 | unusedMergePaths.Add(cachedEntryKVP.Key);
73 |
74 | if (unusedMergePaths.Count > 0)
75 | Log($"");
76 |
77 | foreach (var unusedMergePath in unusedMergePaths)
78 | {
79 | var cacheAbsolutePath = CachedEntries[unusedMergePath].CacheAbsolutePath;
80 | CachedEntries.Remove(unusedMergePath);
81 |
82 | if (File.Exists(cacheAbsolutePath))
83 | File.Delete(cacheAbsolutePath);
84 |
85 | Log($"Old Merge Deleted: {cacheAbsolutePath}");
86 |
87 | var directory = Path.GetDirectoryName(cacheAbsolutePath);
88 | while (Directory.Exists(directory) && Directory.GetDirectories(directory).Length == 0 && Directory.GetFiles(directory).Length == 0 && Path.GetFullPath(directory) != ModTek.CacheDirectory)
89 | {
90 | Directory.Delete(directory);
91 | Log($"Old Merge folder deleted: {directory}");
92 | directory = Path.GetFullPath(Path.Combine(directory, ".."));
93 | }
94 | }
95 |
96 | File.WriteAllText(path, JsonConvert.SerializeObject(this, Formatting.Indented));
97 | }
98 |
99 | ///
100 | /// Updates all absolute path'd cache entries to use a relative path instead
101 | ///
102 | public void UpdateToRelativePaths()
103 | {
104 | var toRemove = new List();
105 | var toAdd = new Dictionary();
106 |
107 | foreach (var path in CachedEntries.Keys)
108 | {
109 | if (Path.IsPathRooted(path))
110 | {
111 | var relativePath = ModTek.GetRelativePath(path, ModTek.GameDirectory);
112 |
113 | toAdd[relativePath] = CachedEntries[path];
114 | toRemove.Add(path);
115 |
116 | toAdd[relativePath].CachePath = ModTek.GetRelativePath(toAdd[relativePath].CachePath, ModTek.GameDirectory);
117 | foreach (var merge in toAdd[relativePath].Merges)
118 | merge.Path = ModTek.GetRelativePath(merge.Path, ModTek.GameDirectory);
119 | }
120 | }
121 |
122 | foreach (var addKVP in toAdd)
123 | CachedEntries.Add(addKVP.Key, addKVP.Value);
124 |
125 | foreach (var path in toRemove)
126 | CachedEntries.Remove(path);
127 | }
128 |
129 | internal class CacheEntry
130 | {
131 | public string CachePath { get; set; }
132 | public DateTime OriginalTime { get; set; }
133 | public List Merges { get; set; } = new List();
134 |
135 | [JsonIgnore] internal string CacheAbsolutePath
136 | {
137 | get
138 | {
139 | if (string.IsNullOrEmpty(_cacheAbsolutePath))
140 | _cacheAbsolutePath = ModTek.ResolvePath(CachePath, ModTek.GameDirectory);
141 |
142 | return _cacheAbsolutePath;
143 | }
144 | }
145 | [JsonIgnore] private string _cacheAbsolutePath;
146 | [JsonIgnore] internal bool CacheHit; // default is false
147 | [JsonIgnore] internal string ContainingDirectory;
148 | [JsonIgnore] internal bool HasErrors; // default is false
149 |
150 |
151 | [JsonConstructor]
152 | public CacheEntry()
153 | {
154 | }
155 |
156 | public CacheEntry(string cacheAbsolutePath, string originalAbsolutePath, List mergePaths)
157 | {
158 | _cacheAbsolutePath = cacheAbsolutePath;
159 | CachePath = ModTek.GetRelativePath(cacheAbsolutePath, ModTek.GameDirectory);
160 | ContainingDirectory = Path.GetDirectoryName(cacheAbsolutePath);
161 | OriginalTime = File.GetLastWriteTimeUtc(originalAbsolutePath);
162 |
163 | if (string.IsNullOrEmpty(ContainingDirectory))
164 | {
165 | HasErrors = true;
166 | return;
167 | }
168 |
169 | // get the parent JSON
170 | JObject parentJObj;
171 | try
172 | {
173 | parentJObj = ModTek.ParseGameJSONFile(originalAbsolutePath);
174 | }
175 | catch (Exception e)
176 | {
177 | Log($"\tParent JSON at path {originalAbsolutePath} has errors preventing any merges!");
178 | Log($"\t\t{e.Message}");
179 | HasErrors = true;
180 | return;
181 | }
182 |
183 | foreach (var mergePath in mergePaths)
184 | Merges.Add(new PathTimeTuple(ModTek.GetRelativePath(mergePath, ModTek.GameDirectory), File.GetLastWriteTimeUtc(mergePath)));
185 |
186 | Directory.CreateDirectory(ContainingDirectory);
187 |
188 | using (var writer = File.CreateText(cacheAbsolutePath))
189 | {
190 | // merge all of the merges
191 | foreach (var mergePath in mergePaths)
192 | {
193 | JObject mergeJObj;
194 | try
195 | {
196 | mergeJObj = ModTek.ParseGameJSONFile(mergePath);
197 | }
198 | catch (Exception e)
199 | {
200 | Log($"\tMod merge JSON at path {originalAbsolutePath} has errors preventing any merges!");
201 | Log($"\t\t{e.Message}");
202 | continue;
203 | }
204 |
205 | if (AdvancedJSONMerger.IsAdvancedJSONMerge(mergeJObj))
206 | {
207 | try
208 | {
209 | AdvancedJSONMerger.ProcessInstructionsJObject(parentJObj, mergeJObj);
210 | continue;
211 | }
212 | catch (Exception e)
213 | {
214 | Log($"\tMod advanced merge JSON at path {mergePath} has errors preventing advanced json merges!");
215 | Log($"\t\t{e.Message}");
216 | }
217 | }
218 |
219 | // assume standard merging
220 | parentJObj.Merge(mergeJObj, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace });
221 | }
222 |
223 | // write the merged onto file to disk
224 | var jsonWriter = new JsonTextWriter(writer)
225 | {
226 | Formatting = Formatting.Indented
227 | };
228 | parentJObj.WriteTo(jsonWriter);
229 | jsonWriter.Close();
230 | }
231 | }
232 |
233 | internal bool MatchesPaths(string originalPath, List mergePaths)
234 | {
235 | // must have an existing cached json
236 | if (!File.Exists(CacheAbsolutePath))
237 | return false;
238 |
239 | // must have the same original file
240 | if (File.GetLastWriteTimeUtc(originalPath) != OriginalTime)
241 | return false;
242 |
243 | // must match number of merges
244 | if (mergePaths.Count != Merges.Count)
245 | return false;
246 |
247 | // if all paths match with write times, we match
248 | for (var index = 0; index < mergePaths.Count; index++)
249 | {
250 | var mergeAbsolutePath = mergePaths[index];
251 | var mergeTime = File.GetLastWriteTimeUtc(mergeAbsolutePath);
252 | var cachedMergeAboslutePath = ModTek.ResolvePath(Merges[index].Path, ModTek.GameDirectory);
253 | var cachedMergeTime = Merges[index].Time;
254 |
255 | if (mergeAbsolutePath != cachedMergeAboslutePath || mergeTime != cachedMergeTime)
256 | return false;
257 | }
258 |
259 | return true;
260 | }
261 |
262 | internal class PathTimeTuple
263 | {
264 | public PathTimeTuple(string path, DateTime time)
265 | {
266 | Path = path;
267 | Time = time;
268 | }
269 |
270 | public string Path { get; set; }
271 | public DateTime Time { get; set; }
272 | }
273 | }
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/ModTek/ModDef.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.IO;
5 | using BattleTech;
6 | using Newtonsoft.Json;
7 | using Newtonsoft.Json.Linq;
8 |
9 | namespace ModTek
10 | {
11 | public class ModDef : IModDef
12 | {
13 | // this path will be set at runtime by ModTek
14 | [JsonIgnore]
15 | public string Directory { get; set; }
16 |
17 | // name will probably have to be unique
18 | [JsonProperty(Required = Required.Always)]
19 | public string Name { get; set; }
20 |
21 | // informational
22 | public string Description { get; set; }
23 | public string Author { get; set; }
24 | public string Website { get; set; }
25 | public string Contact { get; set; }
26 |
27 | // versioning
28 | public string Version { get; set; }
29 | public DateTime? PackagedOn { get; set; }
30 |
31 | // this will abort loading by ModTek if set to false
32 | [DefaultValue(true)]
33 | public bool Enabled { get; set; } = true;
34 |
35 | // load order
36 | public HashSet DependsOn { get; set; } = new HashSet();
37 | public HashSet ConflictsWith { get; set; } = new HashSet();
38 | public HashSet OptionallyDependsOn { get; set; } = new HashSet();
39 |
40 | // adding and running code
41 | public string DLL { get; set; }
42 | public string DLLEntryPoint { get; set; }
43 |
44 | // changing implicit loading behavior
45 | [DefaultValue(true)]
46 | public bool LoadImplicitManifest { get; set; } = true;
47 |
48 | // manifest, for including any kind of things to add to the game's manifest
49 | public List Manifest { get; set; } = new List();
50 |
51 | // a settings file to be nice to our users and have a known place for settings
52 | // these will be different depending on the mod obviously
53 | public JObject Settings { get; set; } = new JObject();
54 |
55 | ///
56 | /// Creates a ModDef from a path to a mod.json
57 | ///
58 | /// Path to mod.json
59 | /// A ModDef representing the mod.json
60 | public static ModDef CreateFromPath(string path)
61 | {
62 | var modDef = JsonConvert.DeserializeObject(File.ReadAllText(path));
63 | modDef.Directory = Path.GetDirectoryName(path);
64 | return modDef;
65 | }
66 |
67 | public class ManifestEntry : IManifestEntry
68 | {
69 | [JsonConstructor]
70 | public ManifestEntry(string path, bool shouldMergeJSON = false)
71 | {
72 | Path = path;
73 | ShouldMergeJSON = shouldMergeJSON;
74 | }
75 |
76 | public ManifestEntry(ManifestEntry parent, string path, string id)
77 | {
78 | Path = path;
79 | Id = id;
80 |
81 | Type = parent.Type;
82 | AssetBundleName = parent.AssetBundleName;
83 | AssetBundlePersistent = parent.AssetBundlePersistent;
84 | ShouldMergeJSON = parent.ShouldMergeJSON;
85 | AddToAddendum = parent.AddToAddendum;
86 | AddToDB = parent.AddToDB;
87 | }
88 |
89 | [JsonProperty(Required = Required.Always)]
90 | public string Path { get; set; }
91 |
92 | [DefaultValue(false)]
93 | public bool ShouldMergeJSON { get; set; } // defaults to false
94 |
95 | [DefaultValue(true)]
96 | public bool AddToDB { get; set; } = true;
97 |
98 | public string AddToAddendum { get; set; }
99 |
100 | public string Type { get; set; }
101 | public string Id { get; set; }
102 | public string AssetBundleName { get; set; }
103 | public bool? AssetBundlePersistent { get; set; }
104 |
105 | private VersionManifestEntry versionManifestEntry;
106 |
107 | public VersionManifestEntry GetVersionManifestEntry()
108 | {
109 | if (versionManifestEntry == null)
110 | versionManifestEntry = new VersionManifestEntry(Id, Path, Type, DateTime.Now, "1", AssetBundleName, AssetBundlePersistent);
111 |
112 | return versionManifestEntry;
113 | }
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/ModTek/ModTek.cs:
--------------------------------------------------------------------------------
1 | using BattleTech;
2 | using BattleTech.Data;
3 | using BattleTechModLoader;
4 | using Harmony;
5 | using HBS.Util;
6 | using JetBrains.Annotations;
7 | using Newtonsoft.Json;
8 | using Newtonsoft.Json.Linq;
9 | using System;
10 | using System.Collections.Generic;
11 | using System.Diagnostics;
12 | using System.IO;
13 | using System.Linq;
14 | using System.Reflection;
15 | using System.Text.RegularExpressions;
16 |
17 | // ReSharper disable FieldCanBeMadeReadOnly.Local
18 |
19 | namespace ModTek
20 | {
21 | using static Logger;
22 |
23 | public static class ModTek
24 | {
25 | private static readonly string[] IGNORE_LIST = { ".DS_STORE", "~", ".nomedia" };
26 |
27 | // game paths/directories
28 | public static string GameDirectory { get; private set; }
29 | public static string ModsDirectory { get; private set; }
30 | public static string StreamingAssetsDirectory { get; private set; }
31 | public static string MDDBPath { get; private set; }
32 |
33 | // file/directory names
34 | private const string MODS_DIRECTORY_NAME = "Mods";
35 | private const string MOD_JSON_NAME = "mod.json";
36 | private const string MODTEK_DIRECTORY_NAME = ".modtek";
37 | private const string CACHE_DIRECTORY_NAME = "Cache";
38 | private const string MERGE_CACHE_FILE_NAME = "merge_cache.json";
39 | private const string TYPE_CACHE_FILE_NAME = "type_cache.json";
40 | private const string LOG_NAME = "ModTek.log";
41 | private const string LOAD_ORDER_FILE_NAME = "load_order.json";
42 | private const string DATABASE_DIRECTORY_NAME = "Database";
43 | private const string MDD_FILE_NAME = "MetadataDatabase.db";
44 | private const string DB_CACHE_FILE_NAME = "database_cache.json";
45 | private const string HARMONY_SUMMARY_FILE_NAME = "harmony_summary.log";
46 |
47 | // ModTek paths/directories
48 | internal static string ModTekDirectory { get; private set; }
49 | internal static string CacheDirectory { get; private set; }
50 | internal static string DatabaseDirectory { get; private set; }
51 | internal static string MergeCachePath { get; private set; }
52 | internal static string TypeCachePath { get; private set; }
53 | internal static string ModMDDBPath { get; private set; }
54 | internal static string DBCachePath { get; private set; }
55 | internal static string LoadOrderPath { get; private set; }
56 | internal static string HarmonySummaryPath { get; private set; }
57 |
58 | // files that are read and written to (located in .modtek)
59 | private static List modLoadOrder;
60 | private static MergeCache jsonMergeCache;
61 | private static Dictionary> typeCache;
62 | private static Dictionary dbCache;
63 |
64 | internal static VersionManifest CachedVersionManifest = null;
65 | internal static List BTRLEntries = new List();
66 |
67 | internal static Dictionary ModAssetBundlePaths { get; } = new Dictionary();
68 | internal static HashSet ModTexture2Ds { get; } = new HashSet();
69 | internal static Dictionary ModVideos { get; } = new Dictionary();
70 |
71 | private static Dictionary cachedJObjects = new Dictionary();
72 | private static Dictionary> entriesByMod = new Dictionary>();
73 | private static Stopwatch stopwatch = new Stopwatch();
74 |
75 |
76 | // INITIALIZATION (called by BTML)
77 | [UsedImplicitly]
78 | public static void Init()
79 | {
80 | stopwatch.Start();
81 |
82 | // if the manifest directory is null, there is something seriously wrong
83 | var manifestDirectory = Path.GetDirectoryName(VersionManifestUtilities.MANIFEST_FILEPATH);
84 | if (manifestDirectory == null)
85 | return;
86 |
87 | // setup directories
88 | ModsDirectory = Path.GetFullPath(
89 | Path.Combine(manifestDirectory,
90 | Path.Combine(Path.Combine(Path.Combine(
91 | "..", ".."), ".."), MODS_DIRECTORY_NAME)));
92 |
93 | StreamingAssetsDirectory = Path.GetFullPath(Path.Combine(manifestDirectory, ".."));
94 | GameDirectory = Path.GetFullPath(Path.Combine(Path.Combine(StreamingAssetsDirectory, ".."), ".."));
95 | MDDBPath = Path.Combine(Path.Combine(StreamingAssetsDirectory, "MDD"), MDD_FILE_NAME);
96 |
97 | ModTekDirectory = Path.Combine(ModsDirectory, MODTEK_DIRECTORY_NAME);
98 | CacheDirectory = Path.Combine(ModTekDirectory, CACHE_DIRECTORY_NAME);
99 | DatabaseDirectory = Path.Combine(ModTekDirectory, DATABASE_DIRECTORY_NAME);
100 |
101 | LogPath = Path.Combine(ModTekDirectory, LOG_NAME);
102 | HarmonySummaryPath = Path.Combine(ModTekDirectory, HARMONY_SUMMARY_FILE_NAME);
103 | LoadOrderPath = Path.Combine(ModTekDirectory, LOAD_ORDER_FILE_NAME);
104 | MergeCachePath = Path.Combine(CacheDirectory, MERGE_CACHE_FILE_NAME);
105 | TypeCachePath = Path.Combine(CacheDirectory, TYPE_CACHE_FILE_NAME);
106 | ModMDDBPath = Path.Combine(DatabaseDirectory, MDD_FILE_NAME);
107 | DBCachePath = Path.Combine(DatabaseDirectory, DB_CACHE_FILE_NAME);
108 |
109 | // creates the directories above it as well
110 | Directory.CreateDirectory(CacheDirectory);
111 | Directory.CreateDirectory(DatabaseDirectory);
112 |
113 | // create log file, overwritting if it's already there
114 | using (var logWriter = File.CreateText(LogPath))
115 | {
116 | logWriter.WriteLine($"ModTek v{Assembly.GetExecutingAssembly().GetName().Version} -- {DateTime.Now}");
117 | }
118 |
119 | // load progress bar
120 | if (!ProgressPanel.Initialize(ModsDirectory, $"ModTek v{Assembly.GetExecutingAssembly().GetName().Version}"))
121 | {
122 | Log("Failed to load progress bar. Skipping mod loading completely.");
123 | CloseLogStream();
124 | }
125 |
126 | // create all of the caches
127 | dbCache = LoadOrCreateDBCache(DBCachePath);
128 | jsonMergeCache = LoadOrCreateMergeCache(MergeCachePath);
129 | typeCache = LoadOrCreateTypeCache(TypeCachePath);
130 |
131 | UpdateAbsCacheToRelativePath(dbCache);
132 | UpdatePathCacheToID(typeCache);
133 | jsonMergeCache.UpdateToRelativePaths();
134 |
135 | // init harmony and patch the stuff that comes with ModTek (contained in Patches.cs)
136 | var harmony = HarmonyInstance.Create("io.github.mpstark.ModTek");
137 | harmony.PatchAll(Assembly.GetExecutingAssembly());
138 |
139 | LoadMods();
140 | BuildModManifestEntries();
141 |
142 | stopwatch.Stop();
143 | }
144 |
145 |
146 | // UTIL
147 | private static void PrintHarmonySummary(string path)
148 | {
149 | var harmony = HarmonyInstance.Create("io.github.mpstark.ModTek");
150 |
151 | var patchedMethods = harmony.GetPatchedMethods().ToArray();
152 | if (patchedMethods.Length == 0)
153 | return;
154 |
155 | using (var writer = File.CreateText(path))
156 | {
157 | writer.WriteLine($"Harmony Patched Methods (after ModTek startup) -- {DateTime.Now}\n");
158 |
159 | foreach (var method in patchedMethods)
160 | {
161 | var info = harmony.GetPatchInfo(method);
162 |
163 | if (info == null || method.ReflectedType == null)
164 | continue;
165 |
166 | writer.WriteLine($"{method.ReflectedType.FullName}.{method.Name}:");
167 |
168 | // prefixes
169 | if (info.Prefixes.Count != 0)
170 | writer.WriteLine("\tPrefixes:");
171 | foreach (var patch in info.Prefixes)
172 | writer.WriteLine($"\t\t{patch.owner}");
173 |
174 | // transpilers
175 | if (info.Transpilers.Count != 0)
176 | writer.WriteLine("\tTranspilers:");
177 | foreach (var patch in info.Transpilers)
178 | writer.WriteLine($"\t\t{patch.owner}");
179 |
180 | // postfixes
181 | if (info.Postfixes.Count != 0)
182 | writer.WriteLine("\tPostfixes:");
183 | foreach (var patch in info.Postfixes)
184 | writer.WriteLine($"\t\t{patch.owner}");
185 |
186 | writer.WriteLine("");
187 | }
188 | }
189 | }
190 |
191 | private static bool FileIsOnDenyList(string filePath)
192 | {
193 | return IGNORE_LIST.Any(x => filePath.EndsWith(x, StringComparison.InvariantCultureIgnoreCase));
194 | }
195 |
196 | internal static string ResolvePath(string path, string rootPathToUse)
197 | {
198 | if (!Path.IsPathRooted(path))
199 | path = Path.Combine(rootPathToUse, path);
200 |
201 | return Path.GetFullPath(path);
202 | }
203 |
204 | internal static string GetRelativePath(string path, string rootPath)
205 | {
206 | if (!Path.IsPathRooted(path))
207 | return path;
208 |
209 | rootPath = Path.GetFullPath(rootPath);
210 | if (rootPath.Last() != Path.DirectorySeparatorChar)
211 | rootPath += Path.DirectorySeparatorChar;
212 |
213 | var pathUri = new Uri(Path.GetFullPath(path), UriKind.Absolute);
214 | var rootUri = new Uri(rootPath, UriKind.Absolute);
215 |
216 | if (pathUri.Scheme != rootUri.Scheme)
217 | return path;
218 |
219 | var relativeUri = rootUri.MakeRelativeUri(pathUri);
220 | var relativePath = Uri.UnescapeDataString(relativeUri.ToString());
221 |
222 | if (pathUri.Scheme.Equals("file", StringComparison.InvariantCultureIgnoreCase))
223 | relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
224 |
225 | return relativePath;
226 | }
227 |
228 | internal static JObject ParseGameJSONFile(string path)
229 | {
230 | if (cachedJObjects.ContainsKey(path))
231 | return cachedJObjects[path];
232 |
233 | // because StripHBSCommentsFromJSON is private, use Harmony to call the method
234 | var commentsStripped = Traverse.Create(typeof(JSONSerializationUtility)).Method("StripHBSCommentsFromJSON", File.ReadAllText(path)).GetValue();
235 |
236 | if (commentsStripped == null)
237 | throw new Exception("StripHBSCommentsFromJSON returned null.");
238 |
239 | // add missing commas, this only fixes if there is a newline
240 | var rgx = new Regex(@"(\]|\}|""|[A-Za-z0-9])\s*\n\s*(\[|\{|"")", RegexOptions.Singleline);
241 | var commasAdded = rgx.Replace(commentsStripped, "$1,\n$2");
242 |
243 | cachedJObjects[path] = JObject.Parse(commasAdded);
244 | return cachedJObjects[path];
245 | }
246 |
247 | private static string InferIDFromJObject(JObject jObj)
248 | {
249 | if (jObj == null)
250 | return null;
251 |
252 | // go through the different kinds of id storage in JSONS
253 | string[] jPaths = { "Description.Id", "id", "Id", "ID", "identifier", "Identifier" };
254 | foreach (var jPath in jPaths)
255 | {
256 | var id = (string)jObj.SelectToken(jPath);
257 | if (id != null)
258 | return id;
259 | }
260 |
261 | return null;
262 | }
263 |
264 | private static string InferIDFromFile(string path)
265 | {
266 | // if not json, return the file name without the extension, as this is what HBS uses
267 | var ext = Path.GetExtension(path);
268 | if (ext == null || ext.ToLower() != ".json" || !File.Exists(path))
269 | return Path.GetFileNameWithoutExtension(path);
270 |
271 | // read the json and get ID out of it if able to
272 | return InferIDFromJObject(ParseGameJSONFile(path)) ?? Path.GetFileNameWithoutExtension(path);
273 | }
274 |
275 | private static VersionManifestEntry GetEntryFromCachedOrBTRLEntries(string id)
276 | {
277 | return BTRLEntries.FindLast(x => x.Id == id)?.GetVersionManifestEntry() ?? CachedVersionManifest.Find(x => x.Id == id);
278 | }
279 |
280 |
281 | // CACHES
282 | internal static void WriteJsonFile(string path, object obj)
283 | {
284 | File.WriteAllText(path, JsonConvert.SerializeObject(obj, Formatting.Indented));
285 | }
286 |
287 | internal static void UpdateAbsCacheToRelativePath(Dictionary cache)
288 | {
289 | var toRemove = new List();
290 | var toAdd = new Dictionary();
291 |
292 | foreach (var path in cache.Keys)
293 | {
294 | if (Path.IsPathRooted(path))
295 | {
296 | var relativePath = GetRelativePath(path, GameDirectory);
297 | toAdd[relativePath] = cache[path];
298 | toRemove.Add(path);
299 | }
300 | }
301 |
302 | foreach (var addKVP in toAdd)
303 | cache.Add(addKVP.Key, addKVP.Value);
304 |
305 | foreach (var path in toRemove)
306 | cache.Remove(path);
307 | }
308 |
309 | internal static void UpdatePathCacheToID(Dictionary cache)
310 | {
311 | var toRemove = new List();
312 | var toAdd = new Dictionary();
313 |
314 | foreach (var path in cache.Keys)
315 | {
316 | var id = Path.GetFileNameWithoutExtension(path);
317 |
318 | if (id == path || toAdd.ContainsKey(id) || cache.ContainsKey(id))
319 | continue;
320 |
321 | toAdd[id] = cache[path];
322 | toRemove.Add(path);
323 | }
324 |
325 | foreach (var addKVP in toAdd)
326 | cache.Add(addKVP.Key, addKVP.Value);
327 |
328 | foreach (var path in toRemove)
329 | cache.Remove(path);
330 | }
331 |
332 | internal static MergeCache LoadOrCreateMergeCache(string path)
333 | {
334 | MergeCache mergeCache;
335 |
336 | if (File.Exists(path))
337 | {
338 | try
339 | {
340 | mergeCache = JsonConvert.DeserializeObject(File.ReadAllText(path));
341 | Log("Loaded merge cache.");
342 | return mergeCache;
343 | }
344 | catch (Exception e)
345 | {
346 | Log("Loading merge cache failed -- will rebuild it.");
347 | Log($"\t{e.Message}");
348 | }
349 | }
350 |
351 | // create a new one if it doesn't exist or couldn't be added'
352 | Log("Building new Merge Cache.");
353 | mergeCache = new MergeCache();
354 | return mergeCache;
355 | }
356 |
357 | internal static Dictionary> LoadOrCreateTypeCache(string path)
358 | {
359 | Dictionary> cache;
360 |
361 | if (File.Exists(path))
362 | {
363 | try
364 | {
365 | cache = JsonConvert.DeserializeObject>>(File.ReadAllText(path));
366 | Log("Loaded type cache.");
367 | return cache;
368 | }
369 | catch (Exception e)
370 | {
371 | Log("Loading type cache failed -- will rebuild it.");
372 | Log($"\t{e.Message}");
373 | }
374 | }
375 |
376 | // create a new one if it doesn't exist or couldn't be added
377 | Log("Building new Type Cache.");
378 | cache = new Dictionary>();
379 | return cache;
380 | }
381 |
382 | internal static List GetTypesFromCache(string id)
383 | {
384 | if (typeCache.ContainsKey(id))
385 | return typeCache[id];
386 |
387 | return null;
388 | }
389 |
390 | internal static List GetTypesFromCacheOrManifest(VersionManifest manifest, string id)
391 | {
392 | var types = GetTypesFromCache(id);
393 | if (types != null)
394 | return types;
395 |
396 | // get the types from the manifest
397 | var matchingEntries = manifest.FindAll(x => x.Id == id);
398 | if (matchingEntries == null || matchingEntries.Count == 0)
399 | return null;
400 |
401 | types = new List();
402 |
403 | foreach (var existingEntry in matchingEntries)
404 | types.Add(existingEntry.Type);
405 |
406 | typeCache[id] = types;
407 | return typeCache[id];
408 | }
409 |
410 | internal static void TryAddTypeToCache(string id, string type)
411 | {
412 | var types = GetTypesFromCache(id);
413 | if (types != null && types.Contains(type))
414 | return;
415 |
416 | if (types != null && !types.Contains(type))
417 | {
418 | types.Add(type);
419 | return;
420 | }
421 |
422 | // add the new entry
423 | typeCache[id] = new List { type };
424 | }
425 |
426 | internal static Dictionary LoadOrCreateDBCache(string path)
427 | {
428 | Dictionary cache;
429 |
430 | if (File.Exists(path) && File.Exists(ModMDDBPath))
431 | {
432 | try
433 | {
434 | cache = JsonConvert.DeserializeObject>(File.ReadAllText(path));
435 | Log("Loaded db cache.");
436 | return cache;
437 | }
438 | catch (Exception e)
439 | {
440 | Log("Loading db cache failed -- will rebuild it.");
441 | Log($"\t{e.Message}");
442 | }
443 | }
444 |
445 | // delete mod db if it exists the cache does not
446 | if (File.Exists(ModMDDBPath))
447 | File.Delete(ModMDDBPath);
448 |
449 | File.Copy(Path.Combine(Path.Combine(StreamingAssetsDirectory, "MDD"), MDD_FILE_NAME), ModMDDBPath);
450 |
451 | // create a new one if it doesn't exist or couldn't be added
452 | Log("Copying over DB and building new DB Cache.");
453 | cache = new Dictionary();
454 | return cache;
455 | }
456 |
457 |
458 | // LOAD ORDER
459 | private static void PropagateConflictsForward(Dictionary modDefs)
460 | {
461 | // conflicts are a unidirectional edge, so make them one in ModDefs
462 | foreach (var modDef in modDefs.Values)
463 | {
464 | if (modDef.ConflictsWith.Count == 0)
465 | continue;
466 |
467 | foreach (var conflict in modDef.ConflictsWith)
468 | {
469 | if (modDefs.ContainsKey(conflict))
470 | modDefs[conflict].ConflictsWith.Add(modDef.Name);
471 | }
472 | }
473 | }
474 |
475 | private static void FillInOptionalDependencies(Dictionary modDefs)
476 | {
477 | // add optional dependencies if they are present
478 | foreach (var modDef in modDefs.Values)
479 | {
480 | if (modDef.OptionallyDependsOn.Count == 0)
481 | continue;
482 |
483 | foreach (var optDep in modDef.OptionallyDependsOn)
484 | {
485 | if (modDefs.ContainsKey(optDep))
486 | modDef.DependsOn.Add(optDep);
487 | }
488 | }
489 | }
490 |
491 | private static List LoadLoadOrder(string path)
492 | {
493 | List order;
494 |
495 | if (File.Exists(path))
496 | {
497 | try
498 | {
499 | order = JsonConvert.DeserializeObject>(File.ReadAllText(path));
500 | Log("Loaded cached load order.");
501 | return order;
502 | }
503 | catch (Exception e)
504 | {
505 | Log("Loading cached load order failed, rebuilding it.");
506 | Log($"\t{e.Message}");
507 | }
508 | }
509 |
510 | // create a new one if it doesn't exist or couldn't be added
511 | Log("Building new load order!");
512 | order = new List();
513 | return order;
514 | }
515 |
516 | private static bool AreDependanciesResolved(ModDef modDef, HashSet loaded)
517 | {
518 | return !(modDef.DependsOn.Count != 0 && modDef.DependsOn.Intersect(loaded).Count() != modDef.DependsOn.Count
519 | || modDef.ConflictsWith.Count != 0 && modDef.ConflictsWith.Intersect(loaded).Any());
520 | }
521 |
522 | private static List GetLoadOrder(Dictionary modDefs, out List unloaded)
523 | {
524 | var modDefsCopy = new Dictionary(modDefs);
525 | var cachedOrder = LoadLoadOrder(LoadOrderPath);
526 | var loadOrder = new List();
527 | var loaded = new HashSet();
528 |
529 | PropagateConflictsForward(modDefsCopy);
530 | FillInOptionalDependencies(modDefsCopy);
531 |
532 | // load the order specified in the file
533 | foreach (var modName in cachedOrder)
534 | {
535 | if (!modDefs.ContainsKey(modName) || !AreDependanciesResolved(modDefs[modName], loaded)) continue;
536 |
537 | modDefsCopy.Remove(modName);
538 | loadOrder.Add(modName);
539 | loaded.Add(modName);
540 | }
541 |
542 | // everything that is left in the copy hasn't been loaded before
543 | unloaded = modDefsCopy.Keys.OrderByDescending(x => x).ToList();
544 |
545 | // there is nothing left to load
546 | if (unloaded.Count == 0)
547 | return loadOrder;
548 |
549 | // this is the remainder that haven't been loaded before
550 | int removedThisPass;
551 | do
552 | {
553 | removedThisPass = 0;
554 |
555 | for (var i = unloaded.Count - 1; i >= 0; i--)
556 | {
557 | var modDef = modDefs[unloaded[i]];
558 |
559 | if (!AreDependanciesResolved(modDef, loaded)) continue;
560 |
561 | unloaded.RemoveAt(i);
562 | loadOrder.Add(modDef.Name);
563 | loaded.Add(modDef.Name);
564 | removedThisPass++;
565 | }
566 | } while (removedThisPass > 0 && unloaded.Count > 0);
567 |
568 | return loadOrder;
569 | }
570 |
571 |
572 | // READING mod.json AND INIT MODS
573 | private static void LoadMod(ModDef modDef)
574 | {
575 | var potentialAdditions = new List();
576 |
577 | // load out of the manifest
578 | if (modDef.LoadImplicitManifest && modDef.Manifest.All(x => Path.GetFullPath(Path.Combine(modDef.Directory, x.Path)) != Path.GetFullPath(Path.Combine(modDef.Directory, "StreamingAssets"))))
579 | modDef.Manifest.Add(new ModDef.ManifestEntry("StreamingAssets", true));
580 |
581 | // note: if a JSON has errors, this mod will not load, since InferIDFromFile will throw from parsing the JSON
582 | foreach (var entry in modDef.Manifest)
583 | {
584 | // handle prefabs; they have potential internal path to assetbundle
585 | if (entry.Type == "Prefab" && !string.IsNullOrEmpty(entry.AssetBundleName))
586 | {
587 | if (!potentialAdditions.Any(x => x.Type == "AssetBundle" && x.Id == entry.AssetBundleName))
588 | {
589 | Log($"\t{modDef.Name} has a Prefab that's referencing an AssetBundle that hasn't been loaded. Put the assetbundle first in the manifest!");
590 | return;
591 | }
592 |
593 | entry.Id = Path.GetFileNameWithoutExtension(entry.Path);
594 | if (!FileIsOnDenyList(entry.Path)) potentialAdditions.Add(entry);
595 | continue;
596 | }
597 |
598 | if (string.IsNullOrEmpty(entry.Path) && string.IsNullOrEmpty(entry.Type) && entry.Path != "StreamingAssets")
599 | {
600 | Log($"\t{modDef.Name} has a manifest entry that is missing its path or type! Aborting load.");
601 | return;
602 | }
603 |
604 | var entryPath = Path.GetFullPath(Path.Combine(modDef.Directory, entry.Path));
605 | if (Directory.Exists(entryPath))
606 | {
607 | // path is a directory, add all the files there
608 | var files = Directory.GetFiles(entryPath, "*", SearchOption.AllDirectories).Where(filePath => !FileIsOnDenyList(filePath));
609 | foreach (var filePath in files)
610 | {
611 | var childModDef = new ModDef.ManifestEntry(entry, Path.GetFullPath(filePath), InferIDFromFile(filePath));
612 | potentialAdditions.Add(childModDef);
613 | }
614 | }
615 | else if (File.Exists(entryPath) && !FileIsOnDenyList(entryPath))
616 | {
617 | // path is a file, add the single entry
618 | entry.Id = entry.Id ?? InferIDFromFile(entryPath);
619 | entry.Path = entryPath;
620 | potentialAdditions.Add(entry);
621 | }
622 | else if (entry.Path != "StreamingAssets")
623 | {
624 | // path is not streamingassets and it's missing
625 | Log($"\tMissing Entry: Manifest specifies file/directory of {entry.Type} at path {entry.Path}, but it's not there. Continuing to load.");
626 | }
627 | }
628 |
629 | // load mod dll
630 | if (modDef.DLL != null)
631 | {
632 | var dllPath = Path.Combine(modDef.Directory, modDef.DLL);
633 | string typeName = null;
634 | var methodName = "Init";
635 |
636 | if (!File.Exists(dllPath))
637 | {
638 | Log($"\t{modDef.Name} has a DLL specified ({dllPath}), but it's missing! Aborting load.");
639 | return;
640 | }
641 |
642 | if (modDef.DLLEntryPoint != null)
643 | {
644 | var pos = modDef.DLLEntryPoint.LastIndexOf('.');
645 | if (pos == -1)
646 | {
647 | methodName = modDef.DLLEntryPoint;
648 | }
649 | else
650 | {
651 | typeName = modDef.DLLEntryPoint.Substring(0, pos);
652 | methodName = modDef.DLLEntryPoint.Substring(pos + 1);
653 | }
654 | }
655 |
656 | BTModLoader.LoadDLL(dllPath, methodName, typeName,
657 | new object[] { modDef.Directory, modDef.Settings.ToString(Formatting.None) });
658 | }
659 |
660 | Log($"{modDef.Name} {modDef.Version} : {potentialAdditions.Count} entries : {modDef.DLL ?? "No DLL"}");
661 |
662 | if (potentialAdditions.Count <= 0)
663 | return;
664 |
665 | // actually add the additions, since we successfully got through loading the other stuff
666 | entriesByMod[modDef.Name] = potentialAdditions;
667 | }
668 |
669 | internal static void LoadMods()
670 | {
671 | ProgressPanel.SubmitWork(LoadMoadsLoop);
672 | }
673 |
674 | internal static IEnumerator LoadMoadsLoop()
675 | {
676 | stopwatch.Start();
677 |
678 | Log("");
679 | yield return new ProgressReport(1, "Initializing Mods", "");
680 |
681 | // find all sub-directories that have a mod.json file
682 | var modDirectories = Directory.GetDirectories(ModsDirectory)
683 | .Where(x => File.Exists(Path.Combine(x, MOD_JSON_NAME))).ToArray();
684 |
685 | if (modDirectories.Length == 0)
686 | {
687 | Log("No ModTek-compatable mods found.");
688 | yield break;
689 | }
690 |
691 | // create ModDef objects for each mod.json file
692 | var modDefs = new Dictionary();
693 | foreach (var modDirectory in modDirectories)
694 | {
695 | ModDef modDef;
696 | var modDefPath = Path.Combine(modDirectory, MOD_JSON_NAME);
697 |
698 | try
699 | {
700 | modDef = ModDef.CreateFromPath(modDefPath);
701 | }
702 | catch (Exception e)
703 | {
704 | Log($"Caught exception while parsing {MOD_JSON_NAME} at path {modDefPath}");
705 | Log($"\t{e.Message}");
706 | continue;
707 | }
708 |
709 | if (!modDef.Enabled)
710 | {
711 | Log($"Will not load {modDef.Name} because it's disabled.");
712 | continue;
713 | }
714 |
715 | if (modDefs.ContainsKey(modDef.Name))
716 | {
717 | Log($"Already loaded a mod named {modDef.Name}. Skipping load from {modDef.Directory}.");
718 | continue;
719 | }
720 |
721 | modDefs.Add(modDef.Name, modDef);
722 | }
723 |
724 | Log("");
725 | modLoadOrder = GetLoadOrder(modDefs, out var willNotLoad);
726 | foreach (var modName in willNotLoad)
727 | {
728 | Log($"Will not load {modName} because it's lacking a dependancy or a conflict loaded before it.");
729 | }
730 | Log("");
731 |
732 | // lists guarentee order
733 | var modLoaded = 0;
734 | foreach (var modName in modLoadOrder)
735 | {
736 | var modDef = modDefs[modName];
737 | yield return new ProgressReport(modLoaded++ / ((float)modLoadOrder.Count), "Initializing Mods", $"{modDef.Name} {modDef.Version}");
738 | try
739 | {
740 | LoadMod(modDef);
741 | }
742 | catch (Exception e)
743 | {
744 | Log($"Tried to load mod: {modDef.Name}, but something went wrong. Make sure all of your JSON is correct!");
745 | Log($"\t{e.Message}");
746 | }
747 | }
748 |
749 | PrintHarmonySummary(HarmonySummaryPath);
750 | WriteJsonFile(LoadOrderPath, modLoadOrder);
751 | stopwatch.Stop();
752 |
753 | yield break;
754 | }
755 |
756 |
757 | // ADDING MOD CONTENT TO THE GAME
758 | private static void AddModEntry(VersionManifest manifest, ModDef.ManifestEntry modEntry)
759 | {
760 | if (modEntry.Path == null)
761 | return;
762 |
763 | VersionManifestAddendum addendum = null;
764 | if (!string.IsNullOrEmpty(modEntry.AddToAddendum))
765 | {
766 | addendum = manifest.GetAddendumByName(modEntry.AddToAddendum);
767 |
768 | if (addendum == null)
769 | {
770 | Log($"\tCannot add {modEntry.Id} to {modEntry.AddToAddendum} because addendum doesn't exist in the manifest.");
771 | return;
772 | }
773 | }
774 |
775 | // add special handling for particular types
776 | switch (modEntry.Type)
777 | {
778 | case "AssetBundle":
779 | ModAssetBundlePaths[modEntry.Id] = modEntry.Path;
780 | break;
781 | case "Texture2D":
782 | ModTexture2Ds.Add(modEntry.Id);
783 | break;
784 | }
785 |
786 | // add to addendum instead of adding to manifest
787 | if (addendum != null)
788 | Log($"\tAdd/Replace: \"{GetRelativePath(modEntry.Path, ModsDirectory)}\" ({modEntry.Type}) [{addendum.Name}]");
789 | else
790 | Log($"\tAdd/Replace: \"{GetRelativePath(modEntry.Path, ModsDirectory)}\" ({modEntry.Type})");
791 |
792 | // not added to addendum, not added to jsonmerges
793 | BTRLEntries.Add(modEntry);
794 | return;
795 | }
796 |
797 | private static bool AddModEntryToDB(MetadataDatabase db, string absolutePath, string typeStr)
798 | {
799 | if (Path.GetExtension(absolutePath)?.ToLower() != ".json")
800 | return false;
801 |
802 | var type = (BattleTechResourceType)Enum.Parse(typeof(BattleTechResourceType), typeStr);
803 | var relativePath = GetRelativePath(absolutePath, GameDirectory);
804 |
805 | switch (type) // switch is to avoid poisoning the output_log.txt with known types that don't use MDD
806 | {
807 | case BattleTechResourceType.TurretDef:
808 | case BattleTechResourceType.UpgradeDef:
809 | case BattleTechResourceType.VehicleDef:
810 | case BattleTechResourceType.ContractOverride:
811 | case BattleTechResourceType.SimGameEventDef:
812 | case BattleTechResourceType.LanceDef:
813 | case BattleTechResourceType.MechDef:
814 | case BattleTechResourceType.PilotDef:
815 | case BattleTechResourceType.WeaponDef:
816 | var writeTime = File.GetLastWriteTimeUtc(absolutePath);
817 | if (!dbCache.ContainsKey(relativePath) || dbCache[relativePath] != writeTime)
818 | {
819 | try
820 | {
821 | VersionManifestHotReload.InstantiateResourceAndUpdateMDDB(type, absolutePath, db);
822 | dbCache[relativePath] = writeTime;
823 | return true;
824 | }
825 | catch (Exception e)
826 | {
827 | Log($"\tAdd to DB failed for {Path.GetFileName(absolutePath)}, exception caught:");
828 | Log($"\t\t{e.Message}");
829 | return false;
830 | }
831 | }
832 | break;
833 | }
834 |
835 | return false;
836 | }
837 |
838 | internal static void BuildModManifestEntries()
839 | {
840 | CachedVersionManifest = VersionManifestUtilities.LoadDefaultManifest();
841 | ProgressPanel.SubmitWork(BuildModManifestEntriesLoop);
842 | }
843 |
844 | internal static IEnumerator BuildModManifestEntriesLoop()
845 | {
846 | stopwatch.Start();
847 |
848 | // there are no mods loaded, just return
849 | if (modLoadOrder == null || modLoadOrder.Count == 0)
850 | yield break;
851 |
852 | Log("");
853 |
854 | var jsonMerges = new Dictionary>();
855 | var manifestMods = modLoadOrder.Where(name => entriesByMod.ContainsKey(name)).ToList();
856 |
857 | var entryCount = 0;
858 | var numEntries = 0;
859 | entriesByMod.Do(entries => numEntries += entries.Value.Count);
860 |
861 | foreach (var modName in manifestMods)
862 | {
863 | Log($"{modName}:");
864 |
865 | foreach (var modEntry in entriesByMod[modName])
866 | {
867 | yield return new ProgressReport(entryCount++ / ((float)numEntries), $"Loading {modName}", modEntry.Id);
868 |
869 | // type being null means we have to figure out the type from the path (StreamingAssets)
870 | if (modEntry.Type == null)
871 | {
872 | // TODO: + 16 is a little bizzare looking, it's the length of the substring + 1 because we want to get rid of it and the \
873 | var relPath = modEntry.Path.Substring(modEntry.Path.LastIndexOf("StreamingAssets", StringComparison.Ordinal) + 16);
874 | var fakeStreamingAssetsPath = Path.GetFullPath(Path.Combine(StreamingAssetsDirectory, relPath));
875 | if (!File.Exists(fakeStreamingAssetsPath))
876 | {
877 | Log($"\tCould not find a file at {fakeStreamingAssetsPath} for {modName} {modEntry.Id}. NOT LOADING THIS FILE");
878 | continue;
879 | }
880 |
881 | var types = GetTypesFromCacheOrManifest(CachedVersionManifest, modEntry.Id);
882 | if (types == null)
883 | {
884 | Log($"\tCould not find an existing VersionManifest entry for {modEntry.Id}. Is this supposed to be a new entry? Don't put new entries in StreamingAssets!");
885 | continue;
886 | }
887 |
888 | // this is getting merged later and then added to the BTRL entries then
889 | if (Path.GetExtension(modEntry.Path).ToLower() == ".json" && modEntry.ShouldMergeJSON)
890 | {
891 | if (!jsonMerges.ContainsKey(modEntry.Id))
892 | jsonMerges[modEntry.Id] = new List();
893 |
894 | if (jsonMerges[modEntry.Id].Contains(modEntry.Path)) // TODO: is this necessary?
895 | continue;
896 |
897 | // this assumes that .json can only have a single type
898 | // typeCache will always contain this path
899 | modEntry.Type = GetTypesFromCache(modEntry.Id)[0];
900 |
901 | Log($"\tMerge: \"{GetRelativePath(modEntry.Path, ModsDirectory)}\" ({modEntry.Type})");
902 |
903 | jsonMerges[modEntry.Id].Add(modEntry.Path);
904 | continue;
905 | }
906 |
907 | foreach (var type in types)
908 | {
909 | var subModEntry = new ModDef.ManifestEntry(modEntry, modEntry.Path, modEntry.Id);
910 | subModEntry.Type = type;
911 | AddModEntry(CachedVersionManifest, subModEntry);
912 |
913 | // clear json merges for this entry, mod is overwriting the original file, previous mods merges are tossed
914 | if (jsonMerges.ContainsKey(modEntry.Id))
915 | {
916 | jsonMerges.Remove(modEntry.Id);
917 | Log($"\t\tHad merges for {modEntry.Id} but had to toss, since original file is being replaced");
918 | }
919 | }
920 |
921 | continue;
922 | }
923 |
924 | // get "fake" entries that don't actually go into the game's VersionManifest
925 | // add videos to be loaded from an external path
926 | switch (modEntry.Type)
927 | {
928 | case "Video":
929 | var fileName = Path.GetFileName(modEntry.Path);
930 | if (fileName != null && File.Exists(modEntry.Path))
931 | {
932 | Log($"\tVideo: \"{GetRelativePath(modEntry.Path, ModsDirectory)}\"");
933 | ModVideos.Add(fileName, modEntry.Path);
934 | }
935 | continue;
936 | case "AdvancedJSONMerge":
937 | var id = AdvancedJSONMerger.GetTargetID(modEntry.Path);
938 |
939 | // need to add the types of the file to the typeCache, so that they can be used later
940 | // if merging onto a file added by another mod, the type is already in the cache
941 | var types = GetTypesFromCacheOrManifest(CachedVersionManifest, id);
942 |
943 | if (!jsonMerges.ContainsKey(id))
944 | jsonMerges[id] = new List();
945 |
946 | if (jsonMerges[id].Contains(modEntry.Path)) // TODO: is this necessary?
947 | continue;
948 |
949 | Log($"\tAdvancedJSONMerge: \"{GetRelativePath(modEntry.Path, ModsDirectory)}\" ({types[0]})");
950 | jsonMerges[id].Add(modEntry.Path);
951 | continue;
952 | }
953 |
954 | // non-streamingassets json merges
955 | if (Path.GetExtension(modEntry.Path)?.ToLower() == ".json" && modEntry.ShouldMergeJSON)
956 | {
957 | // have to find the original path for the manifest entry that we're merging onto
958 | var matchingEntry = GetEntryFromCachedOrBTRLEntries(modEntry.Id);
959 |
960 | if (matchingEntry == null)
961 | {
962 | Log($"\tCould not find an existing VersionManifest entry for {modEntry.Id}!");
963 | continue;
964 | }
965 |
966 | var matchingPath = Path.GetFullPath(matchingEntry.FilePath);
967 |
968 | if (!jsonMerges.ContainsKey(modEntry.Id))
969 | jsonMerges[modEntry.Id] = new List();
970 |
971 | if (jsonMerges[modEntry.Id].Contains(modEntry.Path)) // TODO: is this necessary?
972 | continue;
973 |
974 | Log($"\tMerge: \"{GetRelativePath(modEntry.Path, ModsDirectory)}\" ({modEntry.Type})");
975 |
976 | // this assumes that .json can only have a single type
977 | modEntry.Type = matchingEntry.Type;
978 | TryAddTypeToCache(modEntry.Id, modEntry.Type);
979 | jsonMerges[modEntry.Id].Add(modEntry.Path);
980 | continue;
981 | }
982 |
983 | AddModEntry(CachedVersionManifest, modEntry);
984 | TryAddTypeToCache(modEntry.Id, modEntry.Type);
985 |
986 | // clear json merges for this entry, mod is overwriting the original file, previous mods merges are tossed
987 | if (jsonMerges.ContainsKey(modEntry.Id))
988 | {
989 | jsonMerges.Remove(modEntry.Id);
990 | Log($"\t\tHad merges for {modEntry.Id} but had to toss, since original file is being replaced");
991 | }
992 | }
993 | }
994 |
995 | WriteJsonFile(TypeCachePath, typeCache);
996 |
997 | // perform merges into cache
998 | Log("");
999 | LogWithDate("Doing merges...");
1000 | yield return new ProgressReport(1, "Merging", "");
1001 |
1002 | var mergeCount = 0;
1003 | foreach (var id in jsonMerges.Keys)
1004 | {
1005 | var existingEntry = GetEntryFromCachedOrBTRLEntries(id);
1006 | if (existingEntry == null)
1007 | {
1008 | Log($"\tHave merges for {id} but cannot find an original file! Skipping.");
1009 | continue;
1010 | }
1011 |
1012 | var originalPath = Path.GetFullPath(existingEntry.FilePath);
1013 | var mergePaths = jsonMerges[id];
1014 |
1015 | if (!jsonMergeCache.HasCachedEntry(originalPath, mergePaths))
1016 | yield return new ProgressReport(mergeCount++ / ((float)jsonMerges.Count), "Merging", id);
1017 |
1018 | var cachePath = jsonMergeCache.GetOrCreateCachedEntry(originalPath, mergePaths);
1019 |
1020 | // something went wrong (the parent json prob had errors)
1021 | if (cachePath == null)
1022 | continue;
1023 |
1024 | var cacheEntry = new ModDef.ManifestEntry(cachePath)
1025 | {
1026 | ShouldMergeJSON = false,
1027 | Type = GetTypesFromCache(id)[0], // this assumes only one type for each json file
1028 | Id = id
1029 | };
1030 |
1031 | AddModEntry(CachedVersionManifest, cacheEntry);
1032 | }
1033 |
1034 | jsonMergeCache.WriteCacheToDisk(Path.Combine(CacheDirectory, MERGE_CACHE_FILE_NAME));
1035 |
1036 | Log("");
1037 | Log("Syncing Database");
1038 | yield return new ProgressReport(1, "Syncing Database", "");
1039 |
1040 | // check if files removed from DB cache
1041 | var rebuildDB = false;
1042 | var replacementEntries = new List();
1043 | var removeEntries = new List();
1044 | foreach (var path in dbCache.Keys)
1045 | {
1046 | var absolutePath = ResolvePath(path, GameDirectory);
1047 |
1048 | // check if the file in the db cache is still used
1049 | if (BTRLEntries.Exists(x => x.Path == absolutePath))
1050 | continue;
1051 |
1052 | Log($"\tNeed to remove DB entry from file in path: {path}");
1053 |
1054 | // file is missing, check if another entry exists with same filename in manifest or in BTRL entries
1055 | var fileName = Path.GetFileName(path);
1056 | var existingEntry = BTRLEntries.FindLast(x => Path.GetFileName(x.Path) == fileName)?.GetVersionManifestEntry()
1057 | ?? CachedVersionManifest.Find(x => Path.GetFileName(x.FilePath) == fileName);
1058 |
1059 | if (existingEntry == null)
1060 | {
1061 | Log("\t\tHave to rebuild DB, no existing entry in VersionManifest matches removed entry");
1062 | rebuildDB = true;
1063 | break;
1064 | }
1065 |
1066 | replacementEntries.Add(existingEntry);
1067 | removeEntries.Add(path);
1068 | }
1069 |
1070 | // add removed entries replacements to db
1071 | if (!rebuildDB)
1072 | {
1073 | // remove old entries
1074 | foreach (var removeEntry in removeEntries)
1075 | dbCache.Remove(removeEntry);
1076 |
1077 | using (var metadataDatabase = new MetadataDatabase())
1078 | {
1079 | foreach (var replacementEntry in replacementEntries)
1080 | {
1081 | if (AddModEntryToDB(metadataDatabase, Path.GetFullPath(replacementEntry.FilePath), replacementEntry.Type))
1082 | Log($"\t\tReplaced DB entry with an existing entry in path: {Path.GetFullPath(replacementEntry.FilePath)}");
1083 | }
1084 | }
1085 | }
1086 |
1087 | // if an entry has been removed and we cannot find a replacement, have to rebuild the mod db
1088 | if (rebuildDB)
1089 | {
1090 | if (File.Exists(ModMDDBPath))
1091 | File.Delete(ModMDDBPath);
1092 |
1093 | File.Copy(MDDBPath, ModMDDBPath);
1094 | dbCache = new Dictionary();
1095 | }
1096 |
1097 | // add needed files to db
1098 | var addCount = 0;
1099 | using (var metadataDatabase = new MetadataDatabase())
1100 | {
1101 | foreach (var modEntry in BTRLEntries)
1102 | {
1103 | if (modEntry.AddToDB && AddModEntryToDB(metadataDatabase, modEntry.Path, modEntry.Type))
1104 | {
1105 | yield return new ProgressReport(addCount / ((float)BTRLEntries.Count), "Populating Database", modEntry.Id);
1106 | Log($"\tAdded/Updated {modEntry.Id} ({modEntry.Type})");
1107 | }
1108 | addCount++;
1109 | }
1110 | }
1111 |
1112 | // write db/type cache to disk
1113 | WriteJsonFile(DBCachePath, dbCache);
1114 |
1115 | stopwatch.Stop();
1116 | Log("");
1117 | LogWithDate($"Done. Elapsed running time: {stopwatch.Elapsed.TotalSeconds} seconds\n");
1118 | CloseLogStream();
1119 |
1120 | yield break;
1121 | }
1122 | }
1123 | }
1124 |
--------------------------------------------------------------------------------
/ModTek/ModTek.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | True
6 | 7.2
7 | 0.4
8 |
9 |
10 | Debug
11 | AnyCPU
12 | {8D955C2C-D75B-453C-99D1-B337BBF82CCA}
13 | Library
14 | Properties
15 | ModTek
16 | ModTek
17 | v3.5
18 | 512
19 |
20 |
21 | none
22 | true
23 | bin\Release\
24 |
25 |
26 | prompt
27 | 4
28 |
29 |
30 |
31 | False
32 |
33 |
34 | False
35 |
36 |
37 | False
38 |
39 |
40 |
41 |
42 |
43 |
44 | False
45 |
46 |
47 | False
48 |
49 |
50 | False
51 |
52 |
53 | False
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/ModTek/ModTek.csproj.user.example:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ..\..\..\..\Library\Application Support\Steam\steamapps\common\BATTLETECH\BattleTech.app\Contents\Resources\Data\Managed
5 |
6 |
--------------------------------------------------------------------------------
/ModTek/Patches.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Reflection;
5 | using BattleTech;
6 | using BattleTech.Assetbundles;
7 | using BattleTech.Data;
8 | using BattleTech.Rendering;
9 | using BattleTech.Save;
10 | using BattleTech.UI;
11 | using Harmony;
12 | using RenderHeads.Media.AVProVideo;
13 | using UnityEngine;
14 |
15 | // ReSharper disable InconsistentNaming
16 | // ReSharper disable UnusedMember.Global
17 |
18 | namespace ModTek
19 | {
20 | [HarmonyPatch(typeof(VersionInfo), "GetReleaseVersion")]
21 | public static class VersionInfo_GetReleaseVersion_Patch
22 | {
23 | public static void Postfix(ref string __result)
24 | {
25 | var old = __result;
26 | __result = old + $" w/ ModTek v{Assembly.GetExecutingAssembly().GetName().Version}";
27 | }
28 | }
29 |
30 | [HarmonyPatch(typeof(AssetBundleManager), "AssetBundleNameToFilepath")]
31 | public static class AssetBundleManager_AssetBundleNameToFilepath_Patch
32 | {
33 | public static void Postfix(string assetBundleName, ref string __result)
34 | {
35 | if (!ModTek.ModAssetBundlePaths.ContainsKey(assetBundleName))
36 | return;
37 |
38 | __result = ModTek.ModAssetBundlePaths[assetBundleName];
39 | }
40 | }
41 |
42 | [HarmonyPatch(typeof(AssetBundleManager), "AssetBundleNameToFileURL")]
43 | public static class AssetBundleManager_AssetBundleNameToFileURL_Patch
44 | {
45 | public static void Postfix(string assetBundleName, ref string __result)
46 | {
47 | if (!ModTek.ModAssetBundlePaths.ContainsKey(assetBundleName))
48 | return;
49 |
50 | __result = $"file://{ModTek.ModAssetBundlePaths[assetBundleName]}";
51 | }
52 | }
53 |
54 | [HarmonyPatch(typeof(MetadataDatabase))]
55 | [HarmonyPatch("MDD_DB_PATH", MethodType.Getter)]
56 | public static class MetadataDatabase_MDD_DB_PATH_Patch
57 | {
58 | public static void Postfix(ref string __result)
59 | {
60 | if (string.IsNullOrEmpty(ModTek.ModMDDBPath))
61 | return;
62 |
63 | __result = ModTek.ModMDDBPath;
64 | }
65 | }
66 |
67 | [HarmonyPatch(typeof(AVPVideoPlayer), "PlayVideo")]
68 | public static class AVPVideoPlayer_PlayVideo_Patch
69 | {
70 | public static bool Prefix(AVPVideoPlayer __instance, string video, Language language, Action onComplete = null)
71 | {
72 | if (!ModTek.ModVideos.ContainsKey(video))
73 | return true;
74 |
75 | // THIS CODE IS REWRITTEN FROM DECOMPILED HBS CODE
76 | // AND IS NOT SUBJECT TO MODTEK LICENSE
77 |
78 | var instance = Traverse.Create(__instance);
79 | var AVPMediaPlayer = instance.Field("AVPMediaPlayer").GetValue();
80 |
81 | if (AVPMediaPlayer.Control == null)
82 | {
83 | instance.Method("ConfigureMediaPlayer").GetValue();
84 | }
85 | AVPMediaPlayer.OpenVideoFromFile(MediaPlayer.FileLocation.AbsolutePathOrURL, ModTek.ModVideos[video], false);
86 | if (ActiveOrDefaultSettings.CloudSettings.subtitles)
87 | {
88 | instance.Method("LoadSubtitle", video, language.ToString()).GetValue();
89 | }
90 | else
91 | {
92 | AVPMediaPlayer.DisableSubtitles();
93 | }
94 | BTPostProcess.SetUIPostprocessing(false);
95 |
96 | instance.Field("OnPlayerComplete").SetValue(onComplete);
97 | instance.Method("Initialize").GetValue();
98 |
99 | // END REWRITTEN DECOMPILED HBS CODE
100 |
101 | return false;
102 | }
103 | }
104 |
105 | [HarmonyPatch(typeof(SimGame_MDDExtensions), "UpdateContract")]
106 | public static class SimGame_MDDExtensions_UpdateContract_Patch
107 | {
108 | public static void Prefix(ref string fileID)
109 | {
110 | if (Path.IsPathRooted(fileID))
111 | fileID = Path.GetFileNameWithoutExtension(fileID);
112 | }
113 | }
114 |
115 | [HarmonyPatch(typeof(VersionManifestUtilities), "LoadDefaultManifest")]
116 | public static class VersionManifestUtilities_LoadDefaultManifest_Patch
117 | {
118 | public static bool Prefix(ref VersionManifest __result)
119 | {
120 | // Return the cached manifest if it exists -- otherwise call the method as normal
121 | if (ModTek.CachedVersionManifest != null)
122 | {
123 | __result = ModTek.CachedVersionManifest;
124 | return false;
125 | }
126 | else
127 | {
128 | return true;
129 | }
130 | }
131 | }
132 |
133 | [HarmonyPatch(typeof(BattleTechResourceLocator), "RefreshTypedEntries")]
134 | public static class BattleTechResourceLocator_RefreshTypedEntries_Patch
135 | {
136 | public static void Postfix(ContentPackIndex ___contentPackIndex,
137 | Dictionary> ___baseManifest,
138 | Dictionary> ___contentPacksManifest,
139 | Dictionary>> ___addendumsManifest)
140 | {
141 | if (ModTek.BTRLEntries.Count > 0)
142 | {
143 | foreach(var entry in ModTek.BTRLEntries)
144 | {
145 | var versionManifestEntry = entry.GetVersionManifestEntry();
146 | var resourceType = (BattleTechResourceType)Enum.Parse(typeof(BattleTechResourceType), entry.Type);
147 |
148 | if (___contentPackIndex == null || ___contentPackIndex.IsResourceOwned(entry.Id))
149 | {
150 | // add to baseManifest
151 | if (!___baseManifest.ContainsKey(resourceType))
152 | ___baseManifest.Add(resourceType, new Dictionary());
153 |
154 | ___baseManifest[resourceType][entry.Id] = versionManifestEntry;
155 | }
156 | else
157 | {
158 | // add to contentPackManifest
159 | if (!___contentPacksManifest.ContainsKey(resourceType))
160 | ___contentPacksManifest.Add(resourceType, new Dictionary());
161 |
162 | ___contentPacksManifest[resourceType][entry.Id] = versionManifestEntry;
163 | }
164 |
165 | if (!string.IsNullOrEmpty(entry.AddToAddendum))
166 | {
167 | // add to addendumsManifest
168 | var addendum = ModTek.CachedVersionManifest.GetAddendumByName(entry.AddToAddendum);
169 | if (addendum != null)
170 | {
171 | if (!___addendumsManifest.ContainsKey(addendum))
172 | ___addendumsManifest.Add(addendum, new Dictionary>());
173 |
174 | if (!___addendumsManifest[addendum].ContainsKey(resourceType))
175 | ___addendumsManifest[addendum].Add(resourceType, new Dictionary());
176 |
177 | ___addendumsManifest[addendum][resourceType][entry.Id] = versionManifestEntry;
178 | }
179 | }
180 | }
181 | }
182 | }
183 | }
184 |
185 | // This will disable activateAfterInit from functioning for the Start() on the "Main" game object which activates the BattleTechGame object
186 | // This stops the main game object from loading immediately -- so work can be done beforehand
187 | [HarmonyPatch(typeof(ActivateAfterInit), "Start")]
188 | public static class ActivateAfterInit_Start_Patch
189 | {
190 | public static bool Prefix(ActivateAfterInit __instance)
191 | {
192 | Traverse trav = Traverse.Create(__instance);
193 | if (ActivateAfterInit.ActivateAfter.Start.Equals(trav.Field("activateAfter").GetValue()))
194 | {
195 | GameObject[] gameObjects = trav.Field("activationSet").GetValue();
196 | foreach (var gameObject in gameObjects)
197 | {
198 | if ("BattleTechGame".Equals(gameObject.name))
199 | {
200 | // Don't activate through this call!
201 | return false;
202 | }
203 | }
204 | }
205 | // Call the method
206 | return true;
207 | }
208 | }
209 | }
210 |
211 |
--------------------------------------------------------------------------------
/ModTek/ProgressPanel.cs:
--------------------------------------------------------------------------------
1 | using Harmony;
2 | using System;
3 | using System.Collections;
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 | using System.IO;
7 | using UnityEngine;
8 | using UnityEngine.UI;
9 |
10 | namespace ModTek
11 | {
12 | internal struct ProgressReport
13 | {
14 | public float Progress { get; set; }
15 | public string SliderText { get; set; }
16 | public string LoadingText { get; set; }
17 |
18 | public ProgressReport(float progress, string sliderText, string loadingText)
19 | {
20 | Progress = progress;
21 | SliderText = sliderText;
22 | LoadingText = loadingText;
23 | }
24 | }
25 |
26 | internal static class ProgressPanel
27 | {
28 | public const string ASSET_BUNDLE_NAME = "modtekassetbundle";
29 | private static ProgressBarLoadingBehavior LoadingBehavior;
30 |
31 | public class ProgressBarLoadingBehavior : MonoBehaviour
32 | {
33 | private static readonly int FRAME_TIME = 50; // around 20fps
34 |
35 | public Text SliderText { get; set; }
36 | public Text LoadingText { get; set; }
37 | public Slider Slider { get; set; }
38 | public Action FinishAction { get; set; }
39 |
40 | private LinkedList>> WorkList = new LinkedList>>();
41 |
42 | void Start()
43 | {
44 | StartCoroutine(RunWorkList());
45 | }
46 |
47 | public void SubmitWork(Func> work)
48 | {
49 | WorkList.AddLast(work);
50 | }
51 |
52 | private IEnumerator RunWorkList()
53 | {
54 | var sw = new Stopwatch();
55 | sw.Start();
56 | foreach (var workFunc in WorkList)
57 | {
58 | var workEnumerator = workFunc.Invoke();
59 | while (workEnumerator.MoveNext())
60 | {
61 | if (sw.ElapsedMilliseconds > FRAME_TIME)
62 | {
63 | var report = workEnumerator.Current;
64 | Slider.value = report.Progress;
65 | SliderText.text = report.SliderText;
66 | LoadingText.text = report.LoadingText;
67 |
68 | sw.Reset();
69 | sw.Start();
70 | yield return null;
71 | }
72 | }
73 | yield return null;
74 | }
75 |
76 | Slider.value = 1.0f;
77 | SliderText.text = "Game now loading";
78 | LoadingText.text = "";
79 | yield return null;
80 |
81 | // TODO: why was this here
82 | // Let Finished stay on the screen for a moment
83 | // Thread.Sleep(1000);
84 |
85 | FinishAction.Invoke();
86 | yield break;
87 | }
88 | }
89 |
90 | internal static bool Initialize(string assetDirectory, string panelTitle)
91 | {
92 | var assetBundle = AssetBundle.LoadFromFile(Path.Combine(assetDirectory, ASSET_BUNDLE_NAME));
93 | if (assetBundle == null)
94 | {
95 | string message = $"Error loading asset bundle {ASSET_BUNDLE_NAME}";
96 | return false;
97 | }
98 |
99 | var canvasPrefab = assetBundle.LoadAsset("ProgressBar_Canvas");
100 | var canvasGameObject = GameObject.Instantiate(canvasPrefab);
101 |
102 | var panelTitleText = GameObject.Find("ProgressBar_Title")?.GetComponent();
103 | if (panelTitleText == null)
104 | {
105 | Logger.LogWithDate("Error loading ProgressBar_Title");
106 | return false;
107 | }
108 |
109 | var sliderText = GameObject.Find("ProgressBar_Slider_Text")?.GetComponent();
110 | if (sliderText == null)
111 | {
112 | Logger.LogWithDate("Error loading ProgressBar_Slider_Text");
113 | return false;
114 | }
115 |
116 | var loadingText = GameObject.Find("ProgressBar_Loading_Text")?.GetComponent();
117 | if (loadingText == null)
118 | {
119 | Logger.LogWithDate("Error loading ProgressBar_Loading_Text");
120 | return false;
121 | }
122 |
123 | var sliderGameObject = GameObject.Find("ProgressBar_Slider");
124 | var slider = sliderGameObject?.GetComponent();
125 | if (sliderGameObject == null)
126 | {
127 | Logger.LogWithDate("Error loading ProgressBar_Slider");
128 | return false;
129 | }
130 |
131 | panelTitleText.text = panelTitle;
132 |
133 | LoadingBehavior = sliderGameObject.AddComponent();
134 | LoadingBehavior.SliderText = sliderText;
135 | LoadingBehavior.LoadingText = loadingText;
136 | LoadingBehavior.Slider = slider;
137 | LoadingBehavior.FinishAction = () =>
138 | {
139 | assetBundle.Unload(true);
140 | GameObject.Destroy(canvasGameObject);
141 | TriggerGameLoading();
142 | };
143 |
144 | return true;
145 | }
146 |
147 | internal static void SubmitWork(Func> workFunc)
148 | {
149 | LoadingBehavior.SubmitWork(workFunc);
150 | }
151 |
152 | private static void TriggerGameLoading()
153 | {
154 | // Reactivate the main menu loading by calling the attached ActivateAndClose behavior on the UnityGameInstance (initializes a handful of different things);
155 | var activateAfterInit = GameObject.Find("Main").GetComponent();
156 | Traverse.Create(activateAfterInit).Method("ActivateAndClose").GetValue();
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/ModTek/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | // General Information about an assembly is controlled through the following
5 | // set of attributes. Change these attribute values to modify the information
6 | // associated with an assembly.
7 | [assembly: AssemblyTitle("ModTek")]
8 | [assembly: AssemblyDescription("A mod system for HBS's BattleTech PC Game.")]
9 | [assembly: AssemblyConfiguration("")]
10 | [assembly: AssemblyCompany("")]
11 | [assembly: AssemblyProduct("ModTek")]
12 | [assembly: AssemblyCopyright("Public domain under the Unlicence")]
13 | [assembly: AssemblyTrademark("")]
14 | [assembly: AssemblyCulture("")]
15 |
16 | // Setting ComVisible to false makes the types in this assembly not visible
17 | // to COM components. If you need to access a type in this assembly from
18 | // COM, set the ComVisible attribute to true on that type.
19 | [assembly: ComVisible(false)]
20 |
21 | // The following GUID is for the ID of the typelib if this project is exposed to COM
22 | [assembly: Guid("8d955c2c-d75b-453c-99d1-b337bbf82cca")]
23 |
24 | // Version information for an assembly consists of the following four values:
25 | //
26 | // Major Version
27 | // Minor Version
28 | // Build Number
29 | // Revision
30 | //
31 | // You can specify all the values or you can default the Build and Revision Numbers
32 | // by using the '*' as shown below:
33 | // [assembly: AssemblyVersion("1.0.*")]
34 | [assembly: AssemblyVersion("0.5.0.*")]
35 |
--------------------------------------------------------------------------------
/ModTek/modtekassetbundle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janxious/ModTek/af3a57e4db71087b700bb1e9c7c0ea3eab99c839/ModTek/modtekassetbundle
--------------------------------------------------------------------------------
/ModTek/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/ModTekUnitTests/AdvancedJSONMergeInstructionTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using ModTek;
3 | using Newtonsoft.Json;
4 | using Newtonsoft.Json.Linq;
5 |
6 | namespace ModTekUnitTests
7 | {
8 | [TestClass]
9 | public class AdvancedJSONMergeInstructionTests
10 | {
11 | private JObject root;
12 |
13 | [TestInitialize]
14 | public void Initialize()
15 | {
16 | root = JObject.Parse(
17 | @"
18 | {
19 | ""objectkey1"": ""value1"",
20 | ""objectkey2"": [
21 | ""arrayvalue1"",
22 | {
23 | ""objectkey3"": ""value4"",
24 | ""objectkey4"": [
25 | ""arrayvalue2""
26 | ]
27 | },
28 | {
29 | ""objectkey5"": [
30 | ""arrayvalue3""
31 | ],
32 | ""objectkey6"": ""value5""
33 | },
34 | ""arrayvalue4""
35 | ]
36 | }
37 | ");
38 | }
39 |
40 | [TestMethod]
41 | public void Test()
42 | {
43 | var a = root.SelectToken("$.objectkey1");
44 | var b = a.Parent;
45 | b.Remove();
46 | var test = root["objectkey1"];
47 | Assert.IsNull(test);
48 | }
49 |
50 | [TestMethod]
51 | public void RemoveFromObject()
52 | {
53 | Assert.AreEqual("value1", root["objectkey1"]);
54 | ProcessInstructionJSON(@"
55 | {
56 | ""JSONPath"": ""$.objectkey1"",
57 | ""Action"": ""Remove""
58 | }
59 | ");
60 | Assert.IsNull(root["objectkey1"]);
61 | }
62 |
63 | [TestMethod]
64 | public void ReplaceInObject()
65 | {
66 | Assert.AreNotEqual("newvalue", root["objectkey2"]);
67 | ProcessInstructionJSON(@"
68 | {
69 | ""JSONPath"": ""$.objectkey2"",
70 | ""Action"": ""Replace"",
71 | ""Value"": ""newvalue""
72 | }
73 | ");
74 | Assert.AreEqual("newvalue", root["objectkey2"]);
75 | }
76 |
77 | [TestMethod]
78 | public void RemoveFromArray()
79 | {
80 | Assert.AreEqual("arrayvalue1", root["objectkey2"][0]);
81 | ProcessInstructionJSON(@"
82 | {
83 | ""JSONPath"": ""$.objectkey2[0]"",
84 | ""Action"": ""Remove""
85 | }
86 | ");
87 | Assert.AreNotEqual("arrayvalue1", root["objectkey2"][0]);
88 | }
89 |
90 | [TestMethod]
91 | public void ReplaceInArray()
92 | {
93 | Assert.AreNotEqual("newvalue", root["objectkey2"][0]);
94 | ProcessInstructionJSON(@"
95 | {
96 | ""JSONPath"": ""$.objectkey2[0]"",
97 | ""Action"": ""Replace"",
98 | ""Value"": ""newvalue""
99 | }
100 | ");
101 | Assert.AreEqual("newvalue", root["objectkey2"][0]);
102 | }
103 |
104 | [TestMethod]
105 | public void AddBeforeInArray()
106 | {
107 | var oldFirst = root["objectkey2"][0];
108 | Assert.AreNotEqual("newvalue", root["objectkey2"][0]);
109 | ProcessInstructionJSON(@"
110 | {
111 | ""JSONPath"": ""$.objectkey2[0]"",
112 | ""Action"": ""ArrayAddBefore"",
113 | ""Value"": ""newvalue""
114 | }
115 | ");
116 | Assert.AreEqual("newvalue", root["objectkey2"][0]);
117 | Assert.AreEqual(oldFirst, root["objectkey2"][1]);
118 | }
119 |
120 | [TestMethod]
121 | public void AddAfterInArray()
122 | {
123 | var currentFirst = root["objectkey2"][0];
124 | Assert.AreNotEqual("newvalue", root["objectkey2"][1]);
125 | ProcessInstructionJSON(@"
126 | {
127 | ""JSONPath"": ""$.objectkey2[0]"",
128 | ""Action"": ""ArrayAddAfter"",
129 | ""Value"": ""newvalue""
130 | }
131 | ");
132 | Assert.AreEqual(currentFirst, root["objectkey2"][0]);
133 | Assert.AreEqual("newvalue", root["objectkey2"][1]);
134 | }
135 |
136 | [TestMethod]
137 | public void AddBeforeInArrayWithArray()
138 | {
139 | var oldFirst = root["objectkey2"][0];
140 | Assert.IsNotNull(oldFirst);
141 |
142 | ProcessInstructionJSON(@"
143 | {
144 | ""JSONPath"": ""$.objectkey2[0]"",
145 | ""Action"": ""ArrayAddBefore"",
146 | ""Value"": [""newvalue1"", ""newvalue2""]
147 | }
148 | ");
149 | Assert.AreEqual(new JArray("newvalue1", "newvalue2").ToString(), root["objectkey2"][0].ToString());
150 | Assert.AreEqual(oldFirst, root["objectkey2"][1]);
151 | }
152 |
153 | [TestMethod]
154 | public void AddInArrayWithArray()
155 | {
156 | var oldLast = root.SelectToken("$.objectkey2[-1:]").ToString();
157 | Assert.IsNotNull(oldLast);
158 |
159 | ProcessInstructionJSON(@"
160 | {
161 | ""JSONPath"": ""$.objectkey2"",
162 | ""Action"": ""ArrayAdd"",
163 | ""Value"": [""newvalue1"", ""newvalue2""]
164 | }
165 | ");
166 | Assert.AreEqual(oldLast, root.SelectToken("$.objectkey2[-2:-1:]").ToString());
167 | Assert.AreEqual(new JArray("newvalue1", "newvalue2").ToString(), root.SelectToken("$.objectkey2[-1:]").ToString());
168 | }
169 |
170 | [TestMethod]
171 | public void ConactInArrayWithArray()
172 | {
173 | var oldLast = root.SelectToken("$.objectkey2[-1:]").ToString();
174 | Assert.IsNotNull(oldLast);
175 |
176 | ProcessInstructionJSON(@"
177 | {
178 | ""JSONPath"": ""$.objectkey2"",
179 | ""Action"": ""ArrayConcat"",
180 | ""Value"": [""newvalue1"", ""newvalue2""]
181 | }
182 | ");
183 | Assert.AreEqual(oldLast, root.SelectToken("$.objectkey2[-3:-2:]").ToString());
184 | Assert.AreEqual("newvalue1", root.SelectToken("$.objectkey2[-2:-1:]").ToString());
185 | Assert.AreEqual("newvalue2", root.SelectToken("$.objectkey2[-1:]").ToString());
186 | }
187 |
188 | [TestMethod]
189 | public void MergeRootObject()
190 | {
191 | Assert.IsNull(root["newobjectkey"]);
192 | ProcessInstructionJSON(@"
193 | {
194 | ""JSONPath"": ""$"",
195 | ""Action"": ""ObjectMerge"",
196 | ""Value"": { ""newobjectkey"": ""newvalue"" }
197 | }
198 | ");
199 | Assert.AreEqual("newvalue", root["newobjectkey"]);
200 | }
201 |
202 | [TestMethod]
203 | public void MergeNestedObject()
204 | {
205 | Assert.IsNull(root["objectkey2"][1]["newobjectkey"]);
206 | ProcessInstructionJSON(@"
207 | {
208 | ""JSONPath"": ""$.objectkey2[1]"",
209 | ""Action"": ""ObjectMerge"",
210 | ""Value"": { ""newobjectkey"": ""newvalue"" }
211 | }
212 | ");
213 | Assert.AreEqual("newvalue", root["objectkey2"][1]["newobjectkey"]);
214 | }
215 |
216 | private void ProcessInstructionJSON(string json)
217 | {
218 | var instruction = JsonConvert.DeserializeObject(json);
219 | instruction.Process(root);
220 | }
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/ModTekUnitTests/ModTekUnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {0924FD5F-434E-4278-A326-0F43697F4355}
8 | Library
9 | Properties
10 | ModTekUnitTests
11 | ModTekUnitTests
12 | v4.6.1
13 | 512
14 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | 15.0
16 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
17 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages
18 | False
19 | UnitTest
20 |
21 |
22 |
23 |
24 | true
25 | full
26 | false
27 | bin\Debug\
28 | DEBUG;TRACE
29 | prompt
30 | 4
31 |
32 |
33 | pdbonly
34 | true
35 | bin\Release\
36 | TRACE
37 | prompt
38 | 4
39 |
40 |
41 |
42 | ..\packages\MSTest.TestFramework.1.3.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll
43 |
44 |
45 | ..\packages\MSTest.TestFramework.1.3.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll
46 |
47 |
48 | False
49 | ..\packages\Newtonsoft.Json.10.0.3\lib\net35\Newtonsoft.Json.dll
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {8d955c2c-d75b-453c-99d1-b337bbf82cca}
64 | ModTek
65 |
66 |
67 |
68 |
69 |
70 |
71 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/ModTekUnitTests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | [assembly: AssemblyTitle("")]
5 | [assembly: AssemblyDescription("")]
6 | [assembly: AssemblyConfiguration("")]
7 | [assembly: AssemblyCompany("")]
8 | [assembly: AssemblyProduct("")]
9 | [assembly: AssemblyCopyright("")]
10 | [assembly: AssemblyTrademark("")]
11 | [assembly: AssemblyCulture("")]
12 |
13 | [assembly: ComVisible(false)]
14 |
15 | [assembly: Guid("0924fd5f-434e-4278-a326-0f43697f4355")]
16 |
17 | // [assembly: AssemblyVersion("1.0.*")]
18 | [assembly: AssemblyVersion("1.0.0.0")]
19 | [assembly: AssemblyFileVersion("1.0.0.0")]
20 |
--------------------------------------------------------------------------------
/ModTekUnitTests/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # You should use the version of ModTek at https://github.com/BattletechModders/ModTek/. This was the primary fork for most of 2018 but is no longer actively maintained.
2 |
3 | # ModTek
4 |
5 | No. Really. Use https://github.com/BattletechModders/ModTek/.
6 |
7 | ## License
8 |
9 | ModTek is provided under the [Unlicense](UNLICENSE), which releases the work into the public domain.
10 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------