├── .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 | --------------------------------------------------------------------------------