From 0daf586e23e7b85fd7817987ca55aa48973b1e02 Mon Sep 17 00:00:00 2001 From: jungooji <46273764+zzzz465@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:19:51 +0900 Subject: [PATCH 01/11] fix: load the 1.6 assembly on 1.6 and mark 1.7 as supported LoadFolders.xml only defined v1.1-v1.5, so RimWorld fell back to the highest defined version (1.5) and the assembly in 1.6/Assemblies was never loaded. Add the v1.6 entry and list 1.7 in supportedVersions. Co-Authored-By: Claude Opus 5.5 --- About/About.xml | 1 + LoadFolders.xml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/About/About.xml b/About/About.xml index 07731f9..461b1a1 100644 --- a/About/About.xml +++ b/About/About.xml @@ -11,6 +11,7 @@
  • 1.4
  • 1.5
  • 1.6
  • +
  • 1.7
  • Madeline.ModMismatchFormatter ==english description== diff --git a/LoadFolders.xml b/LoadFolders.xml index dfed61c..a74def4 100644 --- a/LoadFolders.xml +++ b/LoadFolders.xml @@ -20,4 +20,8 @@
  • /
  • 1.5
  • + +
  • /
  • +
  • 1.6
  • +
    From 2b2a222e6f5a529715e6158279acf2b2b67ebe56 Mon Sep 17 00:00:00 2001 From: jungooji <46273764+zzzz465@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:19:51 +0900 Subject: [PATCH 02/11] build: SDK-style project targeting RimWorld 1.6 and 1.7, add CI - Replace the 2019 VS project (hard-coded Steam paths, Harmony 2.0) with an SDK-style multi-target project: - net472 -> RimWorld 1.6 (Unity/Mono), references Krafs.Rimworld.Ref 1.6.4871 and Lib.Harmony.Ref 2.4.3, output 1.6/Assemblies - net10.0 -> RimWorld 1.7 (Waylith/CoreCLR), references the game's Assembly-CSharp.dll and UnityStubs.dll plus Lib.Harmony.Ref 3.0.0-rc.2, output 1.7/Assemblies - The 1.7 target needs a local game copy (RimWorld17Dir, auto-detected from the Steam install or set in Source/Local.props) because no 1.7 reference package exists yet. - Add LoadFolders entry for 1.7. - Add a GitHub Actions workflow that validates mod XML and builds the 1.6 target. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 46 ++++++++ .gitignore | 4 +- 1.6/Assemblies/ModMisMatchWindowPatch.dll | Bin 28672 -> 28160 bytes 1.7/Assemblies/ModMisMatchWindowPatch.dll | Bin 0 -> 28672 bytes LoadFolders.xml | 4 + Source/Directory.Build.props | 24 ++++ Source/ModMisMatchWindowPatch.csproj | 106 ------------------ Source/ModMisMatchWindowPatch.sln | 25 ----- Source/ModMismatchFormatter.slnx | 3 + .../HarmonyPatches.cs | 0 .../ModMismatchFormatter.csproj | 50 +++++++++ .../Refactored/ColorPresets.cs | 0 .../Refactored/ListExtension.cs | 0 .../Refactored/MetaHeaderUtility.cs | 0 .../Refactored/Mod.cs | 0 .../Refactored/ModContentPackExtension.cs | 0 .../Refactored/ModListerElementRenderer.cs | 0 .../Refactored/ModMismatchFormatter.cs | 0 .../Refactored/ModMismatchWindow.cs | 0 .../Refactored/ModPair.cs | 0 .../Refactored/ModPresenter.cs | 0 .../Refactored/Settings.cs | 0 .../Refactored/SimpleLog.cs | 0 .../Refactored/UITester.cs | 0 .../Refactored/iModListerElementRenderer.cs | 0 .../Refactored/iOrderFormatter.cs | 0 Source/Properties/AssemblyInfo.cs | 36 ------ 27 files changed, 130 insertions(+), 168 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 1.7/Assemblies/ModMisMatchWindowPatch.dll create mode 100644 Source/Directory.Build.props delete mode 100644 Source/ModMisMatchWindowPatch.csproj delete mode 100644 Source/ModMisMatchWindowPatch.sln create mode 100644 Source/ModMismatchFormatter.slnx rename Source/{ => ModMismatchFormatter}/HarmonyPatches.cs (100%) create mode 100644 Source/ModMismatchFormatter/ModMismatchFormatter.csproj rename Source/{ => ModMismatchFormatter}/Refactored/ColorPresets.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/ListExtension.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/MetaHeaderUtility.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/Mod.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/ModContentPackExtension.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/ModListerElementRenderer.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/ModMismatchFormatter.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/ModMismatchWindow.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/ModPair.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/ModPresenter.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/Settings.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/SimpleLog.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/UITester.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/iModListerElementRenderer.cs (100%) rename Source/{ => ModMismatchFormatter}/Refactored/iOrderFormatter.cs (100%) delete mode 100644 Source/Properties/AssemblyInfo.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1c58d6e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,46 @@ +name: build + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Validate mod XML + run: | + python3 - <<'PY' + import pathlib, sys, xml.etree.ElementTree as ET + files = [p for p in pathlib.Path('.').rglob('*.xml') if not any(part in ('bin', 'obj', '.git') for part in p.parts)] + failed = False + for path in files: + try: + ET.parse(path) + except ET.ParseError as e: + print(f'::error file={path}::{e}') + failed = True + print(f'checked {len(files)} XML files') + sys.exit(1 if failed else 0) + PY + + # RimWorld 1.7 has no reference package on NuGet yet, so CI builds the + # 1.6 (net472) target only. The 1.7 target is built locally with RimWorld17Dir. + - name: Build + run: dotnet build Source/ModMismatchFormatter.slnx -c Release + + - uses: actions/upload-artifact@v4 + with: + name: assemblies-1.6 + path: 1.6/Assemblies/ModMisMatchWindowPatch.dll diff --git a/.gitignore b/.gitignore index 880b93b..f446019 100644 --- a/.gitignore +++ b/.gitignore @@ -355,4 +355,6 @@ MigrationBackup/ .ionide/ #custom folders -obj \ No newline at end of file +obj +# Machine-specific MSBuild overrides +Source/Local.props diff --git a/1.6/Assemblies/ModMisMatchWindowPatch.dll b/1.6/Assemblies/ModMisMatchWindowPatch.dll index 4ad4ca667ba3fd6e20c16a1d3267cc2ab706c730..d29c33cb7467687d3ca45bf7de386052e37e4f12 100644 GIT binary patch literal 28160 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 XT=X@e{wCP>FT1G3{}lXx?1BFat7KD@ diff --git a/1.7/Assemblies/ModMisMatchWindowPatch.dll b/1.7/Assemblies/ModMisMatchWindowPatch.dll new file mode 100644 index 0000000000000000000000000000000000000000..682ad9c1289c2f3f9829e0f8cb6987ea4750374c GIT binary patch literal 28672 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/

  • 1.6
  • + +
  • /
  • +
  • 1.7
  • +
    diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props new file mode 100644 index 0000000..aad0f0d --- /dev/null +++ b/Source/Directory.Build.props @@ -0,0 +1,24 @@ + + + + + + latest + enable + disable + true + true + + + + + C:\Program Files (x86)\Steam\steamapps\common\RimWorld + $(RimWorldSteamDir) + + diff --git a/Source/ModMisMatchWindowPatch.csproj b/Source/ModMisMatchWindowPatch.csproj deleted file mode 100644 index 8ec6329..0000000 --- a/Source/ModMisMatchWindowPatch.csproj +++ /dev/null @@ -1,106 +0,0 @@ - - - - - Debug - AnyCPU - {B7F7D989-B085-4B3F-9423-462A50D77C02} - Library - Properties - ModMisMatchWindowPatch - ModMisMatchWindowPatch - v4.7.2 - 512 - true - - - - true - full - true - ..\1.6\Assemblies\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - ..\v1.1\Assemblies\ - TRACE - prompt - 4 - false - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\Assembly-CSharp.dll - False - - - - - - ..\..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.dll - False - - - ..\..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll - False - - - ..\..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.IMGUIModule.dll - False - - - ..\..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.TextRenderingModule.dll - False - - - ..\..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.UI.dll - False - - - ..\..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.UIModule.dll - False - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Source/ModMisMatchWindowPatch.sln b/Source/ModMisMatchWindowPatch.sln deleted file mode 100644 index 97df075..0000000 --- a/Source/ModMisMatchWindowPatch.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29230.47 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModMisMatchWindowPatch", "ModMisMatchWindowPatch.csproj", "{B7F7D989-B085-4B3F-9423-462A50D77C02}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B7F7D989-B085-4B3F-9423-462A50D77C02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B7F7D989-B085-4B3F-9423-462A50D77C02}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B7F7D989-B085-4B3F-9423-462A50D77C02}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B7F7D989-B085-4B3F-9423-462A50D77C02}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C8116065-DF87-45E0-A3D0-8E2270ABB376} - EndGlobalSection -EndGlobal diff --git a/Source/ModMismatchFormatter.slnx b/Source/ModMismatchFormatter.slnx new file mode 100644 index 0000000..853540d --- /dev/null +++ b/Source/ModMismatchFormatter.slnx @@ -0,0 +1,3 @@ + + + diff --git a/Source/HarmonyPatches.cs b/Source/ModMismatchFormatter/HarmonyPatches.cs similarity index 100% rename from Source/HarmonyPatches.cs rename to Source/ModMismatchFormatter/HarmonyPatches.cs diff --git a/Source/ModMismatchFormatter/ModMismatchFormatter.csproj b/Source/ModMismatchFormatter/ModMismatchFormatter.csproj new file mode 100644 index 0000000..b0b940d --- /dev/null +++ b/Source/ModMismatchFormatter/ModMismatchFormatter.csproj @@ -0,0 +1,50 @@ + + + + + net472 + net472;net10.0 + + ModMisMatchWindowPatch + Madeline.ModMismatchFormatter + 1.2.0 + none + false + false + + disable + $(NoWarn);CS0162 + + + + ..\..\1.6\Assemblies\ + $(DefineConstants);RW16 + + + + ..\..\1.7\Assemblies\ + $(DefineConstants);RW17 + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Refactored/ColorPresets.cs b/Source/ModMismatchFormatter/Refactored/ColorPresets.cs similarity index 100% rename from Source/Refactored/ColorPresets.cs rename to Source/ModMismatchFormatter/Refactored/ColorPresets.cs diff --git a/Source/Refactored/ListExtension.cs b/Source/ModMismatchFormatter/Refactored/ListExtension.cs similarity index 100% rename from Source/Refactored/ListExtension.cs rename to Source/ModMismatchFormatter/Refactored/ListExtension.cs diff --git a/Source/Refactored/MetaHeaderUtility.cs b/Source/ModMismatchFormatter/Refactored/MetaHeaderUtility.cs similarity index 100% rename from Source/Refactored/MetaHeaderUtility.cs rename to Source/ModMismatchFormatter/Refactored/MetaHeaderUtility.cs diff --git a/Source/Refactored/Mod.cs b/Source/ModMismatchFormatter/Refactored/Mod.cs similarity index 100% rename from Source/Refactored/Mod.cs rename to Source/ModMismatchFormatter/Refactored/Mod.cs diff --git a/Source/Refactored/ModContentPackExtension.cs b/Source/ModMismatchFormatter/Refactored/ModContentPackExtension.cs similarity index 100% rename from Source/Refactored/ModContentPackExtension.cs rename to Source/ModMismatchFormatter/Refactored/ModContentPackExtension.cs diff --git a/Source/Refactored/ModListerElementRenderer.cs b/Source/ModMismatchFormatter/Refactored/ModListerElementRenderer.cs similarity index 100% rename from Source/Refactored/ModListerElementRenderer.cs rename to Source/ModMismatchFormatter/Refactored/ModListerElementRenderer.cs diff --git a/Source/Refactored/ModMismatchFormatter.cs b/Source/ModMismatchFormatter/Refactored/ModMismatchFormatter.cs similarity index 100% rename from Source/Refactored/ModMismatchFormatter.cs rename to Source/ModMismatchFormatter/Refactored/ModMismatchFormatter.cs diff --git a/Source/Refactored/ModMismatchWindow.cs b/Source/ModMismatchFormatter/Refactored/ModMismatchWindow.cs similarity index 100% rename from Source/Refactored/ModMismatchWindow.cs rename to Source/ModMismatchFormatter/Refactored/ModMismatchWindow.cs diff --git a/Source/Refactored/ModPair.cs b/Source/ModMismatchFormatter/Refactored/ModPair.cs similarity index 100% rename from Source/Refactored/ModPair.cs rename to Source/ModMismatchFormatter/Refactored/ModPair.cs diff --git a/Source/Refactored/ModPresenter.cs b/Source/ModMismatchFormatter/Refactored/ModPresenter.cs similarity index 100% rename from Source/Refactored/ModPresenter.cs rename to Source/ModMismatchFormatter/Refactored/ModPresenter.cs diff --git a/Source/Refactored/Settings.cs b/Source/ModMismatchFormatter/Refactored/Settings.cs similarity index 100% rename from Source/Refactored/Settings.cs rename to Source/ModMismatchFormatter/Refactored/Settings.cs diff --git a/Source/Refactored/SimpleLog.cs b/Source/ModMismatchFormatter/Refactored/SimpleLog.cs similarity index 100% rename from Source/Refactored/SimpleLog.cs rename to Source/ModMismatchFormatter/Refactored/SimpleLog.cs diff --git a/Source/Refactored/UITester.cs b/Source/ModMismatchFormatter/Refactored/UITester.cs similarity index 100% rename from Source/Refactored/UITester.cs rename to Source/ModMismatchFormatter/Refactored/UITester.cs diff --git a/Source/Refactored/iModListerElementRenderer.cs b/Source/ModMismatchFormatter/Refactored/iModListerElementRenderer.cs similarity index 100% rename from Source/Refactored/iModListerElementRenderer.cs rename to Source/ModMismatchFormatter/Refactored/iModListerElementRenderer.cs diff --git a/Source/Refactored/iOrderFormatter.cs b/Source/ModMismatchFormatter/Refactored/iOrderFormatter.cs similarity index 100% rename from Source/Refactored/iOrderFormatter.cs rename to Source/ModMismatchFormatter/Refactored/iOrderFormatter.cs diff --git a/Source/Properties/AssemblyInfo.cs b/Source/Properties/AssemblyInfo.cs deleted file mode 100644 index b98c9da..0000000 --- a/Source/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해 -// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면 -// 이러한 특성 값을 변경하세요. -[assembly: AssemblyTitle("ModMisMatchWindowPatch")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ModMisMatchWindowPatch")] -[assembly: AssemblyCopyright("Copyright © 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에 -// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면 -// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요. -[assembly: ComVisible(false)] - -// 이 프로젝트가 COM에 노출되는 경우 다음 GUID는 typelib의 ID를 나타냅니다. -[assembly: Guid("b7f7d989-b085-4b3f-9423-462a50d77c02")] - -// 어셈블리의 버전 정보는 다음 네 가지 값으로 구성됩니다. -// -// 주 버전 -// 부 버전 -// 빌드 번호 -// 수정 버전 -// -// 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호를 -// 기본값으로 할 수 있습니다. -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] From 98838f3a328ef447b2a9cfec60fc84c409ce8fad Mon Sep 17 00:00:00 2001 From: jungooji <46273764+zzzz465@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:19:51 +0900 Subject: [PATCH 03/11] refactor: rewrite on modern C# and stop rewriting save files Structure - Core/: engine-independent ModEntry, AlignedRow/RowState and ModListAligner, testable without the game. - Patches/: attribute-based Harmony patches applied with PatchAll. - UI/: ModMismatchWindow drawn with Verse Widgets/Text instead of hand-made Unity GUIStyles; one scroll view for both columns, only visible rows drawn. - Remove dead code (UITester, NotImplemented helpers, single-impl interfaces, SimpleLog) and enable nullable + warnings-as-errors. Alignment - ModListAligner reproduces the old greedy algorithm exactly (still matching by mod name). Tests compare it with a verbatim copy of the old code on the real sample lists and 6000 random lists. Unlike the old code it no longer throws when one list is empty. Mod versions in the save (behavior change) - Before: after every save the whole save file was loaded with XDocument and written again to add , and opening the mismatch window loaded and re-wrote the selected save as well. - Now the versions are written by the scribe while the meta header is being written (transpiler before Scribe.ExitNode in WriteMetaHeader) and read from the meta header the loader already parsed. Same XML layout, so old saves still work; unknown versions are omitted instead of written as "Unknown". - Running versions come from About/Manifest.xml as before, falling back to in About.xml; results are cached. Dialog (behavior change) - "Load mod list from save" now asks for confirmation and reports missing mods, like the vanilla dialog. "Load anyway" closes the window. - Rows show the package id in a tooltip; long names are truncated. Tests and CI - Add an xunit v3 test project (Microsoft Testing Platform) including a Harmony 3.0 test of the call insertion used by the transpiler, and run it in CI. - Bump mod version to 1.2.0. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 3 + 1.6/Assemblies/ModMisMatchWindowPatch.dll | Bin 28160 -> 23552 bytes 1.7/Assemblies/ModMisMatchWindowPatch.dll | Bin 28672 -> 23552 bytes About/Manifest.xml | 2 +- README.md | 34 ++- .../CallInserterTests.cs | 104 ++++++++ .../Legacy}/ListExtension.cs | 5 +- .../Legacy}/Mod.cs | 5 +- .../Legacy}/ModPair.cs | 5 +- .../Legacy}/ModPresenter.cs | 5 +- .../Legacy}/iOrderFormatter.cs | 5 +- .../ModListAlignerTests.cs | 144 ++++++++++ .../ModMismatchFormatter.Tests.csproj | 21 ++ .../SampleModLists.cs | 184 +++++++++++++ Source/ModMismatchFormatter.slnx | 1 + .../ModMismatchFormatter/Core/AlignedRow.cs | 31 +++ .../Core/IsExternalInit.cs | 6 + Source/ModMismatchFormatter/Core/ModEntry.cs | 7 + .../Core/ModListAligner.cs | 73 +++++ Source/ModMismatchFormatter/HarmonyPatches.cs | 113 -------- Source/ModMismatchFormatter/ModLog.cs | 14 + .../ModMismatchFormatter.cs | 34 +++ .../ModMismatchFormatter.csproj | 7 - Source/ModMismatchFormatter/ModVersions.cs | 120 +++++++++ .../Patches/CallInserter.cs | 34 +++ .../Patches/HarmonyBootstrap.cs | 14 + .../Patches/MetaHeaderPatches.cs | 74 +++++ .../Patches/MismatchDialogPatch.cs | 34 +++ .../Refactored/ColorPresets.cs | 12 - .../Refactored/MetaHeaderUtility.cs | 252 ------------------ .../Refactored/ModContentPackExtension.cs | 78 ------ .../Refactored/ModListerElementRenderer.cs | 188 ------------- .../Refactored/ModMismatchFormatter.cs | 49 ---- .../Refactored/ModMismatchWindow.cs | 157 ----------- .../Refactored/Settings.cs | 18 -- .../Refactored/SimpleLog.cs | 17 -- .../Refactored/UITester.cs | 228 ---------------- .../Refactored/iModListerElementRenderer.cs | 16 -- Source/ModMismatchFormatter/Settings.cs | 21 ++ .../UI/ModMismatchWindow.cs | 188 +++++++++++++ Source/ModMismatchFormatter/UI/Palette.cs | 20 ++ global.json | 5 + 42 files changed, 1185 insertions(+), 1143 deletions(-) create mode 100644 Source/ModMismatchFormatter.Tests/CallInserterTests.cs rename Source/{ModMismatchFormatter/Refactored => ModMismatchFormatter.Tests/Legacy}/ListExtension.cs (81%) rename Source/{ModMismatchFormatter/Refactored => ModMismatchFormatter.Tests/Legacy}/Mod.cs (87%) rename Source/{ModMismatchFormatter/Refactored => ModMismatchFormatter.Tests/Legacy}/ModPair.cs (67%) rename Source/{ModMismatchFormatter/Refactored => ModMismatchFormatter.Tests/Legacy}/ModPresenter.cs (97%) rename Source/{ModMismatchFormatter/Refactored => ModMismatchFormatter.Tests/Legacy}/iOrderFormatter.cs (56%) create mode 100644 Source/ModMismatchFormatter.Tests/ModListAlignerTests.cs create mode 100644 Source/ModMismatchFormatter.Tests/ModMismatchFormatter.Tests.csproj create mode 100644 Source/ModMismatchFormatter.Tests/SampleModLists.cs create mode 100644 Source/ModMismatchFormatter/Core/AlignedRow.cs create mode 100644 Source/ModMismatchFormatter/Core/IsExternalInit.cs create mode 100644 Source/ModMismatchFormatter/Core/ModEntry.cs create mode 100644 Source/ModMismatchFormatter/Core/ModListAligner.cs delete mode 100644 Source/ModMismatchFormatter/HarmonyPatches.cs create mode 100644 Source/ModMismatchFormatter/ModLog.cs create mode 100644 Source/ModMismatchFormatter/ModMismatchFormatter.cs create mode 100644 Source/ModMismatchFormatter/ModVersions.cs create mode 100644 Source/ModMismatchFormatter/Patches/CallInserter.cs create mode 100644 Source/ModMismatchFormatter/Patches/HarmonyBootstrap.cs create mode 100644 Source/ModMismatchFormatter/Patches/MetaHeaderPatches.cs create mode 100644 Source/ModMismatchFormatter/Patches/MismatchDialogPatch.cs delete mode 100644 Source/ModMismatchFormatter/Refactored/ColorPresets.cs delete mode 100644 Source/ModMismatchFormatter/Refactored/MetaHeaderUtility.cs delete mode 100644 Source/ModMismatchFormatter/Refactored/ModContentPackExtension.cs delete mode 100644 Source/ModMismatchFormatter/Refactored/ModListerElementRenderer.cs delete mode 100644 Source/ModMismatchFormatter/Refactored/ModMismatchFormatter.cs delete mode 100644 Source/ModMismatchFormatter/Refactored/ModMismatchWindow.cs delete mode 100644 Source/ModMismatchFormatter/Refactored/Settings.cs delete mode 100644 Source/ModMismatchFormatter/Refactored/SimpleLog.cs delete mode 100644 Source/ModMismatchFormatter/Refactored/UITester.cs delete mode 100644 Source/ModMismatchFormatter/Refactored/iModListerElementRenderer.cs create mode 100644 Source/ModMismatchFormatter/Settings.cs create mode 100644 Source/ModMismatchFormatter/UI/ModMismatchWindow.cs create mode 100644 Source/ModMismatchFormatter/UI/Palette.cs create mode 100644 global.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1c58d6e..0dd83da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,6 +40,9 @@ jobs: - name: Build run: dotnet build Source/ModMismatchFormatter.slnx -c Release + - name: Test + run: dotnet test --project Source/ModMismatchFormatter.Tests/ModMismatchFormatter.Tests.csproj -c Release --no-build + - uses: actions/upload-artifact@v4 with: name: assemblies-1.6 diff --git a/1.6/Assemblies/ModMisMatchWindowPatch.dll b/1.6/Assemblies/ModMisMatchWindowPatch.dll index d29c33cb7467687d3ca45bf7de386052e37e4f12..1fa36b0231e20586403083ee2de8d06e1f739e5f 100644 GIT binary patch literal 23552 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 bzP}wj-jhB<>i-RKYWXDI|409y@xcECEsI2# literal 28160 zcmeHwd3+qzk$-jfOwXy&^yu=1Jw9Z{x{xoh!3N8cYzus0$wuOm$I{rIcr-2djBE>I zAuoYI0s$u+A)5opeGn3d-N5c9A)AC>$Szsf57|w!2@Bb5u1z*&oF>@!CYRxq=k*E9e)$ z*fyQ~^sUQ@-m`^B6Yd)a3-uHn9&Wl+U3KFWH2IpSrrk#;U3E)dx#_;km;CJNCpRiU zG?`(Uvm9Dn=fBdF2(TpmV9dKD;q07xfG7^CC{RX1%vUw{kuO{2Nc2^mWjwT2UPt_~K>H=FbnuFXwLbT-3mXqq>HSoJk6)FAJTnoc*kdMXKaY8bSh zS`1VKbP;GeE4oCJ4UdK$gx#pfR_bY(jIk{>?FL3PB z$rgVK{Wd46fa?j(UWSquJrFjVXg^DyU6TU)V{@w7G)>!XULGz=H4NAeIjdR3#k2cN?p5zLTHqLVv zu|#Sew2C*)khBnJ=#K<+dp+1!?0x<9*PG7jMmY>xEtitkr9x#ff&lZ;q_<)+Vt!f9 z72H3W6!xXiXb8OQFCgZeLXkd!K=C!UHY}e&==jc=dDJUh2Fb=rE=gf9no}D=M?~J3 zK=4$`+hut!Da%I-NTuOM=T@|0UjcM^qw{laQ7pI%-qw^cf~Gb>!V}wSl&SH5e4cCgz{Xqw-$i%moMY37mP( z2ZS@<^1Ct%T$u~NQ8Eji$5^I3bgn+PN~#mhum4fm$B2kZpN*D%#wBH+)tIi6F~ce= z94%Mil8Ooe*UO)QJ+2YXJ|A#pyi8Tkg>T&Zmh0+9r^5Y~Im7?1^IZQ(_gnP(rcgtF zRmi*|ckcDq8#v)dVLJkpbK|{m4idgK!Ms!#{FTD;V-~uBg;QGq?X3Wj=A8i>YjZdn z*JLf%S_FK>J(Ny zvq^&*osx9zO)5Xl$67EBTak6Kwq3=_Uxae!Wq{OnkOIQ8OhnAo4scpJO<8|buKG_F zEG2Q9Q*UFdxm;CvX=&66``K2GCbH7X^Ef7qxvC0JW7hj<<9yYMnyXFo z3RlDEo7g^s&@qD3tb&6^cpub`9=ZZN?n5YtE3LvFT&D!(rCv}<2nJ=Wf+u?+R_J4Z zRBBe~EzyvQT3(t>WcR%&bC=97ObI}&ixPJhZMjCp9q*~I<)H}LuMJP^L(P_&iEH>0 zO|61DJ+)e_7GLXxLEycwC6abO$3$ z^$cKqh5^EyZh$bS8z9VC4Y*XAUJtk^;cm zJ`OM5uPwo)C5Zfi%x^aNYz&Lb4C{v3z)Jzn5QaHdVZ29t6AvI>mBJMoSg}%Dts6?F z-c^&rKE!NUqL|7A-YA@_V2$vC1VDe?4 zvROUkOKn9J)z4o+7gCeEz?W98u=5V&6El@W?Xc|1O(}aA+=!Y(fkGaIwheG{6d?+q)fncb=#@B2FYp{yD_FUZHJPL)D?=Fm+mclN`-S45i!fWX7rcM2Bmx2ZM zT|n88LT1O5mQlYb;V)M!fFI&+bN55yZr$=@GO~$tQro!y%MG7j6d-gZcYrG|L(LLn z+5|D|sdK}y4?uZ2a~OPoqJow@jPW8Y!Iqxlxf-4*LL}mA4LacfJlVX(hoEv|f?w)G zFgXdq+kA6@^Jy&7yu_&ouUbD>kHo3VPQqydCp89L?rIt^RZlig;QZw~iF3Q}Bo=I6 zQzhdRrV=XZKNh} zY1-`EI7Lm<<|Q<_qzkGp>MGpA5-;hgNnB5X<k^Kc0Xx?L3`bm~eVRvD|SAwqu9??&-=74Ee zfr{$ERDd0xWWCrU2Z?W>V6TPx%(LH&h8p_A2@PR;8*p51R~HUqSU$R00y*H^RO;

    mZigJP=J7L14knDek?psTEA{p6)osvo+xphlEmhFyTjmj2Y@Mx=9*Jm`rTj zJOG$fswCP|cXA!hWv*dpH#m^cQatN|U~`HC$rr9Wx_Ln93nl{6GhDc5xVQ@L?HGVT zEN>pUHsM!T!@!Yi`=z24D5vMCDxm%OjsZ3kDuj^q;Cyn5hY}&w;_L{C$&UW1&4c2G zm4)F%NLd(C7KX&aM6fJkg(_^BB78nuzA?oXE1=&DNuL#LNWGPv;Dh#qj07dQOT4Du z#tc7vA|*cYEE|7C|Fm-M6~s7R22ytcy1{fe(3ZknP0 zTOGk{aXVt!*gXF)bfg?n9F3NF@(eB$}-u0eIqG3s90wR+D7bZn{k+`820$r*yLGE(n= z3GR-C^N~pxL8cD*#BhFH3OP>F4Gj%^yB2AEzL|O_7%fOmobBxUK=3}mq^?rkaHujP z&#aqF-H%$W>im&<0JxkdQV#-4y^9rMpS}#*<@hNG7>yo75Zq|q4N2thYtdJnDzE1* zX zD8CO=D~y`v$t%!QW9yvdrg_f9vT7FI3-QL9;3+P!f+<9Ea|&~_xyCdspId2DMJ24d z=St=12`r%LX+pm;Uy* zufN`W(Hpcv>1chn`Jy*yiE6a<*?`;?4GP3*y{}|leNCi!aVTI#ruTvX!i_Dfz=i(BP;ti-NThi15psmZD z{a&GSFR+PEK}&t=aX_3{vk=i6_S84&^?Mqc9AA~Iw6f@cll0hpPt@eWx&T++%j)u( z<2<(Y+NnZ|_=7$3r8!~e>nJd%-v`AfM(|Y}0H>tD9Ch!L&)v8k zyBOVQ*ncFt+EH8GwEaWS&^g9BdBT)*HW_kXqMmQT8a56BR=^3pL(*DQK8 z6yn{-e`>3Fapu>Y@pGhQNm_@&jIf z#2*jhWzxpbKU*e!UEqQ+hwQDuK@g zxo&&dS7p*j4Z~N03>Vh=s=~C!`eh(SUod|e2-Do!Uj|HioA!!s($@eJ^i=JRYLomr zz;(3W zBAQ15v|dle1k0?dW|?sHeX)h~h5BdpF#VPGOB|(N^WOj)9*eNH??ko*6Ld>pM|FaR z;%wX1)(Z&}%P`yU=hdI7GwE-|hWkZlTby-niH@O1Z!#JFxmf;T*b}1@L2f-$!}x2` z_e-EBMn_>`J$+m(e*!Jm(-w>2`vm_pfxoV0&Rc^F=YV6=yx7ctq{LC?d z4?6LZm`B2RBKT-xeTh1YIAKbs(wNqSg4-mYP>;J*l}Pqhv80JR zmR#?X^@_461j?>H6CX!hE`i=SMv}`cP%L?w)T^U5DYK!o8_~U1sCR?96VK9ICDggP zGx57|r+>Fl|0I&jXh5j%M7iE_x>2ZG6KCR&fI1*8{6&PzQkYMZm=enq-;Z?T)1eF* zXgv!KQ*R<;ah$#fzjRYGW;~Z&0v~l#J2{~6&<#9y8*+0|s0W3*n5j~|OQkFh3%K4! zI#Hpe15dOpr)R3JLf_^XH5i?E6~in2g4fshExOJ3xQP~h+W;+kO>cnA zKlmBHzIHzF@7Cj9FZIVCM+`hu^|-Fl7ko?o8hsNQH2RgnFef?(Yns4+yRjDV8v-8_ z_%FT(AyeyTxWuO$7X7JzJ!I}mYy`X`-U0Zbnm3 zG2=SFNfvG_n{+uo;Hsx@qSU1O>N*h-o9eHki|I0b5dE#EA;6yt{GP`6Jbegt3(=h4 zG#}EhqJ5LK#;AYaQHd<`dAfoo)t-hlgBV&s6;%^Bw4;M}6CT zu!5=yyu&Y(3B}J;H3uH7p#BNF96O77c-p1n(eEOf?r^CU(X*JF_o{l-7CDQ1m_Krv`b)XjPHq)=z*R!GjLKT(v7(E|FZP`3x4uU>;A{%>3=91ofc z=(jFa8;^rBSFq08gWd6@b^*m)sxKa=3#h@R-X2J53u%!{y*m&`w&9Oexn8;|sV$;* zml~;x(;`~0D8$lSGfDl5qA%AjGndliLOnyDh$OXT^b4ULpvUwka~b`+P-kdm?JBd8 z=J3W1p7>yRow=OeMEANBM{5&3|nZT7VrGmyK6#H!r~+T2Y^`>oeCA zZ``>oVGWua>1LPOA3S7ULB9~{ox!T0ZFZ1syy>06w}qLqR&qVg6HnQ`~5-sMW( zhZeTdw=QNar|Fx47xZoPW0(3>>;=7tmhr-bvhRSpl8Qpz9^429~|LgTHV9#CcI-@WDz`ecdH@QmvdLj6$V+_Q^b#KxT4+g0~2^JQ+k9*Ual^u}l3MsOxEkOZ`aunt21Qbg5d}76R7mcp5m$0AEi?Di372ZnzGSuuJXKmYGBJWQF7~J?)l# z6_Uf~sYcg^XNM>7QHW01DDawAf&T&Iby|gGNWV*A1 z`k=@#uG6K`N2NjWm9?*lPws&II(AE3*N2mx5B9VE6H==Mnl-8x_)j8VY4iJKTsF$M zj0ye`8CkYdr=N%h1z!@K4~d<>7N2}d{LHwH9T>Ory7E8jVy`v>eg)f=seC2x!6-gQ zi{oBVhP zwQtlvM&sH8$Tdf`@6~*co}y~w^MLOT$LZV95T_Q}8U8E4Y_$f-P1aeIUR3uZoDn8! ze@1s|AE^Bm;M;2ci`LK$Rz%xCe^WbOdzLN@C$$#(*XS~hHMeMIw2#)U(H_z^1vO}X z)}MsTCve*CqMt{5fFH4T0d}F?JGEM4pVYce>k@0X3+!`y)L458r59KIq4tRQ=MnAe zfe(T6gPKojcgoITyXZWs-C6%x?K4_W;5q1FFFmDw+xlzmda-j@?0khZBZ<;l<0slH z^!M21d`bIh;B)k>_FTTw{$+`SQ@YFD^TkffOXZs(dPpHFTi_s-I%NK`u%3{!y~k?imkm6@F&`b zqPq>Ye8AB3`PLA8|KqwWIDZs726%1s7X8f_qf^FRGDde1N6t^Q@6_F6tkAm9i!*d> z{Q&g*Ro#b-#oBLR$x8k9;2#?u`u@5<1Kcd|9XOA?S$w{ob|UJIYNrqn+ewT3#&{3- zJUiZ6`*v8!qyItSY^QF2gYT1W>6ZG7d>tCE(gXS((LKP~C*#_4{LFPHUO@sw*p>99|r8C&jMaWPXqP}|2n`< zJfW!jJMqAv?jI6-$ihJ}mG90>32i%K~2!s2QwtvB0GQI|OzLObZ+mxZlT;lLAi*d|2Qk0-qN6 zvOq#Fb$?ReCV@}m+^G8>#;yYW68N$}3W%h@O#=4|oD}%5z^4ViERcdCA7Qvj;C_LV z0-uhaq+bVK7Py)|L|>+F(K;=yJ*E9Z`&UiZ=j+Y-T76hQs=rzPnErL$Z!9w2X?)c% zd$^8Upu4egHfd`!Qf^ zl;Mlj%vlou8DM|>*MRQ}Fnp$p;Y0n4;30uwTyu*+6uq{%p>fg0Cl^uB-9H`&zs&!FwUz<8;6F z3;L?QmgX4?=~W|%_X50E4Y*=Fa!VRa(U6;W+#rntp+M$*M%W*C)sr?XChWl+?cDT32!=Zl$q zad&#?NKd{iJ5+Q_QL!gKoH?<@u}8LN3m&`UU@_xJfh*^BYvt_S0$cR}$Q$Z`TM?J@ z^?)U&`MG_teg8_@IhM<%59BgHd%E*uBN@k|#Alh|Da_qYW;i>PE>?;cZ3mUP*SK`r zIonyiihA-zKyMg<%{GjyXvui> z_jT{-?b)?+aObYB?wuVwx@pI*uE8GQeLY)xy7!d%_*{y5#|y>GNL$Y?k=oJS*D=`D z(bv(7`95rS=IlbIpN^$-W0}E0LIXQ8#dKG?m?n6)gm<49g`BWQP)^&@&WN2K-<~}{ zBZVQ`$pL}Sy|&<(96E+x7TUIE@);*PL??#vl;@#LaggIFeJIm2Oxx}BFm((M)1J(* z6fpy60a7k{kYh}7bL!H0I5TwQfPG?n`amWJVWE4|$1>Y8sO-oLpe`qUe6xL`*T!_9 z4NAe~M+OHsqiuYUw!nNQy6lxqukZ=YuU+@d4_tn zc_w$PkZ~K8j=|WOGM!_Nlg||Q7Nje=%%C*GLzjij4Y8om_>@2(aG(ZgGHV#oe;@5yY0eEkv-W%hyNRyJ=u|K z5M;yDogbzxSpXi$LykQLbWaA!$mxYIa-0M`8XYTB9q3UdU6B@9$PH(N`(aQcc(cvW zIEeVj52u}B%H|6|hccAM_tYg=cff&&V=4_-IuEMSp`GcW!x?0UWA>2@O!x8)_3#Q{ z7dZFOo-z1tB-1xOn%Rb?z`?v@inFWBQGKHVy5u(2XM63ixXa`kJJR{=!AzmZV@iAT z+2VM2{!libY3td6T!6kJe@x+b+Ol*^XZPVt=*qG1f?Ev=B*sLV#DEG}EMV-2;jNfqgnWH4dt#6oina^IDU5!eh+b2bkSsi@qXE@ZHDaNMYn4)F@HZl{qkW=nw~dnb(2*Gjbp|-j^*_vis5w4BUd>iO3wcog-!W?i1LJ@ZPA* z@>Fm*xSg0;*pojf+Z@zP=gupI&CbE>p)oatWhHwu2Yb^8GsW>em?4ESU->}Bdru~p zJ|S2|xo6pK2UBjSSgz<5u!3}cyjOx*LamqB*Ab#@oK27iKrT}p|dlc8|uy8q++87 zsU@4vmFR=mD+<9AsYJLblG9mN=HRY_$c5N9;ySf}w;csF0G#3XU?E4=q0Jd&E{|Y) zD`pFC0$7Hdn}zMpAJ?aC?x=Hxopxo4?8&3)Ytl}hi~7=s4q-)Sq^EdlgZ>c zZ|_1jl?o?zWEC>bh(rKgX#<_wtx_(Kg}O|!L)^iZW<6XjWzbQvp5TP!6+4JH6ndoW zyejA}`*_|f!}Rdby`#s}VU6cZcP=x6J^X6e{X8OP>mA}>9*cJ>}l zJENF5@Zt~xx=?hGM{Q>p_StEtIEEs1uIV4iwPDFU>d}IC<}ztVA{W_B9Slghx;(6$ z_}n5+GAJmeNteyLh%Ow3c+$!l!ht>fAmHY)ET5>w^lYJGGl{VNADzXerpy^}uflS0 zlEQ30!OQVTj^@Z%SWO+Wi`X1U)$*2G&92f8ocek4r?K^LD2py}_R2VJaH_FDi6dKP zAUkkm%JKFPqC2t$+zc4v@RgeaP$1Tn>4T*bo@aJ`88vp!2&23{NU8Fbis>|u59{H9(%*=PyykI{f>j}hCR}Gm^bhe2eJ}!K$OQo<#cQT zypu$M(q)wWfyIWG5vex9b-kITcKB`Hwy8Jc9LwVLxVT|0Yc1-WC+ca+FgEO-v5ZW)Nf8TKwY0`1X3&fVFeEUbj5in|=_%F<)GA`K40 zEsXTLevvW2&BQ}_Ue&0-%O1k<6Kc`Y@gL$YxB5>j!QL#A96C|)b8HP z;AtxJyA%71L$)(c82;{~V;Iw-;63?_M`2EIDDExFp~Fo$<>k@iyPC1zwQ+D5r=9|D z{dPIsBcq@y0%M3*0I-NUPvALR8=tf#DN6;w0-l=9;{zMXYmut=qQ()tZ8{D}M_9u# zP)E>85|wbfl(D^DDN7%qG0-{G*$2uITWx6X0(S@&QtMPH zukLxILnF{qL^~Af6%4(?X9<+;##6=YlTq;+w~$0%4oll?n}f2lEx)^!7RYC2(RV5M zG|=;V`DUy%N<1!X7mu;(&0%lC#HZ6XA|(&KNql0L#8ctzbPCswincsywrr;SO32gV z^T}_Ay*#7ng6VCuwo)mc%eFBpqv#`#fa`sK5}!I!Xayu&@zxAHraJk7A_9yj*oe66 zLK)C39K_CL_Z-BVo$XC%o;H;Oz+J5j(V_{aQ#ne z(bf)V7R=9oc4oK$31Gwh`P8%lJ z2x?IE)IQR6vrRo7iXx~zXfe^_$sUN!0_bQTs2lZL{Az? zNc9WfR;ufn#fy}gEumJ96YHO9e-eFWD~|(mB+NDi%A+7DC6L6|nlJ%F-ejPNrxo1e z9(O0KWzU~)S}M67^lG|<@}3tt9GA*7DhYAg=U5yOZwyHyK{~2Z{c^`rqeFWjrzCl5 za|%!~&xt3CG<8_CD4hk-#*2ZP0i5!9{J9mjCX4GB-W<1jPDYuq}bN=W+3*(qEX+T=cOBu(?A$Z1<;8FGLqzk)Y z7teZ?TUAu0#ZFG;xK9Fnh-md}n*uc#I9n*JET1GiX}VF+xFsN1nsHScIy12#ZzYvujFb|Rq%YVLPe{$T5v8^TVI~PmD@qOXlhTr z96EDP;%*5Gv)MV-KJO?}Ud?#cs-CDG9fJ+!3|c^>^WXHG%=4ae3(Z&kRcl0LCR&V~ z+zBb}mFKk_XdaL9j)+_4F=vl+8yw|_iR{_d@N({DuX65J+imt9&k43c9W;{SNi}1+ zFW#ELW5E3@x9NKB^wQZjFx*M5?BL37RLhj-Fg`iu@u!y8O5@7G@w?68uCg(nS?p%7 z?(|_O2a?yKS8G^?2ZvrPE1XPAPMqlp zmcvGjzOraMKwoRv{zUw>i}L^W;m`6ZPV*%-Er`d)HLH%%n4lX2NuL(Qi?4pX@Gzm( zV@-Z07|`kxyS0U6s!IUZ0etaJH(7^XgYPRO#_(le-JjI81#{~0%Pw_^y^!~y6#By8 zRL3>w8Yq=TKznkcbs^h_U%3Dm8hxbU*Sm-M$T)qs0RL zM5Zp0)e@OlEHUCsQX*qQeDX^%ei}FfB$yP7z-Pk}8KJZ22xzW{IyKxpD(K+WXaRhW zf?we2p@0uHtYuPrK(m&i>opWK;XbHlFD`&}u?u3+Krjfq5~)N=tQ^o1DRB_!hPbX= z11Bg4v1=A;{;-ZxJg^yyaX}1P6O%9CoA5>~wg7r{6x0O#>^WYtR%n z)!d(>s#|O{XtO`c8!e5@#u@)RXf{yOWF7L;-30uw3*w3rb< zkO&s_s-Ro35PLpxG%?`b@fJU2XNiaaK8A1m#vV1%t9A{KIU<%4 zU1q?i$8;6DS7WHH(-vg47_OnDUyH?cw80?>2-_y_m$5osr<;0h!0=q490B)jV5J*Y z1QJ2(>L{eyV($W!t+lSMvnC&c6#t2j5*vc5o7^V)i)xeiBM#t%C>~n1u8xZeU3u#M z5F(H@@RU)S_!~DN%rE#Ko&;B}ar{UGoS-9G6oWf(`hp-7%Xl;eqKU&(dw_my2ujrP zWK5*=pc+%|RSLcfy3-UTk${oNVfcB>5t*kOA%X9R)*y_4<6&W?LCWVy#GYPJnx$&0 zFVsRDvLb>x?9<{42|sqA8(~dv2m}(7ADj6P)+bJ{wN9_M@Egz&IK4J@0RX>cWF|jW zoAgUTcj9zM>7PGfAo>wadOQ}7`OL)Wy&NMkTy+k#5j+u{Xisc{ixQ{PO4HpKoZTG# z>?6$5&)szWzPW2&z7sKur*Jg}Is~8)nk~68#u3VPC@vyYr;ZGprdpL-fQsg z!n+plIVuYxY|O;u8J~%91~-LwUl0{}2x1_PLizQ0?*~5Y73fh{LfS#vL9}yp@8H>? zbM~>WPHd=6O0Gz3s2u3HBv+L{83w%!$^^QDB%)%(S_YRP?PX$k(i@6YoHOJ!l{kvC zu(XaDDA5-TC*jnX@L&wg4{-K8jVT9>iKEb~l;|M{!oHMsy=!SIj0HpHthJw6i5*st zDni%stuW*Vcqam(L~UXYyrpxOIVd?za0TSbYV?-9&S{CQj3wRn6p3l3$92@@w8AP- zJBCcfmQJ1yL0DDxrQl3_QCT;F$m4i~o!Ovcct^p*Phhh#ESAFeSdcsMTF76-@6+qA zanhrBBC7Nea^GRcK3>ou$I(KxL!$+4JG=WzPXe{Lx86767h5iATLmSt+EStWOf}!u zujN|FQb|&z=WA52?!V7?HiQ3Xg#aGn$)xctl@Kf2R^c~c+Huy>s7;9Gp|$JQtjerf zlWrewU$=gE&2W2W{i;>#*BrQ{J+o?E`?_^&53F9FUVjk3%YrZdUC13+u6m-QvsHMG z!alwOAFyQc**O@Yo;*Iv#q$uuERQGWXTH?a=w<$sx0Ug~W;|J@E3vOs$Po?V5wfma zZU;UQR!{0?GV zflC+O_$Dd#oeuP-8oq5)sc*WM%RX*Kdu#FSo;C1!2JbcKPdn^r2X8&DYXr{#UxmN*pby|1 zDD5JNg#3jEWch~n_vFNf(% z!I$Pescn8~Ue{nIA0Kw?0Vg#S?@i@$b|Jnu9d|}@@k}n>y0tStWcQ_$Wo4m@UDey# zh&F2mU9t4{Kk!<6o#w|&v?WAc;8==#vx8?G-y*(55y5q3H#69N`8*25!X!cG;hz<3xc;N+q}vyRv#{P=R9Wk@7XnX|p(r$|mSryj+w$~QczO{?rtYcKD{CMz3+?`Q zllh$XV<$O8l!o01bY-4yATIKxyr}19lQf zp&30|=(KpEp4FG2tjyVcKDKnG!VK2UikfxRQPaslDuGEHsta1ds3~0w*cjGO{k^9u z10x6pt-=@e)%l}7y8-OitRi=`hBVAqovxtkzeJTV5Pdc22%eapQ87QUjJ0SrlvobL zM#zRjwHqvZC6E$llpAJ=x@G9xr9evk323&;V!CvxvMK?$)y53ziA$Zb600Hlnx#zq z;;D#LXIaWHRZ^qD#X4O=b=Rn@q|*%Av5!J*qfTKO7*Tw-_Yimp#i$Ag=SJA%{1qE^ z85#*$i8XlCV<^c=V77(qbpXoCv&5>zdN2}~k8^#AMrLrtl$?r}IuAmZPOI7)A{8D2QxOYkrjSN7kx(&w5HCpsrD!a&(tdum>!rhN25%c zM`Qe`K6(Y$X`WBT`K|MbE@{nQ$&BCWiOnpnCosh=J%Jf+9Yb`R3+t;F9mC`>&sf{R z%VW}q78bcA0o0PFBuA{qL_|V~RwuSVq+ClNx$|S2dZ2O$?M0gN$h#qDmhL5)PLMI0 zknd80@#ftSk2mc`iCZ_`oZz}KZ9;TTD?;|k6e+wCgyG#8t=F$Jmd zn$noE9uK$X%T2oj(t@(SW2*H^I>0s*o4PMp%brq8_XjvvTe=nCoNUz`&=2zv)%&Xx zJJGEg^{5h}y9W8rqHEw|EXz^j7;2h{t01N)rh$i+sj(ZbVT6+{u}59MpyxFMkcPj`7GE-1Jm^-e)vBbDYC!4l zW(CwJ%_Odc0pbu(>z|SdRCkvKCi&!Y^PP9zNn8gN>}MY#gGwx$^`(6|@Y|SiA{_?!x*!9p= znb-%YAC96CJIiRbJ%oqyt4TpP73**`kHms~PYy-Lu%u2-?#QfG-bTT~BA4*qur`5Q zQ(IykbHF}Y%iKJ$ddr_E;OS3{FtpBC$U|2C0Eh$}U&}k8$(oVitvps`Xyy{$5Y!%n zWs_x)^sW32BEuyK_*b=F8n6$8UA1&$%$Pnsfz3z=yN79pHzpwng2Bsi;+PZ$s;Uw< z0!tKyOpM`i>^%TgiJO@HUI5Q88xRp_Dw;=Pv_h7Cv>uI%dpM%=?}Mo7{Wy6PTQ(D2 zM$WYIH?zJHHnkL>#*s~te#V;1g|~2F*?8e!8?qrX?DvE3W_qKt)-q&+lwX4?oO{$9 zK(8(^2aaK;o0tQ~kU{YbAH&jtrzH!5I|odH%j7fv^BYgEn*@-mF3;Zz1B#fZ=KRw) z+_`@KzifYUodOf3tX!2o_uEgcpMUM`&Fe9Fyt-^c{EKKHeh0wZ&iFfcf2bqIAA1h2VNJqjzM3@%pV5~Bt9hY z!+`dk0KV!SRzNVn+9y79D&()T{O$(U+q*_+<1V!?Jm%CRO7eH1?#mcsJ#jay`UpVc zqX0PfV0R|x9`&qFV2Wa5RjH@sl4jVCp^fS2+?L;374{ zS(a5Fa!zub>N2!)ZrA~R2#M&idr`Z3Sw-Svl+~5W5$G@m-IT30v|6X?k;><`RI7;B zoK2<|;F^^PbI@xN*`oavA)wX1^CI$JO!erI#^)3y{E7CaCwbqFR$S$^UHWY3uda>lv_GWq);zp%(9iE^H3>#?bggTkhE9v(=BhncLI z@A8=;1)J5$3L1RCy6M9bYP}FeV8Lw%$mOa3g3@2)N>QTUbvE72jlM5P9fUU-vDvXvc z`jtu5HAx(&SE97)j8?O#oD4o~Sv8+XJP7s5rS>O*#uxA0`QX(qm)$Ac?wWTSBXFi( zSEd^eGNP(i)x0oCfgvMfKLj;()ss{hGC;zwsjAuFc5FL#j@*y!H4a7&vxtXy0Y7|4 zF;!6dDJZH*z6>X;iN{0Dax(UUfWb9{AqW^;R~Uk*SU7vEE+6;VS!2}-gZ|7|@x10w z;?pqCaJG-DLce&sw-ya(ijFJs8wu z)LpGwG;D4+@p-gRw=pr%rlDH?0`o6%`P`@><75lJ+U0Yrh76bRrHgZPKB8JRI2-fm z>q?)B4y8kdh^i-XQq%7`o&$>gyxpfK*sa^uIjU8Yy(R7l9)U(fVB%4r`7Z*5s!DJi zdJ%op{q|#Elobu>!IF$0<^!nyMzxItFQwg1DIEd zx7JpLPlte~z*Iad>Z@w-M=gNbP*sJ$c2+e&l`<^q+pX-3TD#Ba75=EdZiZE91QwzV zF>9jf+GzdQjXHC8ViM%amndvHOc%oxTClTBhI^7nb>m-Iiu9z`FAk4rr9|B zB%W1?rvNLFrRErizsv<*g%25~ez=xH6j$qIofNzI*CG;4g4!KW181Iv?3 zlFODXTZ#nBC(A70@+zVQ*s`9$vk2!c=qxyy+(4cs;!yH23%s43^z;&SZ@ysT&UU0C z`ZnlcY!MbTXYIW#jE|1>e%IVj1cM1Q_0F%?Ov%C$X?t^osSnfJJ}s_mo)F zEbtwR@!5iZN#K`-lM)Rt3H+4BdPXA;g)I6Rw1p^XCM|Hl578UJq!p$@v{>?D-(@Fue?!nY6!T0GfYJtpCZ9o{|v#ri3*=gLXsYlqRj2 z^hj_(4^evs3o!eSaCUs$Lh_*;+8tqom?dAtURrC#bBSfD7Cq!?DS^jIm>9C>0H(j4YJ<+n# zIn*fdsE_gKWo4xn{ZouvPf9EAhOa{O;V46Ygf&>wRQwZ0C^!_8NcVayyGieAiAv!E^atB5^MixaJQX%^MvI}GYAYG@ zBIF3Qh$)nDE?7skv{R_}3spzECP^k~H*GII5v#DG^b@1X0&Z3SzF*)+1b(KZ!dgl{ zHD^NR!{swM;(SHTqP2kAiw{J80r-r-=Smp=rr?hV{8{K1+C19fn{UmdFP0a9|0DEh z)E-#`IEJWMNej#dz*PprXLW`z0!HaZ1FcxK`lZ%Y#edW<1AI~7DueN7fzP8sV?E$w z0y~Tf;P(mq7GM>11m;8APqk*?^|S$S6Kw;$R@)8uxHgEsycNC|vq!LWtE zK+`IYAiI9brQ(r6P>u5lpS$T7fqP-gM_sDUIAI0oNte2;v_T8e1M`(+r%+wkO%WZZ zM9ICFW&i9_`zuaZrSu0yP4|Zzv@)88{T0y*bS-*WMomJo)l9XIQx95Y#M>xH25t^~ z&MK$BcI$l~d90ki?Na5ix|}dk@fi#JF!B(hY>A?%G4iMtp^v^x%4jU~q!pvcVy2E$ zUFd7pH0ly+jDA%9C_KELmrYpq8(21-HV8FFZy;Arr$M2P(~dxcHl2!C?$I8#guh`; zr|-JdkWjyLsgKilQC5kyo&EVl*{jwJ>K5v_cw#2K?w0+#?=&*^9<2PZOv>g^QK-8E z*ZBU;nnSm!vOv~H{yB8JqUecOgEp5w;!-Edm^$H7XG(qkx%4ZSI$7SJ#ql~>TA*i8 zFHTRp)DyAk{y0T=d4^wJ(Ekm2bB$0uDslgOx>8YrIYt5p`%afi81#HBgBo_Jjn+#4B6?Y<7wDqk5n4pEv4da_{=9q*%5Dmr9zFNfA0Zxom+NW|Efu(0 zV3WXhf!hV{7MKFmC<8c+_6r;n{&7G{I5TOnhMY#b0l@)WgdGgmIxd{2v@MbK^i}N} zW-B1CtGA+VJKz=NTj@n@opm+6qP-nS(W@eVTKh?85crSv9G%wurUgk}NAID}lpVn@ zhSmgb0lrzkgWl9;AZN^`kB9E2Uuz-j3&6GTa^i9CW!!Oee z{jIX^(^GUH`ai+h89PExX=^He1WtYIf6-TIk!7Lo{pRmzo}LN+kuIjU%F47l$X}%0 zL!8$Z>+cH9*EIcMvq4*`Kj2%Zt=3mZen(CE-|4Mdlm5x*mD+aw?Z|d*x4t>rt+ne* z14rl;`nt(&ZHc6`r?j2W|CILm5L?b3dsSBLR|QY$2P=lO)4)do=j%o7ZQwTn=M{Q6 z@DFMG^$$c(Xea68v4;SEAn<<#8P~#})yDK4p~rwf4E}!oi-D)XX*6Ea*3!qKr-1K| z{TMnASNsAp|BRKrmF^1ttF~JEP3#Oxd$Ee0qz9z#m&;6@wUy|{_2pJfXZzP__n>!k z^ppDULkm%QMXXi-v>pvzrN1e*J*BTLxmJHve_GG#FY5C`j{d5?Nc7^vTd(PwaXR{A!0!QGJ1+T0z0bwJ=UZZA z1usayW*~A3#tbcN+yK}bieR*!j@}}i2+hzImma}|>$KR1jd{j5%#RwHQHOm~0U9j) z4u-w@UV--zuTZO@?X%iqLkoY`SZur<`Jr)}NZu*&@G*&prN%&c%v^1xLbJ^#$j>vE z8ea=727C0KZ;hGK{upZor#p0onKBy8ou;OL#k9>)ts{J+88zm? zwg<$9H}ycp9p-+i`>4!_N3|0336tS7=BTXojK64pj%uU-2smi|7;sPdKckd;G-muJ z*r0t$O1~tfPYeFE;3@5r3i3TI@?RJ4-D-R>w$S%&)Ya&F5)aEUUYGfvg5;0r+tQb( zwA(QIUZX8qhwr%YROxo#YxIz|+gA|z*HHIvyfrYX-xtQs$X$RD`aEDIJqL*WoUfGX z=rrIG`YGUY`Z?e#`W4_>`Ym81{lOQYVR{F!fOl~L{Ng7Fc!a{(V?Galobq%gu#ooA zt=7A6@6lJ1#Mx~@Ac?z1?UCEx<;0<5Jw0hiGufNSU(z!v%@;FZGf1Z=|FG}CIK zUMdf^h|V^Fy~61gyeKj^3Fju^oDj})!g)?OUk3~ZPt#2_CunMn2Q}8bK>IN3*Zzh! z23v*S3;da2M(~?}H-=7Vy^y~j_zR&o1b+jiw_0y#pG93mXWI;&IST||Ab5+;`g;Z5 zB=CNL&k1}TlEGk;!7{HKy{P-T;F`&tc>)_v*4!j`N?=jon925!34TK0NpNnpzAEsv zc@xzH$;WjyAM@w={)$!x=Ly~{;NrBJ$#5RFsF&-CqvBjA|GHc#RYE^yixEy z0*eAq2>*oO&kFvm;I9j$AZwtYXcIgx_{Bl?Nu%Jcg6|Q$Pw=APjUnlyz@ors1yTv~ z57EczONo2rjc*%o7~9PObJYB_`H1;-GvsT+hDgKuu48>Saa!|XzF{kY z`;CA16MZ0{gE$rO0}lI20PBo$z(WCsX{!=&cVq_O{rX(MJA4e6m&^_DUW+yR0Cn6O z`T(clcNaQV!yxzz0d?Yer{VUo1o#r1F*WQZ%78b(5*_n73b+olQls^NI^I`K1KtRz zQxjDKZw5@_cOgacX;s>-+V{0zYismR=>MY6H@b`=eyjQ{&ZTxc&_CBF;is(AuU3~EgDL<`P)O3R?`Z0EL%$xiPW5;!N-LGJG zc{Z=1jCt5-P5P8pFfp~=F6`aT=3Rt6=-E8Kv=PJjRp48R?=*a?;KLQT$JtLw+)8D% z<5a-P|GfSN=wDK`@fKZSoTk0TACcjINskzB&=a7a2VF4NX%Fz8aBWN0&ZXC1Ol#M# z?&(?DvxL?*70}ZgNqsjQ}X45!9wzuU*hSJVBwSOd4@Q5AhRNvNIcC^LLX4Aa|bX_VyB%3LKJ2aef z(#|9$1zRYu-sVicz@>%s&{BdD+gaA2Jc`}{yReB;zF{QSyQhJ!N@YjVT_eLVw}DzS zVn@mu1;L6@4%2LBLh-Ucqj(vKVu-C@O>5!Ak!*TBbzoq!nOr&vMn@(;lq&QNZm=CZ z3u!00vmMe~sdF@sCX?-3sn14)0H{$^oo*gsos65fpmKx<)Lzd z8Xo6!bWu~EeA9quM=b5k^{UvVmcexIzPBFq>3|zmjr>7YNVs}F( zokfvUK9}8|M**ZZ!iDFSZaFWv34wtLbxsrRq&>HwGhHa~3#D^Pc4UTj*$&KW%Xyur z4H@99DxK`KM}Tub1{^^#x`g2!#A66wj(X_r=`5sjeJQ7pI(wbW-ZZu6dD?Atb`53< z=}wGVnl`2jn^O6~7X0Z6Z5@^ek0E;nN>Qb63qQJtvhsAhB6wR4ajCFFnSvnPupqD^ zt>O+NiIAZo_gTQ-yY!|C>6TRQV4B=C;$=1AwdW4l`_kySmlUX-vyz?XoWL~-nW1#o z=x}-yItLDVz|;oE9&$N(81Jf7Ic`G+a#?hXM^nt2JiHP^QyASmz9J_G2fHBd# z59GjUN#)Zdb04{g9fMXmEU3%%Es#_$pM{sDGNQo6wyD29Qj8t;L51X{d|N7uXoOHf}Cf4VTb z1Iaf(!Iw1OmQ4?(a|IssNkz&vqGv}sn>r-eq;k(_xVdknw=kj8a}rG9KW&;QXwQ2K z)dbJeB2kkYohWd_PWXk)-VCpR6Qvv`5_}l_QOpU2%i2I|m#pvUIsZ~85uy^8%mWuq zNh*`Vc0_k3mF>*jkS0kyt?B-){nVi*G*3On*o^Ssl+Fwc7Ero5l`pjC`qGDhdzH?r zLdY+9H8@@#@YWx$?b9BD`KcR5ItylPHnLN|1Durk%Cl64nSw~CGVc+76|AM>w`I zOHs_VRfCFd$!5UaY}@;gQJj6$l^O2L3}6zG?PQROfT&e-Gdkgbl697?>K$y#W~n6w zaQ@CD`%uKbY)J!glO9$86%N>bsEo;oDn!U-hM?>~8ozkx-IwQFETpulNY3Zv?oO#G z3sb2%4LhlMq)@PP=mQhz#SU=ooV4YRzclCJ><5_bkvxAoQn^fjI$!X3E!YTQh2eSc zrU#6J5Xfe-dGk7jB;alBHVyC6cy7yjNjE zH>U?uy`wvG`*QZd9AcbTVa`mv-Ne{$ObugzhRF?(X4@{H5iq=YUfR^h-h{@F|ONG8kFUiazh$8<>Lu);fJ7qGtZa*W~Gl_?Ci_aTZi z{TVQkAyOk*WCVobu+1xzJC)Un?O;aR*+OP`oa6;emLU#N-c?A%KvOqQnG{luXO^Tx zoB=$iu#?yYX-E1CKJSpklMv#lSF0-sS2bghzu?vz#%;C_LP|})&8bn~*o`3`ZUDvs zxdWrxngQ2#R2Ic97ibqJdxgr`gW4R2+3xZT)I0m&_`sfXH$6-x`A7jM57}XK7Do2w zy>23ZVKabjUT+>Fp65W}L03s$4q=k`6gECoDsN#;lk|@M=au;rr#ap~jyK}vVYTq3 zozAp#Ak&-9yF0}^#+7#j2o9(Mn_bZ|h%n(jVBX93K>n0_lai7kokrXqzLI?It z*u;7J+fC^#G7VbE^uei+w^i_Q^kY^r!|T*p1#O2gTT}zs;pNF;fNI!u%lJsg8s93-%85;Z8h2Ewv62s?&*+J}a*xMg63%6k)8I@yM?$1#RS#lZyRy@~Y7 zO)%8cgQFud4RyO)I6k>}=%h5--D>yZe32_q?}!7DZ8$uGVB1)=dv16H^|3R~xj3)9Taar!hjHFPI?2gXg_)piczX`HQz)D*G1;)lU6n~6REzN> zS|!{*j1=cdZ`-7TP5g97!H$$dMj65S-k-q(iHV%JwDx=ioIH^(WT~d9x zAUFtgUwTxE1{@dh7Dv88pe63w$v%_watjua0ULkB4$ImQ#-kiHUSLZE<9OBe7v zYlp6*IR4nI6L_YiI1F_i^u#Oq|~#fg;*;l7qm=me-m`2&`a(IEk5Yh!YLJ; z0!l8>R&dBIrx`=8mppm)Qwpuow06;(6ASxctF%#mK%QRzseC7F--oYF2e}u%EF`I< z3Er|{b%AI`2lVB@?MF@SMH26mhS;xjTSO*{cH*L;fG2y1dxSCZUB+<1pZ6%$=23?& zo9dk|=Heoz@ zwAh9VN|hs$-CHNIe0Py zONYi~Jo`M$Xqs2M3DTn&nG_&Zs~+>%aU0`WFXs{B7~-+Z;hA*H&h-$N_rZpPv<-FC zjG|~8MwGp^MeJmcdGYIcW9qn|C&dTPH~8E!ZXk|fQxk-zV3N*mATDxo{J4@Q5l>?; z;3rRrHPAC8CdFl7u(^XYJ1~!ugTS-+HxYW=7Qfn5q11oM@!Xlg@y>Z3_wkV9+~~?yu!b$*b53=6Gnh9u=G_t^x_0!Q)=#Yca?AaV z-;aK0?_1l+jB8rJh~o#{K%!A5!wh^(U!-8zAJ?^5@zJna+kv{!6Dqsc0iofOdDyTw>_}%i@?$~wVa4dy4da>f$A+!Zc{v9%=6&v$02%?49ZkR!_ zu`(0zI;fuw1EC$5Ug|fe#nLb_)*%+_VZ1Mdk_W-!?(j1Z2PX}!Xq82wrh-Z$nyE$R zPJTZbN$`r{_#AW6E0vf`U4D@9q* z?<$=C`VH(?w_tCMj>tJYX zL=Q*DQW7Hjd9X5geT?2`Vq^O`*6?RfW^5ZAa4UXbCNNt3AP+q=cyNwm5NAf9pT}tq zk3AkdddKmH5OeT;80FE31jVhYfXlgeV@L76nkzsPbdSBy2(#Vypu(*4C`9=j3e3?$ z(CCVlNi!~?&xmNQ6)JeftlwD*gD) z6WWaxPfA!9PXgm`6BZ+g{o+aZ88KfxiJ{T)h!sC=TG6qSJQeYnjhFnw<;pyeh#rJ} z@zLnmr)ADPtOELBOl&<$CaQD7xCns1e!m{eU{G1*<7|D#=Ql70_(_2t2}dv{sxNv4 z{5#3PJia>1cUXQsx(fY=JBqKOEx@pP7;sT^mBMFpd{}6sE9Hw zG{k=TZCD4c1s=J)=f>zigf2_|We0-kYNheE6|J&9@Jd#BzHM=s*R6@|MR@9_7{3RI zGy}Zc1W;r^jR0}x#f3zE0MpbN@Q)jGD{NS?N-y;SA7PxchG9jIVhD@(;eTrbd=<(X z@!cHc$d>FCP#9-LVR`drD#;B@$G6+7*rEH_2!c_uN*`vdz%WuUmJf_Gvc-swbR0d( zIPzjJZr~AXjIM%oIJzpxshcC3#|6XJ;aB0mY9=A?d(p2VU1Iao5uJZB3;p=TA*qyt z(1xs<4ICFE5uL-IqXBIrB;wlK;y-|9OE}^%)UR>~N7qKzB9ZB{5o{oWs1%7c;y(*! z!WTZ&#w1YHCFofv3k!rs?hj9dNO(43y0Wtw#u`1S+=~SdQOf)P=L>hiK&F66Xb20V zmu!G~8LTAkF~b444{J<7n(iJh%VP?MsiDA7r&3hdxSdoW^3JnrAw07A9RH)$6w{S zzZv0ciXc~sv&bxss?@!~Ij>5n6hG&|3$!%9#}r~|vH^d6xrAt$MoA%R)2mnZ_xG<{ zwz98b)ruwkD|=H*S6|wi&lQ<(DpBxh$P(Xuw}<;?Ev^t!>RaH{!e})&n9A z(UWrd+m;ede(r-1!Ld@|h!@N#)m1&yXY$P>(TGL^+|6|I+^g`qxOXGrUf0*fJ&!C+ zUYcBe>8hp4WlI}YUs@M$!Sxef-#mD}Wt*F6^QuV4x-Baap zd+!eF_;pvx;h#C-xQTF*U!BS1m;~^r2va`S;u^*K9AG@|QB$AZ|65XVdoayfv)K;3 z*r1_&FXmiYUczzdI}4%X>}IAa`!DK0!2_BM0{+iB#i@-+wjm_1bNDQ`3Eys%oS5{J z!xIMo$t3;pI z{snF${{woL@YZ*{UsF%oZ0AzkKQ!RGM80gtEXcDb)txWj=VmAM>@Vq85@&zDIO0!2 zMs+iGe*>bCJvR)WvF8VHqk;QRNg?l+_P4rx9u;oi^9xgad+?X}cd{L7+_phK-&>5x zZRG#WxV;CzDb{euH(B4=UY_uAEo@&+D-osqZzV0mw-5ebMJq4@%;`lBmqO-J@K=Mg z92|Z&2!1JOuElpn4fuy`0Y0l_jGGYUe19|~_wS?d=7sIx@t*VXVkaY~iGItDIQR7kQ53w>kh|dAvfoFih=LX6LD1VO=$2ykSP^v1 zFaXM=uddyo!f@~{2f;a2R<6ZIENVKVf z6!b;(i(hP;PJTMGf#{_UB2Boj9wb{0I__3m{OrwdxoYwA*FL;n0ivl51&l>_3fc`- z`jwYIc-7+lZ`yR#;&LfWHx_?n>$B@CK)DoyWTZZZCQ@$(SlFFH)KbBM+H`_-!$C#^ zC0l*80tINvDkQ-VoBD|wJWbz;T7DcC;;dN*qEHWzhG=6NZ8tXT)zfIDQP=JHD5&qV zG4f&7snJ~Mgp8$%n!^b?TZ2h=ugNeFI@Frw2gm=@d{A&_$r( zW1>qm*^5x-WLXc(R_dvrs$yH%N0*^}^wn}sKrpNmAJ&ab`3>ZAdYw=aTlrQY;XFd9^+?Pb87i#6DIF)%v~ zu(a7&R?}EL8GusZrkNWyoBjX%ei&=o%UQAkAkug+U^jA=$)G1(p~9bTLRoY&bl$S6 zVAIl2(_*nR7_yro?yP_@LFWpq1&cbTYRiU!Wgt0VuJjU&=L7{yq zK&b3DPoR35K*Cr!XHFXN6HXXT6>DEBxKfl}rwZ7Sr2%DK1H4kQ&oSis6XJ`z8@omGYxyc`hl-M++NS zi}MDwVz&WZwzMh)r!AWkN^b%Quf4#wwF|C`U1^xyhyu%3Do_Ty1$Kw%an_^U)fsf3 zPJfvfP&|7Jq*YKU_2Igtf32n?DlEUf6@^LD=V5_FaA9(u8qc6}f(?-I3|c17cTzxoLb4Dj@!Z>vqRFqJ(GFN(w2i%oi6gXOR9P5Ldrx#3s8d*zjSUY{cH0)k(1Ij^Y!&P&WpUyVv> zER<&98erjc7ofczAkw%aVDA7D4f2X*R+$OoY%MD42_tClM8Sy|6eNt$37+&wk-NZi zUJrk%Vi}s58TNt73~Q$6Ky@0+wb`J>IiIHJx|>x#osYF^0=6RYV=caxl|K&U&Ubi3 zc7qfUmSrtsrn|vuYB$pE4RFT;uy4}MZ ztjuPBea3bXEvLpFnuE-&nGIiX_M_$$d|^087~{3c;pV1zB`WMe{i!H0wMaH#BJoKy>FhbTj$YqTr}kSnKr6av zC1_!EuF53j{03T7CLy;aSsBPN%!&e0=N53gr5febLy{mG<`wqAn45X;g8p(aScNed z!~45S;0aZXSoYB%$`Qs^p%-$~g7l||k+fd}IT02tY_>$4INYUjx$N5xLmo5C>l<1(Wun$a8r!92!0u-y+^|9e<*r48 zGOXOgU?`0pu&H_&R6XR?w`|ACkID=*8(Innplk~1fVZ*7F)dZt2?7T6D25KrFsHXGacU6SC1i9Y^nGXo`_L2~O+nDR=n-rs~M1$@vJ-<2Y~{$FYR#TJy6RCUxyH!IsPGD^|bpMGIKk851#$_?% z1Zo=%JM{GJO!E#B!E-H#4R4*W{`hDWo>&1b{W#0=4S@+?369-|8PSik=74Eq<6%bi zU^>JO$Bhkk*z;S1#Fs@FQq*Uj{ikTCzBioE5Vd~>9FIHHgJT$K$2LhE2b>#Ay?x^} z0or&y#8R93q6uFRSa4ICd#`M21{1tT+D`BcP8i~lQ2NiA@S{M+40RaYC=De{Cbn(r z157DZvUaB5#C15oxrU+LU|&K@^P~%c&1nxLKe+DLraq-Fm|~r0@|Nw>ti#aLI}wY&O4@gC=o&}&X|yxYU`ceJSc8X zSr|@)l!YN>VMr`Y1j`~;sKSg zms(mZf4Yac2Ap5wqV8KE(frw`V~Ogsbyzy)tLv`x-C$cGw?)W`x0oEqpKibT;wqX!KFHU#QD1yeZ`6NI_~lU#l<>g zP!fBzdR~X%Xk=Ai8YdVtu0d1~UCwb4fziUdp{vSgpT@%wX!_C!^2QTaqE*vS_*|t`#zmok6_HEWp!+N!=1lwj7%D4*JGzJ! zQ3_W6LrC4PtbzOI@cckl!qLljVk3s!KxAQqX%;XzVf%i7NT4Zbs4>SWLk($=a|`cZ zcH+q|-{j>IWw`fV2jr}Y&AuCGQ(PjEIeEo&nQ@l+AevY@#U<%#j$WLi8XG>Mryl@c ze=YCXoMY%|eLa5;)~LUhcW%x*${e2ZXCT{f`Q*hYYQh%l!%VV$0s1%CHWz(#zJ?l7 z;Kaii)sH~r2^glO{~DOH3%r_|^hbe*pFlghVt=5_jx03;g$GeyQ&VGq49{gx>?&0c zMoQJA71igJ*;N(n%ggMfu{0PGwU*&(ca>jTQog@J-2a4GGVW9b-?aReIo&)p#-rV4o_$}<$W9-NOyt?H|=qZs9R(g;>Y08!~4HahdGG~`p=-|e2 zZ1N#!sY`zX5T{t2QcS&mcU^;Cx4XW2+7i{ui&Bv4Xvbub}3d(Y> zo-VX7eF0>io)dOHg97v1J5+o+245j;P?Z96)V*o4)lUcq{*2u!AkT4p+engTVLw15 z{Ye;Y26SHm0c_fz0##Z%_UgwU<}TTv2Goy#2Jozo+p5EmoI+cCTO~_R!vb}kdH$y> z_Zpx5S-hTC$Dg^D*EFcXbTIYu+9L~2Ly13q zAu6dvIhIh=+sMr`s_Jd(zIv0!pJ?NS<%N!{ZfR{<)w*g04B}4{vE8Cauz)U^Bx*vU zcnBMVCEZ0Qmp=%*v8idn8;i}7J>B#O+U6lxvUN``lLz8CMgcyGk}X1s^+hAhEnVpj*A7)J`=jVdy(FiYw(56zH)H|8x~ zTcfH+CVeURkCsV0LJUs}JP|x(CFxfXH0guE7s4j}NoZNjqfGl4MWOoo4-JQFbK&$Mr2YxOlilLpXYf+lNz5R1_(;3Q~2aFafxN@I6N zO-jYtp8o11wYIpdj{YU`OX&G`jY~ftU^oT5j_%c&|IR4mZ}tB&P)FaW6;2gf64lQH zcG6YoOPGEVKZDX65-jsKF_w8pY*}m`y`k|6`|b##%%aIxSU1%A4QIctLq-w|g0A3;wS&^_AEVs*3t?arg+u-T+9iyvxW z&pi6G#8>orbdR6;KME{^WEX6!qy4a?j*8)R{yKUiwACTGG29|HQ1q7^9JG!@39+Kl zNhFC4NjttysQVDFU2z*h>qoRP^+uP9h~!VAEa^uCa=n}C6je%5o-5GS}jO$kNu3-)vO18U)_a1lU}ZV93$LY^|-Fl7mW_T zM&E%3jb8FG92K1_lH0+5ldl)>n*u)~@E68Ikg4@ETw+}4v*>gF{g4??8~_}S4*|Y2 zeh6?yas=@1sv_Xp*aYDB!xzFKRmLRnqrOSMNq-w)d)As4`s%1J!gycpdgKg$T?buC zYxLKnr*(8I;0pr(QDeMDzZG?V6?v22H1Erz0pFyp8pHejlm1;P3=G{lXQ%Z552(Bz zt#Zsyq8}c0%KU5v^#zp4bU^W=Ro_8bnYtTgzEZshP*$eC6aJE)Gc3Oz^%wn*`u&Ok zH4I87nv`9T{Dwb7-%WAx-SnHr#8FeyAmB<6^euPV&_};H#k`7DKl4Z3)Ayp$}Z7%5JQRnYHw!PQeKnAMnkihh1uixy+nLyOt|00Z7iL-HMVix`19U)UCnM>J}W#-{?~JS2vj# z&|6*V!Rj@j-sMt1te&S`NFQ*ipI66`FaFx4)&=Hi3+PiW)e(r}6T(Ma>ffUCw1xCF zmwF`{r-k$_Ma}87Hkc`TQBibT%{Ar{+QyqGdXz4S%+oHS2Zg$u?$@t1FQSKqx|9BQ zvd>&f-*wB<;eK-&^)@QM{4&&UUQDNix|8)$o3H4lv zqjfoLa4C+~<+RPEI9eNMmrHT9Hc+2SakMs4#-%t~8|g-&ZVi4iakt(?j!>t9f0uYW zsN-(g4^h@kZ*a?gjk0FC!!7$_xJ7HBw+QuIh-1Em-l@vqGYy&Zj8G@($r`4bu>T=? zPU9JXtm;xcJ66zhLY<^j{^#{o^pZ>M_2I{K|k zy%@jMTu-}JsIpk_&&>_=6`}45{*S=jW*a@}Qq^Imej?PZu=7d1jc6s;dz6Ng&+D7$ z(=N55Zl2aok1Hy8b@=UOJN=VTT<lhE$Gpk0?+H)=yNW0OYC{Qlm6YMMnGLn+t#w=t-&9M&Y0KGE}>2ZUkE)2s?RN} z@|`ieDC3sR@jVFYMpXtoKV@#GsS>5(1bUlL&uN@{cG3s0P`1?6e$l*^o_DF&RX<^N z)7C4w>{0qSR*4=e2*rJ8ig(qV3toeH+e3EYx&yme&KgaLADDY+0nQOjg#u@= zTCa7fxq%0Ky>y#PeZKZz%wGDeP^yLN=qp0Kfu6#rMAy;xl%&ju>**&IlGoD(YOkMa-Lm`Xmu5dT3#EE=fYwz= z9-uZ?at~c&9-uCls@HyJW~iq^a)9=`Wp_YwfQl})L%YTtq&q4khv+SC*;gSs1RFJa zIy5_6S;PDNXRs5~XmJH@hMB(v%aDfkmvJnM!1n|9;5Q?TS89G#I4@T-Kb~axRV-Hc zW*inNjsC)7{8NC~KZxY71YRqpm3llpj(C5$Iv&@lGZ2sa=&PdnDS@i)uOck-{V>CI zxbV`bL7=CBIp@PG`&;RU-@y+WG1Rfv^LJ`5#12iPe=l(~`aF84)8B#PqkqM{fI+)O zhH;(dNFS93#aGsTQhYKDZ|m3*aoxLNtpWR4|6Zx}oX9*T@cjZS?O!6}(jw#XYmBK* zjZ&BG)al!zLBRp>ZMWEbtN5f({LHwHT^6@^XOtn=oeh2g8e<9k9p zi!DGu)@Q~S0fup>&}qf7du7-ruv6f*0{aAJ1m*z!bRVEb*J=zifPU37lrJJ;W4(tbPsXYd$*WQM+>;I~`7x1k~Xw&YhyC1vw zdB`#o+Pjhu(uXhvp8{MEj?-h%5XZ^k+o8t*@liA+7g%Rd`cloabURvqj_%ZMsCfaf zEBP|5rYkJJb|u|gQ>%TNz7U$HHPLO+1sZE!rro2B*0yN(Y5y9~p!sqCJjkp^X5UP& zi*^9-w5|dCS||=}e&25GTj1;iT#ntsx2VI%@IPYnw9R6Fm%tvkk3UGh4y}AG`cCb8 z;Ar$cIu*EIdlvYI0a?Sd(C`VAo(epo-6Z>pE@^i{+fny5$gutoX)l>SgnriWp!Q|! ze`@=sM?=!1pVBXMZr$%YtNoNdABX+g`vMQrm$W}m-cLWJR_uS8r0z`uACpn*68xw1 z+3F75V02n%wJy={Ep1Ef2K`&w8o;9Tr6~5iR^(5*@L<@Fbu3$RP(MivqIrFSdV@v% zds^6<0LKF7Bz-*2@P2{Uh3}^xNsF+PeXhbqp`AVaSnlJN>Zs?ZD^OG5i;i zc`|ah{!>^RM_X;-ztUG|AFF#vXPm^s|usIV2~vd+Qztd{^@8`dr9A37AWK zU;nlARMWp4eO?dgM(`K9rLV2|HTY|MIHc*Du;-tv@#?< zjP4+gv1he+*FJz&Lg>W=Evzd-&s%E0>|3t=5|%8|R|lW;tB7nWGUJR}8mZf5bZWekck0n-5jaQD zN$nf{Gjy%azU|YOneQ-i`f30Bjhn#vlyR+oH1Y-GTiU(W_l!IBOzpGAefl^2&lw-m ze;fG^<3Zi;)BF#~y#Ab6@{r8#kj9agz*sHxC+HS|w*%^Q53K@xCt#5N2Am*$jV=QG zJ6aC-1hoKuSNKl}{~7Qv#z}o{um#^Fya@ZtB&`Xw;GA$3;1aq4u#tuVSJ4LmufRP> z3$@cX0k5H_0K0{MJ>Y$G7#iLydcGw15{-3E2|O+E5rL$$q$V&WaFM_zdI9{6g0~55 z7kEHmM&O{pHwe5>;FknGEAU4GH6QEu30x#_iNH32DT7Nl3Opb%BXCOKNr9&YJ|gfL zf#l~}Q#d_d-0PHck6$n|3pvuF86)J_aon8&eZTkVlnu?6Zn16_P2p$;3UHgcZ3@N7liQ<2wm)J#b0aa zSm`yqeZUM@s^QHyUnVTp@MajqI;vwejR3a*b@ZYNcoI;j6i(R9f#emqGp+v)X zO_RWvqNf@y1JrRhg%dMwq3VIJ!g{P>rJ4(T4I*8`4a$7rm*FIf8yG+x>+?e3mjmjE z`9;951k`CgTF~e!KeU_NoDoeQqt00V zwB?6(j^>KHMlyrhZT1K_tWM;)BC2i87Pr{WXr@@q4x#d{OwK8=42rt5MR3~fd@-9Z z?#c`v?#yq_4Hn%}RP4+TWshxf?9r}V!DF`#7qgBOxN>f{R?OZluvHI$ys93!74bM< z4_M+HKezU`9#}y;#z#gn10z|WogMk{(X8W9;7hGX9C3D>>!ktE3cZpGT2w)+8DzzK9aqP_IB*< z?%cVff5*$e z;E56R2c0XlY|ZAgPHvEn4dMIV2eZX~4xr4zZ08Vl*_k0~8yceB*&!)n2G9beTy&U& zN^wWjqy12J@bG|rtSd8+9f5G$*jP3{R0>^^^6t!$?6xeFIWh%kvy(Zx$v)O?V?NN8 zO5s(9`};S+7JTxyC6^rmAD!+lVt~7g6L9Wq#oly0ufXPPVbIBqao5kUWe3mZ8S2^Q zncVS0)@@We24iQ+w2wPZK3m*VkgkkmcV&u)W=gqgW)^g13dOd;!EB+B9bzN4>2B2Slgh-95zc44N-?%csce?Vq;ZgijR zV8}Z1L$oCazyo>EvB!b#MmS`hZunw^v!F+#V`ZuhJ*uQD@*)elQH`iS1Zos-wiz1x z5g+-Xj59>Jd;#cSmh$*xc?sU_a3JEEO2d`OgQ|3BduH%ZmOArC?88}@?j;=RRwr652yA4f&gXzZN64QC5Q9!$dQmuOUW zRz|tZHFUWg_nwTaOV#5U38`s3p3C5!8}XjTnS7o!7%dfCn6Pa5&{oU^LTVk(9qVU? zqU}O)8l#t|j96Ae(6r#LX@ZZsykRsVjA{9o@~p(og07$4^wE{e!}Tg~vSU~WcmfNv z-5wp_!aez1aiSxCkntTh020f%43n5uF-`5!u?+sCd*7ifpbDW)nL-xno(EaQ2RC*E zOM8}joJ_tjf~4a%k8a6WaMUTp8$3lx230@apcthwVI2idUt-YFLpfluehj{FBnhKv zsI7>v8Vro1XmfU8{NO>3xiYT}DPVM9WTGcmtYr6O95lNHzZa1`YCDI^@*T&pBjEi> zndPbAadu6cS=gB$maPVAW=75{g$>7W?%=o@jIxs5+2QWYaJD$H8%toJ%vXJtk=vae z$s7}`qTI7=mxIM~uvo6>6|jO#exh99#)$BXxq;jWLZDpABOyT`5wEZwXJRX+A%})q zN@XlKu0==?2w;#4JV}M-B~B1lc+|nL@oK9kHWj#PMGcj>5G$iL3feOxgWb8C)rfZ@ zLUWms5O!!&*=rc0Vt>e;3T^nYbnwnZOUQ-cm&&9 z5%;_l&w$&z1cpR_sTF?Jao_45p{gx8PqY79mS?&U#@rvZWzqALnmi>8ss$Nj3PJ6 zHpin~YR}jZ^5$$?q;s}M;+@&#I5y6e<=FVo%6bzgJM3=62wO~Su(5frV+e2i102H9b{G8*@=yB#wm`YNS!x&M@L$)l-}ggf_rlrW86NH%{UUo zNPBA2PRi@$5$25O7IF4LK`DQ_Y~DBUv4m%{?C%|z#^0*jG@j$*uJj^TsMzQs!v9kT zWvMCCM?9^t5%^1CHXpC$C?n@)q_WIGFzh1Mf2msD4y%b)+8w)lk51kRkJ-Eja5J6S z<~dlRhcn|NI9;DlpqJB123rz`a_|Y~#jN87x*AuMII^t)vK{B45eT*X~)+IWd{G)lA;FrAWa>P1L*CKfO=&hIe8pP^OHs zYp_e;wMwdua$Rr2s-1j`w+HLaI!AIiWw|>qRRyHG@D{vNhb&LvcjF*AkX2iZg11G+ z216287M&Tf4`R67Waka2yK+J;_K(>jA6g3RtrCuG;W6{xs#hR&{_C;r5j0%NpryWG zT)YFl#Pp0~oDAtquJ~uhc}|399`k6lG|RSq=!1>7?M7|LsSFzwDI4+CB4`tblqv z26IK6r<_7@r{ksy>hFg?kTa;)b%^*H*MJA}y!v^u$vL@qvpt9-D43;F(&pUoFitIC z@xbwJN9rJ@dbpYf9)+Wc>tz%_}TtJL9it}sp;G(>2YW}a+DsV_S+VjqV%uvrQ8sIw92*e$dHIP^)_%!823lZadbBgSIn4h=h)t#xcA_!8`L=kHWm*sN7SO zQ;VBS%FC$7cQs?fY~$pCkL3%zTioe%jE;e-2#i5a^k7l@F?^TY#^-G*%Hifag*PGy zp9+rvxA8q#T!~N%@L^C6#Cx`7=j zm3u8x^&ZqXjJHik0qHPnI0EW0T1nxD^KcT9TDyUy@(f0F8ql zL7lyz9I@4g_RZi9!a{1EF6Grdk924hT8e0gLfwL)S3bOR9dt~5GA3T*7EX#!I8by9);`#qlb-On zqSb}oXdZ!OWiRfKnpvXt?a-Np2M(j2sxw_OC8IWs(c=0XOB_Kh@{j!7V??Dy2QF7c zE890Bk_W}&6iV~Z!cQK#93tfR$#N?*TP>nR{9%UMQp#;Bh0l#`L`oidQ}}ER)-mB)ecG&m+3<+!k3+skD|gj$)2w#P6=p{V9AZNTKDBY{pyjEhn0K zZxLa|(`rL$4sKzWvQvifW*2+Ym?ul6+qr|4fjKx!)6<8J2ge&4uU*zYTbt}U zZ4K9`9HjqDE!x@!jXb>G6dQsLo@!p`dHikYA1477h#Vx!$?m{K+A!&PFyZ#O+aZ}3 z58$Q-oEA*0QPiO7>3yW@XPa;w$|-z-lc)S%x4j&6=V=Q(%tLmr{w-*WgH~CU2ZoPF z=c=*VE_yOZJ*r;?{2D>$EMBDSY-zM|oLK*K`%~yMTX__aBVo3{FOPx*cpycug&c*{ zbORRgv;sfsad*I4_Wb!Kqmt`Hug;ZF-t!`d<5GD>r5;Z99E+pkjX_BtNJ3SrU+!4q zdu6*JrzCko^Mq6}&*>(I;#@1T`BOn^$c3z+B|AJFV$=) zM=~c|6~RbH>o_^%0vYdeYOPEOxYb2j0oph%mzNl7?11(R#-7_exBm2mOv{f-ZML`^ ziL+{LtJFGuDQHLEc^+}>Pj9bsR?>X6IC4JXWQKE>Seb>Lv_kn9CmEEg5i6o+hwv2z zPJcW~T&D-mqV$UvdC61N^wMU9#L~(Hd|s*AF4)bPqYW)gV6J2UX%R1793=?too9-&Iys@streIf>&Y1^6J*s@XOHY8G&wP*_<$#os(%@`uNs8ofC*-QzuI z+4GE-cWCP_w50s0{KyNJ%9flbIU}{WJ)7x&F6T|)QKAihOs#U;v`6KzcKD2E14kWi zxp;%d{wg4YvOlNyXr@J#5rKS}*(LfommH&h#0YOU&)3Eu)q>@DbSrG-yhjVCw>hJH z(Ju6a=Y(g~42g@&o1^LdSZ>>khf%br*4gRvj}}dzIlP9eO;o}2#c~y`-a5fqRBd&6{#I`P z=#uF@^>XOU-H5vdEX-!-bo;!cM|mCNS*v=YdUOOflrv}nkE*^O$M@*Kt|r9A$rg|*VSa&Y{9bGWN) zjAs_R*{gf*Fq8wyYtgGUB*TLZ_%o!|cF552hk-isc--M((*F+R0vMoQCyegLbM(O4j5Xi>cQ>BkFS z^Rw1lQ=bS1wA#cr{Qa1z9sxYJYxp9c$+h()1!@x$`U2AZDP6l@P91(Xr8dzCc@IjV z5eBC^u0hxKQdtDFCns7Lux0oS32>p&APv8bMX(KDKa9lywZxv75daS9K0Jac9R|dc zu->1zp*E4x5;w$RiL8;L#0@6Ir@o4Qfe9p-5{tlR!xA?Lok2%Hb3N2aa`UL5gPYU> zWT4Sl3_Lx;`h(U5i5sk(SZZB>&L{DA&~P1;vj@S^wIF_WDiVYZqt;EqL@JT8a$@av zEs+vOB~r2axUO6TXDCOpixz19u#QrEu`(9pf*4vzjKUlrYaKOPHTIS-tOdDCqjBpd zxCOGwfS;XeT_F7!PmIT`3*a?)2YYPp>bUALKhV)}sO4SSf)GqU-nt zHy^6S5^a9i+!oW#m`Ugef&(?tw^si3Qv_S$N;6>SFG7$=V=v%aAk%qKO03JAtmOMj#<3dBP@Ay3!U5a?eumU(lV)D2fJri6IEez$0!? zUI>v0N4(%kiYgsSV2%Xz$wj5ftTMs^EzA+EWH8qaExv$anA#Xfy*>~~OnrFff9OHt zWV3a0g@s>nhQP_@*o6T6R&(vcH7UOotWTU=SNhK%@L?(-sPuR&9y83u$?Y5}F+6pS zw=90cBau$5g_{y5`;?|TF-F_aB^fHr<&SRYf1ULY;mZ-K_&X_Ha-7&N4gaSrXoI8-YQ zdI*v*F=h3*#-_qpU1a`Rz069qS?g61x{rklxdI-FKqwJS#NjcWd(FWKgQNn&H;xWV zeMY^2)nh7G(3vGKh}LgiUredN5P2U9R(9V zX3h4nTnayEiiC>S0{)4J9=&d#lNrOerb-`2_Z)KUqXi9e95pya;7@B>c69W#<2xDn zvO|-5BftL0iq;mSsh(I(>Dx!{*QNQgKg3m1EHYoCI&~F4<9i+y#TUi#g`jK(-?kEB zMaxS3CJX*1c$G#iLNpGpS-W~=cIE0!>rm_3bwjI%TC?j`u3Wcz;Ih{2%C)U)*RB~@ zwJx)67{806QT4@aG7x;JeZ~ua{5S3O<6Fw)S;kXkx)SsLA1{j64@Ybs8QG4{cJUpS zL3kxA-{;`cSC&G@xy@9n`fvRIX%FzPqsVWJO-=76Pc_1CkI)Q0%WcEE_cYOI&d~1b zv~M-Az0{2(@?Pqo-M~8Wx8FNJcLHv4e~Ik##?Q`jTGyoo{wq^%3A0up|D7xIroT3W z#9zMR{NO;3I{fevls|>x*F{o`k3f9FDBx(r$0Bu16ao6E!O2&HO-0$i^To?dO;4d= z4ZZgMt;Qu6d;shDuVu=YV|hv?nL0V~S96yjP Better ModMismatch Window - 1.1.3 + 1.2.0 \ No newline at end of file diff --git a/README.md b/README.md index 4482fb3..d8247d0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,38 @@ -![RW_Ver](https://img.shields.io/badge/Rimworld-1.0%201.1%201.2-brightgreen) +![RW_Ver](https://img.shields.io/badge/Rimworld-1.0%20~%201.7-brightgreen) ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/zzzz465/ModMisMatchFormatter?label=mod%20version) ![downloadCounter](https://raster.shields.io/steam/downloads/1872244972.png?color=blue&label=Workshop&logo=steam) ![SteamSub](https://img.shields.io/steam/subscriptions/1872244972) +[![build](https://github.com/zzzz465/ModMisMatchFormatter/actions/workflows/build.yml/badge.svg)](https://github.com/zzzz465/ModMisMatchFormatter/actions/workflows/build.yml) simple mod for comparing two mismatching mod list -https://steamcommunity.com/sharedfiles/filedetails/?id=1872244972 \ No newline at end of file +https://steamcommunity.com/sharedfiles/filedetails/?id=1872244972 + +## Building + +Requires the .NET 10 SDK. + +``` +dotnet build Source/ModMismatchFormatter.slnx -c Release +dotnet test --project Source/ModMismatchFormatter.Tests/ModMismatchFormatter.Tests.csproj +``` + +| Target | RimWorld | Output | References | +| --- | --- | --- | --- | +| `net472` | 1.6 (Unity) | `1.6/Assemblies` | `Krafs.Rimworld.Ref`, `Lib.Harmony.Ref` 2.4 | +| `net10.0` | 1.7 (Waylith) | `1.7/Assemblies` | local game files, `Lib.Harmony.Ref` 3.0 | + +There is no reference package for RimWorld 1.7 yet, so the 1.7 target is only built when +`RimWorld17Dir` points at a folder containing the 1.7 `Assembly-CSharp.dll` and `UnityStubs.dll`. +It is picked up automatically from the default Steam path when that install is on the unstable +branch; otherwise set it in `Source/Local.props` (git-ignored): + +```xml + + + D:\Games\RimWorld-1.7 + + +``` + +CI builds and tests the 1.6 target only. The assemblies for 1.0–1.5 are prebuilt and no longer rebuilt from source. diff --git a/Source/ModMismatchFormatter.Tests/CallInserterTests.cs b/Source/ModMismatchFormatter.Tests/CallInserterTests.cs new file mode 100644 index 0000000..84d17f4 --- /dev/null +++ b/Source/ModMismatchFormatter.Tests/CallInserterTests.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using HarmonyLib; +using Madeline.ModMismatchFormatter.Patches; +using Xunit; + +namespace ModMismatchFormatter.Tests; + +///

    +/// Exercises with real Harmony patching on a method shaped like +/// ScribeMetaHeaderUtility.WriteMetaHeader: if (EnterNode()) try { ... } finally { ExitNode(); } +/// +[Collection(nameof(CallInserterTests))] // Patches static state; keep these tests serial. +public class CallInserterTests +{ + private static readonly List Calls = []; + + private static bool EnterNode() + { + Calls.Add("enter"); + return true; + } + + private static void ExitNode() => Calls.Add("exit"); + + private static void Inserted() => Calls.Add("inserted"); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WriteHeader() + { + if (!EnterNode()) + return; + + try + { + Calls.Add("body"); + } + finally + { + ExitNode(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WriteHeaderThatThrows() + { + if (!EnterNode()) + return; + + try + { + throw new InvalidOperationException("boom"); + } + finally + { + ExitNode(); + } + } + + private static IEnumerable InsertBeforeExitNode(IEnumerable instructions) => + CallInserter.InsertBefore(instructions, Method(nameof(ExitNode)), Method(nameof(Inserted)), out _); + + private static MethodInfo Method(string name) => AccessTools.Method(typeof(CallInserterTests), name); + + private static void Patch(string original, string harmonyId) + { + new Harmony(harmonyId).Patch(Method(original), transpiler: new HarmonyMethod(Method(nameof(InsertBeforeExitNode)))); + } + + [Fact] + public void Inserted_call_runs_inside_the_node_before_it_closes() + { + Patch(nameof(WriteHeader), "tests.write-header"); + Calls.Clear(); + + WriteHeader(); + + Assert.Equal(["enter", "body", "inserted", "exit"], Calls); + } + + [Fact] + public void Finally_block_still_runs_when_the_body_throws() + { + Patch(nameof(WriteHeaderThatThrows), "tests.write-header-throws"); + Calls.Clear(); + + Assert.Throws(WriteHeaderThatThrows); + Assert.Equal(["enter", "inserted", "exit"], Calls); + } + + [Fact] + public void Reports_when_the_target_call_is_missing() + { + var instructions = new List { new(OpCodes.Nop), new(OpCodes.Ret) }; + + var result = CallInserter.InsertBefore(instructions, Method(nameof(ExitNode)), Method(nameof(Inserted)), out bool inserted); + + Assert.False(inserted); + Assert.Equal(2, result.Count); + } +} diff --git a/Source/ModMismatchFormatter/Refactored/ListExtension.cs b/Source/ModMismatchFormatter.Tests/Legacy/ListExtension.cs similarity index 81% rename from Source/ModMismatchFormatter/Refactored/ListExtension.cs rename to Source/ModMismatchFormatter.Tests/Legacy/ListExtension.cs index c7e307b..4cd3b30 100644 --- a/Source/ModMismatchFormatter/Refactored/ListExtension.cs +++ b/Source/ModMismatchFormatter.Tests/Legacy/ListExtension.cs @@ -1,8 +1,11 @@ +// Verbatim copy of the pre-refactor alignment code (commit bbb3964), kept as a test oracle. +#nullable disable +#pragma warning disable using System; using System.Collections.Generic; using System.Linq; -namespace Madeline.ModMismatchFormatter +namespace Legacy { public static class ListExtension { diff --git a/Source/ModMismatchFormatter/Refactored/Mod.cs b/Source/ModMismatchFormatter.Tests/Legacy/Mod.cs similarity index 87% rename from Source/ModMismatchFormatter/Refactored/Mod.cs rename to Source/ModMismatchFormatter.Tests/Legacy/Mod.cs index b422942..bd8d355 100644 --- a/Source/ModMismatchFormatter/Refactored/Mod.cs +++ b/Source/ModMismatchFormatter.Tests/Legacy/Mod.cs @@ -1,7 +1,10 @@ +// Verbatim copy of the pre-refactor alignment code (commit bbb3964), kept as a test oracle. +#nullable disable +#pragma warning disable using System; using System.Collections.Generic; -namespace Madeline.ModMismatchFormatter +namespace Legacy { public class Mod { diff --git a/Source/ModMismatchFormatter/Refactored/ModPair.cs b/Source/ModMismatchFormatter.Tests/Legacy/ModPair.cs similarity index 67% rename from Source/ModMismatchFormatter/Refactored/ModPair.cs rename to Source/ModMismatchFormatter.Tests/Legacy/ModPair.cs index 13a2b6d..8e25338 100644 --- a/Source/ModMismatchFormatter/Refactored/ModPair.cs +++ b/Source/ModMismatchFormatter.Tests/Legacy/ModPair.cs @@ -1,7 +1,10 @@ +// Verbatim copy of the pre-refactor alignment code (commit bbb3964), kept as a test oracle. +#nullable disable +#pragma warning disable using System; using System.Collections.Generic; -namespace Madeline.ModMismatchFormatter +namespace Legacy { public class ModPair { diff --git a/Source/ModMismatchFormatter/Refactored/ModPresenter.cs b/Source/ModMismatchFormatter.Tests/Legacy/ModPresenter.cs similarity index 97% rename from Source/ModMismatchFormatter/Refactored/ModPresenter.cs rename to Source/ModMismatchFormatter.Tests/Legacy/ModPresenter.cs index 7d36ea3..40df034 100644 --- a/Source/ModMismatchFormatter/Refactored/ModPresenter.cs +++ b/Source/ModMismatchFormatter.Tests/Legacy/ModPresenter.cs @@ -1,8 +1,11 @@ +// Verbatim copy of the pre-refactor alignment code (commit bbb3964), kept as a test oracle. +#nullable disable +#pragma warning disable using System; using System.Collections.Generic; using System.Linq; -namespace Madeline.ModMismatchFormatter +namespace Legacy { public class OrderFormatterImpl : iOrderFormatter { diff --git a/Source/ModMismatchFormatter/Refactored/iOrderFormatter.cs b/Source/ModMismatchFormatter.Tests/Legacy/iOrderFormatter.cs similarity index 56% rename from Source/ModMismatchFormatter/Refactored/iOrderFormatter.cs rename to Source/ModMismatchFormatter.Tests/Legacy/iOrderFormatter.cs index 70a49c9..16784e5 100644 --- a/Source/ModMismatchFormatter/Refactored/iOrderFormatter.cs +++ b/Source/ModMismatchFormatter.Tests/Legacy/iOrderFormatter.cs @@ -1,7 +1,10 @@ +// Verbatim copy of the pre-refactor alignment code (commit bbb3964), kept as a test oracle. +#nullable disable +#pragma warning disable using System; using System.Collections.Generic; -namespace Madeline.ModMismatchFormatter +namespace Legacy { public interface iOrderFormatter { diff --git a/Source/ModMismatchFormatter.Tests/ModListAlignerTests.cs b/Source/ModMismatchFormatter.Tests/ModListAlignerTests.cs new file mode 100644 index 0000000..7784643 --- /dev/null +++ b/Source/ModMismatchFormatter.Tests/ModListAlignerTests.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Madeline.ModMismatchFormatter.Core; +using Xunit; + +namespace ModMismatchFormatter.Tests; + +public class ModListAlignerTests +{ + private static readonly Func ByName = mod => mod.Name; + + private static List Mods(IEnumerable names) => + names.Select(name => new ModEntry(name, name)).ToList(); + + private static List<(string?, string?)> Shape(IEnumerable rows) => + rows.Select(row => (row.Saved?.Name, row.Active?.Name)).ToList(); + + private static List<(string?, string?)> LegacyShape(IReadOnlyList saved, IReadOnlyList active) + { + static List ToLegacy(IReadOnlyList mods) => + mods.Select((mod, i) => new Legacy.Mod(mod.PackageId, mod.Name, i)).ToList(); + + return new Legacy.OrderFormatterImpl() + .GetFormattedModPairs(ToLegacy(saved), ToLegacy(active)) + .Select(pair => (pair.Save.isPlaceHolder ? null : pair.Save.ModName, + pair.Loaded.isPlaceHolder ? null : pair.Loaded.ModName)) + .ToList(); + } + + [Fact] + public void Identical_lists_align_row_by_row() + { + var mods = Mods(["a", "b", "c"]); + + var rows = ModListAligner.Align(mods, mods, ByName); + + Assert.Equal([("a", "a"), ("b", "b"), ("c", "c")], Shape(rows)); + Assert.All(rows, row => Assert.Equal(RowState.Unchanged, row.State)); + } + + [Fact] + public void Added_and_removed_mods_get_a_gap_on_the_other_side() + { + var rows = ModListAligner.Align(Mods(["a", "x", "b"]), Mods(["a", "b", "y"]), ByName); + + Assert.Equal([("a", "a"), ("x", null), ("b", "b"), (null, "y")], Shape(rows)); + Assert.Equal( + [RowState.Unchanged, RowState.Removed, RowState.Unchanged, RowState.Added], + rows.Select(row => row.State)); + } + + [Fact] + public void Empty_lists_produce_no_rows() + { + Assert.Empty(ModListAligner.Align([], [], ByName)); + } + + [Fact] + public void Version_difference_is_reported_on_matched_rows() + { + var row = new AlignedRow(new ModEntry("a", "A", "1.0"), new ModEntry("a", "A", "1.1")); + + Assert.Equal(RowState.VersionChanged, row.State); + } + + [Fact] + public void Matches_the_legacy_algorithm_on_the_sample_mod_lists() + { + var saved = Mods(SampleModLists.Saved); + var active = Mods(SampleModLists.Active); + + Assert.Equal(LegacyShape(saved, active), Shape(ModListAligner.Align(saved, active, ByName))); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void Matches_the_legacy_algorithm_on_random_mod_lists(int seed) + { + var random = new Random(seed); + + for (int run = 0; run < 2000; run++) + { + var (saved, active) = RandomModLists(random); + + // The legacy code throws on an empty list (List.Pop on nothing). + if (saved.Count == 0 || active.Count == 0) + continue; + + Assert.Equal(LegacyShape(saved, active), Shape(ModListAligner.Align(saved, active, ByName))); + } + } + + [Fact] + public void Every_mod_appears_once_and_in_its_original_order() + { + var random = new Random(42); + + for (int run = 0; run < 500; run++) + { + var (saved, active) = RandomModLists(random); + + var rows = ModListAligner.Align(saved, active, ByName); + + Assert.Equal(saved, rows.Where(row => row.Saved is not null).Select(row => row.Saved!)); + Assert.Equal(active, rows.Where(row => row.Active is not null).Select(row => row.Active!)); + } + } + + /// Builds a mod list and an edited copy (removals, additions, moves, rare duplicates). + private static (List Saved, List Active) RandomModLists(Random random) + { + int count = random.Next(0, 40); + var saved = Enumerable.Range(0, count).Select(i => $"m{i}").ToList(); + var active = new List(saved); + + int edits = random.Next(0, 10); + for (int i = 0; i < edits; i++) + { + switch (random.Next(4)) + { + case 0 when active.Count > 0: + active.RemoveAt(random.Next(active.Count)); + break; + case 1: + active.Insert(random.Next(active.Count + 1), $"new{i}"); + break; + case 2 when active.Count > 1: + int from = random.Next(active.Count); + string moved = active[from]; + active.RemoveAt(from); + active.Insert(random.Next(active.Count + 1), moved); + break; + case 3 when active.Count > 0 && random.Next(5) == 0: + active.Insert(random.Next(active.Count + 1), active[random.Next(active.Count)]); + break; + } + } + + return (Mods(saved), Mods(active)); + } +} diff --git a/Source/ModMismatchFormatter.Tests/ModMismatchFormatter.Tests.csproj b/Source/ModMismatchFormatter.Tests/ModMismatchFormatter.Tests.csproj new file mode 100644 index 0000000..640cf3e --- /dev/null +++ b/Source/ModMismatchFormatter.Tests/ModMismatchFormatter.Tests.csproj @@ -0,0 +1,21 @@ + + + + + net10.0 + Exe + false + + + + + + + + + + + + + + diff --git a/Source/ModMismatchFormatter.Tests/SampleModLists.cs b/Source/ModMismatchFormatter.Tests/SampleModLists.cs new file mode 100644 index 0000000..011a534 --- /dev/null +++ b/Source/ModMismatchFormatter.Tests/SampleModLists.cs @@ -0,0 +1,184 @@ +namespace ModMismatchFormatter.Tests; + +/// A real save/running mod list pair (Steam workshop ids) from the original UITester. +internal static class SampleModLists +{ + public static readonly string[] Saved = + [ + "Core", + "1567113677", + "1848471817", + "1508850027", + "818773962", + "1507748539", + "839005762", + "932008009", + "753498552", + "1180826364", + "1837246563", + "725219116", + "712141500", + "715565817", + "848972794", + "946390822", + "935732834", + "962732083", + "836308268", + "1496570408", + "1188485723", + "1185269798", + "1185906628", + "1185274792", + "1185276569", + "1244299809", + "1185281838", + "1185265132", + "1192504741", + "1185307686", + "1718190143", + "1718191613", + "1880253632", + "1845154007", + "1836382920", + "1814383360", + "1113137502", + "1541721856", + "1055485938", + "1658356618", + "1590579367", + "1542364782", + "1836900626", + "1895364938", + "1823540489", + "1849183875", + "1849184079", + "1895364782", + "1700683323", + "1895364585", + "1865597143", + "1582570547", + "761421485", + "1833163132", + "1558161673", + "1907283935", + "1907283872", + "1906780517", + "1854607105", + "1854610483", + "1819425140", + "1888705256", + "1914064942", + "1814987817", + "1814988282", + "1917061124", + "826998327", + "1321849735", + "850429707", + "746425621", + "1501832876", + "1424338139", + "1843574431", + "1446523594", + "1544626521", + "1591142767", + "1346064449", + "1537869190", + "725130005", + "1909914131", + "1539312975", + "1713343013", + "1908437382", + ]; + + public static readonly string[] Active = + [ + "Core", + "1567113677", + "1848471817", + "1508850027", + "818773962", + "1507748539", + "839005762", + "932008009", + "753498552", + "1180826364", + "1837246563", + "725219116", + "712141500", + "715565817", + "848972794", + "946390822", + "935732834", + "836308268", + "1496570408", + "1188485723", + "1185269798", + "1185274792", + "1718190143", + "1185276569", + "1718191613", + "962732083", + "1185307686", + "1185265132", + "1185906628", + "1055485938", + "1185281838", + "1244299809", + "1508292711", + "1192504741", + "1211694919", + "1845154007", + "1814383360", + "1578693166", + "1854376306", + "1541721856", + "1632800575", + "1658356618", + "865497369", + "1590579367", + "1113137502", + "1542364782", + "1836900626", + "1895364938", + "1823540489", + "1849183875", + "1849184079", + "1700683323", + "1895364585", + "1865597143", + "1582570547", + "1558161673", + "1907283935", + "1817690190", + "1895364782", + "1907283872", + "832333531", + "1906780517", + "1854607105", + "1917061124", + "735106432", + "1819425140", + "1888705256", + "1854610483", + "1814987817", + "1814988282", + "850429707", + "1914064942", + "746425621", + "1843574431", + "1544626521", + "1591142767", + "1346064449", + "1537869190", + "1216999901", + "1697864590", + "1865654125", + "725130005", + "1909914131", + "818322128", + "1539312975", + "1713343013", + "1446523594", + "1908437382", + ]; +} diff --git a/Source/ModMismatchFormatter.slnx b/Source/ModMismatchFormatter.slnx index 853540d..ed9eacb 100644 --- a/Source/ModMismatchFormatter.slnx +++ b/Source/ModMismatchFormatter.slnx @@ -1,3 +1,4 @@ + diff --git a/Source/ModMismatchFormatter/Core/AlignedRow.cs b/Source/ModMismatchFormatter/Core/AlignedRow.cs new file mode 100644 index 0000000..e0f69da --- /dev/null +++ b/Source/ModMismatchFormatter/Core/AlignedRow.cs @@ -0,0 +1,31 @@ +using System; + +namespace Madeline.ModMismatchFormatter.Core; + +public enum RowState +{ + /// The mod is in both lists at this position. + Unchanged, + + /// The mod is in both lists but its version differs. + VersionChanged, + + /// The mod is only in the save (it was removed). + Removed, + + /// The mod is only in the running list (it was added). + Added, +} + +/// One row of the side-by-side comparison. At least one side is set. +public readonly record struct AlignedRow(ModEntry? Saved, ModEntry? Active) +{ + public RowState State => (Saved, Active) switch + { + (not null, null) => RowState.Removed, + (null, not null) => RowState.Added, + (not null, not null) when Saved.Version != Active.Version => RowState.VersionChanged, + (not null, not null) => RowState.Unchanged, + _ => throw new InvalidOperationException("A row must have at least one mod."), + }; +} diff --git a/Source/ModMismatchFormatter/Core/IsExternalInit.cs b/Source/ModMismatchFormatter/Core/IsExternalInit.cs new file mode 100644 index 0000000..590bf1b --- /dev/null +++ b/Source/ModMismatchFormatter/Core/IsExternalInit.cs @@ -0,0 +1,6 @@ +#if NETFRAMEWORK +namespace System.Runtime.CompilerServices; + +// Lets records and init-only setters compile for net472 (RimWorld 1.6). +internal static class IsExternalInit; +#endif diff --git a/Source/ModMismatchFormatter/Core/ModEntry.cs b/Source/ModMismatchFormatter/Core/ModEntry.cs new file mode 100644 index 0000000..82a356f --- /dev/null +++ b/Source/ModMismatchFormatter/Core/ModEntry.cs @@ -0,0 +1,7 @@ +namespace Madeline.ModMismatchFormatter.Core; + +/// A mod as recorded in a save file or as currently running. +/// The mod's package id. +/// Display name. +/// Mod version, or null when it is not known. +public sealed record ModEntry(string PackageId, string Name, string? Version = null); diff --git a/Source/ModMismatchFormatter/Core/ModListAligner.cs b/Source/ModMismatchFormatter/Core/ModListAligner.cs new file mode 100644 index 0000000..a7950b9 --- /dev/null +++ b/Source/ModMismatchFormatter/Core/ModListAligner.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; + +namespace Madeline.ModMismatchFormatter.Core; + +/// +/// Lines up the save's mod list against the running mod list so that the same mod +/// appears on the same row, with gaps where a mod was added or removed. +/// +public static class ModListAligner +{ + /// + /// Greedy alignment. When the current mods differ and each reappears later in + /// the other list, the side whose match is closer to the end of the list is + /// emitted first. + /// + public static IReadOnlyList Align( + IReadOnlyList saved, + IReadOnlyList active, + Func key) + { + var rows = new List(Math.Max(saved.Count, active.Count)); + int s = 0, a = 0; + + while (s < saved.Count && a < active.Count) + { + var savedMod = saved[s]; + var activeMod = active[a]; + + if (key(savedMod) == key(activeMod)) + { + rows.Add(new AlignedRow(savedMod, activeMod)); + s++; + a++; + continue; + } + + int savedInActive = LastIndexOf(active, a + 1, key(savedMod), key); + int activeInSaved = LastIndexOf(saved, s + 1, key(activeMod), key); + + bool takeActive = (savedInActive, activeInSaved) switch + { + (< 0, _) => false, + (_, < 0) => true, + _ => active.Count - savedInActive > saved.Count - activeInSaved, + }; + + if (takeActive) + rows.Add(new AlignedRow(null, active[a++])); + else + rows.Add(new AlignedRow(saved[s++], null)); + } + + while (s < saved.Count) + rows.Add(new AlignedRow(saved[s++], null)); + + while (a < active.Count) + rows.Add(new AlignedRow(null, active[a++])); + + return rows; + } + + private static int LastIndexOf(IReadOnlyList mods, int start, string value, Func key) + { + for (int i = mods.Count - 1; i >= start; i--) + { + if (key(mods[i]) == value) + return i; + } + + return -1; + } +} diff --git a/Source/ModMismatchFormatter/HarmonyPatches.cs b/Source/ModMismatchFormatter/HarmonyPatches.cs deleted file mode 100644 index 8b337bb..0000000 --- a/Source/ModMismatchFormatter/HarmonyPatches.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using HarmonyLib; -using Verse; -using RimWorld; -using System.Reflection; -using Madeline.ModMismatchFormatter; -using System.IO; -using System.Xml.Linq; -using System.Reflection.Emit; - -namespace ModMisMatchWindowPatch -{ - [StaticConstructorOnStartup] - internal static class HarmonyPatches - { - static HarmonyPatches() - { - Log.Message("Patching Madeline.ModMismatchWindow"); - Harmony HMInstance = new Harmony("Madeline.ModMismatchWindowPatch"); - MethodInfo original = AccessTools.Method(typeof(ScribeMetaHeaderUtility), "TryCreateDialogsForVersionMismatchWarnings"); - MethodInfo prefix = AccessTools.Method(typeof(HarmonyPatches), "Prefix_TryCreateDialogForVersionMismatchWarnings"); - HMInstance.Patch(original, new HarmonyMethod(prefix)); - - var SaveGameOriginal = AccessTools.Method(typeof(GameDataSaveLoader), "SaveGame"); - var SaveGamePrefix = AccessTools.Method(typeof(HarmonyPatches), "Prefix_SaveGame"); - var SaveGamePostfix = AccessTools.Method(typeof(HarmonyPatches), nameof(HarmonyPatches.Postfix_SaveGame)); - HMInstance.Patch(SaveGameOriginal, new HarmonyMethod(SaveGamePrefix), new HarmonyMethod(SaveGamePostfix)); - - var CheckVersionAndLoadOriginal = AccessTools.Method(typeof(PreLoadUtility), "CheckVersionAndLoad"); - var CheckVersionAndLoadPrefix = AccessTools.Method(typeof(MetaHeaderUtility), "UpdateLastAccessedSaveFileInLoadSelection"); - HMInstance.Patch(CheckVersionAndLoadOriginal, new HarmonyMethod(CheckVersionAndLoadPrefix)); - } - - [HarmonyPriority(9999)] - static bool Prefix_TryCreateDialogForVersionMismatchWarnings(ref bool __result, Action confirmedAction) - { - SimpleLog.Log("Checking mod mismatch"); - bool isModListSame = ScribeMetaHeaderUtility.LoadedModsMatchesActiveMods(out _, out _); - bool useVersionCompare = ModMismatchFormatter.useVersionCompare; - - if(isModListSame) - { - SimpleLog.Log("ModList are same"); - if(useVersionCompare) - { - SimpleLog.Log("use version checking"); - bool isVersionSame = MetaHeaderUtility.isVersionSame(ModContentPackExtension.GetModsFromSave(useVersionCompare), ModContentPackExtension.GetModsFromActive(useVersionCompare)); - if(!isVersionSame) - { - SimpleLog.Log("Version is different..."); - string warningMessage = "Checking mismatch for mod and mod versions... please wait"; - Messages.Message(warningMessage, MessageTypeDefOf.SilentInput, false); - CreateModMismatchWindow(confirmedAction, useVersionCompare); - __result = true; - return false; - } - } - - __result = false; - return true; - } - else - { - SimpleLog.Log("Mod mismatch!"); - CreateModMismatchWindow(confirmedAction, useVersionCompare); - __result = true; - return false; - } - } - - [HarmonyPriority(9999)] - static void Prefix_SaveGame(string fileName) - { - SimpleLog.Log("Caching last saved file path"); - bool WriteMeta = ModMismatchFormatter.writeMetaToSave; - if(WriteMeta) - { - MetaHeaderUtility.StoreLastSavedFilePath(fileName); - } - } - - static void Postfix_SaveGame() - { - SimpleLog.Log("Trying to write additional meta headers to save file..."); - bool WriteMeta = ModMismatchFormatter.writeMetaToSave; - if(WriteMeta) - { - try - { - MetaHeaderUtility.UpdateModVersionMetaHeader(); - } - catch(Exception ex) - { - string report = $"exception was raised and cannot write mod meta headers to the save. mod versions are not saved. your save is fine though.\n==original exception==\n{ex.ToString()}"; - Log.Error(report); - } - } - } - - static void CreateModMismatchWindow(Action confirmedAction, bool useVersionCompare) - { - var renderer = new ModListerElementRenderer(); - renderer.useVersionChecking = useVersionCompare; - var formatter = new OrderFormatterImpl(); - Find.WindowStack.Add(new Madeline.ModMismatchFormatter.ModMismatchWindow(renderer, - formatter, - confirmedAction, useVersionCompare)); - } - } -} diff --git a/Source/ModMismatchFormatter/ModLog.cs b/Source/ModMismatchFormatter/ModLog.cs new file mode 100644 index 0000000..b1d44f3 --- /dev/null +++ b/Source/ModMismatchFormatter/ModLog.cs @@ -0,0 +1,14 @@ +using System.Diagnostics; + +namespace Madeline.ModMismatchFormatter; + +internal static class ModLog +{ + private const string Prefix = "[ModMismatchFormatter] "; + + /// Verbose trace; compiled out of release builds. + [Conditional("DEBUG")] + public static void Debug(string message) => Verse.Log.Message(Prefix + message); + + public static void Error(string message) => Verse.Log.Error(Prefix + message); +} diff --git a/Source/ModMismatchFormatter/ModMismatchFormatter.cs b/Source/ModMismatchFormatter/ModMismatchFormatter.cs new file mode 100644 index 0000000..b1251d1 --- /dev/null +++ b/Source/ModMismatchFormatter/ModMismatchFormatter.cs @@ -0,0 +1,34 @@ +using UnityEngine; +using Verse; + +namespace Madeline.ModMismatchFormatter; + +// The class name is part of the settings file name (Mod__ModMismatchFormatter.xml); do not rename. +public sealed class ModMismatchFormatter : Mod +{ + public ModMismatchFormatter(ModContentPack content) : base(content) + { + Settings = GetSettings(); + } + + internal static Settings Settings { get; private set; } = null!; + + public override string SettingsCategory() => "SettingsLabel".Translate(); + + public override void DoSettingsWindowContents(Rect inRect) + { + var listing = new Listing_Standard(); + listing.Begin(inRect); + listing.CheckboxLabeled( + "EnableVersionChecking".Translate(), + ref Settings.useVersionCompare, + "useVersionCompareDescription".Translate()); + listing.CheckboxLabeled( + "EnableWritingCustomVersionHeader".Translate(), + ref Settings.saveVersionToSaveFile, + "writeMetaDescription".Translate()); + listing.End(); + + base.DoSettingsWindowContents(inRect); + } +} diff --git a/Source/ModMismatchFormatter/ModMismatchFormatter.csproj b/Source/ModMismatchFormatter/ModMismatchFormatter.csproj index b0b940d..9e7622f 100644 --- a/Source/ModMismatchFormatter/ModMismatchFormatter.csproj +++ b/Source/ModMismatchFormatter/ModMismatchFormatter.csproj @@ -15,9 +15,6 @@ none false false - - disable - $(NoWarn);CS0162 @@ -43,8 +40,4 @@ - - - - diff --git a/Source/ModMismatchFormatter/ModVersions.cs b/Source/ModMismatchFormatter/ModVersions.cs new file mode 100644 index 0000000..873701d --- /dev/null +++ b/Source/ModMismatchFormatter/ModVersions.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using System.Xml.Linq; +using Verse; + +namespace Madeline.ModMismatchFormatter; + +/// +/// Mod versions of the running mods, and the versions recorded in the save's meta header: +/// +/// <meta> +/// <modMetaDatas> +/// <li><Identifier>packageId</Identifier><version>1.2.3</version></li> +/// </modMetaDatas> +/// </meta> +/// +/// +internal static class ModVersions +{ + private const string ListNode = "modMetaDatas"; + private const string IdNode = "Identifier"; + private const string VersionNode = "version"; + + // Older releases wrote this placeholder instead of omitting the version. + private const string LegacyUnknown = "Unknown"; + + private static readonly Dictionary RunningVersionCache = []; + private static Dictionary savedVersions = new(StringComparer.OrdinalIgnoreCase); + + /// The running version of , or null when it does not declare one. + public static string? Running(ModContentPack mod) + { + if (!RunningVersionCache.TryGetValue(mod.RootDir, out var version)) + { + version = ReadManifestVersion(mod) ?? NullIfEmpty(mod.ModMetaData?.ModVersion); + RunningVersionCache[mod.RootDir] = version; + } + + return version; + } + + /// The version recorded in the last loaded meta header, or null when it is not recorded. + public static string? Saved(string packageId) => + savedVersions.TryGetValue(packageId, out var version) ? version : null; + + /// Reads the recorded versions from the meta header the scribe loader currently holds. + public static void ReadFromLoadedMetaHeader() + { + var versions = new Dictionary(StringComparer.OrdinalIgnoreCase); + var list = Scribe.loader.curXmlParent?["meta"]?[ListNode]; + + if (list is not null) + { + foreach (XmlNode item in list.ChildNodes) + { + var id = item[IdNode]?.InnerText; + var version = NullIfEmpty(item[VersionNode]?.InnerText); + if (id is not null && version is not null && version != LegacyUnknown) + versions[id] = version; + } + } + + savedVersions = versions; + ModLog.Debug($"Read {versions.Count} mod versions from the meta header"); + } + + /// Writes the running mod versions. Must be called while the scribe is inside the meta node. + public static void WriteToMetaHeader() + { + if (!Scribe.EnterNode(ListNode)) + return; + + try + { + foreach (var mod in LoadedModManager.RunningMods) + { + if (!Scribe.EnterNode("li")) + continue; + + try + { + var id = mod.PackageId; + var version = Running(mod); + Scribe_Values.Look(ref id, IdNode); + Scribe_Values.Look(ref version, VersionNode); + } + finally + { + Scribe.ExitNode(); + } + } + } + finally + { + Scribe.ExitNode(); + } + } + + private static string? ReadManifestVersion(ModContentPack mod) + { + // Manifest.xml is the convention introduced by Fluffy's Mod Manager. + var path = Path.Combine(mod.RootDir, "About", "Manifest.xml"); + if (!File.Exists(path)) + return null; + + try + { + return NullIfEmpty(XDocument.Load(path).Root?.Element("version")?.Value); + } + catch (Exception ex) + { + ModLog.Error($"Could not read {path}: {ex.Message}"); + return null; + } + } + + private static string? NullIfEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); +} diff --git a/Source/ModMismatchFormatter/Patches/CallInserter.cs b/Source/ModMismatchFormatter/Patches/CallInserter.cs new file mode 100644 index 0000000..4de673a --- /dev/null +++ b/Source/ModMismatchFormatter/Patches/CallInserter.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; + +namespace Madeline.ModMismatchFormatter.Patches; + +internal static class CallInserter +{ + /// + /// Inserts a call to right before the first call to . + /// Labels and exception blocks move to the inserted call, so it also works when the target call + /// starts a finally block. + /// + public static List InsertBefore( + IEnumerable instructions, MethodInfo target, MethodInfo insert, out bool inserted) + { + var result = new List(); + inserted = false; + + foreach (var instruction in instructions) + { + if (!inserted && instruction.Calls(target)) + { + result.Add(new CodeInstruction(OpCodes.Call, insert).MoveLabelsFrom(instruction).MoveBlocksFrom(instruction)); + inserted = true; + } + + result.Add(instruction); + } + + return result; + } +} diff --git a/Source/ModMismatchFormatter/Patches/HarmonyBootstrap.cs b/Source/ModMismatchFormatter/Patches/HarmonyBootstrap.cs new file mode 100644 index 0000000..09c4738 --- /dev/null +++ b/Source/ModMismatchFormatter/Patches/HarmonyBootstrap.cs @@ -0,0 +1,14 @@ +using HarmonyLib; +using Verse; + +namespace Madeline.ModMismatchFormatter.Patches; + +[StaticConstructorOnStartup] +internal static class HarmonyBootstrap +{ + static HarmonyBootstrap() + { + new Harmony("Madeline.ModMismatchWindowPatch").PatchAll(typeof(HarmonyBootstrap).Assembly); + ModLog.Debug("Patched"); + } +} diff --git a/Source/ModMismatchFormatter/Patches/MetaHeaderPatches.cs b/Source/ModMismatchFormatter/Patches/MetaHeaderPatches.cs new file mode 100644 index 0000000..eae39fd --- /dev/null +++ b/Source/ModMismatchFormatter/Patches/MetaHeaderPatches.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using HarmonyLib; +using Verse; + +namespace Madeline.ModMismatchFormatter.Patches; + +/// Remembers whether the meta header being written belongs to a savegame. +[HarmonyPatch(typeof(GameDataSaveLoader), nameof(GameDataSaveLoader.SaveGame))] +internal static class SaveGamePatch +{ + public static bool IsSavingGame { get; private set; } + + private static void Prefix() => IsSavingGame = true; + + private static void Finalizer() => IsSavingGame = false; +} + +/// Adds the running mod versions to a savegame's meta header while it is written. +[HarmonyPatch(typeof(ScribeMetaHeaderUtility), nameof(ScribeMetaHeaderUtility.WriteMetaHeader))] +internal static class WriteMetaHeaderPatch +{ + // WriteMetaHeader is: if (Scribe.EnterNode("meta")) try { ... } finally { Scribe.ExitNode(); } + // Writing right before that ExitNode puts the versions inside . + private static IEnumerable Transpiler(IEnumerable instructions) + { + var result = CallInserter.InsertBefore( + instructions, + AccessTools.Method(typeof(Scribe), nameof(Scribe.ExitNode)), + AccessTools.Method(typeof(WriteMetaHeaderPatch), nameof(WriteModVersions)), + out bool inserted); + + if (!inserted) + ModLog.Error("Could not find Scribe.ExitNode in WriteMetaHeader; mod versions will not be saved."); + + return result; + } + + private static void WriteModVersions() + { + if (Scribe.mode != LoadSaveMode.Saving || !SaveGamePatch.IsSavingGame || !ModMismatchFormatter.Settings.saveVersionToSaveFile) + return; + + try + { + ModVersions.WriteToMetaHeader(); + } + catch (Exception ex) + { + // Never break saving: the save is fine, it just will not carry mod versions. + ModLog.Error($"Could not write mod versions to the save. The save itself is fine.\n{ex}"); + } + } +} + +/// Reads the recorded mod versions whenever a meta header is loaded. +[HarmonyPatch(typeof(ScribeMetaHeaderUtility), nameof(ScribeMetaHeaderUtility.LoadGameDataHeader))] +internal static class LoadGameDataHeaderPatch +{ + private static void Postfix() + { + if (Scribe.mode != LoadSaveMode.LoadingVars) + return; + + try + { + ModVersions.ReadFromLoadedMetaHeader(); + } + catch (Exception ex) + { + ModLog.Error($"Could not read mod versions from the save.\n{ex}"); + } + } +} diff --git a/Source/ModMismatchFormatter/Patches/MismatchDialogPatch.cs b/Source/ModMismatchFormatter/Patches/MismatchDialogPatch.cs new file mode 100644 index 0000000..afa2103 --- /dev/null +++ b/Source/ModMismatchFormatter/Patches/MismatchDialogPatch.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using HarmonyLib; +using Madeline.ModMismatchFormatter.UI; +using RimWorld; +using Verse; + +namespace Madeline.ModMismatchFormatter.Patches; + +/// Replaces the vanilla mod mismatch dialog with . +[HarmonyPatch(typeof(ScribeMetaHeaderUtility), nameof(ScribeMetaHeaderUtility.TryCreateDialogsForVersionMismatchWarnings))] +internal static class MismatchDialogPatch +{ + [HarmonyPriority(9999)] // Ahead of other mods that replace this dialog. + private static bool Prefix(Action confirmedAction, ref bool __result) + { + bool compareVersions = ModMismatchFormatter.Settings.useVersionCompare; + + if (ScribeMetaHeaderUtility.LoadedModsMatchesActiveMods(out _, out _)) + { + if (!compareVersions || !AnyVersionChanged()) + return true; // Same mods, same versions: let vanilla check the game version. + + Messages.Message("Checking mismatch for mod and mod versions... please wait", MessageTypeDefOf.SilentInput, false); + } + + Find.WindowStack.Add(new ModMismatchWindow(confirmedAction, compareVersions)); + __result = true; + return false; + } + + private static bool AnyVersionChanged() => + LoadedModManager.RunningMods.Any(mod => ModVersions.Saved(mod.PackageId) != ModVersions.Running(mod)); +} diff --git a/Source/ModMismatchFormatter/Refactored/ColorPresets.cs b/Source/ModMismatchFormatter/Refactored/ColorPresets.cs deleted file mode 100644 index 4cc654e..0000000 --- a/Source/ModMismatchFormatter/Refactored/ColorPresets.cs +++ /dev/null @@ -1,12 +0,0 @@ -using UnityEngine; - -namespace Madeline.ModMismatchFormatter -{ - public static class ColorPresets - { - public static Color Red = new Color(0.29f, 0.09f, 0.09f, 1); - public static Color Green = new Color(0.29f, 0.33f, 0.19f, 1); - public static Color Background = new Color(0.11f, 0.11f, 0.11f, 1); - public static Color Yellow = new Color32(178, 144, 83, 255); - } -} \ No newline at end of file diff --git a/Source/ModMismatchFormatter/Refactored/MetaHeaderUtility.cs b/Source/ModMismatchFormatter/Refactored/MetaHeaderUtility.cs deleted file mode 100644 index d47a68d..0000000 --- a/Source/ModMismatchFormatter/Refactored/MetaHeaderUtility.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Reflection.Emit; -using Verse; -using System.Text; -using System.Xml.Linq; -using System.IO; -using System.Linq; - -namespace Madeline.ModMismatchFormatter -{ - public static class MetaHeaderUtility - { - #region Harmony Patch - internal static bool UpdateLastAccessedSaveFileInLoadSelection(string path) - { - LastAccessedSaveFilePathInLoadSelection = path; - return true; - } - #endregion - public static string LastAccessedSaveFilePathInLoadSelection { get; private set; } // 게임 시작 전, 세이브 파일 선택할때 마지막으로 접근한 파일 경로 - private static string CurrnetUsingFilePath { get; set; } - static readonly string MOD_META_DATAS = "modMetaDatas"; - static readonly string MOD_IDENTIFIER_NODENAME = "Identifier"; - static readonly string VERSION_NODENAME = "version"; - static XDocument xdoc; - static string SaveFilePathCache; - static Dictionary versionDict = new Dictionary(); - static bool isVersionDictDirty = false; - public static void BeginReading(string filePath) - { - if(xdoc != null || string.IsNullOrEmpty(CurrnetUsingFilePath)) - { - Log.Warning("xdoc is still open. force-closing"); - xdoc = null; - CurrnetUsingFilePath = null; - } - SimpleLog.Log($"Trying to load {filePath}"); - xdoc = XDocument.Load(filePath); - CurrnetUsingFilePath = filePath; - } - - public static void EndReading() - { - xdoc.Save(CurrnetUsingFilePath); - SimpleLog.Log("Saving xdoc"); - xdoc = null; - CurrnetUsingFilePath = null; - isVersionDictDirty = true; - } - - public static string GetVersion(string Identifier) - { - string version = "Unknown"; - if(xdoc == null) - throw new Exception($"invoke BeginReading before calling {nameof(RebuildVersionDict)}"); - - if(isVersionDictDirty) - { - RebuildVersionDict(); - } - - if(versionDict.TryGetValue(Identifier, out var value)) - version = value; - - return version; - } - - static void RebuildVersionDict() - { - SimpleLog.Log("Rebuilding VersionDict"); - isVersionDictDirty = false; - versionDict.Clear(); - - if(xdoc == null) - { - Log.Error($"invoke BeginReading before calling {nameof(RebuildVersionDict)}"); - return; - } - - try - { - var ModMetaHeaders = xdoc.Root.Element("meta")?.Element(MOD_META_DATAS); - if(ModMetaHeaders == null) - return; - - var elements = ModMetaHeaders.Elements(); - foreach(var node in elements) - { - var Identifier = node.Element(MOD_IDENTIFIER_NODENAME)?.Value; - var Version = node.Element(VERSION_NODENAME)?.Value; - - if(Identifier == null || Version == null) // variable sanity check - continue; - - versionDict[Identifier] = Version; - - SimpleLog.Log($"Set {Identifier} {Version}"); - } - } - catch(Exception ex) - { - Log.Error($"Exception while rebuilding VersionDict, exception : {ex.Message}"); - return; - } - } - - public static void SetVersions(List metaHeaders) - { - if(xdoc == null) - throw new Exception("Invoke BeginReading before using this method"); - - var meta = xdoc.Root.Element("meta"); - var modVersionsNode = GetElementWithForceCreation(meta, MOD_META_DATAS); - modVersionsNode.RemoveAll(); - - if(meta.Element(MOD_META_DATAS) == null) - throw new Exception("F"); - - for(int i = 0; i < metaHeaders.Count; i++) - { - var data = metaHeaders[i]; - var liNode = new XElement("li"); - liNode.Add(new XElement(MOD_IDENTIFIER_NODENAME) { Value = data.Identifier }); - liNode.Add(new XElement(VERSION_NODENAME) { Value = data.Version }); - SimpleLog.Log($"Writing {data.Identifier} {data.Version} to versionNode"); - modVersionsNode.Add(liNode); - } - } - - static XElement GetElementWithForceCreation(XElement node, string name) - { - var result = node.Element(name); - if(result == null) - node.Add(new XElement(name)); - - result = node.Element(name); - return result; - } - - public static bool isVersionSame(List saveMods, List activeMods) - { // saveMods와 activeMods는 길이가 무조건 같음 - SimpleLog.Log("Checking version difference..."); - if(saveMods.Count != activeMods.Count) - { - string ListOfSaveMods = string.Join(", ", saveMods.Select(mod => mod.Identifier)); - string ListOfActiveMods = string.Join(", ", activeMods.Select(mod => mod.Identifier)); - - StringBuilder exceptionMessage = new StringBuilder(); - - exceptionMessage.AppendLine("saveMods length and activeMods length are different."); - exceptionMessage.AppendLine($"Save Mod Count : {saveMods.Count} Active Mods Count : {activeMods.Count}"); - exceptionMessage.AppendLine("Active Mods List"); - exceptionMessage.AppendLine(ListOfActiveMods); - exceptionMessage.AppendLine("Save Mods List"); - exceptionMessage.AppendLine(ListOfSaveMods); - - throw new Exception(exceptionMessage.ToString()); - } - - int length = saveMods.Count; - for(int i = 0; i < length; i++) - { - var save = saveMods[i]; - var active = activeMods[i]; - - SimpleLog.Log($"Comparing {save.Identifier} ({save.Version}) - {active.Identifier} ({active.Version})"); - - if(save.Identifier != active.Identifier) - { - string ListOfSaveMods = string.Join(", ", saveMods.Select(mod => mod.Identifier)); - string ListOfActiveMods = string.Join(", ", activeMods.Select(mod => mod.Identifier)); - - StringBuilder exceptionMessage = new StringBuilder(); - - exceptionMessage.AppendLine($"two mod's identifier is different while comparing version, save mod identifier : {save.Identifier}, active mod identifier : {active.Identifier}"); - exceptionMessage.AppendLine($"Save Mod Count : {saveMods.Count} Active Mods Count : {activeMods.Count}"); - exceptionMessage.AppendLine("Active Mods List"); - exceptionMessage.AppendLine(ListOfActiveMods); - exceptionMessage.AppendLine("Save Mods List"); - exceptionMessage.AppendLine(ListOfSaveMods); - - throw new Exception(exceptionMessage.ToString()); - } - - if(save.Version != active.Version) - return false; - } - - return true; - } - - public static void StoreLastSavedFilePath(string path) - { - SaveFilePathCache = path; - } - - public static void UpdateModVersionMetaHeader() - { - string rawFilePath = SaveFilePathCache; - string filePath = GenFilePaths.FilePathForSavedGame(rawFilePath); - MetaHeaderUtility.BeginReading(filePath); - List metaHeaders = new List(); - foreach(var modContentPack in LoadedModManager.RunningMods) - { - // var metadata = modContentPack.GetMetaData(); - var version = MetaHeaderUtility.GetVersionFromManifestFile(modContentPack); - metaHeaders.Add(new ModMetaHeader() { Identifier = modContentPack.PackageId, Version = version }); - SimpleLog.Log($"Add metadata to metaHeaders list : {modContentPack.PackageId}, {version}"); - } - MetaHeaderUtility.SetVersions(metaHeaders); - MetaHeaderUtility.EndReading(); - } - - public static string GetVersionFromManifestFile(ModContentPack pack) - { - string MenifestFilePath = Path.Combine(pack.RootDir, Path.Combine("About", "Manifest.xml")); - string version = "Unknown"; - if(File.Exists(MenifestFilePath)) - { - try - { - XDocument Xdoc = XDocument.Load(MenifestFilePath); - var versionNode = Xdoc.Root.Element("version"); - version = versionNode.Value ?? "Unknown"; - } - catch(Exception ex) - { - string error = $"exception in GetVersionFromManifestFile line\n{ex.ToString()}"; - Log.Error(error); - } - } - else - { - NoManifestFileInModWarning(pack.Name); - } - return version; - } - - static void NoManifestFileInModWarning(string modName) - { - - } - } - - public struct ModMetaHeader - { - public string Identifier; - public string Version; - } -} \ No newline at end of file diff --git a/Source/ModMismatchFormatter/Refactored/ModContentPackExtension.cs b/Source/ModMismatchFormatter/Refactored/ModContentPackExtension.cs deleted file mode 100644 index 259e5c8..0000000 --- a/Source/ModMismatchFormatter/Refactored/ModContentPackExtension.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Verse; -using System.Collections.Generic; -using System.Linq; -using System; - -namespace Madeline.ModMismatchFormatter -{ - public static class ModContentPackExtension - { - static Dictionary pairs = new Dictionary(); - public static ModMetaData GetMetaData(this ModContentPack pack) - { - throw new NotImplementedException(); - if(!(pairs.ContainsKey(pack))) - { - SetModContentPackInDict(pack); - } - - return pairs[pack]; - } - - static void SetModContentPackInDict(ModContentPack pack) - { - throw new NotImplementedException(); - var result = from meta in ModsConfig.ActiveModsInLoadOrder - where meta.Name == pack.Name - select meta; - pairs[pack] = result.First(); - } - - public static List GetModsFromActive(bool useVersionChecking = false) - { - List activeMods = new List(); - foreach(var RimworldMod in LoadedModManager.RunningMods) - { - var mod = new Mod(RimworldMod.PackageId, RimworldMod.Name, RimworldMod.loadOrder); - if(useVersionChecking) - mod.Version = MetaHeaderUtility.GetVersionFromManifestFile(RimworldMod); - activeMods.Add(mod); - } - return activeMods; - } - public static List GetModsFromSave(bool CheckModVersion = false) - { - List saveMods = null; - if (ScribeMetaHeaderUtility.loadedModNamesList != null && ScribeMetaHeaderUtility.loadedModIdsList != null) - { - CheckSaveHeaderValid(); - saveMods = ReadModsFromSaveHeader(CheckModVersion); - } - return saveMods; - } - - static void CheckSaveHeaderValid() - { - var modNamesCount = ScribeMetaHeaderUtility.loadedModNamesList.Count; - var modIdsCount = ScribeMetaHeaderUtility.loadedModIdsList.Count; - if(modNamesCount != modIdsCount) - throw new Exception("Mod Name length and ModID length in savefile is not matched"); - } - - static List ReadModsFromSaveHeader(bool readModVersion) - { - List saveMods = new List(); - MetaHeaderUtility.BeginReading(MetaHeaderUtility.LastAccessedSaveFilePathInLoadSelection); - for(int i = 0; i < ScribeMetaHeaderUtility.loadedModNamesList.Count; i++) - { - var mod = new Mod(ScribeMetaHeaderUtility.loadedModIdsList[i], ScribeMetaHeaderUtility.loadedModNamesList[i], i); - string identifier = mod.Identifier; - if(readModVersion) - mod.Version = MetaHeaderUtility.GetVersion(identifier); - saveMods.Add(mod); - } - MetaHeaderUtility.EndReading(); - return saveMods; - } - } -} \ No newline at end of file diff --git a/Source/ModMismatchFormatter/Refactored/ModListerElementRenderer.cs b/Source/ModMismatchFormatter/Refactored/ModListerElementRenderer.cs deleted file mode 100644 index bec5a29..0000000 --- a/Source/ModMismatchFormatter/Refactored/ModListerElementRenderer.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using Verse; -using System.Linq; - -namespace Madeline.ModMismatchFormatter -{ - public class ModListerElementRenderer : iModListerElementRenderer - { - public GUIStyle ModStateStyle { get; set; } - public GUIStyle ModDescriptionStyle { get; set; } - public GUIStyle ModVersionStyle { get; set; } - public float LeftListCenterPos { get; protected set; } - public float RightListCenterPos { get; protected set; } - public bool useVersionChecking { get; set; } - enum ModState - { - Add, - Remove, - VersionChange, - None - } - enum VersionState - { - Upversion, - DownVersion, - None - } - public ModListerElementRenderer() - { - InitializeDefaultFont(); - } - - void InitializeDefaultFont() - { - ModDescriptionStyle = new GUIStyle(); - ModDescriptionStyle.fontSize = 15; - ModDescriptionStyle.alignment = TextAnchor.MiddleLeft; - ModDescriptionStyle.normal.textColor = Color.white; - ModDescriptionStyle.onNormal.textColor = Color.white; - ModDescriptionStyle.hover.textColor = Color.white; - ModDescriptionStyle.onHover.textColor = Color.white; - - ModStateStyle = new GUIStyle(); - ModStateStyle.fontSize = ModDescriptionStyle.fontSize; - ModStateStyle.alignment = TextAnchor.MiddleLeft; - ModStateStyle.normal.textColor = Color.white; - ModStateStyle.onNormal.textColor = Color.white; - ModStateStyle.hover.textColor = Color.white; - ModStateStyle.onHover.textColor = Color.white; - - - ModVersionStyle = new GUIStyle(); - ModVersionStyle.fontSize = 11; - ModVersionStyle.alignment = TextAnchor.MiddleLeft; - ModVersionStyle.normal.textColor = Color.white; - ModVersionStyle.onNormal.textColor = Color.white; - ModVersionStyle.hover.textColor = Color.white; - ModVersionStyle.onHover.textColor = Color.white; - - } - - public void RenderSaveMod(Rect left, ModPair pair) - { - var saveMod = pair.Save; - if(saveMod.isPlaceHolder) - RenderPlaceHolderMod(left); - else if(pair.Loaded.isPlaceHolder) - RenderSingleMod(left, saveMod, ModState.Remove); - else - RenderSingleMod(left, saveMod, ModState.None); - } - - public void RenderActiveMod(Rect right, ModPair pair) - { - if(pair.Loaded.isPlaceHolder) - RenderPlaceHolderMod(right); - else if(pair.Save.isPlaceHolder) - RenderSingleMod(right, pair.Loaded, ModState.Add); - else if(useVersionChecking && pair.Loaded.isVersionDifferent(pair.Save)) - RenderSingleMod(right, pair.Loaded, ModState.VersionChange); - else - RenderSingleMod(right, pair.Loaded, ModState.None); - } - - void RenderPlaceHolderMod(Rect root) - { - Widgets.DrawBoxSolid(root, ColorPresets.Background); - } - - void RenderSingleMod(Rect root, Mod mod, ModState modState) - { - Color bgColor; - if(modState == ModState.Add) - bgColor = ColorPresets.Green; - else if(modState == ModState.Remove) - bgColor = ColorPresets.Red; - else if(useVersionChecking && modState == ModState.VersionChange) - bgColor = ColorPresets.Yellow; - else - bgColor = ColorPresets.Background; - - Widgets.DrawBoxSolid(root, bgColor); - - float leftBoxWidth = 16f; - Rect ModStateRect = new Rect(root.x, root.y, leftBoxWidth, root.height); - // padding - switch (modState) - { - case ModState.Add: - ModStateRect.x += 4; - break; - case ModState.Remove: - ModStateRect.x += 6; - ModStateRect.y -= 2; - break; - } - - RenderModState(ModStateRect, modState); - - Rect DescriptionRect = new Rect(ModStateRect.xMax, root.y, root.width - ModStateRect.width, root.height); - RenderModDescriptionAndVersion(DescriptionRect, mod); - } - void RenderModDescriptionAndVersion(Rect Root, Mod mod) - { - string version = mod.Version ?? "Unknown"; - version = $"ver : {version}"; - string description = $"{mod.ModName}"; - - var DescriptionTextHeight = ModDescriptionStyle.CalcSize(new GUIContent(description)).y; - var VersionTextHeight = ModVersionStyle.CalcSize(new GUIContent(version)).y; - - float minimumHeight; - if(useVersionChecking) - minimumHeight = DescriptionTextHeight + VersionTextHeight; - else - minimumHeight = DescriptionTextHeight; - - if(minimumHeight > Root.height) - throw new Exception($"Root rect's height is smaller than minimum size {minimumHeight}"); - - // HACK: somehow text alignment is not correct so move label 3 pixel down - Rect DescriptionRect = new Rect(Root.x, Root.y + 3, Root.width, DescriptionTextHeight); - GUI.Label(DescriptionRect, description, ModDescriptionStyle); - - if(useVersionChecking) - { //TODO - mod을 보고 versionStyle를 넣어주자 - Rect VersionRect = new Rect(Root.x, Root.yMax - VersionTextHeight, Root.width, VersionTextHeight); - RenderVersionText(VersionRect, version, ModVersionStyle, VersionState.None); - } - } - - void RenderVersionText(Rect versionRect, string content, GUIStyle style, VersionState state) - { - //TODO - VersionState에 맞는 색깔 넣어주기 - GUI.Label(versionRect, content, style); - } - - void SetColor(GUIStyle style, Color color) - { // FIXME - 색깔이 고정되어있음 - style.alignment = TextAnchor.MiddleLeft; - style.normal.textColor = Color.white; - style.onNormal.textColor = Color.white; - style.hover.textColor = Color.white; - style.onHover.textColor = Color.white; - } - - void RenderModState(Rect root, ModState state) - { - GUIStyle style = ModStateStyle; - - string content = string.Empty; - if(state == ModState.Add) - content = "+"; - else if(state == ModState.Remove) - content = "-"; - else if(state == ModState.VersionChange) - content = "?"; - else if(state == ModState.None) - content = " "; - else - throw new Exception($"ModState {state.ToString()} is not supported yet. please contact to modder."); - - GUI.Label(root, content, style); - } - } -} \ No newline at end of file diff --git a/Source/ModMismatchFormatter/Refactored/ModMismatchFormatter.cs b/Source/ModMismatchFormatter/Refactored/ModMismatchFormatter.cs deleted file mode 100644 index ae83359..0000000 --- a/Source/ModMismatchFormatter/Refactored/ModMismatchFormatter.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Verse; - -namespace Madeline.ModMismatchFormatter -{ - public class ModMismatchFormatter : Verse.Mod - { - static ModMismatchFormatter instance; - Settings settings; - internal static bool useVersionCompare - { - get { return instance.settings.useVersionCompare; } - } - internal static bool writeMetaToSave - { - get { return instance.settings.saveVersionToSaveFile; } - } - public ModMismatchFormatter(ModContentPack pack) : base(pack) - { - this.settings = GetSettings(); - instance = this; - } - - public override void DoSettingsWindowContents(UnityEngine.Rect inRect) - { - Listing_Standard listingStandard = new Listing_Standard(); - listingStandard.Begin(inRect); - - bool useVersionCompare = this.settings.useVersionCompare; - string description = "useVersionCompareDescription".Translate(); //"Enable Version checking between saved file and current version. check steam workshop for the detailed description."; - listingStandard.CheckboxLabeled("EnableVersionChecking".Translate(), ref useVersionCompare, description); - - bool writeMetaToSave = this.settings.saveVersionToSaveFile; - string writeMetaDescription = "writeMetaDescription".Translate(); - listingStandard.CheckboxLabeled("EnableWritingCustomVersionHeader".Translate(), ref writeMetaToSave, writeMetaDescription); - - this.settings.useVersionCompare = useVersionCompare; - this.settings.saveVersionToSaveFile = writeMetaToSave; - - listingStandard.End(); - - base.DoSettingsWindowContents(inRect); - } - - public override string SettingsCategory() - { - return "SettingsLabel".Translate(); - } - } -} \ No newline at end of file diff --git a/Source/ModMismatchFormatter/Refactored/ModMismatchWindow.cs b/Source/ModMismatchFormatter/Refactored/ModMismatchWindow.cs deleted file mode 100644 index f71e13b..0000000 --- a/Source/ModMismatchFormatter/Refactored/ModMismatchWindow.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Collections.Generic; -using Verse; -using System.Linq; -using System; -using UnityEngine; - -namespace Madeline.ModMismatchFormatter -{ - public class ModMismatchWindow : Window - { - iModListerElementRenderer renderer; - iOrderFormatter formatter; - List pairs; - Action confirmAction; - Vector2 scrollPosition = Vector2.zero; - bool useVersionChecking; - public override Vector2 InitialSize - { - get { return new Vector2(650, 700); } - } - - public ModMismatchWindow(iModListerElementRenderer renderer, iOrderFormatter formatter, Action confirmAction, bool useVersionChecking) - { // 3항 연산자는..?? - this.renderer = renderer; - this.formatter = formatter; - this.confirmAction = confirmAction; - this.doCloseX = true; - this.useVersionChecking = useVersionChecking; - } - - public override void PreOpen() - { - base.PreOpen(); - InitializePair(); - SetDrawerFonts(); - } - - void SetDrawerFonts() - { - //renderer.ModDescriptionStyle = Text.CurFontStyle; - //renderer.ModStateStyle = Text.CurFontStyle; - ////renderer.ModVersionStyle = new GUIStyle(Text.CurFontStyle) { fontSize = Text.CurFontStyle.fontSize - 3 }; - } - - void InitializePair() - { - List activeMods = ModContentPackExtension.GetModsFromActive(useVersionChecking); - List saveMods = ModContentPackExtension.GetModsFromSave(useVersionChecking); - var result = formatter.GetFormattedModPairs(saveMods, activeMods); - pairs = result.ToList(); - } - - public override void DoWindowContents(Rect canvas) - { - int ItemCount = pairs.Count + 1; - //윗쪽 타이틀 설정 - Rect TitleRect = new Rect(canvas.xMin, canvas.yMin, canvas.width, 60f); - Widgets.Label(TitleRect, "ModsMismatchWarningTitle".Translate()); - - //1개 모드 사이즈 - float RectWidth = canvas.width / 2 - 40; - Vector2 SingleItemSize = new Vector2(RectWidth, 34); - if(!useVersionChecking) - SingleItemSize.y = 24f; - Rect SingleModItemRect = new Rect(Vector2.zero, SingleItemSize); - - //막대기 사이즈 - Rect Bar = new Rect(0, 0, RectWidth, 2f); - - //하위 버튼의 크기 - Vector2 ButtonSize = new Vector2(RectWidth - 50f, 40f); - - //TODO - 클래스로 분류해내기 - //좌측(Save) - GUI.contentColor = Color.white; - Rect LeftSaveRect = new Rect((canvas.width / 2) - RectWidth, TitleRect.yMax, RectWidth, canvas.height - TitleRect.height - 60); // 이걸 가지고 다른 모든 상자들의 길이를 측정함 - Rect LeftinSaveRect = new Rect(LeftSaveRect.xMin, LeftSaveRect.yMin, LeftSaveRect.width - 18f, SingleModItemRect.height * ItemCount + Bar.height * (ItemCount - 1)); - Widgets.DrawBoxSolid(LeftSaveRect, ColorPresets.Background); - - Rect LeftTitle = new Rect(LeftSaveRect.xMin, LeftSaveRect.yMin - 26f, RectWidth - 18f, 26f); // 18f는 스크롤바 크기 - Widgets.Label(LeftTitle, "SaveModListTitle".Translate()); - - //우측(Active) - Rect RightActiveRect = new Rect(LeftSaveRect.xMax + 16f, LeftSaveRect.yMin, LeftSaveRect.width, LeftSaveRect.height); // LeftSaveRect에 종속 - Rect RightinActiveRect = new Rect(RightActiveRect.xMin, RightActiveRect.yMin, RightActiveRect.width, LeftinSaveRect.height); // RightActiveRect에 종속 - Widgets.DrawBoxSolid(RightActiveRect, ColorPresets.Background); - - Rect RightTitle = new Rect(RightActiveRect.xMin, RightActiveRect.yMin - 26f, RectWidth - 18f, 26f); - Widgets.Label(RightTitle, "ActiveModListTitle".Translate()); - //개수 안맞으면 PlaceHolder 넣어줘야함 - - - Widgets.BeginScrollView(LeftSaveRect, ref scrollPosition, LeftinSaveRect, true); - SingleModItemRect.position = new Vector2(LeftinSaveRect.x, LeftinSaveRect.y); // 위치 초기화 - Bar.position = new Vector2(SingleModItemRect.x, SingleModItemRect.yMax + 1f); - Bar.width = SingleModItemRect.xMax - SingleModItemRect.xMin; - foreach(var pair in pairs) - { - renderer.RenderSaveMod(SingleModItemRect, pair); - Bar.y = SingleModItemRect.yMax; - RenderSeperationLine(Bar); - SingleModItemRect.y = Bar.yMax; - - } - Widgets.EndScrollView(); - - Widgets.BeginScrollView(RightActiveRect, ref scrollPosition, RightinActiveRect, false); - SingleModItemRect.position = new Vector2(RightinActiveRect.xMin, RightinActiveRect.yMin); // 위치 초기화 - Bar.position = new Vector2(SingleModItemRect.x, SingleModItemRect.yMax + 1f); - Bar.width = SingleModItemRect.xMax - SingleModItemRect.xMin; - foreach(var pair in pairs) - { - renderer.RenderActiveMod(SingleModItemRect, pair); - Bar.y = SingleModItemRect.yMax; - RenderSeperationLine(Bar); - SingleModItemRect.y = Bar.yMax; - } - Widgets.EndScrollView(); - - - float ButtonXPos = LeftSaveRect.xMin + ( (LeftSaveRect.width - ButtonSize.x) / 2 ) - 7f; // micro control. I'm lack of math skill. - float ButtonYPos = canvas.yMax - 10f - ButtonSize.y; - Rect LoadFromSaveButton = new Rect(new Vector2(ButtonXPos, ButtonYPos), ButtonSize); - RenderLoadFromSaveButton(LoadFromSaveButton); - - ButtonXPos = RightActiveRect.xMin + ( (RightActiveRect.width - ButtonSize.x) / 2 ) - 7f; - Rect LoadAnywayButton = new Rect(new Vector2(ButtonXPos, ButtonYPos), ButtonSize); - RenderLoadAnywayButton(LoadAnywayButton); - } - - void RenderSeperationLine(Rect rect) - { - Widgets.DrawBoxSolid(rect, Color.grey); - } - - void RenderLoadAnywayButton(Rect LoadAnywayRect) - { - if (Widgets.ButtonText(LoadAnywayRect, "LoadAnyway".Translate())) - { - confirmAction(); - } - } - - void RenderLoadFromSaveButton(Rect LoadFromSaveRect) - { - if (Widgets.ButtonText(LoadFromSaveRect, "ChangeLoadedMods".Translate())) - { - if (Current.ProgramState == ProgramState.Entry) - { - ModsConfig.SetActiveToList(ScribeMetaHeaderUtility.loadedModIdsList); - } - ModsConfig.SaveFromList(ScribeMetaHeaderUtility.loadedModIdsList); - ModsConfig.RestartFromChangedMods(); - } - } - } -} \ No newline at end of file diff --git a/Source/ModMismatchFormatter/Refactored/Settings.cs b/Source/ModMismatchFormatter/Refactored/Settings.cs deleted file mode 100644 index 3d4218e..0000000 --- a/Source/ModMismatchFormatter/Refactored/Settings.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using Verse; - -namespace Madeline.ModMismatchFormatter -{ - public class Settings : ModSettings - { - public bool useVersionCompare; - public bool saveVersionToSaveFile; - public override void ExposeData() - { - base.ExposeData(); - Scribe_Values.Look(ref useVersionCompare, "useVersionCompare", true, true); - Scribe_Values.Look(ref saveVersionToSaveFile, "saveVersionToSaveFile", true, true); - } - } -} \ No newline at end of file diff --git a/Source/ModMismatchFormatter/Refactored/SimpleLog.cs b/Source/ModMismatchFormatter/Refactored/SimpleLog.cs deleted file mode 100644 index f14e75c..0000000 --- a/Source/ModMismatchFormatter/Refactored/SimpleLog.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; -namespace Madeline.ModMismatchFormatter -{ - - public static class SimpleLog - { - public static bool PrintLog = false; - public static void Log(string msg) - { - if(PrintLog) - Verse.Log.Message(msg); - } - } - -} \ No newline at end of file diff --git a/Source/ModMismatchFormatter/Refactored/UITester.cs b/Source/ModMismatchFormatter/Refactored/UITester.cs deleted file mode 100644 index e8fbc34..0000000 --- a/Source/ModMismatchFormatter/Refactored/UITester.cs +++ /dev/null @@ -1,228 +0,0 @@ -using UnityEngine; -using System.Collections.Generic; -using System.Linq; - -namespace Madeline.ModMismatchFormatter -{ - public class UITester// : MonoBehaviour - { - iOrderFormatter formatter; - //iCompareRectRenderer renderer; - bool initialized = false; - List result; - private void Awake() - { - //renderer = new CompareRectRenderer(); - //formatter = new ModPresenter(); - //Test(); - } - - private void OnGUI() - { - if(initialized) - { - //renderer.Render(new Rect(0, 0, 1200, 800), result); - } - } - - void Test() - { - string save = @"Core -1567113677 -1848471817 -1508850027 -818773962 -1507748539 -839005762 -932008009 -753498552 -1180826364 -1837246563 -725219116 -712141500 -715565817 -848972794 -946390822 -935732834 -962732083 -836308268 -1496570408 -1188485723 -1185269798 -1185906628 -1185274792 -1185276569 -1244299809 -1185281838 -1185265132 -1192504741 -1185307686 -1718190143 -1718191613 -1880253632 -1845154007 -1836382920 -1814383360 -1113137502 -1541721856 -1055485938 -1658356618 -1590579367 -1542364782 -1836900626 -1895364938 -1823540489 -1849183875 -1849184079 -1895364782 -1700683323 -1895364585 -1865597143 -1582570547 -761421485 -1833163132 -1558161673 -1907283935 -1907283872 -1906780517 -1854607105 -1854610483 -1819425140 -1888705256 -1914064942 -1814987817 -1814988282 -1917061124 -826998327 -1321849735 -850429707 -746425621 -1501832876 -1424338139 -1843574431 -1446523594 -1544626521 -1591142767 -1346064449 -1537869190 -725130005 -1909914131 -1539312975 -1713343013 -1908437382"; - - string active = @"Core -1567113677 -1848471817 -1508850027 -818773962 -1507748539 -839005762 -932008009 -753498552 -1180826364 -1837246563 -725219116 -712141500 -715565817 -848972794 -946390822 -935732834 -836308268 -1496570408 -1188485723 -1185269798 -1185274792 -1718190143 -1185276569 -1718191613 -962732083 -1185307686 -1185265132 -1185906628 -1055485938 -1185281838 -1244299809 -1508292711 -1192504741 -1211694919 -1845154007 -1814383360 -1578693166 -1854376306 -1541721856 -1632800575 -1658356618 -865497369 -1590579367 -1113137502 -1542364782 -1836900626 -1895364938 -1823540489 -1849183875 -1849184079 -1700683323 -1895364585 -1865597143 -1582570547 -1558161673 -1907283935 -1817690190 -1895364782 -1907283872 -832333531 -1906780517 -1854607105 -1917061124 -735106432 -1819425140 -1888705256 -1854610483 -1814987817 -1814988282 -850429707 -1914064942 -746425621 -1843574431 -1544626521 -1591142767 -1346064449 -1537869190 -1216999901 -1697864590 -1865654125 -725130005 -1909914131 -818322128 -1539312975 -1713343013 -1446523594 -1908437382 -"; - List saveMods = new List(); - List activeMods = new List(); - using(System.IO.StringReader reader = new System.IO.StringReader(save)) - { - string line; - while((line = reader.ReadLine()) != null) - { - saveMods.Add(new Mod(line, line) { Identifier = line }); - } - } - - using (System.IO.StringReader reader = new System.IO.StringReader(active)) - { - string line; - while ((line = reader.ReadLine()) != null) - { - activeMods.Add(new Mod(line, line) { Identifier = line }); - } - } - result = formatter.GetFormattedModPairs(saveMods, activeMods).ToList(); - initialized = true; - } - - - } -} \ No newline at end of file diff --git a/Source/ModMismatchFormatter/Refactored/iModListerElementRenderer.cs b/Source/ModMismatchFormatter/Refactored/iModListerElementRenderer.cs deleted file mode 100644 index 51e6521..0000000 --- a/Source/ModMismatchFormatter/Refactored/iModListerElementRenderer.cs +++ /dev/null @@ -1,16 +0,0 @@ -using UnityEngine; -using System.Collections.Generic; - -namespace Madeline.ModMismatchFormatter -{ - public interface iModListerElementRenderer - { - GUIStyle ModStateStyle { get; set; } - GUIStyle ModDescriptionStyle { get; set; } - GUIStyle ModVersionStyle { get; set; } - bool useVersionChecking { set; } - - void RenderActiveMod(Rect right, ModPair pair); - void RenderSaveMod(Rect left, ModPair pair); - } -} \ No newline at end of file diff --git a/Source/ModMismatchFormatter/Settings.cs b/Source/ModMismatchFormatter/Settings.cs new file mode 100644 index 0000000..190af10 --- /dev/null +++ b/Source/ModMismatchFormatter/Settings.cs @@ -0,0 +1,21 @@ +using Verse; + +namespace Madeline.ModMismatchFormatter; + +// The type name is written into the settings file as Class="..."; do not rename. +// Both options start off on a fresh install (no settings file yet), as they always have. +public sealed class Settings : ModSettings +{ + /// Compare each mod's version with the one recorded in the save. + public bool useVersionCompare; + + /// Record running mod versions in the save's meta header. + public bool saveVersionToSaveFile; + + public override void ExposeData() + { + base.ExposeData(); + Scribe_Values.Look(ref useVersionCompare, "useVersionCompare", true, true); + Scribe_Values.Look(ref saveVersionToSaveFile, "saveVersionToSaveFile", true, true); + } +} diff --git a/Source/ModMismatchFormatter/UI/ModMismatchWindow.cs b/Source/ModMismatchFormatter/UI/ModMismatchWindow.cs new file mode 100644 index 0000000..a95eb27 --- /dev/null +++ b/Source/ModMismatchFormatter/UI/ModMismatchWindow.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Madeline.ModMismatchFormatter.Core; +using RimWorld; +using UnityEngine; +using Verse; +using Verse.Sound; + +namespace Madeline.ModMismatchFormatter.UI; + +/// Shows the save's mod list and the running mod list side by side, aligned row by row. +public sealed class ModMismatchWindow : Window +{ + private const float TitleHeight = 36f; + private const float HeaderHeight = 26f; + private const float ColumnGap = 16f; + private const float SeparatorHeight = 2f; + private const float ButtonHeight = 40f; + private const float ButtonMargin = 10f; + + private readonly Action loadGame; + private readonly bool showVersions; + private readonly Dictionary truncateCache = []; + private IReadOnlyList rows = []; + private Vector2 scrollPosition; + + public ModMismatchWindow(Action loadGame, bool showVersions) + { + this.loadGame = loadGame; + this.showVersions = showVersions; + doCloseX = true; + } + + public override Vector2 InitialSize => new(650f, 700f); + + public override void PreOpen() + { + base.PreOpen(); + rows = ModListAligner.Align(SavedMods(), RunningMods(), mod => mod.Name); + } + + public override void DoWindowContents(Rect inRect) + { + Text.Font = GameFont.Small; + Widgets.Label(new Rect(inRect.x, inRect.y, inRect.width, TitleHeight), "ModsMismatchWarningTitle".Translate()); + + var buttons = new Rect(inRect.x, inRect.yMax - ButtonHeight - ButtonMargin, inRect.width, ButtonHeight); + var list = new Rect(inRect.x, inRect.y + TitleHeight + HeaderHeight, inRect.width, buttons.y - ButtonMargin - (inRect.y + TitleHeight + HeaderHeight)); + float columnWidth = (list.width - GenUI.ScrollBarWidth - ColumnGap) / 2f; + + Widgets.Label(new Rect(list.x, list.y - HeaderHeight, columnWidth, HeaderHeight), "SaveModListTitle".Translate()); + Widgets.Label(new Rect(list.x + columnWidth + ColumnGap, list.y - HeaderHeight, columnWidth, HeaderHeight), "ActiveModListTitle".Translate()); + + DrawRows(list, columnWidth); + DrawButtons(buttons, columnWidth); + } + + private float RowHeight => showVersions + ? Text.LineHeightOf(GameFont.Small) + Text.LineHeightOf(GameFont.Tiny) + : Text.LineHeightOf(GameFont.Small); + + private void DrawRows(Rect outRect, float columnWidth) + { + Widgets.DrawBoxSolid(outRect, Palette.Background); + + float stride = RowHeight + SeparatorHeight; + var viewRect = new Rect(0f, 0f, outRect.width - GenUI.ScrollBarWidth, rows.Count * stride); + + Widgets.BeginScrollView(outRect, ref scrollPosition, viewRect); + for (int i = 0; i < rows.Count; i++) + { + float y = i * stride; + + // Only draw what is visible; mod lists can be very long. + if (y + stride < scrollPosition.y || y > scrollPosition.y + outRect.height) + continue; + + var row = rows[i]; + DrawCell(new Rect(0f, y, columnWidth, RowHeight), row.Saved, row.State == RowState.Removed ? RowState.Removed : RowState.Unchanged); + DrawCell(new Rect(columnWidth + ColumnGap, y, columnWidth, RowHeight), row.Active, row.State == RowState.Removed ? RowState.Unchanged : row.State); + + Widgets.DrawBoxSolid(new Rect(0f, y + RowHeight, columnWidth, SeparatorHeight), Color.grey); + Widgets.DrawBoxSolid(new Rect(columnWidth + ColumnGap, y + RowHeight, columnWidth, SeparatorHeight), Color.grey); + } + Widgets.EndScrollView(); + } + + private void DrawCell(Rect rect, ModEntry? mod, RowState state) + { + if (mod is null) + { + Widgets.DrawBoxSolid(rect, Palette.Background); + return; + } + + Widgets.DrawBoxSolid(rect, Palette.For(state)); + + var markRect = new Rect(rect.x + 4f, rect.y, 12f, rect.height); + var textRect = new Rect(markRect.xMax + 2f, rect.y, rect.xMax - markRect.xMax - 4f, rect.height); + float nameHeight = Text.LineHeightOf(GameFont.Small); + + Text.Anchor = TextAnchor.MiddleLeft; + Text.Font = GameFont.Small; + Widgets.Label(new Rect(markRect.x, rect.y, markRect.width, nameHeight), Mark(state)); + Widgets.Label(new Rect(textRect.x, rect.y, textRect.width, nameHeight), mod.Name.Truncate(textRect.width, truncateCache)); + + if (showVersions) + { + Text.Font = GameFont.Tiny; + Widgets.Label(new Rect(textRect.x, rect.y + nameHeight, textRect.width, rect.height - nameHeight), $"ver : {mod.Version ?? "Unknown"}"); + } + + Text.Font = GameFont.Small; + Text.Anchor = TextAnchor.UpperLeft; + + TooltipHandler.TipRegion(rect, $"{mod.Name}\n{mod.PackageId}"); + } + + private static string Mark(RowState state) => state switch + { + RowState.Added => "+", + RowState.Removed => "-", + RowState.VersionChanged => "?", + _ => "", + }; + + private void DrawButtons(Rect area, float columnWidth) + { + float buttonWidth = columnWidth - 50f; + float leftX = area.x + (columnWidth - buttonWidth) / 2f; + float rightX = area.x + columnWidth + ColumnGap + (columnWidth - buttonWidth) / 2f; + + if (Widgets.ButtonText(new Rect(leftX, area.y, buttonWidth, area.height), "ChangeLoadedMods".Translate())) + { + Find.WindowStack.Add(Dialog_MessageBox.CreateConfirmation( + "ConfirmLoadSaveList".Translate(), ChangeLoadedModsToSave, destructive: true)); + } + + if (Widgets.ButtonText(new Rect(rightX, area.y, buttonWidth, area.height), "LoadAnyway".Translate())) + LoadAnyway(); + } + + public override void OnAcceptKeyPressed() => LoadAnyway(); + + private void LoadAnyway() + { + SoundDefOf.Click.PlayOneShotOnCamera(); + loadGame(); + Close(); + } + + // Mirrors vanilla Dialog_ModMismatch: activate the save's mod list and restart. + private static void ChangeLoadedModsToSave() + { + var ids = ScribeMetaHeaderUtility.loadedModIdsList.Select(id => id.ToLower()).ToList(); + var names = ScribeMetaHeaderUtility.loadedModNamesList; + + if (Current.ProgramState == ProgramState.Entry) + ModsConfig.SetActiveToList(ids); + ModsConfig.SaveFromList(ids); + + var missing = ids + .Select((id, i) => (id, name: i < names.Count ? names[i] : id)) + .Where(mod => ModLister.GetModWithIdentifier(mod.id) is null) + .Select(mod => mod.name) + .ToList(); + if (missing.Count > 0) + Messages.Message($"{"MissingMods".Translate()}: {missing.ToCommaList()}", MessageTypeDefOf.RejectInput, historical: false); + + ModsConfig.RestartFromChangedMods(); + } + + private List SavedMods() + { + var ids = ScribeMetaHeaderUtility.loadedModIdsList ?? []; + var names = ScribeMetaHeaderUtility.loadedModNamesList ?? []; + + return ids + .Select((id, i) => new ModEntry(id, i < names.Count ? names[i] : id, showVersions ? ModVersions.Saved(id) : null)) + .ToList(); + } + + private List RunningMods() => + LoadedModManager.RunningMods + .Select(mod => new ModEntry(mod.PackageId, mod.Name, showVersions ? ModVersions.Running(mod) : null)) + .ToList(); +} diff --git a/Source/ModMismatchFormatter/UI/Palette.cs b/Source/ModMismatchFormatter/UI/Palette.cs new file mode 100644 index 0000000..66d4ce8 --- /dev/null +++ b/Source/ModMismatchFormatter/UI/Palette.cs @@ -0,0 +1,20 @@ +using Madeline.ModMismatchFormatter.Core; +using UnityEngine; + +namespace Madeline.ModMismatchFormatter.UI; + +internal static class Palette +{ + public static readonly Color Background = new(0.11f, 0.11f, 0.11f, 1f); + public static readonly Color Removed = new(0.29f, 0.09f, 0.09f, 1f); + public static readonly Color Added = new(0.29f, 0.33f, 0.19f, 1f); + public static readonly Color VersionChanged = new Color32(178, 144, 83, 255); + + public static Color For(RowState state) => state switch + { + RowState.Added => Added, + RowState.Removed => Removed, + RowState.VersionChanged => VersionChanged, + _ => Background, + }; +} diff --git a/global.json b/global.json new file mode 100644 index 0000000..3140116 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} From a079ac7d91331abb3765ee3920b96f1adda93e17 Mon Sep 17 00:00:00 2001 From: jungooji <46273764+zzzz465@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:19:52 +0900 Subject: [PATCH 04/11] feat: optimal alignment by package id and a more usable mismatch window Alignment - Align with a longest-common-subsequence pass keyed by package id (case-insensitive; folder name for pre-1.1 saves) instead of the greedy pass keyed by display name. Common prefix/suffix are trimmed first; lists whose differing middle would need more than 4M table cells fall back to the old greedy pass. - Mods present on both sides but in a different place are reported as MovedFrom/MovedTo instead of removed + added. - A version change needs both versions known, so saves without version data no longer open the window for every mod with a version. - Tests check the result against a textbook LCS on 6000 random lists, that it never matches fewer mods than the old algorithm, and that the greedy fallback still equals the old algorithm. Window - Summary line with colored counts (added / removed / load order changed / version changed) that doubles as the legend. - Search box (vanilla QuickSearchWidget) over names and package ids. - Toggles to hide unchanged mods and load order changes, remembered in the settings (requested in workshop comments). - Row heights follow the current font instead of a fixed 34px; the old window threw "Root rect's height is smaller than minimum size" and drew nothing when text was taller. - Zebra rows, hover highlight, per-row tooltip with package id, version and what the state means; "old -> new" version on changed rows. - "Save mod list" button like the vanilla dialog. - Translations for the new strings in English, Korean, Japanese and Simplified Chinese. Settings - Writing mod versions into saves is now on for fresh installs; it no longer rewrites the save file, and comparisons need the data. Co-Authored-By: Claude Opus 5.5 --- 1.6/Assemblies/ModMisMatchWindowPatch.dll | Bin 23552 -> 32768 bytes 1.7/Assemblies/ModMisMatchWindowPatch.dll | Bin 23552 -> 32768 bytes .../ChineseSimplified/Keyed/Translation.xml | 11 + Languages/English/Keyed/Translation.xml | 11 + Languages/Japanese/Keyed/Translation.xml | 11 + Languages/Korean/Keyed/Translation.xml | 13 +- .../ModListAlignerTests.cs | 126 +++++++--- .../ModMismatchFormatter/Core/AlignedRow.cs | 21 +- Source/ModMismatchFormatter/Core/ModEntry.cs | 17 +- .../Core/ModListAligner.cs | 137 ++++++++-- .../Patches/MismatchDialogPatch.cs | 3 +- Source/ModMismatchFormatter/Settings.cs | 15 +- .../UI/ModMismatchWindow.cs | 233 ++++++++++++++---- Source/ModMismatchFormatter/UI/Palette.cs | 12 + 14 files changed, 489 insertions(+), 121 deletions(-) diff --git a/1.6/Assemblies/ModMisMatchWindowPatch.dll b/1.6/Assemblies/ModMisMatchWindowPatch.dll index 1fa36b0231e20586403083ee2de8d06e1f739e5f..7d4ff274de577988c61a49abfc983a86ae8222f3 100644 GIT binary patch literal 32768 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 literal 23552 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 bzP}wj-jhB<>i-RKYWXDI|409y@xcECEsI2# diff --git a/1.7/Assemblies/ModMisMatchWindowPatch.dll b/1.7/Assemblies/ModMisMatchWindowPatch.dll index d5eb22cd02658e0d8f135a91d45c2e2486cbd8bb..256c1c6a88b1e9a64a633611f3fe398160e80064 100644 GIT binary patch literal 32768 zcmeHwdwg5fk^kI>uC63owq#p!-p-2%zY^y`97uv4I|+t_JnRq$3XWncv4|`gNls#% z7#nCQuacC)k`~$kEn8UVmQohjF59FOy0oP&yJac6lqLnXu+SH!58D31?>lp@WZ4Pr z_V@XH_K)9h9W-}l&YU@O=FFLM?zvZD{ly2#AR-f=_unUa99RBp68NXV2#WnP{@zbt z_B=Q9ac$jmGdp_YnX3MDYF9eiSJfR&CR5p}9kHtPK(Z>HtZLoRUe%ZCiPaYud*?W+ zH?AgHrx~>37hTWfT02g&s(ji4qJ7|4itE1}*D8FD;6qd@xUTGG2HP)x#{t3TKZCY^ zm__-&v8zKe3xEG`8_@x%{+ftZ z6_A4d0{!AYwoNA&ZEPjlxQR#;?q3epq*1e9kA#T4kuX5@I8G&VF4T?4Bp`YOvNaJG z2Dy6jFbp=Ur|TnfU6pQtn#dH^0=Ijs%NDrlddHm1G;rn`sp))4uOBBEfq$(19_UIB zjw_hOl>tW1P0f&ckBlo;^}Jl~*W(H%)Js*NGVGs;OEol>EikAJSM($>>_&Axg&N)< zrDo$&w@8m*M69`Wm1pRwIpEB#GtVfmai!)0j})WGTTshNodLF$(_5C)i$F5iH-uPH#IMTRKx_dTOTx%-V3kOvfP=!6f$9dMr=Cl&;w+ z46C>9ldqK90|*7J%oT9ex&yA%La<-9M!2KZq+!15bUt1FWh#e(=&MQlamDlui20F4 ztVOG)$YLO=B>>)olKgFHu=Zr%AMVH+k7co&2{Z19ZzO_q{^yYW5+~Oj>dgRZWJc*XWTJNYuIY6lS=$y1e@G%xWMb9J6Bq zRV_s9FAZtYV`f+x|#lYA;-s@DLQ0Ajtt`8OfMbdRHyau z0j`J(OXI>KdSorzRklE6!DuI#9u-V(bBA6$rwwH3yJez&)P@5J^BUU=(S^ z)XO95fa}HqOdwM?4eR(tr?_$Z}ntJ0M+QXiylGSNJ;@c zvXQGqFjp;HFgGoqZl#bNy$9e^Q+F#IS<2KccjPh%D7*9_Hg6b#Y0j++pD~OuHBVZ* zP@dV0+H*&^gcD!QFk-e)rk7#at4x*cH-g&Zx)>oF{*RWQ2l1 zi5A%kQEz3W6PL&~03Uc4<7(jwH+$Wg4*{mPVR%%sy%c0vD4Dx5=~N( z)4XK~4dJTh(+Goqv{?zmCzb`;Ue)Oh=9EG={N@F_a@97+Dj_0lS>9p+_}xEApHVsqK5DeOpq>@LlIywSwM~e(HT9N@O6b!go>A~hsuO03zvfUqxpF89I_uohUvFKs+ z0wW#JhdA{>6VDh&Gb&x(0-Prg8`N}3B&AB(XlYJAY4Kdl^n+2Jx7jiCujhz>{)}GWtCpGxVBqT!l(^a}dzcERy zOcqrlXul@n-a3xR-t*k%S;0hH1>E{qT zq>l3!mP)Hm4^=BRI|OyTR&B^B^SLK)3okLlMKyY;$VLHVCpKNDhpQ`Z#u};5xn3POq+x(c{;Nn`|<$S5lw z^Imvcfef^BFm^)rYLv*P+Vr^qfDdOALvDGBaRS*1H1calwXC|oF<;H+naN#W^5?8 zt8~@8%@#jSid+v3D0Zg)Say>7wm8#iwgwK69OY5nPptz3>lFuyKE%78$PGY6L`7ol zMIp7S-^l!%0G#Cwm&+8zm7P$qw>hn`Z(V*fGCkK|3q4FsLz<<@b&a9c|Pvw5Q#O|SL}dll{c}R)?j7`14DpayY$tfH(-|^ zb|Et#zz*FEQjqpDlAF{=BoeST9B2YJ6r@OmJMax|ZVKzi-GD1H2Wl#F z;%ELDaZ8UNR4v?;tLv4+x1+W4$X@~KLuD`{bqAy2)L~qBPMhRG5R~wo#=3`l+fSlk zxKX;1-v(dKUHKFW=8kX)-_z7Y;LjSLHGCJ?2b!7tX<&8ORDT9ncjO2|1SJ+bD|0u9 z2pXvwM(`Bwj(2nEy(pbK!X**-wr2S3@iGaP`K-urNhFYKy~s{|4(w3F+MrSCYzmF^ zxp^N+I2dxvrDDmjL!rohY|CE*VBT@041b<=MDAzy0{{pa_{BvZ!xiQP3S1~RuaO|y z_h$YEqI0d`bH>}+$F@GmMI&6o3!d`Sfrpg*^6~P?F?lX2kpBYYeG*>o^330Ykie06 zR&y6_2xKy-@3r&;EvPI*$MZPzMTo1B$&&|GK?1{prQXUsjPeM!e>D+omujlh(l0Eb zXL8|}xDd0)Df~C1wxM7xHYCxd9)VC@cpU$HR2F`9xH?q*8$-=6PNn*8V~D2}LbIy5 z^WsjYh_{Zaz+qsZ)qbq{K0{6cjMSI8y6jZzjPe@GkW&F8^(af8hX%zTy5uqpOF$2A zTa9}(%x(zaqvJE#`P5VmSHTEx&Gg|Jat#g7l&gOjhJkO%2EfS~N++hqpV67#ig1I@ z^d)i)rMu+nPj}1JO7G&UveTr;UPyPDyD`$P^mQ^d!vz+EClvZJIsp=rWjP|Pcamm0U zZ{%@qnVm7q`{L}Cvu^p?fHY88n_Z;Nu)0wVVI9akfm&WyXtSRBdtknrbKI$~04Z{< ztvk~;BL4uqw#piQ5?3?xkAQaMs{)S#rj7x)!kaByFt<9xdGZynyVi0$hfBG0rBYhE zRh`)kr{i#E=4+_?J?vNY$WyH9>j06b0dOWco9)Dzq`HRrI1A}8q^`2xTLt?O`vLuX ztylm>L;8MbEiaEe!+L!=z3H!vagl)QpvkK9rhf>o9NQrF_)Kpz^kEtWhQEQ@;YDSU z`4FrvlGBMH1feO%UeId2Qim<&jF$W&cdeCQ_1MhO{w;Ad5zjC>DPD3_(42U>OJmMx#V*x|Cfgxl$`|5=3DX{mn!+F5;> zF;D`7YKwK_bBrjhA@p6HnWOtu(1amlllngD*3K_z#E>CU{BUHf6+=cU^#bIvMC^1t zw+RP??5IsHHEcjaKRD39*T}!Jg8J{3=4L7f1-0_Yhlb?k{{FwF}H=a|LK_94PE>f`2m_R(tUYVaNqf;o{O*R3Jca16nv_% z;KG7}M+*zMXG)a{uC1UsxbWzwzF6lzrm&V<(rxt7pgv_5MP7nkdZdl({~J&zw*H;D zGAgN;f#yO^v3W>N!-f-2$L8Tt($#0_#y}|#yVxRj9B}M_oqLViw6erVu|1I=qUGAI z$e1mLvgj4&uXXrrj3MEY68?OL&(;_cHHsg0_-u|L!6SU}$aI}=*R1MSRB-C59Tmh% zw~Az?PldD6At7CTgEN>s7kKP=F7Qxo@`J%cvx&Q2MV=MJw@7L~;i4VbpiDwO(o;V| zj|_p4<3KYf0K6ez4NfFFeRzbx<@2YzmB{=G9EAUvYp6>EYnWB{La3<5l(RJ@?%pXV)m24#S}FM|Zs}`gS~{K(NdwL`^D0_Py#^3)g**AK-@WkPbjL0? zRPv3oRcts0<~ca46w||w2PeU=+guo|qlHtxz$Qag?j1__x?)-E> zbOzj^`S+umvT*)ih007gz3~Rj107B(bi0g6?K~6wxo~^FkO?(FV|}h!2S17}F1E#l zQLaw3yKu_*>g5GhCYj|%z;!xp7@hgEG0Nx0ae`Dc=>t_jZsCtn{(*WBC_l#fW6>`7 zehRPSaI%~|VV;;FKIi@|dY-uo++vS)U<0@wC%pLqlIbyQ@d-ahGCf0FKEzc$LsB}# z?9_iCTx#dI3VCDQjzVdhWX? z;qE^pz3@0BPT!EDX5n#lHt-BOJe7TZHTs47%wcR0)lP0LkdxMx+})qZRO1pp)2f6MdS7KACcu%sH~ua_fe>Q+#`mG?Sy-sY{T zKno(A`cE`at6SBWj9$x_|C5U9`pizWJhB*OU{|7cDzls;wd(0c>Mhj3mCuuEnEol# ztBj$a;Tnqk9IzaV?sQ}57hJGV-3KcvNb~J*UhVn19K7o49CQYn%@=*#z$2Yz*dPZa z3k{>DNYQ5Mmr#*;r%=z&Sa{V0VWyR^K7@UyS$GfbK~v@G_Pai_hr7%B`h4S;DWmy; zf^)+jX=mf=GlMM4k&~CLHimu$Tbw?>%VvSaSBJ%=dFh-(cj#l&SeI#R$PBJ7EHL!3 zsh!s#W9lO83kVJiWUhrp7&$TG0IbAeHp99};IS14u$X|yux?7@U8r_yL2Z3aXStHm zhdBNaejkS5*u#iAV^WPaCDRCIam`NEM7cgB@qg0#$OKuO)J}F}&sMTNrM<{)yXD~3 z7ARij<8m$Ri2KqnU`vnB$@ZU?rS28+rFyTy0G9+3neMJh>~w5;vh+Q!&=Fx3tse5p$*B&0f2u z4KYEFgTA8#^r}Q^2Mgm*#~QhH#yuX77sT(iMSR1_t&iY?c}DO*!6h_`4`d0wk(Wx2 z;fm(@IX|D}^8T7{k)!4?g#dRR}mVjlQ&z4L-z`kl4GW8r1GgTOyjyustA zV8wHGkghhqVTNc{$P3B00}bUCz31jyU)H~2T6AW~JinLXWgFmvXTq%Egr7NQRXk`} z^fkx?>05w)y3Kgbw&>%f+-}@p4L$lizn_i}H7Q^GC20vWVey1_G@CKW$!dg=K}#^0=D`AcArMKh(>2hgHL z$1B)|jiu|u7X4H#87*QB_Y`q2P6QaH1$Vm{|ES=1o7~nkrY7;3Y5Z1jkEr;1i1)isP{10y~Zq!^Y5)C9`Ofz#*IQmrI{SPa7+_^@C_-GX2c` zT#1(k;7KoC8De-m?kTz8)IZeHMN;3@diOHpZS#KZV%DfJYwPW#34B^Vcrl9g}xr_)y;MXQJbhGCV_6szJ8u4_ZlkrK(S?C zsQiPyg6eTrH45svQl=KtWpoXw7<#(``eS6wETyM}TFey6&W0aW;9p88PF?roEbR)R z&On6$x4^TCpVw#7M*CCtOqx@EP@72)7JUZz)6lHZ zPs;BF{O9t&0erap%YZkBo&>x@ptt0xeHJ}$Jq<{HhL5-y1_VwP_z+-#e$Vz;`^_iq zi$>~P-vpd2@DYIlfs6di-(2!dwEn8=JAhG(;rSMG{>gO{V5P-yP+&b^`^X(-Pud~6 z)AuQ~JJbAU;4|o70q@sN0RBe*72qGSsSi<&Q4IK)F%@vRRs(43HJA~G`#bR7?VfJ} zUTZD^XCr)K(e$#N$TkX&mM!zJ{uQXTk(vQtw=NLQR^We6mjJ$`B>)2k<98Ve=y}+_ z0Ptn>!lHKs{y-lF=MLA^fZy>k++uL)3BZk22nD*G?kj4=Ox)p6{~lDM|lpXSzbc;}j><^XQ4(hLjdWP<*+>aIU^A2@Kc`vA@nS{UFX=BlyY=uMJ z?YakNlaD*p$&!Vdmp(g7Nrp?A>X^;cA$r(D%Iq5} zI`lBDQxv^Y{1Ml-$HFR^)#^LA^LWRDI_WUjnbCj8=l#;*`clyiXV7z z*_-qZ%I*{DR=hp^JI@??%_;Me=ABEWi&*k@dN^Qv&!8TM`do3TcOE@1)FBz82<>02 zB&T~q-U$6sC{H+n>Lp|>6@-8O#Qr7tq`zc=!s8WaO_4Ij{P^CkqJ!w!g9jej&I-NzeLOnxQ z73~MLPpDD4w0y{W7FpQ6!k_k2l~;S0QqZ9u1GS8%3iS+aaeqlWhd$&`HQKKh zA3)h$hZ?o_(`u@7s2l7DysPP~9Od7Sr=2Su>URGF-Ziw=p>9RJwY0&Z?m@k^)G5^M z^q$TV;iBJM}iHvS{TBUk@#BR@6dwhc8A6hk7#{ z^YzlF6(zG{H$5Pf%2~VVOF~^kXVO03Zu*KtJ)&LZOVCpeMaIp(KKhnJS=uLkN%|Lu zT4H?0m!j7k>X`9>ublT-h9_ ztX}_$FGFnwWqW9oQ+A#94c{KRStvEb_tD`3$$fOUBe@N$(LQ>jKyr|tE|46g=N!qW z==;7w`hi3JSUcgnl8!snWaIn3A$rT9?lVsK_S3I~Qaw6Ae;7v@E!^8{=&!VQJO^m1 zLtUW1;~AzULaBOJQIk{luJLoX5nHrHeMcZ&2Fah0QuWXqT26^rXME)TI01!<;>ZttkAj#hzDWbPDnRnhmV^fnv7g zU6U<&&}K`X^s^sSyM;Q(aDyHyhXgd`aYI`t{>ybvU_)2iHQHmEOJa`jP z!#;`kW0L^A$ZJ-xlo|vs6WA=UP2eVh+XO}dHHrgP;BJWFsPGQ~TEdx3ZJHIVqCr55 z4gsD)cLUM~Y`@Gxblp7iydIr|lKmHvRGP-%wPyU9DPF ze;{;$c7^_A#U^bKuO)YCaXo=t_&h!A=CEf##az!QMiYbPjU9t6BMd{FbiSGNM^y>csE6Z(?24l+lzL;B3hXS5E%Kjd%@ zR+j2_)0veW`eFTwP$ytVMHe{FhdTh*6*IJg-TM9dKNar=ejNM&^k)IHt}6i_DK4de z_S2I6#4JgP63ad{Yhe?oskzsz$Kt-S90rtZ;#mCr-xVeGi? z*T3TWKJXWz|CqkVe?os#Ec_*HGk>DLMEAk}_v`1mexoNG%YSVLj6=E>t}%W|Re=@8 z^ZMhy*J-iz_m{L3cb8d5a&P5kApf^DDn;ys1AQ_#W^( zJ>N9;ON)cD3w%!>@xNm5$ga@t*Q4Q5Q!{QTo@EA%r~MH;X)1O#nlp`07OylLjO)!7 z^G*7R8+U!UlU#2e*6i?3^Crjs=@omxnFLO=vEOsMxykrl=uW`506&qJdDOg9@Xrc- zQ2cO0KN>1`@ko`@HlsFN<%%1Ry63t!87pyb)*y50%Oc;PogLh1HfVfrdxc>IH@F@I z-=cqz(yt4AUjIdK*manA&v}@52e}Ozy7fWGU+)?;o~$_J`nJ^izRZr7Wp<1jA1`~> zb-nRo@xQpDkpF>e)L7>~4!8pF+f?j&+jSTkN^wRr(|*S_s$E|B2XMYqtXU5mf3%9M znfin7Gpxf}SMV%ro6(3~ye9TMEA~IDJz=f47yOqrp6jfjWvvFk5%3eoNEMsNdsGYjs8*}}#682kSn%ZnTLoq@)*pAT)3#Jj!>s&t=wA0r+SSNl z9U9M?_lyIjkGVTEo;wdpG0jv1Dt9UN$rW-x;@QuEkO6u{wh)7XA-u7z%oPzU&dfek~1FWILfQ#r+z;ozp zfGzZ0zzc-m4%kdXXuXBHsl2#FbgmZIEu3z_M?~ft;anq}Bf@z~I8O=Z>wx~^6WBe@ zE;cpBXKS~zX6;k7qqtQ#Z5p@L4bBgWuK~Qoe?;qsx5$l`gkzRPD7s8n-oF?Hk3#VB)+l3PqPEynJJuRf!l>26?{ZWj|e;}{G)<@9i{%_;V3WY@0!IWM5qMPK zYsDN1BKT`crj+H62s|oqAAK69 zU}tHEwO0M(`VaMA>Z^>jagXtq@q1&lS?}s}UE}(s%eJOl3#_%)CDtRB+g;|4xUX}6 z%>7ljf&I6IJ+&97pt;>JeyE}Bf8k!^9iNWJn+m_B0;v*SECRgSRSNi`kKsX21>kb` zWWe8+PX~P0oCElv&G1P-!~1~qy#}}C0@U%gxeKri-{#b53U=T8jdLGL7Xa$C3}>Pm zo<0-EAZwQZUx6LEMk@hzJclR)Y{PrR{4M%0_!j``v<~}sjn)I|bRl%(4h>Mp z+4MBvn*i%^qBTM;Eu;-R5G@54lUn2kWAvy`ubE)X625q zuEh(w7SQsQ%euN2HZZm^9ox_!OLCE7tnBXUYK>?56VbtzL^P9GBw}1akmlszMX_`y zo=Ud#Mw7c@JzP_XOsu$=^|z*@dshu)v#Dfe<*u$Sh3;P@WfPlHOtiFwYf9_!orBzf zFeZu%vn_K`JQLrMh)EBY<~p}XFlFMpRJ147!w%?3wMX}eq$;1N(c!{^MUAY9rN{iY zE}qG9E2`|2y32CiZHXlkEaxbYmL0k|5#N|Us#t6<>~x{!U;Av(3*i{_x1+5a3GP0!tp@cRwoDgV(C0}#XvNh zBR0pPJsXmV!Io4a5$nz(aHIl667ej!ef`mNEM1@^n-VHlkG;;N*;ro#wY1M!zOu2a z3qN!dO=Li)5~=i}h18bJ0_L0vYlo;V8(_uSbZVd2VK4{`yFhb+3EO6P4^G|?6?cy!Sg7yT()P@X|( z_OL07lqnOmEuNr!Jb4zkM9VQH1~74`13yGGkd7^ErS+I8iFh(rkD~SQOkXtH-Mc20 z#x)yD*KcW~4N1I-Lu+G6z~w79Q2Ss8eXDQVKs_m>oJ_2f_OMI3x(LG>rHx1<8xV^n{h&mm3CJ@SbGPE1~u=}qn3lI&KgjFPF%vCKdsn_15tM}?N& zSoiK7seS9BJ7NjY84m4|u1O_&V(HVDwP&N*(NBQnESTw>6bUk5(shPNX$) z;H)ZE-<}!(&RySy*#$JiPDA|A@&zlqy4ukpCe++y3~|+qOCLU*JJ4nq`jG62rhBNp zJ00H>Vga-Ztvwg=rL!*~SH}~AuTEnAC~RLmD+t>r2n^yh^W#|i2 zYtpGchtrYTwF?F8U8fDTTCRzMn?OG~pd1IG%T9#J%qt8z>f|sIIYD@4<~d_=CiMzQ zuH+;W$66%!z8%O#v?Ym!Vs&yC<0{|6avI3w>~z);+S-dj`!gx5B?XZ-nX;>5JDK1i z-;nN!C!>kBT}g0SqL~;;E<+M$XQEY(MCx$H14uNPNnm88vP=-^kHT2tM)d=Dim^Gh z*OB3=Bz5+3;L+AJ!;R4drUshCQV3tCCBs3mr#Xw4B6kd+s5Q1@VAn1l)G=Oe`5ogl zBOmqcNDOwwvxR~k(KJk2gWuze?MBl09)=kfVt)W=;tYyAube7PckfI%G1b z`+-u>zcapTAT4rZN;b!KwnulyvV)tk_+-ZTvWl)w#QI{%EJsX1k#ddb*&Itm_X$=| zo--P5?-}UMj_J%f38wI`?Tr<*WpewAFCEGSLKdpd;PAs%<6fY%mmo2+mQV0gc}dv_*JiXDpN#fDa;Qi)hJ zNzOK#yb}ETXcN#Yy8&es{bV&V}nDHLm@~X77dLM7+ z*iaR*GRM>L3>fQTyQ1BLTavqzsl7?%o{kh|3hypB0rJfUf@E#9A0ydMPGrF|S+ovD z2Yi%`_fsyd)U~wtM$`So+n9bdha}dG&M8w<=?xq(=`0tiJvD}`9<%j|94$DHFe}~> zO;gsn=4M-SvZn>3lEzjtDkI4g23w{4%uowXN2-9MHbf$rrrhM>spzm0tRn3=XVpNQ z_ldN_xjOn0kI^JHE@?_-u>|wp0wcRMp6zYx!GwtKjDv~n6UELNH)^qTe~K4RCt0ZN zM>^-OJjrXUtcM)%oINGRpsAB*UleI9XOCdVAdE{LTiGo)$L0Onk~q!}!0Fo1Td zi{w9XJ9s>~)Xo5e( zXEVrDJ>PEv%0+fj?b(4H88utib!T!8Lz2bL2U}&Z;yoF}PliL0XTMsfJmC67WXiA4l9$-RJDy2mWcXOjAj$cjiBdmzeo zP%nVMd zQYaZ4<@{}>N>i)%Wn)P`goEPo39q0egK3;guo*S?VXl*1l@s%XGj$MbP9jAljAc<6 zK6Z?Uq#sXa*JG=RU2*Q#;=EV_DK#U(jJD!Ea2aZ)RQU6ZMJ8ZI&O;Llx^ZiV^40q= z(K!Hj;_kZFxqZR@IYWEYDWBkMj38+)fpXj>$ry>RyT-wB^FO~q>ylFO(K;_E40(uVDnvS4lPKVovo>E+)JVk@4$PS)4LSaJ&;C)jZxeb ztJYcu(t^WzJhCz=)&Q3^L=f(G3y-S0VOOZyxhhA?kx)3qKw`l(x+osot9C;Lv|7V@5DygHkx8X@NVCmy zu%TuIuROWUufsu+s9k6opD@{$N8GO^H66AtMF@!JPCPH z#Clc@lAKiH%El{*A8a3_<}^c2?>H*k%Z#xDYmvpl7wyI;cZw>^+zyA&*Jy#W5t8_B zAHXQ$XSh33W9Olme(EHs1)G3fDU_nit6|$9M}BS!%PdI7oMU1qS&wa8wq)flEVt`I zO2S4uJ1B?JZ98TA%g<|Q{v~+YmBO1w>+ud6QY}R(i?;!K@y>IK(hi-)H+s@^DOKSM z;qAb)_!lSaT~T8lYU}_nf$!I@29K!(=!>KdvLy5*@wP%QXnqSp)uUwtsC%kZ3N7|S zM;c|V_+(HgjXM3(B5R`N!dA9QY0jD!d}k;FN!BvH{qvwJie7R*=*+!NEgXh`lSRn~ zwB_k@%4t%cabp`?dsX0B`l>`-3i5za}#D-9~Du;YNf!noQB zskHQywRNBrS%}-4nX^FETqpIig%YkRir}TtGw~(6W?00d&VBhn`6a4kq6Mc$b{D>h z)+7Bu-i;Twx9L1_k_Y~%Z*<4Rp#e%`(GFKM0UA7`d6#7?0v*1@-Gu5(u7+=h6Z z$8#u&xfO>+eaKo$CKq$LXrpkBqiW{)6}cMC&@zZw5e1~MiY=by9L@RGj2gsg9eR2$ zuJ{_aM(6VE=9$9tK8b6+BRw%Xw@P^r?AwdC4szL;@V(J}D8ar%dc^Z3m-})vV0?6- z&m*5ZUE*LDBwJ-pj{fd?sR63<>)`v0O$HDc!gJ&F}3x;J0|eBJJ>`)E`cauiValG z;O-;(ww}(8u{`_1-Q0xr7bRQDGZ z8E(ywR{d5*a0_(URk~hArDd>!f}<{OuZ+sHQnync+!nml?+->@cpGQb<3(E)!O>#K z;APj*3I>zVLU0?*fWR~p@KUIs3Im}Xm|o;ID}phY7+fzF>wbKP1>JcREbb0p@%Adq z;9Vaj3N>ZqE7eS`bUNXy^t$1N)yRC7+kiE3d>t6w=Pm|E8&uOvgjj|Sfwv?$y39=` z%lJ`Mw@NjdChk!#996E|q6Zpni~TpcTDNo;I$u;&>egY^s1|H;E^H)|=#1?FewQ9t z9%y9Ysg*NJb(VehVN=ad=-4+(TA zjRiVH6v}kJ7cyu_sjy4j?xLc=rog76z{vIZ4;JEgF%V6oo8YV>ca@3$6cyQ|Ymnkt z&^6n(7HAp_b$K~V;2nIinalkZZcov)ionRtQ17?#@!L>t=QxH{M)=?h>(IZ=#(Q!c zWk8uR@?pPjIbz3?YSgubh)e1WP zsB$IyP!E)H)L{(r-sQN9BNkw(qafI%mq9^+2C2#uq6r2T!6iPo#r?72{Y`i~8+Me! z?!YD;)5vCXZQDjHmyzvaAF_OKqt5r5m3v2S^Kk$csVRi15g2(${EiEz$Drmf z!tVqGHYwcpAZXP9!JI&2ky{URz^jMgw%gdff&}o(?(mF`E;t;Savv@sS8x=A`(AKB zhqrI$`5e509dH|l0FirJaP%(D8+w`B40ej2{0gE?p2l#Trz{%4! z(~ENM%joAk=)bDRn%*@0DhT}kDW|LcrZJuD=^B0?+%K`>_ZOqqUEGhGp;GsFwbSwa zsNnE98L^e@RbY>5a0B-c-{vwjKdwk3JhGUD>w_bQc~IA@xIs>Fd38YKF(k^v9A;YZ zLp)8{Y$Qrp6>L(|7L}VY9r*%>>3)|RBk&N<5*PmP!x^9gqfeH*d6J>6k%uK4jXVsD zn-dlzNLeEfBP}4SjXaD|&~XWlJYiaa(Pue>;xZNQjtZA6^Ozy5kgZ0J1xBBjRQZC+ zJ1=0W=mGqs`N$KT=cfVi$L-dGaf~0U{2})@?n1Qt{dh-JFZGvVK-j~<^*Z_x9C;nO z0R8G>xDa5Cx+12+%+c2|2_)$PDr2O427767)6^#?}ZgleuZa*RXybq(2? z`*#fgRk>9vlu-*d1)FTh9*{)I(6(9UFM+A}%Zot~mwWOIFRXvlU=6y@jag?;V_^H- z?qI^UgZ;LxV?4{)E{(@C>m*AKC{@b|DJGoEnkx%76+3aog{Kq;mKPQWmgBd>ronWK z0xtw*a1Y0d%CJ~~G&S7Q;Vu=;7yvAjegvc|xR3QCn=H4#cy8BKffu}I*Z*bbAC+o@ zFm`OfTM6ELm7neuz^`s`ayw%G_%i^L@axlY>w)^#q$V&X*# z!6JuP;DrMD1_%O$kw)C8qH&~-Ca!`|!+N&KE)8_>{A|J&NTr=dwCoQwP9|)Ya=Az4 z8OaP!lwubQe}HRZa5FO(i7!4gRm}vN6=C5_S_>fFZdU zTCt~LK|O8~I{fANmy?_q&-rlP!&RzS zWQs;1bt*28`P8YnJj=tD65klWD?u@SXG@5N`i1zpIs9zOB8}>WsM)!6M^8g^!Qvf@ z8yj~l09<-jL&L(J?!`UJ7A%NHmqufYcPwgXz>jQr@ePdD)vLCw#i65E4~VIVo|Mev zp7`)hdR!3l_*pvDJw4uI^7F)eAxd#Kp$c96B)!K{k0gi&G}^_TkJX?0q`kH(cZ%-3 zCs13J^HW3p()wjf8yo5uH7s1Vw6>}RPj~Rj$%YE z)JrVtX-o!Bah*ZUKjMj|`N0-Wvk=L=1|o0}T!=V)!|2pM`BF;(`?cG~h?9(0jnRcz z?*H+scz7I}~4_^hi9XIy;B$bWNWE_I-k&lxi)=Z&m- zRwn}VMgGDabnw|9KY`=(?Nc?qjE4Dpce#Iy@jMGYfc5;HIzCTT&-3`qJa zI{W66arMmc1J1r7f05|Rwf_N6>;4b)F5r8-e80w@HnN=!IAdLi&jR_d9sHd@_N00y z%FjC!1wH#q`c;q9OFkUuKWreNcO6D)muO_q^}}cE`CT}<#PdN(U6(oENmP6u6>dN0 z7pC|G^)K^pp&e@6HlkgAZZd$=T7Ke?)B2a`<$hg_Z)R#VUf)D7kNLO;@wpTc)C0dq z;d73r#rWoZqePX$m(p1%ZNRAXz`DhN%Ov`usIe5EnABP%GT5=;uTjRh8Bxw>Ng#3jEWch~n_vFNf(% z!I$Pescn8~Ue{nIA0Kw?0Vg#S?@i@$b|Jnu9d|}@@k}n>y0tStWcQ_$Wo4m@UDey# zh&F2mU9t4{Kk!<6o#w|&v?WAc;8==#vx8?G-y*(55y5q3H#69N`8*25!X!cG;hz<3xc;N+q}vyRv#{P=R9Wk@7XnX|p(r$|mSryj+w$~QczO{?rtYcKD{CMz3+?`Q zllh$XV<$O8l!o01bY-4yATIKxyr}19lQf zp&30|=(KpEp4FG2tjyVcKDKnG!VK2UikfxRQPaslDuGEHsta1ds3~0w*cjGO{k^9u z10x6pt-=@e)%l}7y8-OitRi=`hBVAqovxtkzeJTV5Pdc22%eapQ87QUjJ0SrlvobL zM#zRjwHqvZC6E$llpAJ=x@G9xr9evk323&;V!CvxvMK?$)y53ziA$Zb600Hlnx#zq z;;D#LXIaWHRZ^qD#X4O=b=Rn@q|*%Av5!J*qfTKO7*Tw-_Yimp#i$Ag=SJA%{1qE^ z85#*$i8XlCV<^c=V77(qbpXoCv&5>zdN2}~k8^#AMrLrtl$?r}IuAmZPOI7)A{8D2QxOYkrjSN7kx(&w5HCpsrD!a&(tdum>!rhN25%c zM`Qe`K6(Y$X`WBT`K|MbE@{nQ$&BCWiOnpnCosh=J%Jf+9Yb`R3+t;F9mC`>&sf{R z%VW}q78bcA0o0PFBuA{qL_|V~RwuSVq+ClNx$|S2dZ2O$?M0gN$h#qDmhL5)PLMI0 zknd80@#ftSk2mc`iCZ_`oZz}KZ9;TTD?;|k6e+wCgyG#8t=F$Jmd zn$noE9uK$X%T2oj(t@(SW2*H^I>0s*o4PMp%brq8_XjvvTe=nCoNUz`&=2zv)%&Xx zJJGEg^{5h}y9W8rqHEw|EXz^j7;2h{t01N)rh$i+sj(ZbVT6+{u}59MpyxFMkcPj`7GE-1Jm^-e)vBbDYC!4l zW(CwJ%_Odc0pbu(>z|SdRCkvKCi&!Y^PP9zNn8gN>}MY#gGwx$^`(6|@Y|SiA{_?!x*!9p= znb-%YAC96CJIiRbJ%oqyt4TpP73**`kHms~PYy-Lu%u2-?#QfG-bTT~BA4*qur`5Q zQ(IykbHF}Y%iKJ$ddr_E;OS3{FtpBC$U|2C0Eh$}U&}k8$(oVitvps`Xyy{$5Y!%n zWs_x)^sW32BEuyK_*b=F8n6$8UA1&$%$Pnsfz3z=yN79pHzpwng2Bsi;+PZ$s;Uw< z0!tKyOpM`i>^%TgiJO@HUI5Q88xRp_Dw;=Pv_h7Cv>uI%dpM%=?}Mo7{Wy6PTQ(D2 zM$WYIH?zJHHnkL>#*s~te#V;1g|~2F*?8e!8?qrX?DvE3W_qKt)-q&+lwX4?oO{$9 zK(8(^2aaK;o0tQ~kU{YbAH&jtrzH!5I|odH%j7fv^BYgEn*@-mF3;Zz1B#fZ=KRw) z+_`@KzifYUodOf3tX!2o_uEgcpMUM`&Fe9Fyt-^c{EKKHeh0wZ&iFfcf2bqIAA1h2VNJqjzM3@%pV5~Bt9hY z!+`dk0KV!SRzNVn+9y79D&()T{O$(U+q*_+<1V!?Jm%CRO7eH1?#mcsJ#jay`UpVc zqX0PfV0R|x9`&qFV2Wa5RjH@sl4jVCp^fS2+?L;374{ zS(a5Fa!zub>N2!)ZrA~R2#M&idr`Z3Sw-Svl+~5W5$G@m-IT30v|6X?k;><`RI7;B zoK2<|;F^^PbI@xN*`oavA)wX1^CI$JO!erI#^)3y{E7CaCwbqFR$S$^UHWY3uda>lv_GWq);zp%(9iE^H3>#?bggTkhE9v(=BhncLI z@A8=;1)J5$3L1RCy6M9bYP}FeV8Lw%$mOa3g3@2)N>QTUbvE72jlM5P9fUU-vDvXvc z`jtu5HAx(&SE97)j8?O#oD4o~Sv8+XJP7s5rS>O*#uxA0`QX(qm)$Ac?wWTSBXFi( zSEd^eGNP(i)x0oCfgvMfKLj;()ss{hGC;zwsjAuFc5FL#j@*y!H4a7&vxtXy0Y7|4 zF;!6dDJZH*z6>X;iN{0Dax(UUfWb9{AqW^;R~Uk*SU7vEE+6;VS!2}-gZ|7|@x10w z;?pqCaJG-DLce&sw-ya(ijFJs8wu z)LpGwG;D4+@p-gRw=pr%rlDH?0`o6%`P`@><75lJ+U0Yrh76bRrHgZPKB8JRI2-fm z>q?)B4y8kdh^i-XQq%7`o&$>gyxpfK*sa^uIjU8Yy(R7l9)U(fVB%4r`7Z*5s!DJi zdJ%op{q|#Elobu>!IF$0<^!nyMzxItFQwg1DIEd zx7JpLPlte~z*Iad>Z@w-M=gNbP*sJ$c2+e&l`<^q+pX-3TD#Ba75=EdZiZE91QwzV zF>9jf+GzdQjXHC8ViM%amndvHOc%oxTClTBhI^7nb>m-Iiu9z`FAk4rr9|B zB%W1?rvNLFrRErizsv<*g%25~ez=xH6j$qIofNzI*CG;4g4!KW181Iv?3 zlFODXTZ#nBC(A70@+zVQ*s`9$vk2!c=qxyy+(4cs;!yH23%s43^z;&SZ@ysT&UU0C z`ZnlcY!MbTXYIW#jE|1>e%IVj1cM1Q_0F%?Ov%C$X?t^osSnfJJ}s_mo)F zEbtwR@!5iZN#K`-lM)Rt3H+4BdPXA;g)I6Rw1p^XCM|Hl578UJq!p$@v{>?D-(@Fue?!nY6!T0GfYJtpCZ9o{|v#ri3*=gLXsYlqRj2 z^hj_(4^evs3o!eSaCUs$Lh_*;+8tqom?dAtURrC#bBSfD7Cq!?DS^jIm>9C>0H(j4YJ<+n# zIn*fdsE_gKWo4xn{ZouvPf9EAhOa{O;V46Ygf&>wRQwZ0C^!_8NcVayyGieAiAv!E^atB5^MixaJQX%^MvI}GYAYG@ zBIF3Qh$)nDE?7skv{R_}3spzECP^k~H*GII5v#DG^b@1X0&Z3SzF*)+1b(KZ!dgl{ zHD^NR!{swM;(SHTqP2kAiw{J80r-r-=Smp=rr?hV{8{K1+C19fn{UmdFP0a9|0DEh z)E-#`IEJWMNej#dz*PprXLW`z0!HaZ1FcxK`lZ%Y#edW<1AI~7DueN7fzP8sV?E$w z0y~Tf;P(mq7GM>11m;8APqk*?^|S$S6Kw;$R@)8uxHgEsycNC|vq!LWtE zK+`IYAiI9brQ(r6P>u5lpS$T7fqP-gM_sDUIAI0oNte2;v_T8e1M`(+r%+wkO%WZZ zM9ICFW&i9_`zuaZrSu0yP4|Zzv@)88{T0y*bS-*WMomJo)l9XIQx95Y#M>xH25t^~ z&MK$BcI$l~d90ki?Na5ix|}dk@fi#JF!B(hY>A?%G4iMtp^v^x%4jU~q!pvcVy2E$ zUFd7pH0ly+jDA%9C_KELmrYpq8(21-HV8FFZy;Arr$M2P(~dxcHl2!C?$I8#guh`; zr|-JdkWjyLsgKilQC5kyo&EVl*{jwJ>K5v_cw#2K?w0+#?=&*^9<2PZOv>g^QK-8E z*ZBU;nnSm!vOv~H{yB8JqUecOgEp5w;!-Edm^$H7XG(qkx%4ZSI$7SJ#ql~>TA*i8 zFHTRp)DyAk{y0T=d4^wJ(Ekm2bB$0uDslgOx>8YrIYt5p`%afi81#HBgBo_Jjn+#4B6?Y<7wDqk5n4pEv4da_{=9q*%5Dmr9zFNfA0Zxom+NW|Efu(0 zV3WXhf!hV{7MKFmC<8c+_6r;n{&7G{I5TOnhMY#b0l@)WgdGgmIxd{2v@MbK^i}N} zW-B1CtGA+VJKz=NTj@n@opm+6qP-nS(W@eVTKh?85crSv9G%wurUgk}NAID}lpVn@ zhSmgb0lrzkgWl9;AZN^`kB9E2Uuz-j3&6GTa^i9CW!!Oee z{jIX^(^GUH`ai+h89PExX=^He1WtYIf6-TIk!7Lo{pRmzo}LN+kuIjU%F47l$X}%0 zL!8$Z>+cH9*EIcMvq4*`Kj2%Zt=3mZen(CE-|4Mdlm5x*mD+aw?Z|d*x4t>rt+ne* z14rl;`nt(&ZHc6`r?j2W|CILm5L?b3dsSBLR|QY$2P=lO)4)do=j%o7ZQwTn=M{Q6 z@DFMG^$$c(Xea68v4;SEAn<<#8P~#})yDK4p~rwf4E}!oi-D)XX*6Ea*3!qKr-1K| z{TMnASNsAp|BRKrmF^1ttF~JEP3#Oxd$Ee0qz9z#m&;6@wUy|{_2pJfXZzP__n>!k z^ppDULkm%QMXXi-v>pvzrN1e*J*BTLxmJHve_GG#FY5C`j{d5?Nc7^vTd(PwaXR{A!0!QGJ1+T0z0bwJ=UZZA z1usayW*~A3#tbcN+yK}bieR*!j@}}i2+hzImma}|>$KR1jd{j5%#RwHQHOm~0U9j) z4u-w@UV--zuTZO@?X%iqLkoY`SZur<`Jr)}NZu*&@G*&prN%&c%v^1xLbJ^#$j>vE z8ea=727C0KZ;hGK{upZor#p0onKBy8ou;OL#k9>)ts{J+88zm? zwg<$9H}ycp9p-+i`>4!_N3|0336tS7=BTXojK64pj%uU-2smi|7;sPdKckd;G-muJ z*r0t$O1~tfPYeFE;3@5r3i3TI@?RJ4-D-R>w$S%&)Ya&F5)aEUUYGfvg5;0r+tQb( zwA(QIUZX8qhwr%YROxo#YxIz|+gA|z*HHIvyfrYX-xtQs$X$RD`aEDIJqL*WoUfGX z=rrIG`YGUY`Z?e#`W4_>`Ym81{lOQYVR{F!fOl~L{Ng7Fc!a{(V?Galobq%gu#ooA zt=7A6@6lJ1#Mx~@Ac?z1?UCEx<;0<5Jw0hiGufNSU(z!v%@;FZGf1Z=|FG}CIK zUMdf^h|V^Fy~61gyeKj^3Fju^oDj})!g)?OUk3~ZPt#2_CunMn2Q}8bK>IN3*Zzh! z23v*S3;da2M(~?}H-=7Vy^y~j_zR&o1b+jiw_0y#pG93mXWI;&IST||Ab5+;`g;Z5 zB=CNL&k1}TlEGk;!7{HKy{P-T;F`&tc>)_v*4!j`N?=jon925!34TK0NpNnpzAEsv zc@xzH$;WjyAM@w={)$!x=Ly~{;NrBJ$#5RFsF&-CqvBjA|GHc#RYE^yixEy z0*eAq2>*oO&kFvm;I9j$AZwtYXcIgx_{Bl?Nu%Jcg6|Q$Pw=APjUnlyz@ors1yTv~ z57EczONo2rjc*%o7~9PObJYB_`H1;-GvsT+hDgKuu48>Saa!|XzF{kY z`;CA16MZ0{gE$rO0}lI20PBo$z(WCsX{!=&cVq_O{rX(MJA4e6m&^_DUW+yR0Cn6O z`T(clcNaQV!yxzz0d?Yer{VUo1o#r1F*WQZ%78b(5*_n73b+olQls^NI^I`K1KtRz zQxjDKZw5@_cOgacX;s>-+V{0zYismR=>MY6H@b`=eyjQ{&ZTxc&_CBF;is(AuU3~EgDL<`P)O3R?`Z0EL%$xiPW5;!N-LGJG zc{Z=1jCt5-P5P8pFfp~=F6`aT=3Rt6=-E8Kv=PJjRp48R?=*a?;KLQT$JtLw+)8D% z<5a-P|GfSN=wDK`@fKZSoTk0TACcjINskzB&=a7a2VF4NX%Fz8aBWN0&ZXC1Ol#M# z?&(?DvxL?*70}ZgNqsjQ}X45!9wzuU*hSJVBwSOd4@Q5AhRNvNIcC^LLX4Aa|bX_VyB%3LKJ2aef z(#|9$1zRYu-sVicz@>%s&{BdD+gaA2Jc`}{yReB;zF{QSyQhJ!N@YjVT_eLVw}DzS zVn@mu1;L6@4%2LBLh-Ucqj(vKVu-C@O>5!Ak!*TBbzoq!nOr&vMn@(;lq&QNZm=CZ z3u!00vmMe~sdF@sCX?-3sn14)0H{$^oo*gsos65fpmKx<)Lzd z8Xo6!bWu~EeA9quM=b5k^{UvVmcexIzPBFq>3|zmjr>7YNVs}F( zokfvUK9}8|M**ZZ!iDFSZaFWv34wtLbxsrRq&>HwGhHa~3#D^Pc4UTj*$&KW%Xyur z4H@99DxK`KM}Tub1{^^#x`g2!#A66wj(X_r=`5sjeJQ7pI(wbW-ZZu6dD?Atb`53< z=}wGVnl`2jn^O6~7X0Z6Z5@^ek0E;nN>Qb63qQJtvhsAhB6wR4ajCFFnSvnPupqD^ zt>O+NiIAZo_gTQ-yY!|C>6TRQV4B=C;$=1AwdW4l`_kySmlUX-vyz?XoWL~-nW1#o z=x}-yItLDVz|;oE9&$N(81Jf7Ic`G+a#?hXM^nt2JiHP^QyASmz9J_G2fHBd# z59GjUN#)Zdb04{g9fMXmEU3%%Es#_$pM{sDGNQo6wyD29Qj8t;L51X{d|N7uXoOHf}Cf4VTb z1Iaf(!Iw1OmQ4?(a|IssNkz&vqGv}sn>r-eq;k(_xVdknw=kj8a}rG9KW&;QXwQ2K z)dbJeB2kkYohWd_PWXk)-VCpR6Qvv`5_}l_QOpU2%i2I|m#pvUIsZ~85uy^8%mWuq zNh*`Vc0_k3mF>*jkS0kyt?B-){nVi*G*3On*o^Ssl+Fwc7Ero5l`pjC`qGDhdzH?r zLdY+9H8@@#@YWx$?b9BD`KcR5ItylPHnLN|1Durk%Cl64nSw~CGVc+76|AM>w`I zOHs_VRfCFd$!5UaY}@;gQJj6$l^O2L3}6zG?PQROfT&e-Gdkgbl697?>K$y#W~n6w zaQ@CD`%uKbY)J!glO9$86%N>bsEo;oDn!U-hM?>~8ozkx-IwQFETpulNY3Zv?oO#G z3sb2%4LhlMq)@PP=mQhz#SU=ooV4YRzclCJ><5_bkvxAoQn^fjI$!X3E!YTQh2eSc zrU#6J5Xfe-dGk7jB;alBHVyC6cy7yjNjE zH>U?uy`wvG`*QZd9AcbTVa`mv-Ne{$ObugzhRF?(X4@{H5iq=YUfR^h-h{@F|ONG8kFUiazh$8<>Lu);fJ7qGtZa*W~Gl_?Ci_aTZi z{TVQkAyOk*WCVobu+1xzJC)Un?O;aR*+OP`oa6;emLU#N-c?A%KvOqQnG{luXO^Tx zoB=$iu#?yYX-E1CKJSpklMv#lSF0-sS2bghzu?vz#%;C_LP|})&8bn~*o`3`ZUDvs zxdWrxngQ2#R2Ic97ibqJdxgr`gW4R2+3xZT)I0m&_`sfXH$6-x`A7jM57}XK7Do2w zy>23ZVKabjUT+>Fp65W}L03s$4q=k`6gECoDsN#;lk|@M=au;rr#ap~jyK}vVYTq3 zozAp#Ak&-9yF0}^#+7#j2o9(Mn_bZ|h%n(jVBX93K>n0_lai7kokrXqzLI?It z*u;7J+fC^#G7VbE^uei+w^i_Q^kY^r!|T*p1#O2gTT}zs;pNF;fNI!u%lJsg8s93-%85;Z8h2Ewv62s?&*+J}a*xMg63%6k)8I@yM?$1#RS#lZyRy@~Y7 zO)%8cgQFud4RyO)I6k>}=%h5--D>yZe32_q?}!7DZ8$uGVB1)=dv16H^|3R~xj3)9Taar!hjHFPI?2gXg_)piczX`HQz)D*G1;)lU6n~6REzN> zS|!{*j1=cdZ`-7TP5g97!H$$dMj65S-k-q(iHV%JwDx=ioIH^(WT~d9x zAUFtgUwTxE1{@dh7Dv88pe63w$v%_watjua0ULkB4$ImQ#-kiHUSLZE<9OBe7v zYlp6*IR4nI6L_YiI1F_i^u#Oq|~#fg;*;l7qm=me-m`2&`a(IEk5Yh!YLJ; z0!l8>R&dBIrx`=8mppm)Qwpuow06;(6ASxctF%#mK%QRzseC7F--oYF2e}u%EF`I< z3Er|{b%AI`2lVB@?MF@SMH26mhS;xjTSO*{cH*L;fG2y1dxSCZUB+<1pZ6%$=23?& zo9dk|=Heoz@ zwAh9VN|hs$-CHNIe0Py zONYi~Jo`M$Xqs2M3DTn&nG_&Zs~+>%aU0`WFXs{B7~-+Z;hA*H&h-$N_rZpPv<-FC zjG|~8MwGp^MeJmcdGYIcW9qn|C&dTPH~8E!ZXk|fQxk-zV3N*mATDxo{J4@Q5l>?; z;3rRrHPAC8CdFl7u(^XYJ1~!ugTS-+HxYW=7Qfn5q11oM@!Xlg@y>Z3_wkV9+~~?yu!b$*b53=6Gnh9u=G_t^x_0!Q)=#Yca?AaV z-;aK0?_1l+jB8rJh~o#{K%!A5!wh^(U!-8zAJ?^5@zJna+kv{!6Dqsc0iofOdDyTw>_}%i@?$~wVa4dy4da>f$A+!Zc{v9%=6&v$02%?49ZkR!_ zu`(0zI;fuw1EC$5Ug|fe#nLb_)*%+_VZ1Mdk_W-!?(j1Z2PX}!Xq82wrh-Z$nyE$R zPJTZbN$`r{_#AW6E0vf`U4D@9q* z?<$=C`VH(?w_tCMj>tJYX zL=Q*DQW7Hjd9X5geT?2`Vq^O`*6?RfW^5ZAa4UXbCNNt3AP+q=cyNwm5NAf9pT}tq zk3AkdddKmH5OeT;80FE31jVhYfXlgeV@L76nkzsPbdSBy2(#Vypu(*4C`9=j3e3?$ z(CCVlNi!~?&xmNQ6)JeftlwD*gD) z6WWaxPfA!9PXgm`6BZ+g{o+aZ88KfxiJ{T)h!sC=TG6qSJQeYnjhFnw<;pyeh#rJ} z@zLnmr)ADPtOELBOl&<$CaQD7xCns1e!m{eU{G1*<7|D#=Ql70_(_2t2}dv{sxNv4 z{5#3PJia>1cUXQsx(fY=JBqKOEx@pP7;sT^mBMFpd{}6sE9Hw zG{k=TZCD4c1s=J)=f>zigf2_|We0-kYNheE6|J&9@Jd#BzHM=s*R6@|MR@9_7{3RI zGy}Zc1W;r^jR0}x#f3zE0MpbN@Q)jGD{NS?N-y;SA7PxchG9jIVhD@(;eTrbd=<(X z@!cHc$d>FCP#9-LVR`drD#;B@$G6+7*rEH_2!c_uN*`vdz%WuUmJf_Gvc-swbR0d( zIPzjJZr~AXjIM%oIJzpxshcC3#|6XJ;aB0mY9=A?d(p2VU1Iao5uJZB3;p=TA*qyt z(1xs<4ICFE5uL-IqXBIrB;wlK;y-|9OE}^%)UR>~N7qKzB9ZB{5o{oWs1%7c;y(*! z!WTZ&#w1YHCFofv3k!rs?hj9dNO(43y0Wtw#u`1S+=~SdQOf)P=L>hiK&F66Xb20V zmu!G~8LTAkF~b444{J<7n(iJh%VP?MsiDA7r&3hdxSdoW^3JnrAw07A9RH)$6w{S zzZv0ciXc~sv&bxss?@!~Ij>5n6hG&|3$!%9#}r~|vH^d6xrAt$MoA%R)2mnZ_xG<{ zwz98b)ruwkD|=H*S6|wi&lQ<(DpBxh$P(Xuw}<;?Ev^t!>RaH{!e})&n9A z(UWrd+m;ede(r-1!Ld@|h!@N#)m1&yXY$P>(TGL^+|6|I+^g`qxOXGrUf0*fJ&!C+ zUYcBe>8hp4WlI}YUs@M$!Sxef-#mD}Wt*F6^QuV4x-Baap zd+!eF_;pvx;h#C-xQTF*U!BS1m;~^r2va`S;u^*K9AG@|QB$AZ|65XVdoayfv)K;3 z*r1_&FXmiYUczzdI}4%X>}IAa`!DK0!2_BM0{+iB#i@-+wjm_1bNDQ`3Eys%oS5{J z!xIMo$t3;pI z{snF${{woL@YZ*{UsF%oZ0AzkKQ!RGM80gtEXcDb)txWj=VmAM>@Vq85@&zDIO0!2 zMs+iGe*>bCJvR)WvF8VHqk;QRNg?l+_P4rx9u;oi^9xgad+?X}cd{L7+_phK-&>5x zZRG#WxV;CzDb{euH(B4=UY_uAEo@&+D-osqZzV0mw-5ebMJq4@%;`lBmqO-J@K=Mg z92|Z&2!1JOuElpn4fuy`0Y0l_jGGYUe19|~_wS?d=7sIx@t*V将Mod元数据写入存档(元数据包含Mod版本等信息)。推荐启用。 + 新增:{0} + 移除:{0} + 加载顺序变更:{0} + 版本变更:{0} + 显示未变更的Mod + 显示加载顺序变更 + 存档中没有此Mod,是之后新增的。 + 存档中有此Mod,但当前未载入。 + 存档时的位置。此Mod仍在载入,但加载顺序已改变。 + 当前位置。存档时此Mod位于其他位置。 + 与存档中记录的版本不同。 \ No newline at end of file diff --git a/Languages/English/Keyed/Translation.xml b/Languages/English/Keyed/Translation.xml index eb88014..2c0e56a 100644 --- a/Languages/English/Keyed/Translation.xml +++ b/Languages/English/Keyed/Translation.xml @@ -7,4 +7,15 @@ Enable Version checking between saved file and current version. check steam workshop for the detailed description. Enable writing mod metadata to the save Enable writing mod metadata to the save (mod versions, etc). enable recommended + Added: {0} + Removed: {0} + Load order changed: {0} + Version changed: {0} + Show unchanged mods + Show load order changes + Not in the save; added since. + In the save, but not running now. + Position in the save. This mod still runs, but at a different place in the load order now. + Current position. This mod was at a different place in the save's load order. + The version differs from the one recorded in the save. \ No newline at end of file diff --git a/Languages/Japanese/Keyed/Translation.xml b/Languages/Japanese/Keyed/Translation.xml index b10c49d..081e715 100644 --- a/Languages/Japanese/Keyed/Translation.xml +++ b/Languages/Japanese/Keyed/Translation.xml @@ -8,4 +8,15 @@ セーブデータにModのメタデータを書き込むことを有効化する: セーブデータへのModのメタデータ(各Modのバージョン情報など)の書き込みを有効化します。比較時にエラーを起こさないために有効化することを推奨 + 追加: {0} + 削除: {0} + ロード順の変更: {0} + バージョン変更: {0} + 変更のないModを表示 + ロード順の変更を表示 + セーブにはなく、その後追加されたModです。 + セーブにはありますが、現在は読み込まれていません。 + セーブ時の位置です。このModは現在も読み込まれていますが、ロード順が変わりました。 + 現在の位置です。セーブ時は別の位置にありました。 + セーブに記録されたバージョンと異なります。 \ No newline at end of file diff --git a/Languages/Korean/Keyed/Translation.xml b/Languages/Korean/Keyed/Translation.xml index 04d5db6..b749e6b 100644 --- a/Languages/Korean/Keyed/Translation.xml +++ b/Languages/Korean/Keyed/Translation.xml @@ -4,7 +4,18 @@ 현재 실행중인 모드배열 모드 불일치 창 설정 버전 비교 활성화 - 버전 비교 기능을 활성화 합니다. Manifest.xml에서 데이터를 읽어옵니다. + 버전 비교 기능을 활성화합니다. 버전은 About/Manifest.xml 또는 About.xml의 modVersion에서 읽어옵니다. 세이브 파일에 모드 메타데이터 기록하기 세이브 파일에 모드에 대한 메타데이터를 기록합니다. 사용하는 것을 권장합니다. + 추가됨: {0} + 제거됨: {0} + 로드 순서 변경: {0} + 버전 변경: {0} + 변경 없는 모드 표시 + 로드 순서 변경 표시 + 세이브에는 없고, 이후에 추가된 모드입니다. + 세이브에는 있지만, 지금은 실행 중이 아닌 모드입니다. + 세이브에 기록된 위치입니다. 이 모드는 지금도 실행 중이지만 로드 순서가 바뀌었습니다. + 현재 위치입니다. 세이브에서는 로드 순서상 다른 위치에 있었습니다. + 세이브에 기록된 버전과 다릅니다. \ No newline at end of file diff --git a/Source/ModMismatchFormatter.Tests/ModListAlignerTests.cs b/Source/ModMismatchFormatter.Tests/ModListAlignerTests.cs index 7784643..cf522d2 100644 --- a/Source/ModMismatchFormatter.Tests/ModListAlignerTests.cs +++ b/Source/ModMismatchFormatter.Tests/ModListAlignerTests.cs @@ -8,18 +8,33 @@ namespace ModMismatchFormatter.Tests; public class ModListAlignerTests { - private static readonly Func ByName = mod => mod.Name; - - private static List Mods(IEnumerable names) => - names.Select(name => new ModEntry(name, name)).ToList(); + private static List Mods(IEnumerable ids) => + ids.Select(id => new ModEntry(id, id)).ToList(); private static List<(string?, string?)> Shape(IEnumerable rows) => - rows.Select(row => (row.Saved?.Name, row.Active?.Name)).ToList(); + rows.Select(row => (row.Saved?.PackageId, row.Active?.PackageId)).ToList(); + + private static List<(string?, string?)> Shape(IEnumerable<(ModEntry? Saved, ModEntry? Active)> pairs) => + pairs.Select(pair => (pair.Saved?.PackageId, pair.Active?.PackageId)).ToList(); + + private static int Matched(IEnumerable rows) => rows.Count(row => row.Saved is not null && row.Active is not null); + + /// Independent textbook LCS length, used as the optimum to compare against. + private static int LcsLength(IReadOnlyList a, IReadOnlyList b) + { + var table = new int[a.Count + 1, b.Count + 1]; + for (int i = 1; i <= a.Count; i++) + for (int j = 1; j <= b.Count; j++) + table[i, j] = ModEntry.SameMod(a[i - 1], b[j - 1]) + ? table[i - 1, j - 1] + 1 + : Math.Max(table[i - 1, j], table[i, j - 1]); + return table[a.Count, b.Count]; + } private static List<(string?, string?)> LegacyShape(IReadOnlyList saved, IReadOnlyList active) { static List ToLegacy(IReadOnlyList mods) => - mods.Select((mod, i) => new Legacy.Mod(mod.PackageId, mod.Name, i)).ToList(); + mods.Select((mod, i) => new Legacy.Mod(mod.PackageId, mod.PackageId, i)).ToList(); return new Legacy.OrderFormatterImpl() .GetFormattedModPairs(ToLegacy(saved), ToLegacy(active)) @@ -33,7 +48,7 @@ public void Identical_lists_align_row_by_row() { var mods = Mods(["a", "b", "c"]); - var rows = ModListAligner.Align(mods, mods, ByName); + var rows = ModListAligner.Align(mods, mods); Assert.Equal([("a", "a"), ("b", "b"), ("c", "c")], Shape(rows)); Assert.All(rows, row => Assert.Equal(RowState.Unchanged, row.State)); @@ -42,7 +57,7 @@ public void Identical_lists_align_row_by_row() [Fact] public void Added_and_removed_mods_get_a_gap_on_the_other_side() { - var rows = ModListAligner.Align(Mods(["a", "x", "b"]), Mods(["a", "b", "y"]), ByName); + var rows = ModListAligner.Align(Mods(["a", "x", "b"]), Mods(["a", "b", "y"])); Assert.Equal([("a", "a"), ("x", null), ("b", "b"), (null, "y")], Shape(rows)); Assert.Equal( @@ -51,33 +66,69 @@ public void Added_and_removed_mods_get_a_gap_on_the_other_side() } [Fact] - public void Empty_lists_produce_no_rows() + public void Removed_is_shown_before_added_at_the_same_position() + { + var rows = ModListAligner.Align(Mods(["a", "x", "b"]), Mods(["a", "y", "b"])); + + Assert.Equal([("a", "a"), ("x", null), (null, "y"), ("b", "b")], Shape(rows)); + } + + [Fact] + public void Reordered_mod_is_reported_as_moved_not_added_and_removed() { - Assert.Empty(ModListAligner.Align([], [], ByName)); + var rows = ModListAligner.Align(Mods(["a", "b", "c", "d"]), Mods(["a", "c", "d", "b"])); + + Assert.Equal([("a", "a"), ("b", null), ("c", "c"), ("d", "d"), (null, "b")], Shape(rows)); + Assert.Equal(RowState.MovedFrom, rows[1].State); + Assert.Equal(RowState.MovedTo, rows[4].State); } [Fact] - public void Version_difference_is_reported_on_matched_rows() + public void Matches_package_ids_case_insensitively_and_old_saves_by_folder_name() { - var row = new AlignedRow(new ModEntry("a", "A", "1.0"), new ModEntry("a", "A", "1.1")); + var saved = new List { new("Ludeon.RimWorld", "Core"), new("1234567", "Workshop mod") }; + var active = new List { new("ludeon.rimworld", "Core"), new("author.mod", "Workshop mod", FolderName: "1234567") }; + + var rows = ModListAligner.Align(saved, active); + + Assert.Equal(2, rows.Count); + Assert.All(rows, row => Assert.Equal(RowState.Unchanged, row.State)); + } + + [Theory] + [InlineData("1.0", "1.1", RowState.VersionChanged)] + [InlineData("1.0", "1.0", RowState.Unchanged)] + [InlineData(null, "1.1", RowState.Unchanged)] // Save has no version record. + [InlineData("1.0", null, RowState.Unchanged)] + public void Version_change_needs_both_versions_known(string? savedVersion, string? activeVersion, RowState expected) + { + var rows = ModListAligner.Align([new ModEntry("a", "A", savedVersion)], [new ModEntry("a", "A", activeVersion)]); + + Assert.Equal(expected, Assert.Single(rows).State); + } - Assert.Equal(RowState.VersionChanged, row.State); + [Fact] + public void Empty_lists_produce_no_rows() + { + Assert.Empty(ModListAligner.Align([], [])); } [Fact] - public void Matches_the_legacy_algorithm_on_the_sample_mod_lists() + public void Sample_mod_lists_align_optimally() { var saved = Mods(SampleModLists.Saved); var active = Mods(SampleModLists.Active); - Assert.Equal(LegacyShape(saved, active), Shape(ModListAligner.Align(saved, active, ByName))); + var rows = ModListAligner.Align(saved, active); + + Assert.Equal(LcsLength(saved, active), Matched(rows)); } [Theory] [InlineData(1)] [InlineData(2)] [InlineData(3)] - public void Matches_the_legacy_algorithm_on_random_mod_lists(int seed) + public void Matches_as_many_mods_as_possible_and_never_fewer_than_the_old_algorithm(int seed) { var random = new Random(seed); @@ -85,16 +136,18 @@ public void Matches_the_legacy_algorithm_on_random_mod_lists(int seed) { var (saved, active) = RandomModLists(random); - // The legacy code throws on an empty list (List.Pop on nothing). - if (saved.Count == 0 || active.Count == 0) - continue; + var rows = ModListAligner.Align(saved, active); - Assert.Equal(LegacyShape(saved, active), Shape(ModListAligner.Align(saved, active, ByName))); + Assert.Equal(LcsLength(saved, active), Matched(rows)); + if (saved.Count > 0 && active.Count > 0) // The old code throws on an empty list. + Assert.True(Matched(rows) >= LegacyShape(saved, active).Count(p => p.Item1 is not null && p.Item2 is not null)); } } - [Fact] - public void Every_mod_appears_once_and_in_its_original_order() + [Theory] + [InlineData(ModListAligner.DefaultMaxTableCells)] + [InlineData(0)] // Forces the greedy fallback for the middle part. + public void Every_mod_appears_once_and_in_its_original_order(int maxTableCells) { var random = new Random(42); @@ -102,14 +155,34 @@ public void Every_mod_appears_once_and_in_its_original_order() { var (saved, active) = RandomModLists(random); - var rows = ModListAligner.Align(saved, active, ByName); + var rows = ModListAligner.Align(saved, active, maxTableCells); Assert.Equal(saved, rows.Where(row => row.Saved is not null).Select(row => row.Saved!)); Assert.Equal(active, rows.Where(row => row.Active is not null).Select(row => row.Active!)); + Assert.All(rows.Where(row => row.Saved is not null && row.Active is not null), + row => Assert.True(ModEntry.SameMod(row.Saved!, row.Active!))); + } + } + + [Fact] + public void Greedy_fallback_is_the_original_algorithm() + { + var random = new Random(7); + + for (int run = 0; run < 2000; run++) + { + var (saved, active) = RandomModLists(random); + if (saved.Count == 0 || active.Count == 0) + continue; + + var pairs = new List<(ModEntry?, ModEntry?)>(); + ModListAligner.AlignGreedy(saved, active, ModEntry.SameMod, pairs); + + Assert.Equal(LegacyShape(saved, active), Shape(pairs)); } } - /// Builds a mod list and an edited copy (removals, additions, moves, rare duplicates). + /// Builds a mod list and an edited copy (removals, additions, moves). private static (List Saved, List Active) RandomModLists(Random random) { int count = random.Next(0, 40); @@ -119,7 +192,7 @@ private static (List Saved, List Active) RandomModLists(Rand int edits = random.Next(0, 10); for (int i = 0; i < edits; i++) { - switch (random.Next(4)) + switch (random.Next(3)) { case 0 when active.Count > 0: active.RemoveAt(random.Next(active.Count)); @@ -133,9 +206,6 @@ private static (List Saved, List Active) RandomModLists(Rand active.RemoveAt(from); active.Insert(random.Next(active.Count + 1), moved); break; - case 3 when active.Count > 0 && random.Next(5) == 0: - active.Insert(random.Next(active.Count + 1), active[random.Next(active.Count)]); - break; } } diff --git a/Source/ModMismatchFormatter/Core/AlignedRow.cs b/Source/ModMismatchFormatter/Core/AlignedRow.cs index e0f69da..58004a4 100644 --- a/Source/ModMismatchFormatter/Core/AlignedRow.cs +++ b/Source/ModMismatchFormatter/Core/AlignedRow.cs @@ -1,5 +1,3 @@ -using System; - namespace Madeline.ModMismatchFormatter.Core; public enum RowState @@ -7,7 +5,7 @@ public enum RowState /// The mod is in both lists at this position. Unchanged, - /// The mod is in both lists but its version differs. + /// The mod is in both lists at this position but its version differs. VersionChanged, /// The mod is only in the save (it was removed). @@ -15,17 +13,16 @@ public enum RowState /// The mod is only in the running list (it was added). Added, + + /// The mod's position in the save; it is still running but loads elsewhere now. + MovedFrom, + + /// The mod's new position in the running list; it was elsewhere in the save. + MovedTo, } /// One row of the side-by-side comparison. At least one side is set. -public readonly record struct AlignedRow(ModEntry? Saved, ModEntry? Active) +public readonly record struct AlignedRow(ModEntry? Saved, ModEntry? Active, RowState State) { - public RowState State => (Saved, Active) switch - { - (not null, null) => RowState.Removed, - (null, not null) => RowState.Added, - (not null, not null) when Saved.Version != Active.Version => RowState.VersionChanged, - (not null, not null) => RowState.Unchanged, - _ => throw new InvalidOperationException("A row must have at least one mod."), - }; + public bool IsDifference => State != RowState.Unchanged; } diff --git a/Source/ModMismatchFormatter/Core/ModEntry.cs b/Source/ModMismatchFormatter/Core/ModEntry.cs index 82a356f..ca7e604 100644 --- a/Source/ModMismatchFormatter/Core/ModEntry.cs +++ b/Source/ModMismatchFormatter/Core/ModEntry.cs @@ -1,7 +1,20 @@ +using System; + namespace Madeline.ModMismatchFormatter.Core; /// A mod as recorded in a save file or as currently running. -/// The mod's package id. +/// The mod's package id. Saves made before RimWorld 1.1 store the folder name here. /// Display name. /// Mod version, or null when it is not known. -public sealed record ModEntry(string PackageId, string Name, string? Version = null); +/// Folder name of a running mod, used to match pre-1.1 saves. +public sealed record ModEntry(string PackageId, string Name, string? Version = null, string? FolderName = null) +{ + /// Whether a mod from the save and a running mod are the same mod (ignoring version). + public static bool SameMod(ModEntry saved, ModEntry active) => + string.Equals(saved.PackageId, active.PackageId, StringComparison.OrdinalIgnoreCase) + || (active.FolderName is not null && string.Equals(saved.PackageId, active.FolderName, StringComparison.OrdinalIgnoreCase)); + + /// Versions differ only when both are known; a save without version data proves nothing. + public static bool VersionChanged(string? saved, string? active) => + saved is not null && active is not null && saved != active; +} diff --git a/Source/ModMismatchFormatter/Core/ModListAligner.cs b/Source/ModMismatchFormatter/Core/ModListAligner.cs index a7950b9..ac0161c 100644 --- a/Source/ModMismatchFormatter/Core/ModListAligner.cs +++ b/Source/ModMismatchFormatter/Core/ModListAligner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Madeline.ModMismatchFormatter.Core; @@ -9,17 +10,90 @@ namespace Madeline.ModMismatchFormatter.Core; /// public static class ModListAligner { + // An LCS table of this many cells is 16 MB. Bigger differences fall back to the greedy pass. + internal const int DefaultMaxTableCells = 4_000_000; + + /// + /// Aligns the lists so that as many mods as possible share a row (longest common subsequence), + /// matching mods with . Mods present in both lists but in a + /// different order are reported as /. + /// + public static IReadOnlyList Align(IReadOnlyList saved, IReadOnlyList active) => + Align(saved, active, DefaultMaxTableCells); + + internal static IReadOnlyList Align(IReadOnlyList saved, IReadOnlyList active, int maxTableCells) + { + var pairs = new List<(ModEntry? Saved, ModEntry? Active)>(Math.Max(saved.Count, active.Count)); + + // Mod lists usually share long runs at both ends; only the middle needs the table. + int prefix = 0; + while (prefix < saved.Count && prefix < active.Count && ModEntry.SameMod(saved[prefix], active[prefix])) + prefix++; + + int suffix = 0; + while (suffix < saved.Count - prefix && suffix < active.Count - prefix + && ModEntry.SameMod(saved[saved.Count - 1 - suffix], active[active.Count - 1 - suffix])) + suffix++; + + for (int i = 0; i < prefix; i++) + pairs.Add((saved[i], active[i])); + + var savedMiddle = Slice(saved, prefix, saved.Count - prefix - suffix); + var activeMiddle = Slice(active, prefix, active.Count - prefix - suffix); + + if ((long)(savedMiddle.Count + 1) * (activeMiddle.Count + 1) <= maxTableCells) + AlignLcs(savedMiddle, activeMiddle, pairs); + else + AlignGreedy(savedMiddle, activeMiddle, ModEntry.SameMod, pairs); + + for (int i = suffix; i > 0; i--) + pairs.Add((saved[saved.Count - i], active[active.Count - i])); + + return Classify(pairs); + } + + private static void AlignLcs(IReadOnlyList saved, IReadOnlyList active, List<(ModEntry?, ModEntry?)> pairs) + { + int n = saved.Count, m = active.Count, width = m + 1; + + // lcs[i * width + j] = length of the LCS of saved[i..] and active[j..]. + var lcs = new int[(n + 1) * width]; + for (int i = n - 1; i >= 0; i--) + { + for (int j = m - 1; j >= 0; j--) + { + lcs[i * width + j] = ModEntry.SameMod(saved[i], active[j]) + ? lcs[(i + 1) * width + j + 1] + 1 + : Math.Max(lcs[(i + 1) * width + j], lcs[i * width + j + 1]); + } + } + + int s = 0, a = 0; + while (s < n && a < m) + { + if (ModEntry.SameMod(saved[s], active[a])) + pairs.Add((saved[s++], active[a++])); + else if (lcs[(s + 1) * width + a] >= lcs[s * width + a + 1]) + pairs.Add((saved[s++], null)); // On ties, show what was removed before what was added. + else + pairs.Add((null, active[a++])); + } + + while (s < n) + pairs.Add((saved[s++], null)); + while (a < m) + pairs.Add((null, active[a++])); + } + /// - /// Greedy alignment. When the current mods differ and each reappears later in - /// the other list, the side whose match is closer to the end of the list is - /// emitted first. + /// The original (pre-1.2) greedy alignment: when the current mods differ and each reappears + /// later in the other list, the side whose match is closer to the end of the list is emitted first. + /// Linear in memory, so it is kept for lists too different for the LCS table. /// - public static IReadOnlyList Align( - IReadOnlyList saved, - IReadOnlyList active, - Func key) + internal static void AlignGreedy( + IReadOnlyList saved, IReadOnlyList active, + Func same, List<(ModEntry?, ModEntry?)> pairs) { - var rows = new List(Math.Max(saved.Count, active.Count)); int s = 0, a = 0; while (s < saved.Count && a < active.Count) @@ -27,16 +101,16 @@ public static IReadOnlyList Align( var savedMod = saved[s]; var activeMod = active[a]; - if (key(savedMod) == key(activeMod)) + if (same(savedMod, activeMod)) { - rows.Add(new AlignedRow(savedMod, activeMod)); + pairs.Add((savedMod, activeMod)); s++; a++; continue; } - int savedInActive = LastIndexOf(active, a + 1, key(savedMod), key); - int activeInSaved = LastIndexOf(saved, s + 1, key(activeMod), key); + int savedInActive = LastIndex(active, a + 1, mod => same(savedMod, mod)); + int activeInSaved = LastIndex(saved, s + 1, mod => same(mod, activeMod)); bool takeActive = (savedInActive, activeInSaved) switch { @@ -46,25 +120,48 @@ public static IReadOnlyList Align( }; if (takeActive) - rows.Add(new AlignedRow(null, active[a++])); + pairs.Add((null, active[a++])); else - rows.Add(new AlignedRow(saved[s++], null)); + pairs.Add((saved[s++], null)); } while (s < saved.Count) - rows.Add(new AlignedRow(saved[s++], null)); - + pairs.Add((saved[s++], null)); while (a < active.Count) - rows.Add(new AlignedRow(null, active[a++])); + pairs.Add((null, active[a++])); + } + + private static IReadOnlyList Classify(List<(ModEntry? Saved, ModEntry? Active)> pairs) + { + // A mod left unmatched on both sides is still running; only its load order changed. + var unmatchedActive = pairs.Where(p => p.Saved is null).Select(p => p.Active!).ToList(); + var unmatchedSaved = pairs.Where(p => p.Active is null).Select(p => p.Saved!).ToList(); - return rows; + return pairs.Select(pair => pair switch + { + (null, { } active) => new AlignedRow(null, active, + unmatchedSaved.Any(saved => ModEntry.SameMod(saved, active)) ? RowState.MovedTo : RowState.Added), + ({ } saved, null) => new AlignedRow(saved, null, + unmatchedActive.Any(active => ModEntry.SameMod(saved, active)) ? RowState.MovedFrom : RowState.Removed), + ({ } saved, { } active) => new AlignedRow(saved, active, + ModEntry.VersionChanged(saved.Version, active.Version) ? RowState.VersionChanged : RowState.Unchanged), + _ => throw new InvalidOperationException("A row must have at least one mod."), + }).ToList(); + } + + private static IReadOnlyList Slice(IReadOnlyList mods, int start, int count) + { + var slice = new ModEntry[count]; + for (int i = 0; i < count; i++) + slice[i] = mods[start + i]; + return slice; } - private static int LastIndexOf(IReadOnlyList mods, int start, string value, Func key) + private static int LastIndex(IReadOnlyList mods, int start, Func match) { for (int i = mods.Count - 1; i >= start; i--) { - if (key(mods[i]) == value) + if (match(mods[i])) return i; } diff --git a/Source/ModMismatchFormatter/Patches/MismatchDialogPatch.cs b/Source/ModMismatchFormatter/Patches/MismatchDialogPatch.cs index afa2103..0aa2b30 100644 --- a/Source/ModMismatchFormatter/Patches/MismatchDialogPatch.cs +++ b/Source/ModMismatchFormatter/Patches/MismatchDialogPatch.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using HarmonyLib; +using Madeline.ModMismatchFormatter.Core; using Madeline.ModMismatchFormatter.UI; using RimWorld; using Verse; @@ -30,5 +31,5 @@ private static bool Prefix(Action confirmedAction, ref bool __result) } private static bool AnyVersionChanged() => - LoadedModManager.RunningMods.Any(mod => ModVersions.Saved(mod.PackageId) != ModVersions.Running(mod)); + LoadedModManager.RunningMods.Any(mod => ModEntry.VersionChanged(ModVersions.Saved(mod.PackageId), ModVersions.Running(mod))); } diff --git a/Source/ModMismatchFormatter/Settings.cs b/Source/ModMismatchFormatter/Settings.cs index 190af10..7648b63 100644 --- a/Source/ModMismatchFormatter/Settings.cs +++ b/Source/ModMismatchFormatter/Settings.cs @@ -3,19 +3,26 @@ namespace Madeline.ModMismatchFormatter; // The type name is written into the settings file as Class="..."; do not rename. -// Both options start off on a fresh install (no settings file yet), as they always have. public sealed class Settings : ModSettings { - /// Compare each mod's version with the one recorded in the save. + /// Compare each mod's version with the one recorded in the save. Off on a fresh install. public bool useVersionCompare; - /// Record running mod versions in the save's meta header. - public bool saveVersionToSaveFile; + /// Record running mod versions in the save's meta header. Cheap since 1.2, so on by default. + public bool saveVersionToSaveFile = true; + + /// Mismatch window: list mods that did not change. + public bool showUnchanged = true; + + /// Mismatch window: list mods whose load order changed. + public bool showOrderChanges = true; public override void ExposeData() { base.ExposeData(); Scribe_Values.Look(ref useVersionCompare, "useVersionCompare", true, true); Scribe_Values.Look(ref saveVersionToSaveFile, "saveVersionToSaveFile", true, true); + Scribe_Values.Look(ref showUnchanged, "showUnchanged", true); + Scribe_Values.Look(ref showOrderChanges, "showOrderChanges", true); } } diff --git a/Source/ModMismatchFormatter/UI/ModMismatchWindow.cs b/Source/ModMismatchFormatter/UI/ModMismatchWindow.cs index a95eb27..13c3904 100644 --- a/Source/ModMismatchFormatter/UI/ModMismatchWindow.cs +++ b/Source/ModMismatchFormatter/UI/ModMismatchWindow.cs @@ -12,17 +12,23 @@ namespace Madeline.ModMismatchFormatter.UI; /// Shows the save's mod list and the running mod list side by side, aligned row by row. public sealed class ModMismatchWindow : Window { - private const float TitleHeight = 36f; - private const float HeaderHeight = 26f; + private const float Gap = 10f; private const float ColumnGap = 16f; - private const float SeparatorHeight = 2f; - private const float ButtonHeight = 40f; - private const float ButtonMargin = 10f; + private const float ToolbarHeight = 24f; + private const float HeaderHeight = 24f; + private const float ButtonHeight = 38f; + private const float MarkWidth = 14f; private readonly Action loadGame; private readonly bool showVersions; + private readonly QuickSearchWidget search = new(); private readonly Dictionary truncateCache = []; + private IReadOnlyList rows = []; + private List visibleRows = []; + private Dictionary counts = []; + private int savedCount; + private int activeCount; private Vector2 scrollPosition; public ModMismatchWindow(Action loadGame, bool showVersions) @@ -30,114 +36,222 @@ public ModMismatchWindow(Action loadGame, bool showVersions) this.loadGame = loadGame; this.showVersions = showVersions; doCloseX = true; + absorbInputAroundWindow = true; } - public override Vector2 InitialSize => new(650f, 700f); + public override Vector2 InitialSize => new(860f, 760f); + + private static Settings Settings => ModMismatchFormatter.Settings; public override void PreOpen() { base.PreOpen(); - rows = ModListAligner.Align(SavedMods(), RunningMods(), mod => mod.Name); + + var saved = SavedMods(); + var active = RunningMods(); + savedCount = saved.Count; + activeCount = active.Count; + rows = ModListAligner.Align(saved, active); + counts = rows.GroupBy(row => row.State).ToDictionary(group => group.Key, group => group.Count()); + RefreshVisibleRows(); } public override void DoWindowContents(Rect inRect) { + float y = inRect.y; + + Text.Font = GameFont.Medium; + float titleHeight = Text.LineHeight; + Widgets.Label(new Rect(inRect.x, y, inRect.width, titleHeight), "ModsMismatchWarningTitle".Translate()); Text.Font = GameFont.Small; - Widgets.Label(new Rect(inRect.x, inRect.y, inRect.width, TitleHeight), "ModsMismatchWarningTitle".Translate()); + y += titleHeight + 4f; + + DrawSummary(new Rect(inRect.x, y, inRect.width, Text.LineHeight)); + y += Text.LineHeight + Gap; + + DrawToolbar(new Rect(inRect.x, y, inRect.width, ToolbarHeight)); + y += ToolbarHeight + Gap; - var buttons = new Rect(inRect.x, inRect.yMax - ButtonHeight - ButtonMargin, inRect.width, ButtonHeight); - var list = new Rect(inRect.x, inRect.y + TitleHeight + HeaderHeight, inRect.width, buttons.y - ButtonMargin - (inRect.y + TitleHeight + HeaderHeight)); + var buttons = new Rect(inRect.x, inRect.yMax - ButtonHeight, inRect.width, ButtonHeight); + var list = new Rect(inRect.x, y + HeaderHeight, inRect.width, buttons.y - Gap - (y + HeaderHeight)); float columnWidth = (list.width - GenUI.ScrollBarWidth - ColumnGap) / 2f; - Widgets.Label(new Rect(list.x, list.y - HeaderHeight, columnWidth, HeaderHeight), "SaveModListTitle".Translate()); - Widgets.Label(new Rect(list.x + columnWidth + ColumnGap, list.y - HeaderHeight, columnWidth, HeaderHeight), "ActiveModListTitle".Translate()); + Widgets.Label(new Rect(list.x, y, columnWidth, HeaderHeight), $"{"SaveModListTitle".Translate()} ({savedCount})"); + Widgets.Label(new Rect(list.x + columnWidth + ColumnGap, y, columnWidth, HeaderHeight), $"{"ActiveModListTitle".Translate()} ({activeCount})"); DrawRows(list, columnWidth); - DrawButtons(buttons, columnWidth); + DrawButtons(buttons); + } + + // Colored counts that double as the legend. + private void DrawSummary(Rect rect) + { + // A moved mod has one MovedFrom and one MovedTo row; count it once. + (RowState State, string Key)[] entries = + [ + (RowState.Added, "ModMismatchFormatter_Added"), + (RowState.Removed, "ModMismatchFormatter_Removed"), + (RowState.MovedFrom, "ModMismatchFormatter_Moved"), + (RowState.VersionChanged, "ModMismatchFormatter_VersionChanged"), + ]; + + float x = rect.x; + foreach (var (state, key) in entries) + { + if (state == RowState.VersionChanged && !showVersions) + continue; + + string label = key.Translate(counts.TryGetValue(state, out int count) ? count : 0); + float width = Text.CalcSize(label).x; + + var swatch = new Rect(x, rect.y + (rect.height - 12f) / 2f, 12f, 12f); + Widgets.DrawBoxSolid(swatch, Palette.For(state)); + Widgets.Label(new Rect(swatch.xMax + 4f, rect.y, width, rect.height), label); + x = swatch.xMax + 4f + width + 18f; + } + } + + private void DrawToolbar(Rect rect) + { + search.OnGUI(new Rect(rect.x, rect.y, Mathf.Min(260f, rect.width / 3f), rect.height), RefreshVisibleRows); + + float x = rect.x + Mathf.Min(260f, rect.width / 3f) + 24f; + bool changed = Toggle(ref x, rect, "ModMismatchFormatter_ShowUnchanged", ref Settings.showUnchanged); + changed |= Toggle(ref x, rect, "ModMismatchFormatter_ShowOrderChanges", ref Settings.showOrderChanges); + + if (changed) + { + Settings.Write(); + RefreshVisibleRows(); + } + } + + private static bool Toggle(ref float x, Rect toolbar, string key, ref bool value) + { + string label = key.Translate(); + float width = Text.CalcSize(label).x + 24f + 6f; + bool before = value; + Widgets.CheckboxLabeled(new Rect(x, toolbar.y, width, toolbar.height), label, ref value, placeCheckboxNearText: true); + x += width + 24f; + return before != value; + } + + private void RefreshVisibleRows() + { + visibleRows = rows.Where(IsVisible).ToList(); + search.noResultsMatched = search.filter.Active && visibleRows.Count == 0; + } + + private bool IsVisible(AlignedRow row) + { + if (!Settings.showUnchanged && row.State == RowState.Unchanged) + return false; + if (!Settings.showOrderChanges && row.State is RowState.MovedFrom or RowState.MovedTo) + return false; + if (!search.filter.Active) + return true; + + return new[] { row.Saved, row.Active }.Any(mod => + mod is not null && (search.filter.Matches(mod.Name) || search.filter.Matches(mod.PackageId))); } private float RowHeight => showVersions ? Text.LineHeightOf(GameFont.Small) + Text.LineHeightOf(GameFont.Tiny) - : Text.LineHeightOf(GameFont.Small); + : Text.LineHeightOf(GameFont.Small) + 2f; private void DrawRows(Rect outRect, float columnWidth) { Widgets.DrawBoxSolid(outRect, Palette.Background); - float stride = RowHeight + SeparatorHeight; - var viewRect = new Rect(0f, 0f, outRect.width - GenUI.ScrollBarWidth, rows.Count * stride); + float rowHeight = RowHeight; + var viewRect = new Rect(0f, 0f, outRect.width - GenUI.ScrollBarWidth, visibleRows.Count * rowHeight); Widgets.BeginScrollView(outRect, ref scrollPosition, viewRect); - for (int i = 0; i < rows.Count; i++) + // Only rows inside the viewport are drawn; mod lists can have well over a thousand entries. + int first = Mathf.Max(0, Mathf.FloorToInt(scrollPosition.y / rowHeight)); + int last = Mathf.Min(visibleRows.Count - 1, Mathf.CeilToInt((scrollPosition.y + outRect.height) / rowHeight)); + for (int i = first; i <= last; i++) { - float y = i * stride; - - // Only draw what is visible; mod lists can be very long. - if (y + stride < scrollPosition.y || y > scrollPosition.y + outRect.height) - continue; + var row = visibleRows[i]; + var rowRect = new Rect(0f, i * rowHeight, viewRect.width, rowHeight); + if (i % 2 == 0) + Widgets.DrawLightHighlight(rowRect); - var row = rows[i]; - DrawCell(new Rect(0f, y, columnWidth, RowHeight), row.Saved, row.State == RowState.Removed ? RowState.Removed : RowState.Unchanged); - DrawCell(new Rect(columnWidth + ColumnGap, y, columnWidth, RowHeight), row.Active, row.State == RowState.Removed ? RowState.Unchanged : row.State); - - Widgets.DrawBoxSolid(new Rect(0f, y + RowHeight, columnWidth, SeparatorHeight), Color.grey); - Widgets.DrawBoxSolid(new Rect(columnWidth + ColumnGap, y + RowHeight, columnWidth, SeparatorHeight), Color.grey); + DrawCell(new Rect(0f, rowRect.y, columnWidth, rowHeight), row.Saved, row, isSavedSide: true); + DrawCell(new Rect(columnWidth + ColumnGap, rowRect.y, columnWidth, rowHeight), row.Active, row, isSavedSide: false); } Widgets.EndScrollView(); } - private void DrawCell(Rect rect, ModEntry? mod, RowState state) + private void DrawCell(Rect rect, ModEntry? mod, AlignedRow row, bool isSavedSide) { if (mod is null) { - Widgets.DrawBoxSolid(rect, Palette.Background); + Widgets.DrawBoxSolid(rect.ContractedBy(0f, 1f), Palette.EmptyCell); return; } - Widgets.DrawBoxSolid(rect, Palette.For(state)); + // Each state belongs to one side: removals/old positions on the left, the rest on the right. + bool ownsState = row.State is RowState.Removed or RowState.MovedFrom ? isSavedSide : !isSavedSide; + var state = ownsState ? row.State : RowState.Unchanged; + if (state != RowState.Unchanged) + Widgets.DrawBoxSolid(rect.ContractedBy(0f, 1f), Palette.For(state)); + Widgets.DrawHighlightIfMouseover(rect); - var markRect = new Rect(rect.x + 4f, rect.y, 12f, rect.height); - var textRect = new Rect(markRect.xMax + 2f, rect.y, rect.xMax - markRect.xMax - 4f, rect.height); float nameHeight = Text.LineHeightOf(GameFont.Small); + var nameRect = new Rect(rect.x + 4f + MarkWidth, rect.y, rect.width - MarkWidth - 8f, nameHeight); Text.Anchor = TextAnchor.MiddleLeft; - Text.Font = GameFont.Small; - Widgets.Label(new Rect(markRect.x, rect.y, markRect.width, nameHeight), Mark(state)); - Widgets.Label(new Rect(textRect.x, rect.y, textRect.width, nameHeight), mod.Name.Truncate(textRect.width, truncateCache)); + Widgets.Label(new Rect(rect.x + 4f, rect.y, MarkWidth, nameHeight), Palette.Mark(state)); + Widgets.Label(nameRect, mod.Name.Truncate(nameRect.width, truncateCache)); if (showVersions) { Text.Font = GameFont.Tiny; - Widgets.Label(new Rect(textRect.x, rect.y + nameHeight, textRect.width, rect.height - nameHeight), $"ver : {mod.Version ?? "Unknown"}"); + GUI.color = Color.gray; + Widgets.Label(new Rect(nameRect.x, nameRect.yMax, nameRect.width, rect.height - nameHeight), VersionLabel(mod, row, isSavedSide)); + GUI.color = Color.white; + Text.Font = GameFont.Small; } - - Text.Font = GameFont.Small; Text.Anchor = TextAnchor.UpperLeft; - TooltipHandler.TipRegion(rect, $"{mod.Name}\n{mod.PackageId}"); + TooltipHandler.TipRegion(rect, () => Tooltip(mod, state), mod.PackageId.GetHashCode() ^ (isSavedSide ? 1 : 2)); + } + + private static string VersionLabel(ModEntry mod, AlignedRow row, bool isSavedSide) + { + string version = mod.Version ?? "Unknown"; + return row.State == RowState.VersionChanged && !isSavedSide + ? $"ver : {row.Saved!.Version} -> {version}" + : $"ver : {version}"; } - private static string Mark(RowState state) => state switch + private static string Tooltip(ModEntry mod, RowState state) { - RowState.Added => "+", - RowState.Removed => "-", - RowState.VersionChanged => "?", - _ => "", - }; + string text = $"{mod.Name}\n{mod.PackageId}"; + if (mod.Version is not null) + text += $"\nver : {mod.Version}"; + if (state != RowState.Unchanged) + text += $"\n\n{$"ModMismatchFormatter_State_{state}".Translate()}"; + return text; + } - private void DrawButtons(Rect area, float columnWidth) + private void DrawButtons(Rect area) { - float buttonWidth = columnWidth - 50f; - float leftX = area.x + (columnWidth - buttonWidth) / 2f; - float rightX = area.x + columnWidth + ColumnGap + (columnWidth - buttonWidth) / 2f; + const int count = 3; + float width = (area.width - Gap * (count - 1)) / count; + Rect Button(int i) => new(area.x + i * (width + Gap), area.y, width, area.height); - if (Widgets.ButtonText(new Rect(leftX, area.y, buttonWidth, area.height), "ChangeLoadedMods".Translate())) + if (Widgets.ButtonText(Button(0), "ChangeLoadedMods".Translate())) { Find.WindowStack.Add(Dialog_MessageBox.CreateConfirmation( "ConfirmLoadSaveList".Translate(), ChangeLoadedModsToSave, destructive: true)); } - if (Widgets.ButtonText(new Rect(rightX, area.y, buttonWidth, area.height), "LoadAnyway".Translate())) + if (Widgets.ButtonText(Button(1), "SaveModList".Translate())) + SaveRunningModList(); + + if (Widgets.ButtonText(Button(2), "LoadAnyway".Translate())) LoadAnyway(); } @@ -171,6 +285,19 @@ private static void ChangeLoadedModsToSave() ModsConfig.RestartFromChangedMods(); } + // Mirrors vanilla Dialog_ModMismatch: save the running mod list as a preset. + private static void SaveRunningModList() + { + SoundDefOf.Click.PlayOneShotOnCamera(); + var running = LoadedModManager.RunningMods.ToList(); + var modList = new ModList + { + ids = running.Select(mod => mod.PackageId.ToLower()).ToList(), + names = running.Select(mod => mod.Name).ToList(), + }; + Find.WindowStack.Add(new Dialog_ModList_Save(modList)); + } + private List SavedMods() { var ids = ScribeMetaHeaderUtility.loadedModIdsList ?? []; @@ -183,6 +310,6 @@ private List SavedMods() private List RunningMods() => LoadedModManager.RunningMods - .Select(mod => new ModEntry(mod.PackageId, mod.Name, showVersions ? ModVersions.Running(mod) : null)) + .Select(mod => new ModEntry(mod.PackageId, mod.Name, showVersions ? ModVersions.Running(mod) : null, mod.FolderName)) .ToList(); } diff --git a/Source/ModMismatchFormatter/UI/Palette.cs b/Source/ModMismatchFormatter/UI/Palette.cs index 66d4ce8..3355c15 100644 --- a/Source/ModMismatchFormatter/UI/Palette.cs +++ b/Source/ModMismatchFormatter/UI/Palette.cs @@ -6,15 +6,27 @@ namespace Madeline.ModMismatchFormatter.UI; internal static class Palette { public static readonly Color Background = new(0.11f, 0.11f, 0.11f, 1f); + public static readonly Color EmptyCell = new(0.07f, 0.07f, 0.07f, 1f); public static readonly Color Removed = new(0.29f, 0.09f, 0.09f, 1f); public static readonly Color Added = new(0.29f, 0.33f, 0.19f, 1f); public static readonly Color VersionChanged = new Color32(178, 144, 83, 255); + public static readonly Color Moved = new(0.16f, 0.24f, 0.36f, 1f); public static Color For(RowState state) => state switch { RowState.Added => Added, RowState.Removed => Removed, RowState.VersionChanged => VersionChanged, + RowState.MovedFrom or RowState.MovedTo => Moved, _ => Background, }; + + public static string Mark(RowState state) => state switch + { + RowState.Added => "+", + RowState.Removed => "-", + RowState.VersionChanged => "?", + RowState.MovedFrom or RowState.MovedTo => "~", + _ => "", + }; } From 91e0a3fcea927d71a21fb938f6c89c6064a145b2 Mon Sep 17 00:00:00 2001 From: jungooji <46273764+zzzz465@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:19:52 +0900 Subject: [PATCH 05/11] ci: build the RimWorld 1.7 target with steamcmd Add a Windows job that downloads the RimWorld unstable branch with steamcmd (STEAM_USERNAME + STEAM_CONFIG_VDF secrets), compiles the 1.7 target against its Assembly-CSharp.dll and UnityStubs.dll, and uploads the resulting DLL. Without the secrets the job is skipped with a warning. Game files are deleted after the build and never cached, since fork pull requests can restore caches of a public repository; fork pull requests do not run the job at all. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 73 ++++++++++++++++++++++++++++++++++++- README.md | 16 +++++++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0dd83da..6cd9d4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,8 +35,7 @@ jobs: sys.exit(1 if failed else 0) PY - # RimWorld 1.7 has no reference package on NuGet yet, so CI builds the - # 1.6 (net472) target only. The 1.7 target is built locally with RimWorld17Dir. + # Builds the 1.6 (net472) target; the 1.7 target needs game files (see build-1-7). - name: Build run: dotnet build Source/ModMismatchFormatter.slnx -c Release @@ -47,3 +46,73 @@ jobs: with: name: assemblies-1.6 path: 1.6/Assemblies/ModMisMatchWindowPatch.dll + + # RimWorld 1.7 (Waylith) has no reference package on NuGet yet, so this job downloads the + # unstable branch with steamcmd using the repository owner's Steam login and compiles against it. + # Secrets: STEAM_USERNAME, STEAM_CONFIG_VDF (base64 of a logged-in steamcmd config/config.vdf). + # Game files are never cached or uploaded: on a public repository, pull requests from forks can + # restore caches. Fork pull requests do not get secrets, so the job is skipped for them. + build-1-7: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: windows-latest + env: + STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }} + STEAM_CONFIG_VDF: ${{ secrets.STEAM_CONFIG_VDF }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Check Steam secrets + id: secrets + shell: pwsh + run: | + if ($env:STEAM_USERNAME -and $env:STEAM_CONFIG_VDF) { + "available=true" >> $env:GITHUB_OUTPUT + } else { + "available=false" >> $env:GITHUB_OUTPUT + "::warning::RimWorld 1.7 build skipped: STEAM_USERNAME / STEAM_CONFIG_VDF secrets are not set." + } + + - name: Download RimWorld 1.7 (unstable branch) + if: steps.secrets.outputs.available == 'true' + shell: pwsh + run: | + $steamcmdDir = Join-Path $env:RUNNER_TEMP 'steamcmd' + $gameDir = Join-Path $env:RUNNER_TEMP 'rimworld' + $refsDir = Join-Path $env:RUNNER_TEMP 'rimworld-1.7-refs' + New-Item -ItemType Directory -Force $steamcmdDir, "$steamcmdDir\config", $refsDir | Out-Null + + Invoke-WebRequest https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip -OutFile "$steamcmdDir\steamcmd.zip" + Expand-Archive "$steamcmdDir\steamcmd.zip" $steamcmdDir -Force + # The first run only updates steamcmd itself. + & "$steamcmdDir\steamcmd.exe" +quit | Out-Null + + [IO.File]::WriteAllBytes("$steamcmdDir\config\config.vdf", [Convert]::FromBase64String($env:STEAM_CONFIG_VDF)) + try { + & "$steamcmdDir\steamcmd.exe" +force_install_dir $gameDir +login $env:STEAM_USERNAME +app_update 294100 -beta unstable +quit + } finally { + Remove-Item -Force "$steamcmdDir\config\config.vdf" -ErrorAction SilentlyContinue + } + + foreach ($file in 'Assembly-CSharp.dll', 'UnityStubs.dll', 'Version.txt') { + if (-not (Test-Path "$gameDir\$file")) { throw "steamcmd did not produce $file (login or download failed)." } + Copy-Item "$gameDir\$file" $refsDir + } + "RimWorld $(Get-Content "$refsDir\Version.txt")" + Remove-Item -Recurse -Force $gameDir + "RIMWORLD17_DIR=$refsDir" >> $env:GITHUB_ENV + # steamcmd exits non-zero even on success; the files checked above are the real result. + $global:LASTEXITCODE = 0 + + - name: Build 1.7 + if: steps.secrets.outputs.available == 'true' + run: dotnet build Source/ModMismatchFormatter/ModMismatchFormatter.csproj -c Release -p:RimWorld17Dir=${{ env.RIMWORLD17_DIR }} + + - uses: actions/upload-artifact@v4 + if: steps.secrets.outputs.available == 'true' + with: + name: assemblies-1.7 + path: 1.7/Assemblies/ModMisMatchWindowPatch.dll diff --git a/README.md b/README.md index d8247d0..d5087e8 100644 --- a/README.md +++ b/README.md @@ -35,4 +35,18 @@ branch; otherwise set it in `Source/Local.props` (git-ignored): ``` -CI builds and tests the 1.6 target only. The assemblies for 1.0–1.5 are prebuilt and no longer rebuilt from source. +## CI + +`.github/workflows/build.yml` has two jobs: + +- `build` (Ubuntu): validates the mod XML, builds the 1.6 target and runs the tests. +- `build-1-7` (Windows): downloads the RimWorld unstable branch with steamcmd, builds the 1.7 + target against it and uploads the DLL. It needs two repository secrets and is skipped with a + warning when they are missing: + - `STEAM_USERNAME`: a Steam account that owns RimWorld. + - `STEAM_CONFIG_VDF`: base64 of `config/config.vdf` from a steamcmd that has logged in to that + account (so no Steam Guard code is needed). It expires eventually and must then be refreshed. + + Game files are not cached or uploaded, and the job does not run for pull requests from forks. + +The assemblies for 1.0–1.5 are prebuilt and no longer rebuilt from source. From c8c2d348afe09c2999ed8e496c79f4a7d67ce08a Mon Sep 17 00:00:00 2001 From: jungooji <46273764+zzzz465@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:42:42 +0900 Subject: [PATCH 06/11] ci: time out the 1.7 job if steamcmd waits for login input Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6cd9d4f..a5cfd27 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,6 +55,9 @@ jobs: build-1-7: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: windows-latest + # If the cached Steam login is rejected, steamcmd waits for a password or a mobile + # confirmation forever; fail instead of holding a runner for hours. + timeout-minutes: 30 env: STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }} STEAM_CONFIG_VDF: ${{ secrets.STEAM_CONFIG_VDF }} From 3273e5007987ce2b423c299189b87c8854d951f7 Mon Sep 17 00:00:00 2001 From: jungooji <46273764+zzzz465@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:58:47 +0900 Subject: [PATCH 07/11] ci: replace build CI with a tag-triggered release workflow Downloading RimWorld 1.7 with steamcmd on every run was too slow, so assemblies are built locally only. Remove build.yml (and its steamcmd job) and add release.yml: on a v* tag it checks the tag against About/Manifest.xml and the csproj version, checks the committed 1.6/1.7 assemblies, validates XML, runs the tests, zips the mod files and creates a GitHub release (suffixed tags become pre-releases). Also stop embedding the git commit hash in AssemblyInformationalVersion: the DLLs are committed, so the hash made every rebuild after a commit differ from the committed file. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 121 ---------------------- .github/workflows/release.yml | 90 ++++++++++++++++ 1.6/Assemblies/ModMisMatchWindowPatch.dll | Bin 32768 -> 32768 bytes 1.7/Assemblies/ModMisMatchWindowPatch.dll | Bin 32768 -> 32256 bytes README.md | 19 ++-- Source/Directory.Build.props | 2 + 6 files changed, 101 insertions(+), 131 deletions(-) delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index a5cfd27..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,121 +0,0 @@ -name: build - -on: - push: - branches: [master] - pull_request: - workflow_dispatch: - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 10.0.x - - - name: Validate mod XML - run: | - python3 - <<'PY' - import pathlib, sys, xml.etree.ElementTree as ET - files = [p for p in pathlib.Path('.').rglob('*.xml') if not any(part in ('bin', 'obj', '.git') for part in p.parts)] - failed = False - for path in files: - try: - ET.parse(path) - except ET.ParseError as e: - print(f'::error file={path}::{e}') - failed = True - print(f'checked {len(files)} XML files') - sys.exit(1 if failed else 0) - PY - - # Builds the 1.6 (net472) target; the 1.7 target needs game files (see build-1-7). - - name: Build - run: dotnet build Source/ModMismatchFormatter.slnx -c Release - - - name: Test - run: dotnet test --project Source/ModMismatchFormatter.Tests/ModMismatchFormatter.Tests.csproj -c Release --no-build - - - uses: actions/upload-artifact@v4 - with: - name: assemblies-1.6 - path: 1.6/Assemblies/ModMisMatchWindowPatch.dll - - # RimWorld 1.7 (Waylith) has no reference package on NuGet yet, so this job downloads the - # unstable branch with steamcmd using the repository owner's Steam login and compiles against it. - # Secrets: STEAM_USERNAME, STEAM_CONFIG_VDF (base64 of a logged-in steamcmd config/config.vdf). - # Game files are never cached or uploaded: on a public repository, pull requests from forks can - # restore caches. Fork pull requests do not get secrets, so the job is skipped for them. - build-1-7: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - runs-on: windows-latest - # If the cached Steam login is rejected, steamcmd waits for a password or a mobile - # confirmation forever; fail instead of holding a runner for hours. - timeout-minutes: 30 - env: - STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }} - STEAM_CONFIG_VDF: ${{ secrets.STEAM_CONFIG_VDF }} - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 10.0.x - - - name: Check Steam secrets - id: secrets - shell: pwsh - run: | - if ($env:STEAM_USERNAME -and $env:STEAM_CONFIG_VDF) { - "available=true" >> $env:GITHUB_OUTPUT - } else { - "available=false" >> $env:GITHUB_OUTPUT - "::warning::RimWorld 1.7 build skipped: STEAM_USERNAME / STEAM_CONFIG_VDF secrets are not set." - } - - - name: Download RimWorld 1.7 (unstable branch) - if: steps.secrets.outputs.available == 'true' - shell: pwsh - run: | - $steamcmdDir = Join-Path $env:RUNNER_TEMP 'steamcmd' - $gameDir = Join-Path $env:RUNNER_TEMP 'rimworld' - $refsDir = Join-Path $env:RUNNER_TEMP 'rimworld-1.7-refs' - New-Item -ItemType Directory -Force $steamcmdDir, "$steamcmdDir\config", $refsDir | Out-Null - - Invoke-WebRequest https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip -OutFile "$steamcmdDir\steamcmd.zip" - Expand-Archive "$steamcmdDir\steamcmd.zip" $steamcmdDir -Force - # The first run only updates steamcmd itself. - & "$steamcmdDir\steamcmd.exe" +quit | Out-Null - - [IO.File]::WriteAllBytes("$steamcmdDir\config\config.vdf", [Convert]::FromBase64String($env:STEAM_CONFIG_VDF)) - try { - & "$steamcmdDir\steamcmd.exe" +force_install_dir $gameDir +login $env:STEAM_USERNAME +app_update 294100 -beta unstable +quit - } finally { - Remove-Item -Force "$steamcmdDir\config\config.vdf" -ErrorAction SilentlyContinue - } - - foreach ($file in 'Assembly-CSharp.dll', 'UnityStubs.dll', 'Version.txt') { - if (-not (Test-Path "$gameDir\$file")) { throw "steamcmd did not produce $file (login or download failed)." } - Copy-Item "$gameDir\$file" $refsDir - } - "RimWorld $(Get-Content "$refsDir\Version.txt")" - Remove-Item -Recurse -Force $gameDir - "RIMWORLD17_DIR=$refsDir" >> $env:GITHUB_ENV - # steamcmd exits non-zero even on success; the files checked above are the real result. - $global:LASTEXITCODE = 0 - - - name: Build 1.7 - if: steps.secrets.outputs.available == 'true' - run: dotnet build Source/ModMismatchFormatter/ModMismatchFormatter.csproj -c Release -p:RimWorld17Dir=${{ env.RIMWORLD17_DIR }} - - - uses: actions/upload-artifact@v4 - if: steps.secrets.outputs.available == 'true' - with: - name: assemblies-1.7 - path: 1.7/Assemblies/ModMisMatchWindowPatch.dll diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6bda82d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: release + +# Assemblies are built locally (the 1.7 target needs game files) and committed. +# Pushing a tag like v1.2.0 checks, tests and packages the mod, then creates a GitHub release. +# Tags with a suffix (v1.2.0-beta.1) become pre-releases. + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # the assembly freshness check reads history + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: Check version + run: | + version="${GITHUB_REF_NAME#v}" + version="${version%%-*}" + manifest=$(python3 -c "import xml.etree.ElementTree as ET; print(ET.parse('About/Manifest.xml').getroot().findtext('version'))") + project=$(python3 -c "import xml.etree.ElementTree as ET; print(ET.parse('Source/ModMismatchFormatter/ModMismatchFormatter.csproj').getroot().findtext('PropertyGroup/Version'))") + echo "tag $version, About/Manifest.xml $manifest, csproj $project" + if [ "$version" != "$manifest" ] || [ "$version" != "$project" ]; then + echo "::error::Tag $GITHUB_REF_NAME must match About/Manifest.xml ($manifest) and the csproj ($project)." + exit 1 + fi + + - name: Check assemblies + run: | + source_time=$(git log -1 --format=%ct -- Source/ModMismatchFormatter) + for v in 1.6 1.7; do + dll="$v/Assemblies/ModMisMatchWindowPatch.dll" + if [ ! -f "$dll" ]; then + echo "::error::$dll is missing. Build locally and commit it." + exit 1 + fi + # A warning, not an error: a source change that does not change the IL leaves the DLL untouched. + if [ "$(git log -1 --format=%ct -- "$dll")" -lt "$source_time" ]; then + echo "::warning::$dll was committed before the last change in Source/ModMismatchFormatter. Make sure it was rebuilt." + fi + done + + - name: Validate mod XML + run: | + python3 - <<'PY' + import pathlib, sys, xml.etree.ElementTree as ET + files = [p for p in pathlib.Path('.').rglob('*.xml') if not any(part in ('bin', 'obj', '.git') for part in p.parts)] + failed = False + for path in files: + try: + ET.parse(path) + except ET.ParseError as e: + print(f'::error file={path}::{e}') + failed = True + print(f'checked {len(files)} XML files') + sys.exit(1 if failed else 0) + PY + + - name: Test + run: dotnet test --project Source/ModMismatchFormatter.Tests/ModMismatchFormatter.Tests.csproj -c Release + + - name: Package + run: | + shopt -s nullglob + stage=dist/ModMismatchFormatter + mkdir -p "$stage" + # Mod content only: metadata, translations and the per-version assembly folders. + cp -r About Languages LoadFolders.xml README.md [0-9]* v[0-9]* "$stage/" + asset="dist/ModMismatchFormatter-$GITHUB_REF_NAME.zip" + (cd dist && zip -qr "../$asset" ModMismatchFormatter) + unzip -l "$asset" + echo "ASSET=$asset" >> "$GITHUB_ENV" + + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + run: | + flags=() + if [[ "$GITHUB_REF_NAME" == *-* ]]; then flags+=(--prerelease); fi + gh release create "$GITHUB_REF_NAME" "$ASSET" --title "$GITHUB_REF_NAME" --generate-notes --verify-tag "${flags[@]}" diff --git a/1.6/Assemblies/ModMisMatchWindowPatch.dll b/1.6/Assemblies/ModMisMatchWindowPatch.dll index 7d4ff274de577988c61a49abfc983a86ae8222f3..3cb73274ace5f05287f79affc266b5605f771a24 100644 GIT binary patch delta 1001 zcmXw&O=uHA7>2)@v`sdhO=^o#N{ac>O0||)P(h*DkRk{X#b1gC6Ou#0i&B*~wRU5Q z78FSw>A{00^wLTNVK0JEwlN5o`!>X>zw|BUj_e(Plaf!*xAyfJchc2FRsH-vm_1VRmwc0Z(+$hg(Kf_s8~ zi2}F^mNnzJuP)LaH6rIx4W&pJ%qveP`jP=Xqp5wSrpg6vf&zL0rV6ftZB*A_)?k`2 zU$kEG%2$bjS!aM(b;x+p&8u$1gekyOU@9n)J1DO-b&k6OxbKr^|aW29}Hjyn|T_*XIZ}TY* zfemCDnS(!4!Albv@~aJd{i^jSGKZW)Hc@RUwzTAfeO+cSMR%TGOxjCDYSy>O0VZQpgVL8D(NnJNB}6GX vmVd!lxcr@uuz9O*dJlKSs6)nQ4$9S;q&zfrK`Z_HU0X-39e1}_Ip+Qg1ZeWi delta 1114 zcmXw(QAkr!7{|YJX5MY?Y^Ei#l;&278MeDEH+yh*Oah_MiVEqW-R&xd1cgLHGw+;G zQH0|_JwYo`k1&$PCH!ZOMeU`$u!c?D$}f~On1qBrc0I!u4zcJ0-7>%8M%tpRWz$;YG}T> z{1D}DQ$6$I0Jof=yK$OZJ`EF10gZ*mLQ_UlMN>tyie_CTVRcMsk`n?=a;AovL6bo? zkxgVv=Df3ts*0+HTtlum`Qf_{JaSL~Q^Pbo(l2`CA6`VZSWrBODrOC{&g8=cuk;OG z$pK^yGlOZMKV#vdh8gh54aa?Q^bn?jxrkZAYF)DJNYqmBhQ&69Jz zmwRCX;*bRcRL~&|F~KoNh+kH$_BamxqN+kfbmc_VfB{hpi+)|~)nOcWMX@IVz;DhC Txt8|656yA=;DcYJ6juHLUl8e^ diff --git a/1.7/Assemblies/ModMisMatchWindowPatch.dll b/1.7/Assemblies/ModMisMatchWindowPatch.dll index 256c1c6a88b1e9a64a633611f3fe398160e80064..578996247dba2e8d8fa181cb65e8f64a946b3871 100644 GIT binary patch delta 1045 zcmXw&O-NKx6vxkf<9xjFz8PyGU0BT<$0SM{rKljB89$If$RG!#MKe61q6mWgK(l!| zE~MEwT-?;6CvG&Qh=Guxc%l%|B52tn#&jTX5p7yXi|+kf7w+Nre&^f|4)5BwX8&RH zbFl|+%@X|^6^cb`S%PR(3g3bJzEFCR3C%AMT~)#%TK1I82zQCN{C+4Cjl$H)7h8b+Y zOcPjSll@wiieGmeh%UVA zTcTfF45S((KEw2@qLg24k@gqKC-SMA`6|x_l*t8Dc38$Lw_%)sC5;1i5jSMx>WvC` z8}&>tQj@`SwKl5baxf-L3MLJchRMR@U~Zc2{0aJmgz-+@% zP}NXS9n6Bb1WXjhgh?Z2;d5XcSqJg5i-e1hEt#(J3QPrN7lvxp0nu7@SQMNBi-xYo zNG3eY3zP}Za@C4i#iFn4?4Zw}kZNE}NVeKYfrr61I38B@sj#Z)0cXL(U>iIOcEG!e z%b&Vho9p5;r`|PKPkU!ROy0`$tz^rA+n>1}WaMpc>u5`q^$|xCYh!hX*X3LLKfWlA zmdvB_Emg+`MQEba=vU9}-{==Z6VJ6ZP3%v^*j)M8a=)2)h$}buj}&% QPk8ry`OxE^td#Ko1x#cNumAu6 delta 1167 zcmXw(L1+^}6o%hy+BDl{wn_quR!vg1hibdqh9-jAv{s5zup$cL!EAObir`6Xv}(4d z2QeNjgCdBy#gmGH1O&lDq$-F9FCILIq_lTEd5|K$`KufD@$L8inH`4A2CE_nj5{|o z`+lDOK>w~LWl|l&CMvUtu1V|O{L>TGo4Z6uv=^Nwg}DL zLh+~r;sKT97|bD|=keHll~t~%tWbEIe9XM$SWgi8h`Nl%GIPyb7SDKC#Lx?h7}|7$ zRdIO2u%vo1oJ}UB+o~s@MGaeZR{a{zFEDk0OQ-B$P8G$LnxG=6Z6*GXEHTBdXcmdIdsrR!1^A@HF(}Y=qp?002 zcKu?}jzz$vVA3!ns6}`e9H1Mb?rosaz{5LkTe1n$gjs>14*h~uhkjWKTmY+$qg{zg zrys9UrE@N>w_;AS8XxUH-Y?108`xs;R?7m|2M6HoojPCW)R{4G5$uBl@EkY;ex7*r?S4IW_H%q-P*9)KH_&JIp2?Mb$F;MiY(8JIx#or( zXTbNe{vO+Q-JCm>Ee$$OzTUhT8tU3~Lay+k{D1vX52rWtfqOlDyq>!HMJTJhUxxZ% z{<}hTvi!)Pi27a5h;Q{Rm$w;_8~k?~p6NZ-OJ^uYCGyE3mu$*1XDQFW5_iwACGlI_4QYOh ``` -## CI +## Releasing -`.github/workflows/build.yml` has two jobs: +The assemblies are built locally and committed; CI never downloads the game. -- `build` (Ubuntu): validates the mod XML, builds the 1.6 target and runs the tests. -- `build-1-7` (Windows): downloads the RimWorld unstable branch with steamcmd, builds the 1.7 - target against it and uploads the DLL. It needs two repository secrets and is skipped with a - warning when they are missing: - - `STEAM_USERNAME`: a Steam account that owns RimWorld. - - `STEAM_CONFIG_VDF`: base64 of `config/config.vdf` from a steamcmd that has logged in to that - account (so no Steam Guard code is needed). It expires eventually and must then be refreshed. +1. Bump the version in `About/Manifest.xml` and `` in `Source/ModMismatchFormatter/ModMismatchFormatter.csproj`. +2. Build both targets (`dotnet build Source/ModMismatchFormatter.slnx -c Release`, with RimWorld 1.7 + available) and commit `1.6/Assemblies` and `1.7/Assemblies`. +3. Push a tag: `git tag v1.2.0 && git push origin v1.2.0`. Tags with a suffix (`v1.2.0-beta.1`) become pre-releases. - Game files are not cached or uploaded, and the job does not run for pull requests from forks. +`.github/workflows/release.yml` then checks that the tag matches both versions, that the committed +assemblies exist (and warns if they predate the last source change), validates the XML, runs the +tests, and creates a GitHub release with `ModMismatchFormatter-.zip` (mod files only). The assemblies for 1.0–1.5 are prebuilt and no longer rebuilt from source. diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index aad0f0d..cd23bcd 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -7,6 +7,8 @@ enable disable true + + false true From 316e423532614f4f6a01584d765e5cfd8199c499 Mon Sep 17 00:00:00 2001 From: jungooji <46273764+zzzz465@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:03:07 +0900 Subject: [PATCH 08/11] ci: upload releases to the Steam Workshop On a v* tag without a suffix, a Windows job downloads the packaged mod and updates Workshop item About/PublishedFileId.txt with steamcmd (workshop_build_item, content and change note only), logging in with the cached login in the STEAM_USERNAME / STEAM_CONFIG_VDF secrets. Running the workflow manually only checks that this login still works. Both jobs time out after 15 minutes in case steamcmd waits for input. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/steam-workshop.ps1 | 72 ++++++++++++++++++++++++++++++ .github/workflows/release.yml | 48 +++++++++++++++++++- README.md | 12 ++++- 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 .github/scripts/steam-workshop.ps1 diff --git a/.github/scripts/steam-workshop.ps1 b/.github/scripts/steam-workshop.ps1 new file mode 100644 index 0000000..a241d29 --- /dev/null +++ b/.github/scripts/steam-workshop.ps1 @@ -0,0 +1,72 @@ +# Logs in to Steam with steamcmd using a cached login and, in Upload mode, updates the Workshop item. +# +# Environment (repository secrets): +# STEAM_USERNAME Steam account that owns the Workshop item. +# STEAM_CONFIG_VDF base64 of config/config.vdf from a steamcmd that has logged in to that account, +# so no password or Steam Guard confirmation is needed. Refresh it when it expires. +param( + [Parameter(Mandatory)] [ValidateSet('LoginCheck', 'Upload')] [string] $Mode, + [string] $ContentFolder, + [string] $ChangeNote = '' +) + +$ErrorActionPreference = 'Stop' + +if (-not $env:STEAM_USERNAME -or -not $env:STEAM_CONFIG_VDF) { + throw 'The STEAM_USERNAME and STEAM_CONFIG_VDF secrets are required.' +} + +$steamcmdDir = Join-Path $env:RUNNER_TEMP 'steamcmd' +New-Item -ItemType Directory -Force $steamcmdDir, "$steamcmdDir\config" | Out-Null +Invoke-WebRequest https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip -OutFile "$steamcmdDir\steamcmd.zip" +Expand-Archive "$steamcmdDir\steamcmd.zip" $steamcmdDir -Force +# The first run only updates steamcmd itself. +& "$steamcmdDir\steamcmd.exe" +quit | Out-Null + +$steamArgs = @('+login', $env:STEAM_USERNAME) + +if ($Mode -eq 'Upload') { + # Forward slashes avoid any question of backslash escaping in the VDF. + $content = (Resolve-Path $ContentFolder).Path -replace '\\', '/' + $publishedId = (Get-Content (Join-Path $ContentFolder 'About/PublishedFileId.txt') -Raw).Trim() + $note = $ChangeNote -replace '"', "'" + + # Only the content and change note are set; title, description, preview and visibility stay as they are. + $itemVdf = Join-Path $env:RUNNER_TEMP 'workshop_item.vdf' + @" +"workshopitem" +{ + "appid" "294100" + "publishedfileid" "$publishedId" + "contentfolder" "$content" + "changenote" "$note" +} +"@ | Set-Content -Path $itemVdf -Encoding utf8 + + Write-Host "Updating Workshop item $publishedId from $content" + $steamArgs += @('+workshop_build_item', $itemVdf) +} + +$steamArgs += '+quit' + +[IO.File]::WriteAllBytes("$steamcmdDir\config\config.vdf", [Convert]::FromBase64String($env:STEAM_CONFIG_VDF)) +try { + # Print each line as it arrives and keep it for the checks below. + $output = & "$steamcmdDir\steamcmd.exe" @steamArgs | ForEach-Object { Write-Host $_; $_ } + $exitCode = $LASTEXITCODE +} +finally { + Remove-Item -Force "$steamcmdDir\config\config.vdf" -ErrorAction SilentlyContinue +} + +$text = $output -join "`n" +if ($text -notmatch 'Waiting for user info\.\.\.OK') { + throw 'Steam login failed. The cached login was probably rejected; log in with steamcmd locally and update STEAM_CONFIG_VDF.' +} +if ($Mode -eq 'Upload' -and ($exitCode -ne 0 -or $text -match 'ERROR|FAILED')) { + throw "Workshop upload failed (steamcmd exit code $exitCode)." +} + +Write-Host "Steam $Mode succeeded." +# steamcmd's exit code was checked above; do not let the runner fail the step on it. +$global:LASTEXITCODE = 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bda82d..af2183c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,18 +1,22 @@ name: release # Assemblies are built locally (the 1.7 target needs game files) and committed. -# Pushing a tag like v1.2.0 checks, tests and packages the mod, then creates a GitHub release. -# Tags with a suffix (v1.2.0-beta.1) become pre-releases. +# Pushing a tag like v1.2.0 checks, tests and packages the mod, creates a GitHub release and +# updates the Steam Workshop item. Tags with a suffix (v1.2.0-beta.1) become GitHub pre-releases +# and are not uploaded to the Workshop. +# Running the workflow manually only checks that the Steam login in the secrets still works. on: push: tags: ['v*'] + workflow_dispatch: permissions: contents: write jobs: release: + if: github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -81,6 +85,11 @@ jobs: unzip -l "$asset" echo "ASSET=$asset" >> "$GITHUB_ENV" + - uses: actions/upload-artifact@v7 + with: + name: mod + path: dist/ModMismatchFormatter + - name: Create release env: GH_TOKEN: ${{ github.token }} @@ -88,3 +97,38 @@ jobs: flags=() if [[ "$GITHUB_REF_NAME" == *-* ]]; then flags+=(--prerelease); fi gh release create "$GITHUB_REF_NAME" "$ASSET" --title "$GITHUB_REF_NAME" --generate-notes --verify-tag "${flags[@]}" + + workshop: + needs: release + if: ${{ !contains(github.ref_name, '-') }} + runs-on: windows-latest + # A rejected cached login makes steamcmd wait for a password or a phone confirmation. + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + + - uses: actions/download-artifact@v8 + with: + name: mod + path: mod + + - name: Upload to Steam Workshop + shell: pwsh + env: + STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }} + STEAM_CONFIG_VDF: ${{ secrets.STEAM_CONFIG_VDF }} + run: ./.github/scripts/steam-workshop.ps1 -Mode Upload -ContentFolder mod -ChangeNote "${{ github.ref_name }}: https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }}" + + steam-login-check: + if: github.event_name == 'workflow_dispatch' + runs-on: windows-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + + - name: Check Steam login + shell: pwsh + env: + STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }} + STEAM_CONFIG_VDF: ${{ secrets.STEAM_CONFIG_VDF }} + run: ./.github/scripts/steam-workshop.ps1 -Mode LoginCheck diff --git a/README.md b/README.md index 182aa88..0b260b5 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,16 @@ The assemblies are built locally and committed; CI never downloads the game. `.github/workflows/release.yml` then checks that the tag matches both versions, that the committed assemblies exist (and warns if they predate the last source change), validates the XML, runs the -tests, and creates a GitHub release with `ModMismatchFormatter-.zip` (mod files only). +tests, creates a GitHub release with `ModMismatchFormatter-.zip` (mod files only), and updates +the Steam Workshop item from `About/PublishedFileId.txt` with the same files. Pre-release tags skip +the Workshop. + +The Workshop upload uses steamcmd with a cached login, from two repository secrets: + +- `STEAM_USERNAME`: the account that owns the Workshop item. +- `STEAM_CONFIG_VDF`: base64 of `config/config.vdf` from a steamcmd that has logged in to that account. + When it expires, log in with steamcmd again and update the secret. + +Running the workflow manually (Actions → release → Run workflow) only checks that this login still works. The assemblies for 1.0–1.5 are prebuilt and no longer rebuilt from source. From 7c78d31ddcca250d0a9d62d3609e94b1f41840f6 Mon Sep 17 00:00:00 2001 From: jungooji <46273764+zzzz465@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:20:36 +0900 Subject: [PATCH 09/11] docs: settings file name uses the mod folder, not the package id Co-Authored-By: Claude Opus 5.5 --- Source/ModMismatchFormatter/ModMismatchFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ModMismatchFormatter/ModMismatchFormatter.cs b/Source/ModMismatchFormatter/ModMismatchFormatter.cs index b1251d1..2540cf2 100644 --- a/Source/ModMismatchFormatter/ModMismatchFormatter.cs +++ b/Source/ModMismatchFormatter/ModMismatchFormatter.cs @@ -3,7 +3,7 @@ namespace Madeline.ModMismatchFormatter; -// The class name is part of the settings file name (Mod__ModMismatchFormatter.xml); do not rename. +// The class name is part of the settings file name (Mod__ModMismatchFormatter.xml); do not rename. public sealed class ModMismatchFormatter : Mod { public ModMismatchFormatter(ModContentPack content) : base(content) From 14aa57857fed2880f816e473516c84c101895664 Mon Sep 17 00:00:00 2001 From: jungooji <46273764+zzzz465@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:43:46 +0900 Subject: [PATCH 10/11] fix(ui): readable version-changed rows, no "Unknown" noise Found while testing the window in RimWorld 1.7: - Saves without version data showed "ver : Unknown" on every row; the version line is now left empty when the version is unknown. - Gray version text on the bright amber version-changed row was hard to read. The amber is now as dark as the other state colors and secondary text on colored cells is near-white. - Drop the untranslated "Checking mismatch... please wait" message that flashed above the window. Co-Authored-By: Claude Opus 5.5 --- 1.6/Assemblies/ModMisMatchWindowPatch.dll | Bin 32768 -> 32768 bytes 1.7/Assemblies/ModMisMatchWindowPatch.dll | Bin 32256 -> 32256 bytes .../Patches/MismatchDialogPatch.cs | 11 +++-------- .../UI/ModMismatchWindow.cs | 18 ++++++++---------- Source/ModMismatchFormatter/UI/Palette.cs | 6 +++++- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/1.6/Assemblies/ModMisMatchWindowPatch.dll b/1.6/Assemblies/ModMisMatchWindowPatch.dll index 3cb73274ace5f05287f79affc266b5605f771a24..98b476133d9e5cffde763b27ffa7fac1efec625c 100644 GIT binary patch delta 11653 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 CrL=Yc delta 11767 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 Tooltip(mod, state), mod.PackageId.GetHashCode() ^ (isSavedSide ? 1 : 2)); } - private static string VersionLabel(ModEntry mod, AlignedRow row, bool isSavedSide) - { - string version = mod.Version ?? "Unknown"; - return row.State == RowState.VersionChanged && !isSavedSide - ? $"ver : {row.Saved!.Version} -> {version}" - : $"ver : {version}"; - } + // Nothing for unknown versions: saves made without version data would otherwise repeat "Unknown" on every row. + private static string? VersionLabel(ModEntry mod, AlignedRow row, bool isSavedSide) => + mod.Version is null ? null + : row.State == RowState.VersionChanged && !isSavedSide ? $"ver : {row.Saved!.Version} -> {mod.Version}" + : $"ver : {mod.Version}"; private static string Tooltip(ModEntry mod, RowState state) { diff --git a/Source/ModMismatchFormatter/UI/Palette.cs b/Source/ModMismatchFormatter/UI/Palette.cs index 3355c15..5bda492 100644 --- a/Source/ModMismatchFormatter/UI/Palette.cs +++ b/Source/ModMismatchFormatter/UI/Palette.cs @@ -9,9 +9,13 @@ internal static class Palette public static readonly Color EmptyCell = new(0.07f, 0.07f, 0.07f, 1f); public static readonly Color Removed = new(0.29f, 0.09f, 0.09f, 1f); public static readonly Color Added = new(0.29f, 0.33f, 0.19f, 1f); - public static readonly Color VersionChanged = new Color32(178, 144, 83, 255); + // As dark as the other state colors so white text stays readable on it. + public static readonly Color VersionChanged = new(0.40f, 0.31f, 0.12f, 1f); public static readonly Color Moved = new(0.16f, 0.24f, 0.36f, 1f); + /// Secondary text on a state-colored cell, where plain gray would blend in. + public static readonly Color TextOnState = new(1f, 1f, 1f, 0.8f); + public static Color For(RowState state) => state switch { RowState.Added => Added, From fdb27d556ad7d6683c96bc26fecae776811a2415 Mon Sep 17 00:00:00 2001 From: jungooji <46273764+zzzz465@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:53:19 +0900 Subject: [PATCH 11/11] fix(ui): compact one-line rows like the old window Compared side by side in RimWorld 1.7 against the 1.1.3 window, rows had grown from 34px to 38px and two toolbar lines were added on top, so fewer mods fit than before. - Each row is one line again: the version is drawn small and right-aligned on the same line ("1.2.0 -> 1.2.1" when it changed), and the name is truncated to make room for it. Row stride is now about 23px, so roughly 24 rows fit instead of 14 (1.1.3: 15). - The summary/legend shares the title line and only wraps below it when it does not fit; spacing between sections is 6px instead of 10. Co-Authored-By: Claude Opus 5.5 --- 1.6/Assemblies/ModMisMatchWindowPatch.dll | Bin 32768 -> 33792 bytes 1.7/Assemblies/ModMisMatchWindowPatch.dll | Bin 32256 -> 33280 bytes .../UI/ModMismatchWindow.cs | 76 ++++++++++++------ 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/1.6/Assemblies/ModMisMatchWindowPatch.dll b/1.6/Assemblies/ModMisMatchWindowPatch.dll index 98b476133d9e5cffde763b27ffa7fac1efec625c..9ba062a2763ab32ae47ce3e6cd1723aa46a8a112 100644 GIT binary patch literal 33792 zcmeIb33y!9kuP3%>D!B1Epm?aK`jCnAz1LTDaFd=~iLK5s@NeD|wz%wBs#2&~(k|8*OBqS5_|5csat(I)a z6gCP_LCVxFZ zk9c34@|f20;*_@SiFD=8l)Wt#>#6LBC6jiha%;RY)tjtLBrBWNv{v@mo$>m@Lf>>p z_1fh`Et)~U`PSz9v#lMasg(uVETXHxu@raCow!!wdjMaeV!?G~H!~bDqN5c@ z%qsyLei+oqF?Dls1Swh?K<{rkZrJz7HKtG-lS^~lXqayJM2|wYCh9{dRF4~UarH_1kX)CgW%a38>)O& zb>@uH8kao{cr=6}UtTTC#x(O;S-lt-rMH0fW>xB?)sPrJ%b@R}Tt~anD%48P0Pqc- z2G}13Pz{YwP1b0qeR_`JOP!u&6u1}u12nj8wCA&H0Q@zVdhA*tnH3xasm82m*6KQx z6{a?wiY=WfF}=0pgJx|cXr>a7iq@k-t=IAfP3fA)o(V?Xoo|$Sddty7i7V);bq8HG zl85hgYlu5qO&aFEPUq2Oj}Rh>&{tEY8)7Ud=0_19zB;X%qH}=Qa{+ucD=d2+kb+bn zH_Q^XbJ4l^K>Y3~M%F3}>C&ara1?H<2^rE8hkC<`E`aFkmNN0nZxma#mZc0+CDj`I zs?$uWy-1}mohoSk4#KE2KS0}h6`$?h4IZX`P=!P4YS@(8%Z8naMtoLuAue?oO0uGu za6bENfb5X5f1``Qh@O+A7rf5Gk_j@370Ho;6`z2!5ieRW*-tw zq4^h3CFYQ}LXS2;qRy?`%W$nOt-dVX2xJJ>nGBq(wJC)R)=>kB_9#g7WAEU`ICsU^cYl(!U9y z5b_t|YI!~Bi*R*EF9wW=upUJtR*4xj2N8_<7NyvtOCVS^#3dYdH6<=DJbS32J?LtO z(#XT9$_;m$tY2YUWs;c3;*!N8`!z!pYXr z$B0X-qCSf8vMkW=HnjUfmLrGD7T5`P`Laz;6GBAU7a@06o#@H-DrAX=nYVNkt3;nS zITfVaU=1~J9~DGTOFSXBa&u7n*B~~f_F{-qpUsZbO_<=C-VZOBH({)_Xbd*Ua5$BN z9(LnUL%VgdmcwJTaCnZ*(CsLl8XpG{Nk4tSF&8di_ZXU(aPgr$mB;CQ?@E}6Kz;a-vM{BfE|j(SzT>;($-dEQ=n-70w9i{hZcD;*gDp(rX$=!f@%*i=s#k zJO{W>Vhw!Ab3Ton;48H;53@7mBkDR65Bm`#bp*o`MXT0Tu*QsHBV#dpyqRTl8W#4J z+F}v3(`Z0I1t4i$`xK^If!Y4H3W0Tgg!|>5nT1CGrx!=9qXaUELA3!Yv;z{MteuT2m z-UU!wtoM&Yf9%~LptME{5A*?5F>lfy==Px>($9W&NL>MJ#2RGP>EUX{W{04z&#DbO zWd-hWnV3-V5u2s$~>0!OOJ>+cThLJ4(4 zzY3#lx+Bp6h=mo^KM35qD0($6(IJ4mijhruGSJRL)(+WWmc0hRUsyA!Qq8Rd5#PufM`q2!~)AV{Q2?b6^=dZS^y(8*fP4tb9rIdej+&7nMJ& ziwqfW-h`Id1B{#Nij-J2M!3WZT6OLKkA00EFbgbqoe{2aJMsfC3F;ZvR*qG#_#+9% zh<<_{(9heEI-|5EpzLx5%;lf<^J++gTg)|8P*94 zlh#V#j8f$|8~Y^7z6Epu{w%N2BC+VLpsL@|ZLFGB?QAILSsa;bn6=pLGP4@I2Cm0M zY8w)s;zvKpHL(fjI5H`QD?$l-cJ6KJa*^ZqM9$|l5VBIeqc!-9+8Qf$9hj+o0LD!v zyl37A-I63_VOL}+c-i|qi++jrkO6|o!UNc0n;{C(Rg7e3wOYfl4h*12W;jIAS!~$% z(8x&ojYJ|?c*>A(#=E1RLIbr6qPO9aI-V69T!7kDbtN-OPgtizfvg@?L(GkoMxwVv z*#0y?B-=oR>*E`6Md325JS#r?Z-`rZGzxj#->d7DgLj~{(&%RZ_5Lu-u>Xd z-p{(|9ry-WImmeu_sDmmVDKF2Ms913#mP$l4-`}laY+>7HBqGOngVO^F0cnyF!yd? zb=au?6Rz&)XBi@@`nm1_5Yb{(7+gGBbB0SlhtjGcF5z4Fn!zQbWqMiW9+BY^zB5y; z`#tu(V25YdrdGl+sZCk$NE~>=;plzbE`rTB_<0tL{!gaA0N~^^L^q)y!8|v~jV_f4Hk$wo`%KJHTU?n8rax6hs`d?7aCqp$+40}y= zO8SK*aW)rzi3=mS!b_d3S!l?{FQPUF5fvY2!43>dP?Z_7Db)LM!(u89PD>t!I?mgg z75y@6{tAO}0J5{K{*h5TJ8$VWWYXJ@aHYs7K4$>oS4XPD)qgP56ul6F`VOO?XC~4p z_B$80J4G8%qzW7cXJSdO)t@)yK*F#eLpli@y;2IdLlB+dX$yZjI)hT)IV|Au3N8mzd zs#mVzRG(Y}sR6lKsbRhROwUie_`>|i*H@F7_6*j?)=mjUwC2h)b)@3)8eVO{_Fea zBrC)pDVmxw;u;qP;<81 z{yGr9Yh~T(9wYh$@Y+gi@Ef?A>2CshqTdquB%pl=z!h0tgMrNPu5xH^3pbzer3)1&{&s-6OfJ`I55)6>{a9G|Ldgb$gJZo}$|F}LI( z87U#Czud(4maHN56ttF>MxO!xGUl|>n>sSWMS`CXO;(*R^+#~!cnL8y9&w2l2=h2N z_$+Ei=9cggnN{nT1C@R(YNi~|L96vfi7)j}7EY_w66j_YIpd$+JXDlZoMs>=tRcvxzXKIjLtMgl2Q}3f;;?BZC>%D; z;)`nlS&WCFpSjgjonmHixiAC)LnK*3kZL*nvce|}IsaOUJ|GPS)6bzrpOxB+&fDJw zKEp~K%;CBfg>e>6S0`86&r9j#0SB>P04=(%fi74ZG*b8HY8rADXxT4vB_s7jenBI9 z!zp+rzhF5Ea2t6dj9H+Ier1x8dOKfiCBzE&MtC>ICr>O=U5``q^HIuocr%K~;Da`+ z`b6|4SfN~Ie-CKo=^HlOccH^&cL}%MVZSWG>@@rPK-;S?HF^=&dTpU@+{cL08b;sM zxjed0c}*BHHueutw{~V;BZdr-;zyz*tr#*=HnN)!xxdTt+&Y{`vZL0y)WEe>e+ol6 z2UZ~#=JLfgFpn<*a{vzO3yfM1#-d*J3I5?VVJWWpF<0pMh?u*C zwSRof?SL-+ME?y<7^%rQRdC;WRL{oO!u*1^yn>)T7Lz=)c1*J-UwT{}8AXTdy%!M#cUS&}_&lHV?@u*gP<&VDs=O zcJ&#$(ObsDF1Cmr*a^u8cJ4B2Q_2#@Yx(gf=Bq`kx6>GlfCnX3Qk?Mqk>rJR*|gqsc=?0B&4hV!5K`R3p{o_ z7kH@F1;F5;S;t+kB=0#C-Np@ZoVjQ#HXaqoN4otJ^vDnxeFJFvrvSciK@E;8+6!>w z#^p07xs}MwG90`g7ER2Q;j zo{?5$6CYcIXT4xWFGF?lH5ye~+0=H)+Bki}#7}gYx6oMCN|EHkn47$TJ??*Ld;mps3OH4TZ z%zwZv(BUK*SI6x#Cbg%T;8(%zxk4t?0FCsyrUiZ!TU>0538P%?Xm`%|(bY@ys#KVz zM$mOCZ5W-I(=f_Q5;$I}srX10kel-#DE~-32$cW8`eV^H_#Ss8!Sp9E9(*o^8E%FP z!u~V=h@Pi!0JqR<4Pf?|UMIWTu&9l>KA9YSDZ5c1hBoVCrtxg9`_4+mj2j`Dq>N!X6^dmq8{Ui1t1r-Q5DEZNDm06Agp;qLxSrW%*ASe4q&#J(79%`{By{ zlhJ1xGk;N5U7t>(<)JfS26iQCr!v(!M5~@;*l(i-uKakeruvGhxb13Cx7#B}m3$tF z$002JYnEPS^#2CeaP&U`OR*$RGWy@)f;sB;S4lzoQD$ZvJx`Y-TU}08`H{4o7`@6O zpE69ne+4X*qm?;^QR7#%Y5x}G>38$>yu-rShmI0vS_$j@=K{~a^$wt^Qgzo|pT3%% zfsOTuGcHv|^ZAlX0 zNp8seuFfwo^pT05(;#aq78#nvF@H1c!KjJ}16YG`c+0S^0z9^1V59ID){Q^&Zd5zD zptioIz0~Sw8NHul6QOwv1V9ozs!vj)~#b?aYQ@Lkv&Vv z7AWn0x5q69vL1orQ#r1QIN}AVPhh*3{yhx8(wpn?8Sn!eCkq9(7}7&SQRvXJ*7KHW z&f8Fa&{xc>pH)A1*4)_(LEs60H{hX4qG^LfwU}6UApuNl&7=~^ZE2SH&RIk+c);7x zO1EQ=Uku)~l^dFI*G%_<=7}|JSvT)2<)dT$+&KAjUat@2pS8Js^NF-i2qT(?FZ@q% z3EhA%WC^|DdtAc4jmM1NIgp2g5nN~D%XPi@7UGK;htFLB=E19UL&1k3i`;&OH~8)d z713!W%zwr=qtK#v3LYr3=nR3s_c2~9_!k5|B%BRCt~=iUi-1LCWej)v7=BamH=xr; z=eU1>&Qu3};0@4YWldg-UNR=QLiC;RcOlsoda~4_Sst$C0pFtkEc!*jM@LJz)`s#Q zK>j}Ayk2(PvM4MvlK}&C%wTPg8eF#rec403A-4Z9{TG2SofT?=hE#-WJp`F?^smOg zVvE*#e-Q}Ka+FS@Zxxwn@hP3#omBilk&n8Hxy22T@zLABC($noO&9nTj1N|@{AAc; z(T~J?-$z>(8Nt^h7A-UxuM%&pDP#>h3%M5;g%~asyhXfmf#CmXa$9{tuJs;l@X;kw z`a0ST(P8&@10lLH@?Geu2(pGR!Y3AeSoo-E(KVh&anAHgc(`~fJt-NGw6SU zk7vy2qmP!04KA_pVvMGb_K8kYdiQNV*L~N|{DVQC+edTJ3m^Sgn4#fgSR%dtfk=KJ zlFMb3R|w}PBDtZI{oGe7elD@Y7Jc4rhbK{icz%V%$zRJKD4Imgknz!ju*ah5G9vHG zIQ~)aKb3N1z9F1H%UC=C`z`tjN;&%RFa{r)**NxT^y#JZbJSgkYNmKD7Smyj^L*f_ z0BVC!2IkoisO!TYh8j>}3Dr~hOi)o8%3caS6KsZ@N6J25%4HVjA(#EO|0 zIYQm)P>azX@_=v9dxWC2l)SLzwDLjk{m2zd#g=m_2E7evdo2y4Y;%yQGpLL9fO;4% zXrKkS?=_|rQ#n#i1J1`lq3kp8W&=(IcMJ6+Q1??4T`AOVwAp}@XGLKl2DK7z;EiZ$ z#w)EOEu2rod1Wn3p@nCp-a7h(P~{k$23n6i$YnF6g$=Y?)rTaa=uM2!mpr`mf&ukpB{sl6(mHx$J(S_cY6@zFfLjn?hF>y1g1*UseEk zd07eI+Oi3N(a3R+Mso$eR8-|%H}rtt*3Y1@yV`pO9SSh~lZ#=OKwIGN1Qz?My+K-H z!7A%J=5fyjL)W-wdM_BV1^!83m%!f#m>({l345Z}T)@u@ywze(q4l0;3SA=bTLRO7 ztwS?Pk9)#&q2EC3J>~*$7zfB_1O7}~0l3U)2Rxe+fUC7Wz+J{QfQGpb@D}X=;Klj@ z%(`>ka{;S-GXY;VzX&)9{;}xZk~fgc6#P@kL(nx}P`7!WEeZKddR?e} zo>xjDpxzSdS*oqrg)H$Whgwk90qT=e2%kG>QsG1&p6fc)&t21e9^yB?xa{HLI?YG# zIn;NBx|g>Iw2yj=Ci;AI=5$5fQZdcvr}Gq5etoD;E2Qm0Jxd?J|Aq9FP|7cbbTmgr zeTB4$CoDbd`I>(wUWZyG)IRA^5w)o@s>SZ0h)VfJ0e4I@(1#*g8)a&kZZ4VcE2dY4 z+D9c7%Y8xm#_6i;?&1zS1-XiEG+^~*ctNj}-V$nR9vSdzZTf(8&4Iy`GL;iK)-J?wL8?g#n<|#(RYLzrk|E};2FU; zX0hb6^dMID8T7PJ!}MY~Q*Q~ik2d@3v>9}Cw$iyR!qioBm>Q<1LpS+m&`S>Wj!?gL zs0Zl|l$mptB2h`i)R`;LYOaeYLc7 z9&5RS(t%feb#%K>`(!lhDKlS{9rV8DtEcybQe|_f^b9HU{M7TFZw`$YYMdh=+)0&Za+v=kT2qb&~A z?Rw952JID!BdfgN4Emg+JkPnN7A&MMI@Bw!(?EU2p|0>xEjWvwaHwJbX`r5Vs9$?$ z7Mx8lIMn;zd7zFs)PVaaEuy1BJxi+!cY&(KT_}&yuS*vdETXRqb%&?DqOsr{de)&@ zKrNw{g?g6mbU&yyP_gXppQVb@^9ve?U#=$lxmHoy;af%nLS0WUm#r;mq|Yb{{kx=K z1${%PVbA`uc)@w}9aZL8;M#?^&tG<^<*pqld(ENT-d%J)9doE+?~a1==~oW5x@Z@* z(C-}T!lE4oE%X=s`OFhVy;bBF>JFOeP8F=Cr4D7geuUM$O(^A;^>jce z<(KsoIh(bxUs46FG|iz}Ky9Eig;IXmNDn&{`(+~?5{muusIQ%x7l|#%h*JwT(K?}q z;g>5x-RP9PTsBm2Aw3|}9W<}#rh-f98$#U%>R`bZs$I-l_R-CuuN8FA7nU$}8~w2S zNI@4(Xi#PEmjAWjGQ4@Ks9o+-{|>5Lrl{TJRsJNcSCq_-%jr^~RCc?Zx`f(;z1Nxk z%ZcA!C%nD)TWyIyMY|lT#aQJ})71{O94lak?slklt;^p__c_$CvBQ4_ebu1~%`5!7 z=vxl;kJ_+*H$CrAUHY)Uk6sZ<*>WZQ*eQEN+lAfHJEO|};FMjXf5LwyjputsXXP58 zX+rIxr?lJr1GHHvHQ%qM9eI*h6Tg(t{TrY={a4f8Jjo&YOrGQreb$jI(+>ED=!*_@ zhW?;`n7-^#UB&_bHS|q~`knEhe-Aw)l2)dHiw>xaqWbF>s9@6?xoT zIr6%+ZV;E6bW5Hk+`2A&L-@*0Jh{f*zlNO>!xa@H)-tYSG@35@mHkRy!5_eWo!)^B z2DL@_zU2`5o9!c)YIIi-%Nz_d{&SS-^l5m^pvls%@?Jg+;*>(8_oZK#NUeN)hFDS^ z)J2;v+Vq^ZX3;;#TU=~l2VPukQY{W7vi9V+qVV5{{9Q7F`S^d&2G+bgz?L+-*pfM3 zwq&@7{h->-*Exb4bhdaVpW~LX&gU$E43#FTi`;04Fa15t`oRPUJ(DvzHDnG2X34XD|nOsqz zAEd|1H|hIzf8-LtKbLg^-s9h{d$oh*HcGdk^dRI@`u+M1fh+Y+r|$ToPl7)#z@;{1 zUei8R_Ah!jO84PzO|{tVeM!H{!JjTWq3_f0iqsm{!h0RYP2#<7nql-C-SlJQYU3a+ z32#CxWbFao1)VpE{~y=SD*B}Hw&*!d_nCJXuhD#j{{8yhuKSGJ9sZrcL&kIZ75<|% zPkMcvZo<9!T~6y(#Svq_{-*yO<98S;8s3fUH9yeX19#&o+>J%|n3}QG^%b+i*dBP?Jl!~A ze#?BDCgGIf7(MO%j=5jEC-R2*i0Is}Jpz28@uBzkX0vfyuJGX z5csP2;WhoD$mOne#_cg9&<3a4_7s{M^T_hK3_lKZa_@}Nu+-d*Z^_;%BMIRtz!=tzpAo)ByO`K!f!jZFQ}(_M_cmoH4a~TCHL2gYt{Pxh1gGde~@i z_gGW(g<<4ktm6u~yE{y!e{hIb)!CiEf#&hAg zF)LW)xk}?1@v6k(Rj9QOzlrD47U6#&z6#)ku-+5EJ@F-g6X*)SO4&fQ@t!+HJ%N^nz!(=aYf92Cwy4&=J`fPY2c z5bdBx3ZI6aCkx-Csq_pW+i7Tw8ye##;FE=w8n-)5_|t^HR5%UVIC{0PNo1OZ(;+e) zB6Gj+pVInK_c_7e)b`Nb0Yl$IB}MqXC><>{^>Os|;%VUAQM44lOnI`f5%`4SCMj)_ z(mlf8gWp_hzCHSFY(F^96y778rv!gW@Hcg~(=fzZfsFz?1nv=dkHDt{zA5l6gZpTh zQcGZ?z%6)2@MK|!;7^-rdbaqO;BNtct=M!i=Ky|iqxlZFxb7jr4+(x$@S}ns6G#@z zXaXzoOGV9BDfkqD4Fb1-|4d;_@L~KO?8(Am!4C-kpl}Wf=cvGA;NTd`Et=hoPZ8K4 za0`CR1#N;4yV={rf*%xkNJL86FBQB&@GXK5 z2|OV1kif%IdQ2dBxu@h63-MeA5e0~d5`LxdrweDP;9CU7@Z`k`3<*9gwGN2<0pT3< zrYRISDEvdhIV_yR!Z|9OW5PKm9L>k_8s3*gUwqORAM0N#c!Th_2p$uBNbq674+wrx z@I&~y&Xa|QgmYLpM}>1#ILCxT1uR1a(%%AUQ8<;t*&_Im;Mbz`$-)DI-z)ebDLo{d zqk&Vhv42%-l(;Y$9)sR_roSnLGI-&OIgl9$1;a=zo|4V=hpC5h|_!=4tRIjuK=4}?*h*8G8`^qSPNXo?%zOLcrJ(5B4|y=s^SKmf)X81 zcnVN@rr6TJPpt4~ zCX$`@u0}hVi6=8@sVODvdy`4LSFy_O6y`i(az#NJl6@D%Q|W}AY}_78Zi{zvMI|z( z#kq61oz~u-o>;1Hc@i&c#8oSEnSYXfr(({XFQ%qqyO#B4GIlb(c-!X93cX5|v0Wl0 zolJD~u{pvR+kh~e(-$PtiLKpn>F$}*CN#_yOm(Qmj&;U6x%X{$YwQYwHVsjV}}-uQ@RxV zvG&d5+R8ydMmy*f;`6iOuo%T3*&Eg+Lw_nyp>RxJX48t^WXG1-w7R#uI|kPSZC;-2 z?TM#y)aAXgOqN(5k9Dp|cK0>f-QDqy3<6CmK%_g70k>yoEEP}XDaqJEW$Uruxik~+ znN5wY(-$p1bMt2W(WY2;8r)=N?wp)+fsLLwPSO0$Kt}t^DZ!9)(MNV)mB(N<0%tx( zP<4I|T@dT;jkopgMEB=V^U9RnyAyK^cvC|9h$+GaFdDMxww=n^GIy*kVDPA-EiU>v zTTq@xPx&2<5{Mx zQCM$Md>QX_3Z!_I%|h$rJ#yt#vdrGyYIi3(5x`l~7wFtg9<9oDbr(8ImSOD!pYv*ddwD)w&)$xSjSRdhEE?vGmkr9M#69k5; zhF2kg5WdvoT%E-xyB^07fyi)I*gK#lmtd)xw*hvV^DFx0tbc^MnM6;#t#4=iT=X4f zu%4+EDZ9tvwAtIXp@9AAw4s*76$x-RB$1AnC%17HXkLZcjxHigjN&)iDen1LHs_J` z7)8uW*=KNhHyp(g>3A4Eb3#<6YJSX8JIA5O3Bpr3#~BG!sh3Z3B`hCXVQ{{YPHINa zsW+YNr?bq^#_bs9k*osz<1-(37(>7ld|AAU2@aSwsm??)*4?}<2~J}y4LdjmBKPya z#7W|q+EQ32F_nPClId>7xUx(y>5Rgdw^se_N5x=ou1P91(!75D>|@m+Rm$A~=kqn!y}^<&*5%&Za)JSP%QE^JQ1;mDp+ z_amjCzbmn=Hzjf-O4i4_T4P=DOy7D0V|s)yOZM{acn|i-9AtS#%4MQweY`uiTd=(H ztkD>(&fbp9h|a8&Wb3^n>t1)@^U&^_BCCtV1eao_a$ex>QP+Sai_ba^=t}$x>siyQ4L6C1%bp zUKa&rW15jO(5@=tT-_A!TGK^NUgPYe7%iBi=f)G;wr5Z(`5pW*Vwj1U?On8@JGPC` zi?kY8&TD-4lAW`Tm+GB(;F7l3Hr%$Tm&XYSP2M8Ut_0MqZi{%4WnuP@y-meBIvz`P zY$wj=oT}Dx-zdBNQ4Uo+&iX9DdzEvspN*#!UuGfiXY!e70y@x}j_s;#CF^W)6oIhj zrqE-|VSrV!)Q*jbPGB8&cW+Np;q2mF!cCC9^)fbIVl0{AT>{pGG0&1Wq_g^vEc2X7Tew-&5@l-Alb*dC0(JGY;o9}j_nQI-8|;q+~O(v zQs0T9>>Uu2&ra;)aW9k|BdmwgIPZQXxCBut--sA7skTSC+4bHGSL-J zXR^FTY+N&Oi909nU^I|jQawnYva8Sb5ZT4KmqDWKjAMbwq>vK0{d7kPH#%$Wv?M)V zzHy(^iF3fFfzByY?UXDrsSFpXy*!4k z9uxKQEG;;XGArI1OHszTvhR4bcvfIzmJ5HiD7D#n9JT!t!4zd@5YIP<)y*nWjxUz= zCU{p$Tb-+;A0Ze^Vx>xvox$?W`x*@7#zba&b0?xc(Ukzx>Ac$LfaE1t7C;VR&WI8* z5Nzie6hl7BwyH(|jRFhFhb@^U_67@V|<$uDcTtWz%7}|*0^B- z$~%Pyg(5bbG9(aS%Tp;9aCRO*v$yED6J-xN`$?wi`R)`@F5;$^j&wE%kOi^J!KNCl zL}wZ|EopWH55HPi;9}GWDEI z8Zi8QX|R{`8A)0qj=SF#!;R^x*zPvoqH(@WQ_m=h?@ZG;<+4#SGVX4|avmlWk}CK1cR zf-Rgqa>cT!iEKQh(iZYF)3Qu)erxVbk7jWH6l;B)BcVCDvo|A)_i7~Ptg|^$BkLyR zs)kum?p$*cYmrbm4nc;&Jh~te-=#J-d9+%Lqu7SQ_Sb#&`3ssw;sD@;qO&w-V}90!LFkFC_Zooy+=24m)vW;}yjJ zx1f(2QVco$`2^W8zFoSm}w&NL|O(}=YP#pLrc$P%tTY+crUxKg~MU58J*a}`Z zez(0GJf^xqUm$gmE1@TeXN229ci_DYRgV_-qVCC3Hd@>X9VwJG;frhsJOLiJ$eO4j zzm<(rnzg2pSS!*2w2W^5T0f+>3hr zYOaUfX9!e+7EsCakUXrDd{I`YeOm054Ts+PqXko zdL@2=*iW;8osM?0Rw->QqAQbMtEvIE@%VGEK2mDFDj93b$uYhezaH$A9-)^D|NFJp zqxBwmi>*M-zoR{F!>P6Mzu(^I@yqu;T3`O(*VFpZV|@PKujM17!COgnv>0Qn8F-Z( zzo+INr$9^O7WnYaff{`}=kIBiXCUXM&Dq*h@;%W4&#F1k^Ok2X;_@_}OG(VJ1lsRG z_EIw0ILk&DMY0@KGsiE>)@Xp1KFp37AO)56+#40U6j8Cp^Pl59*S>)trx|td1Af9{ z;Ew}nw1nq9&mqnTNnGn4>9NteQOY}E@h&{W&*o_gE=Qi=S+rXEh?z7cn-#M&Vsx~? z3q$vvDy6Vfk}#zb$v~xDPVDt~sbh2ll_FP(*ETpc^8jSiVt$%E8+v-gq)KPFv&k@x zl3l3TjsHdx8n;!6AyNsd6YbK_&8ISRBzu2c1H1}U!Z==cM%2~=ZyUqo?qFX9xo*S- zQ*5Ae26sOfo0pu*nvp#G(Vgvrqj*g^l|4D_WepMm+aw0?^b9q2;g8jGL?OEev+>Bo za}IMNwSf8A&*7IbzZE@FbHm|IV{S8g)`b3b;Xj^#akSMT{t=Bq6{WnsVyn+_UBPk2 zt2Q2PLXKBDM_&o+TMa(1kPfecd81>^DVfYAoNajxd5nZY_WY16EmR19W&Qb-jd&OJD$nhF#oV36*HYZl^r7DRfC7 z5Q@3*2Umx^J{VON8ZLy4X@!Q%7*wEz&?Z<7fr%#IB~U*B20}Y9-S0NbLUAp$Dzr*0 z)&qE{2;F%UEbb0pJ(b|Zp%tyNDAbftL9u3P#giy7iFCsUtC5N;pMueyU=;Rzu2wA zs$nhE=3LlFCeay>7x+OvxHj0r!V}7;6zeSgD7-RspFT;OqY{38d54e6>hH|LkiZgRu&w(5&VEZxGuQP z4_p0kh!z}P2e&aA9J(%`CnqYHIuDAF6+EW?7lqtNm|IKXQi^&U@^xYdYbcb*lqbyx-Vy+ z>VBa}WvgLb7nu_L@sNC3!zv+sHIFzVIP@SUh96N2-*_tx_$JgF>NVY%GC}gNt6*$s zIEH_^?vd^U*XkVqal|AX?=VAcIy*7X59~ypCuSQ)!H)qcfZ4%y9uJ~mou5qTF2>*l z*Xfwf9yY<_@nBAskjKR%xX$l(hdSAgb=(U6Vhazj8r`A&1w0clWIUHKN6`-%c3dz| zd$oWce^(;7PT|l8sR@3!9^4LJd>S#apT|;=AphS`)&+E!?r>zv595{2(C~FgB%xkC z5FEad^GWD(?)W~uafz9;FEqTL^Oj!hHbULvdyb1?ExkMThjB7uPJbwYFuKY$93Jf}N0h1~`0Q4JmC zK0FA2Y5`o4rg+RS&sT+p?&jfIrA8T9%jMGnktvai@8+P;LOVGTu-Qn(uqxE15(6r? zVZ!qT;e;Q@U;;kwfuA@b1BIXTfXfZXKUnJKsf_l99+sRp^e`}PQdo>2F%CV93PXoEK7XcRE#Q6~#=lCpO0+U!p|(()2eJc_ zh8cQ1tn)Z96$ZG}!)ap6hD!L|9t!L|7NbQ57c{8a+)aG0p{jnz$4qdW=T zQc(^6V+{`Ig>F}9=q6T<9I@8(wJR^apkm(98|(kRgOGBy+skfzn`DQ=dy~>r9R~z> z7lTI|*n9E5B{2Fhz$E-#cYvV=#u$_@1K7gYQU-VW}3($Wb$7=HLQs) z-NHYQZUqc06wPihfcN6t$MgV}WsjPv3XugG0XlRYg9i;O*aF!$e7E>mPA$bjh2~s~ z)piWWQ{*t%&%8=VD+dvhk{CuZ$!Id*Rn;*2(eyerTZ?Zd$JGnC+=(@mT$OD%8!Pz3 zp<-n&Hu38_i=&X)nCNXTcAD%0T|9j!y5O}?l=Hs80JcKd#$oCq_w-_ef&_nE5pR}I zTa=9I`DL@XMkq;kGz)u&-9 zj45?OL|l@Kg)sMD>CO%vS1(pg^7d2ZJ^W8moJ`310(rTOvoCJ}@!POq3wSD{_>p;Y z1jdl}MiT9CO(1}+dv-Adnq5cX2EtsUAuAhTHnm7x8cTppk5DINtzHGxvoN9`RfSAQFx9BuB zw?LC0OCZ3BN>v|R{Nz9GQg3$0*&6)#&r(l7i2q91wLAES z7;SnXUJS8!t-`aQ1m1iG!`qz1BYC`v*2!Wm@;4OzrkEJ;H?;L?w2eI-uRr;%f!fOK zQv>G}huX^QpvgiJ&&tRQF%{|hJrT9%S9M5H=oI@W!E#`PqQEu=vCMwEjeJGQO_XFTU z5>urB#F)?6pAdQO91)nxc;yJ4kCo=h5&oi#ooedtUWM07)NAbVxV$RGH9o9@jWMJ`UYKBI4L2@&m+MLX1xpVPtKS0#NJ67k@V$1_}0nR5k11`rYXDdz> z*Wf&GGp?&~Mz}^KziIyVghVt?0ep1lDu4J1G{-wni~LU$2n|~K`BL^NRx@_nUD$K; zXS&pCLn%L}O5-$r#iVmS=a zXQGmpE|vEA*%I@4RJi@DUzp-E=)cdu`F5yrTMPaCjHnmq-TW*itM%{G%l(>#Uyb6g zc#hIH*2^P4Cj6q7$D;ue$Sq865Jkli0ix#g-irTiZx7iE(V? zCN1Sv2!*utlr|~Q77i5XgO*b635E7j3O(esw7_X;0;Po>TFO18J$>A8e=}=KmYqOP z?|1L{?jKh{vu9?_nl)?ItXXTXy=69Bd+PM;nvhsJ0Hs~y7$G3O%Ph=zgaQOH(Dy$FS>iNj8=K2^`kbyaEs)I=w+mWf;k z{VJR9qU-Gbr6++o+psXc-r1=QV`QTqDBKUPI#L5;3TAL+fb99!6sh->F~zE0G1q%{ zOu@K%)>KqR7pCE&>@uhcn$elSFgoMVtq6*OWKGAVc99+pz@XW+m2-3ps=Tvn9dpX7 zoz_g?(IASv1+`2I)68q;^kQI?-XhkUQ>m9%L1NN;gZ>%iI@*oSMy>Q50PpZwfP+y0 z)zGBWG>!VKxp{^+H8;m7a-DNMG`K9Z=e4Q$~#++!*>ROZ)r`pfN zmd=zpJT;R7j+$`5kxD=+T89cX9@7(WNY~ufJTPiM^-8(BzYtNvVr0$Ype|i14@Kd&>YyP#v8h+g=u(LO)Kn&Z`IS<$#x#{-s-#MTUv-*C zHJ7OLrPBo3vWGD0%=c4wzv8pKoC-1Z11cO+*TAOK4Q$vlG~zX*m}TBt3?-S-bAejt z0px~^{Tp2lMs!7<>x`~s21iWMD-mDn9>|!f?*O{3^SPv+feXduMX!*nihW2lotA8) z2)v zt?!rl@^&G|4|Sz<=u|r4+5QJZ=htMf-kp{g(NC!YM3%x@~2@btHdI50V7(&8v=#C+z8*b%7W`>8F zaj9B`xww5bz{=I>4a}G*37`u7b`mlJGAqU|dJ$KNR`E2|qnIIPk#1TW!5l&kap*3E zBPBR=(-rN2fU-*;Wb=kG$d1{y;W@(?bjK;P2j%Has69K&B~i3dJ&e&Wl<8#|6_7u=JrZ)@}F<0^?JsThUpO;czHM;gu`4F0hOC5*u_P0;RD zZQfu`DRf_oHo>l3)oxoQM3gNH8o9PbPn#_e)TL+OMH<>!<*|mgcDsV~3#@EIdmd8# zC1sAFp}HP$NCnmN8_@IAXLH@Z5fe_+2YGOBgzL2EWw0T7xfmjq1CBOP+Gd`v<%w}x zI4s6#(UAzFSQt#*&2vu1%SrVZFU;J)#G9~gO`08=kopx=rFWp&yM+_3OieMya4ILJ zwvXjZ2&Zlu!?7LbEeiRTy@-f*x$hEXgFB(OcrXTtbRAw)q!020Ip-nB1RVMxPa#jx zw01$D)(BPW0h1%!8|`E;k*gtVN8>;SyO?qX-5gFFYPfXc0vFYb_u+T;SABy38K8jM z6U8E6Mt4In=#D0oJOa(u5pdaBWPs3;xW7;^;9{k>G_-l$;WnS!^|8eH)82dUJ>(9P z9!4+RJa-2x(GO_iXty<^(uW#>bKtW<^_N7iP^E0NG^d|3c^;+rfKhH?!g5_!@ji9g z5X2tVdn5G%+#f}&=6+6VQ3R36>`9Iso70@Ich;1OpoQ6v#KJ8i$6)Sy2y=lvJP@PX zAFLL6XRGEI*0&Wo*XbA8T^r*w(-|r+NB%{!@apE|Fjq?U8fDl}57hpH5t9fG=EvnFJh6}cw1hnG0QzG^+>bE5#VsOb%AT#L7lL}O{cxqj6^;%-K}b=9R|7XM zi4Nit-Oq|gy%%mTkb!m%#x}?fvFtE_uef?x$$PA8KqHko#S{Y1>nISuM6x z(JZ8N5eYhFVmeF-@1R566>x;3C^dD?gxQ9+(}e0#7;uC{$P;cW_PAFLVunRWpjWMt zFThtwCxJA^-}IV}p$7EX9AsEK$&jX5_xaHep~~>|oIk5d42c0wA(eFi6BjwdWoESz zDl-FSt;^5xP_6qNMW&0BkIR-HS|#ck)W^k8sqYqN+Re^~10+XzRQD}REfZ@E2Z=s7fi3$m zP!Um)SktQ{oqk7jCWOlz*fO_=)T%3esc{X@LB(ok^dnsLS^#_X!iAShTfZGG?skmINb5kE$leV zntHSbN1NeAhNC8s>PB8uC33%4B@Br0e3S~Q{!xi~R*T&(=9zb{ zR*Spx2^7rEatYtES4WX2tBcIxJHQ@V!`x2+tHmbxA8>U=Kg|#kx0mbQ2_lMQQ9Zn3 ztmZy0{VYmnXSsxL(yE6MKZQHqD_Q1<$Z$y%#;VqRZtE!6p#?Rmt#C}LJLesV19vDC z{T#P@8Nl#eEEv6;>3aYq4xNNSq!RSKz)f;P83nW2oBli`XPd+4kF^IYpqc&x7iGC5 zig-}=41H0_*N>IQVwscYk^=dAAzvgRsG?Ezdt3I?2m1THl z*P3(E{|IsA{X98@Dh{|DtB#qzALV@VP|bIY)m16!7na64E_{FsF?a03myMbQ1#7G! zv(ow!glfZM_#07K_*LPmP}Ls{HHSDw>U)eqo;t{mk%qR5+w7vPC{hJBqZ1Fbd6t4%}h)D7z)mfU~_#UHx(1N{l;;r2C7_y*mQ8*d#~>`CQY9T`SC zb$F|%4#N;!L&LM=>K~phS93VR*Fv3=hdxke>KX(Vbfyl-HI%wmuKv^way3&o^Hu3| z&<|k?e`b7X-N_XNfY-wG!>~CUfeAC_UV8J20x()Sol8$%ecQ?zZ*Bg@3I#?>xm&9A z$#=iFa>k`MuUZLKPBV9Y#&gd;yK=^sZ+vFujGMpx?uvp^cEt?uPiAsKzo;*)%MKFZ z0`T0k&9=>RP^!+HI?y-xK9K%0`r>tlw&>PZfE87r=d!*E#OGXBJJ)SQzXrS}Vh%ro zt0Vn&KzH;T0v`pmz6szAZ!z71xzw4<e zkp}eWw^-HV0MY*jfMZ0YKRHHJ*Kl+R2hh}zy2>7J73@do2lVF~`TmYIq>e*td3p4o zfIp8pr}U;?8|5MaFN7wu)|+ygNZBqGmy>W-u?+gK+60D=p>}vtS#&YVYJ75cX{L~C7L_56HP(r-7}Z%>r{SCn8A%kJ=Nhj7vg)kI zORj*@QHHZYwyOemDMOHa9oWR;w)qK|S>rQnTztTUt$4UfGLUYjL!c!C8R0FVs{15k z3^$>ooM5a6a?0Efne?}zVs@5Gc;T)d^FZOGWjd^;S6-Ymc7QJTb4s_mn3iuo|^rn49E&pP0LtwSRET?SU@-M4v|!MyfWi z3hrZrbMdvVuwZjR!6k(SEd>Srg$3L*rAh_YrJy;u@T8@#Sm!=kJHV1|qmKslDzhkh z9Cpber)m8QP&>AM$Xpo}>qkIyA*a|pBxhjrz?^~2!=u<$XX{3P1rNK}B6bYfcEHZP zMomgtVptc!*ysyrxn@Un)D}Zo^ke3)v-xa{A>m>Pf1b@}YYd4R#ShzjHph_Q5x#h2 zs-BLqcV1D!sjGHW5G&m(l9fIc&Ps=bbk+6tVDenxvE#YW!dE{STN&Z5N652+_ykSu z6P&b5!+|IAk#7A2Ju(DFUj&+d3BVgFs>TUITM>@yxqRMKmlBy*fkWq)xrVy%(W_be zLdaL`kh3r)?rNX6(5)%}Jxf>IaJ#!*)m24#S}FObP?@uOx~b!7j5J_h)BlRrte*k| zoZ&XUZ+9(xS-0(SK_#D)n8k)|V4j1sMKL|xb_E{(a3_FW?6_in5zav6M9Zw61$6iV z42pfz#GOh145G8M%r&E3V7PHSFweC65HQDdAx9P%X@=YRC?PceS(8svl)tg;!rv3} z{Z9mnfZB=n7@9 z56kJ~k^-HrAa&foev~CnqE&uPK%GNlapvc8)6u8N=#xVZe>q2%nl9aNwTW*(C;8H` z@GYM5vcAm0bxMBDhugi?6=*?(tyej0bh8SR(Q6v>epOLb$Hz5h78_^YyQ-ba4ExBd zYN}zqh8no?$#pf;uQR>M82lx!q3Evw%dzNAH3r|{f`#h-R!KqnL1yOEUZBfCsjki~ zXQA1A(X9p^!K{G|a=5Y3Fsgluc35vhMf%r;dVb5oYsU#Qt%UW#b-)Ynt^H`KT-{dJ zr3bmY>miYE{BmV9pRw3C;(P#V!ZpJz%aN0ptuhAx4Yt^Qev8cli?23|OY_pXp6}8} zr?D>6*pL}qRaju?qf|Mo140MWI8h zS}s_n@kuH^K76lQTsOaN(fmaV&S8#|dH}yQjc6uDFB-uY3*gL_Oe&G=PP4=&$ok?+HZ=P1|p;9HC@W)(gc`c-dCT2*vk(4;2>uJSen zOXy#M@HSoJeZgna7mK!+nDjd@!v_RDBXF1ScMD$PWeqPB9rl}aZ#ly%FT>3h3_l5- zUV78K5uJId_(qSPnk!DZO}g568t228h6W%L2)2})^lKN(e^r0lfm$Vp{a(tHajiGQ zH+oFkB%HpAJ57_mEi&H+^wUiSYl|6Nw^Kjt57Fy^Q*JL#&sp+f@F!BzSYB$UTrO3}Rv*8~vJs4nkm5=LwR{GlCCtA?+Irl>__w8WL?pi` z`0vC^1kO|{E*C!sjgU!OTt;XreYTQ0Be%7$?Ju4Z!!0@3$~f`QIvB0PjN7mIgLY||LHkgwj@t&Lp(FZv#^u~G0sbXqXMWB zp$yEhEU1a0`+^N9F@>63+!s)khO&V`U!VzcZYevroXbqiKrTB_))(kSCMg%{mGFGW zl{kYAiR8PU`HlvP;O!@Fp*;{RYoN1eAt<6(OBA(8%H|gEb|l%vm$Gemi*XJ$mu+^>p~rn&0c%Ry+?%tZ z%1hheuj5D83qMT68IT`&v*{O&VOU8%#5SWy)9ekJPbHV z;N1d)0zc@&ZEvt06iwdD+EphY{`DJtj!&wa^DqbeS+gN zz^BpY0Pocv0Q{}~Pk?`==K*ILF9AMiyaBje`y-%R|0BHjd$eNG&pn3$_d7Jqz~|r@ zla`g$V?HZ*W0}*#`aPZyZK7hpADdy}%mn^TIt%b6Z5g1)VElGt8T7p44zo7V|C+!T z^$Wnc&bbkAt&iabgIj+Ia8m>!LLaHDMpn?MEl*wU!8>(%ssq$Dd5X(EmZvyZ+$q#y znpu24s0VF|g6AVYJdu}NjrS*C5bERZi^~!o2mMs2L+(=MB>x9i z{}A16Q-5%M0;9aBTFTttsc6!})TSs3RNUza)4>{5ugib8X9Df6W9ksi_dn>FLdR_C z-%GyXnNIWPDakY}n@O{9yo(-DJ032~q8gz_C|1c-vrvaO5$=RLFOX}b(NAI}{6y-C-|Tk};Y~`xmRSOFX~z%%#5yrOIk)@)9X?-{JOnYiXuXhurtMi$PT@ zib9pOS{*I2smB6LEw`!nN`u}yy3wYVl-FwW=!8v8LA`l2>uhe}5VXwp&ZFHnRq6D2 z7tqIr;&`bAb)TZ#{m!M{#q?#Hy2iO2)MGX^-?!Aegr2mi^L)!eJ!@0YwcWg~P=`DX7@`nFJq-M_4C^Dd(w*wo9Q&ZQTH z`Yx??J)kY8O5AJmxIa*~)4QDJ3-wFwfwE8FtnkA^eS|tI;@*|?Sw*3LDQ^QkCDe$! zw&H5<8hT!pxnFba#cKAVO}*pDqU@wiJ>lMqC#Y}Q)HCj^cP;%7oBES~FRi0L+my2; z>s?0%?hKHZ+&@IU^;Be2ucF?1Di`W7{i}Jcw~5x<)FkK2bRk_Tl=904`l3+EFB@nY zFUG`vxz>9TRom3dpf*vxP|7b`=uw+uzigqWg<`)v<7uJI=ZR&=b4$Ig)GpKr{Bkp> zLv~qb#qHj$^i`n_)6e}!z3udrP#*{Ni1!j&j2k+RrDefqy_eHNE1CK@^;G`a8>3nE zOdXbj>a5B7TRO%Y2pR7A5FnMak^wp)R3R&g!A0PzPu(Z7AxYy*Bj~ zt+l9^uCb~A(03Ij=~|mIHLJ*?n{BGr$Q1R_5u19{xTfez`l3y()vhl}(bsG$s9#@{ zrpJX+t!L;vcG*5{FZMn!jww4~m(}UF6lKV{ihHE$?WJO&4$!sQr;GMdlTd1g575>E z$pN~|mTafHiUz2^K=NuDDUiIHuCpbdqNCb8 zMMLzUP|C7l`uZ5^pKQrbXwP_t=|!8mP=CgA4ZTw!nWewlWp5cz6=mu5#?ju6&~Jn~ zKz}iw@r=-4Y|86+#`7VFY4j!6DEwrI@jn+aTvfqZ_@iMbp)khrB9Q%2{G9wPxW~pD z4CT4H8ts6F99&Y#rLCp>`-UuVhuzBW!YtDSNu3_YEwYl=rA33d)IsOMLiJHC>cTgK zpX2--x7ZqXISkoajeaQF6s}4Y?1F_leF!ZY^hSv9Eqa_RuX@d;8m+^bfKE%q8@HlV zr;RvzFtC!0N-B8;zlDBjbe;6#9qDx;{(@NYLa7d!Tb;US)AQQih0Qv>8uUZ)XCdb<@r;UrBJrVukI0Cqu^SI_k-###J<5f+ zlCKhNr^7;R#{;<@X|&B(jyuE(yn^AuZ)P;?mUy>10nm%=Rvj#*1p?0z*dVY;;AVmC z0%L$C4FFbP7s9m~wCdm_x)Kn(W5DTj2cU*sJaZBve_WeZaSnFbpLd-P$UE9Y;I9VE zmR&$6wSVwzr8l)dR$NN&NUc99)e z8}OEYpoBR$`45pt-|e{%cy;h0nxKE7^f8*QUlTe;bM=G%?*N_*exDZTQ-a6oQMxho zV{mQ`@1^6~FDqXLr#pOto}h!+7f;YfT<_31`t89#VCOa^=+cB=|c52lN>ec590TUoP+@;6d#q?Qz@y_}%agng_o6 z7;xStucj}D9?+U0^O$y>zPj=WtyS<#ZO&IJi}a(^T-l@_(vO5T0Di2Z6`bFPn*hI8 z%&$lNA-X3+ykAL z`5x3g+LFpgpy7wuSs&H!cRmXI&uHZd{dNC$^*6^h0yB)``h4FB!tM{fdxw_cUNmD%eyVb*aY(<_x5+rF|6{nr@Mv=; zJc3r%hx&{sba!Px;7Y+?_xBr%>7aiQlCM;Z0Nxp{)n3w#eN`mq zYOTR~$6Sr?gs(KJgL9qtgKyHer1U9)$8~R6oAVChz2zOmJH^e=(562tl0TC8|Ea|P zfbrF`Tb(1uTK}h=?T|U@955d9-wSv@;Irg+e$9CZt>AP@3%Vb34rn)3J_XJS|1-{e zjo+9rI49_Ly54dg(guQmbpAqGe^jh}RJ+ggn+z+=5!sD1KEr&A=7tsku63Pf{$B9k z3;q`N+D+gv91%N@V_&)1JZ`*ET&w+6B>yUs4vjT9G}bVny)!1L%Hz()Eq z;Dy3(0c^zY{1&+zv92s~uMwRc!s!rB2Tu1U7atJL0pT0~r?L2`aPG1p*S!n;D~gZN z9-OB=1wBvuUc(PHegMdJ8XDt<#<&CUX~Y19Yg^&<~KyKNIkJe?5M3@wBfIcxlN2!4Ke9yKSBW`p4N$aDL#s zOE^yo{-ofq>1>Z-h*bg`1$GELAn-1McN=W~lY+k{&~QjCf%Q0fN9%%j2s|k;i{Fa0 zd9qHHJR4Mh_zEfZ)I8XZy3Os_tsW#6M;U5*rW5PKmoF{~HLhzGR1t0OG@nCR7I7bCPD)=!eeL^@-2#AO)CTFL0;8G}f+P&;so??R8TU262BPL zk?%c#i(!REOT?Cy{49sg2jq7dOMo{3>a+^`b`3c=0DLv_wN4jdPmjAd=+kh%9R|J` zyLyeb0P56&Y_4IIm;`()-lD*L0N`Bg^#^fl5YRH(e(fIZVeQx2U$mR_-{^J5dC?dF~4{pMH9$IW-lVpompGFRH= z$nw30u5MlgUW0p|{H-)Nhg^>dso_54+1cyXDAB**PIUaI9d}vd>ki11wXu?YXOOGG z9W38BIpF2o=QFq;AFr0=^URorLdXe5YU~nu?R;Y4}F)osRDe zd}rdj0KV^~xmquEYB!Um-9@iy_tJd*B=FbpeVZ;c7HE4MuR&&o_K%K}c*3{z&fu}!J?#=dxxixgvJ$BrGV6Y0L5*g#`XES+8?Vq8IxhUCDu zcq*N+l8w7#$?kY3*Hj|oD=udJt5dPPtNJq;E16!|y<>+$_b-yN@l7cvT3W(2rS(MD z05>3v@#4a4N^eV~6T5oi(u1YB&Mgv5nb>T_I^&(}fL5y|c9lq~^6?sNE-YBIj5V?J zsQ;Q1=?u4`%1*0$POiI+@tz)*vlU3oHr>#Z=uWOn#p9jQElAiSv0(g2fHA&>#XFcO z?g28^J!cSKk`sr$D1P6*ur3+OGx-UH<7{3)Yx|QOI~UMJ{XIP~I38%znq+@(Je8-e z?2lz~#Fluhb7Qh+pwa5-iFafWI8p&3J&6psy?wD%JXN41V+ob3$6n{sOuTmiHMY!J zzH-@)9r!~!v7R)z$;_gKc_#uJ8_ssok{v+C`pYiCfOFCNc3+jpU^c>L38skZ{6g9m z>*>vzhj5Z;CY)r;m zc3YW^$;KE52X)6YJ6L}_6I+e=pf&sYP*KqJu~e^>9B5ALqTY0emFfY4_|G*n8awGa z*bq-8Iz-#1SjV1Ncf6^S($GCh_2u~-)6~!@-#Fm->_zF_*50kj4wcI&X>Ez8`+G9! z4eS9_Xxts|*t5&p*Bsjw?*Y9r+0fAu@5@{mA3&(3(_kf%>3Avwg!{*{Oj)C_{-pRa z-f0&|@dk^9w#0koDjCw+*JAY~IuXD*(^u|6wgiE&TAPUXph#4l&TdMh08;DV<x zwFv-L6|ZZt`hj!TyD_bRrrBu-99lkq`+-fVlrsP%Do;VzrOSja-Yqgc;Y>QsnYa!RoBzohm1AX!J@C3@x z7pB&xtX`YbYIS#`fW2$Cp_a$B32?V2k(}2gyE!K`ZNS_|hmkYJ@EffZ`(Zqrv&t5X zDQ2l`JGi_DZsYj0{S6P=0V@-?FoLO#qf+Dq;fbB+jE1e$DtZVdWpjsgC`d5}AdC5NZ`s(2R@96lRUorz?ur>Q#$PGc+$J2)X?CE$UH zlSDPO+G7hOmQ43xl%=vvHtCGQSm7r1*ZCBKy}2=|@GaI}Tb`$=)ZfbiP1{opH^q7| zFzN^2xo5O09ms{~iISt5w zy}Nn_S`(QGE|O2XmDrBe5O zrJ%nn(cPaCxltus;$1DVu6Sl(3xY8{%9jOvO;5Zxp3HEN6%;9#iJmR-p4dLY3d(av zW3W2=J2Im>b54RO{09U_3!2ioEzKw|r$r(%IWSsaPhjC^61x(-R~ju{HGo0oP?2p2 zLTX@LJfm<~g%NwP9;=&ITh2PNDyi&w=Iw>(QVCvS(MI!&mrbjbOO3Ifj+VsL%7P{& z3N$9PWbW1Ru8mz}r#4PGiqVXTxjvrg-km{dPH>zUre9`v7p?7ybrVcatI^CQ!rVl) zT~lWV0TKzd#=3DQqTX{RWHEW+Iky;4({?N_)?Qv`S(u$-b*lhJZ)2&B-NY%K^V23? z_9?dmQeIR%PW&vv+m-d$(#BJYFH?`VG=)qw0UhW~$8OcOlC^g^ia;!LQ|K`!F2IIZ zYR~pWC$J8yr@uF;a318n!cCC1>k2kr0xOx}jRF>eG&XQ8{asy&js${u&Axa?KhF{5 z2^4c}Wq_g^vF7ceX3N?m&5@N#Aj8LcBuAl^>~h$gj@=DCJv`7o+~OJfQrC$h_9)-% zAmj95E6KVUX299E9V`1(s@$sP$VyPw92x2;;|&z zyL*yb5)4z4R>jc)YEATQ0b(W9q)_*#(ykpO!beYXB+tYRv1Fnvp3dZWjo7zh?d1$& zCmM`|5XhIZsn7Ke>BPR3L1yiYV@1Hqtt8SNDct95veJ^}uvlO|COUD4ty0pOeY|;O zLsg8*{7xm(U^K_OV;uuqlY5fZ-lTF*tAz=|dmB!EXo|yPU91md*GG0N!7~}O4n`|{ zlu7haE~V5qw(O3j`iM7CeP|8|tOK1>rdlakQ&Jf&Qd@ZpSsfHmiqKq-QC^dly=!yTR*}tmc-JO zA}fRSnl~~S$nA;D?xs#ee4;A>rrmk9vjNF#tgLt(zMKLjVj$SYvnPgJlxtOu0Lab# zm>VsU4zLhm_eNdnBFPH=!yZ2+Q$pM^dZa$w{O`^*b#1V)6+!s3 z+0E8oT-6+Hjtv0EP8cC`H87sNTQI(>6DjJ80pN~IrDxo)0OhSggF+D-b{Ucfur;X^ z3)ou?pt(DA+=#LV?F}STb$oLQC>L>4b4NNC1jvBc)?iN!R-!YFdzLgif`?zNByzYR z=~H3(L9_3m(rW2|r_!oFqo+~4Vb3>|OX@rvyi;k3r>?^OJZwzYDWn+`=)U> z0)}_g>D)S!mfJaO$l@tw9!fZwvBU?BP@SYaNF|ZsVUyf6sKqs%Kj+|ONj03ivys8b z6X;_ehl}(0@|BBs?8xBHE+b(fJEX0yOdY3<28{DS8tgTE7Lu0G;(m6;a09v_wy%{p zUz}9a)H{aaJJ2-FvMiL04sq^w-ChRQ?90THd`t($V{=qN3Ge-LIWeu~FM44oMCHsp zVW$a1nw|R)5u=F@hL0ZfAx|fgnGM((A`|ECMb^iAkOk8c$!IIli6Kl=eov#ej8J2# z$-wgz)0-JNPmU|-z|A4b*X+ae=E&^A{rGPC1_(RlG+m_*1_fv91W9oTl;b{&=Mz?8 zi6t2WIgd&rn|5T-w=FofA*&zvGPrKsbp>Bqa0q})ioxo5M=qTfCJ)s!!Zbk#sVCm0 z8W`RGkU9fZn<2?u$gVb}HW4<#F%FvxsNJ%G5?42KF3Beq!a}|q*7oga>g~fKpMa?< zt&gr%b!&RTZc6j`rx1I*bCbT?AxArQz=McAYK$j(S}mNdVJl`?DTTJ#`((b|&7-TW z4%~@Ckay3W4XJKLb@ZnowFxIdDB^B3_NN4g$9Pm_;;aQOi-I5`#UrY2_ZtOO8%~AG zaEmg|i6nW*K6`-^kvDo{oaiMNA)DA|-ok<{oF?+cvU-VZJfrdx5;4=V9&sXT>P(Mi zaQ_r*OPnL2DcRSbkyZL4WZ|5%IU^&zCgrMzSy1j=Qxc1gP&oELhQU1AmWc0FJD37m ztyG;Dc#7>xTd7^rRD&Fas2RZPPHu~8a}d?yntci)dk8YXNOLR=b35bvc>C0qzy%4N zueqB9b^5T%+Q;)kqCeibYJlWW6IV7~!R}OSfErQ^InU!TZ7(xM&$LArM`N@H-`s(# zFmt;SK7ph8_U1(bxuqW?hrit2YKA;1@;~-d5Ux_s@`M@t5i{c)MUX-kr86 zWz!jo1HTMUa%fTu@C^PX2%AmRXhw});Nh2-)oZ|Gst5Eose>E|JxM%;+YPz{Z!D;K zbWT6&o-Spf#Xjgrp=>q2Y1B!fPM@^Mny8_$mF-fRv!;<)E7Ab8jBS5Cbj8q1?g!1? zYuCbg2RIp&yiZ%6Ub~zo^x9s^$+MqgXpJhGL~l|o?1HV*M#)uj&Gny3w!-#3_*%4= zd*SSXBo#HlTNbR&;7>JdfW9QSU8u>usKYO_dfBhj8bziD?L(BER0tc3+Uel#hKF=K1la*hbLt{WwtMALI+8)1j?DvxkZ0$y35 zq6%w1^cBX{c1We9pRBDFrARy6UfCec$1ltx_&wYpEdVwb?c}Ud+L}dIrm)uR2H3{q z&%JtIsU@mpye+54_zpZf?vx&(m*@QFYi&X6z3>)Wftr6ud)$UyYu$goy|Lq0=zFxj z=0C5e<^9L_!oOe3`$j`C_b`RAl}PnOsFqfO(hU5p8o%`&Q(EN3Q-2i;wBqlvf#)M< zsvWu7(+mC53_q(3z`2ey9-{Oto>@uExCE^0MJ`h^xv0y<8-;TmRWr}8$kk|omI2I_ z7$60d_1sq#$rM(R$Jv0RKHt7kjaQ*-;fM2a#c#^-8Utqmo>811lDO8{(&J-yyOejr z;=OpDp3B-4Sc9y=bLt}LBj(lgT#n4mkFl`>FJ$jIQ-WcqBwo^{F2fHiC;rA^l=j;`c2vMVvkIl=@WX)(+e*eyP!BMGQ`Yr;W*Gro>fq7$N z%`Tb7C7gSCMdOyo)okQnU9*q&;@WGk=@D69I8#KVFA>z{6_8iT2xNJ5auI-F!RLqB zC$=4Jt=jm_bo}fOU3OR_M?}-y2GWibNFczZU(j~|)x#q`Jv~Bi8oSMH>_xpT?OY@^azgZF7 z3LWlI630^9M-;7EgR>P~dt=PPyTuxdmLw%Qjql1X&N?E!v=9@rFUX5q<| z(@S-hei&ZK-lXR8U_yubSRAlIz$x8 zbiWreXh^AWdt6SRFR(eV*$3P32RRTzBOBl*pDW@(PkcT%=^Dzj?~21FPd0`2SGYXB zDHVb2bx`DYT~x|vseFT%-(u|`{-h=I0( z{lR{R3xO6OH%P>s9vq3|KV5fAHv*gV38h*=7avwmV{hq!QjRZpJMTx1umnaOmf8w} zt$G<06ljpD9Qv&=&<7V3xlHbl8-CvGBM0m#h24S8Iwp*p&2_uoh}tr8JK1NOeJ)q9 zQ%4m)w?c-vRQWS|sEA|Dr)CRgK_GjtI2sqsgaOU(!`~GMY*sk*A?hdlTzX(Pym=Gc zb%@<6NC1E89Zl#k-R8)o`Y>Kx431m}7YF-wc=;xtvB4|3=B z*@j^Db{@$MDp-&*oL(IeX$pDpc8)DA*vGSw%|;%CRl!y@D^a->vyCtCmhN}D;Qo8d zT|8x=I{Sd+mh1z-Si7(oK?2G?fJu(TlYIal*KrAEzveIlBj4eCh|6TWU@Ba$%;SU5 zLGH;u9vFE}65nx^MUG=S=mGp~^X%6+Yfk~-FB_d;eJ?o?$$mdx8r4hvrSLmDGPps9 zsln{epaamaE`}39)T}FFGRzw}i2#?>39O7@g}aOtKhO+2k;d67)*l#o9jduP_HmBV zlNxd^_v>-|7jdBydM|?$Yz?-$Av+}Lj-lJlI)4L9#aLbpin!d9XLzCJq`?~WA{SvjjT2i9z@r zPh3~`_Z>o1s=ZZio4H!{1H2O`Khv$aU)^2gwwwO3H{uS$pYI09GC0PdauvW%#_$@f z#z1GlZ^$~B>A<`u2mf(P!<^#OP5kH9O}}9Vqq%h)ct5@aOb=o8bE`S25LpKipV<#H zxYsZP&5&)ycc+)-)QT5SXx^(>)W&h#qLmYoO!?6qF_!duR6WdVG_@Jc)ZmN1X(3Hv zIutQxhZaiq=i14I2L2YLh~`5PGv4H&V>ae>tCO81n=2>J)+tVSDH!EsFVK%226ji7 zX~-)5Soe|DKkS89OL+|_1p~8q6PIFb@M0~+x`y!Jr2*Ln2!e-^X52RBT0|9uAC|Dq z?$W?+p0=&n-|&KYFE$Ij7i*qK*a_uQkxDy~D85#zV<>rXf(}xd8+it@DmC%c?Jw0m zSYRbin;qC?wYnnOv`R2Q@cd^Y(2RC5&3^{_c-OIs^WbD3>Xl;t;6E00pyOBxpaRK3 zs!2X#&FrgSYY{{^JMq7ALSp84DNGR+gp@j->7IgQK1OOxk~s^3JFe6oU~x%)BtzVP zrMoakm8vFrPo|O-{x1@C+Tw|gB-+ZUlJ`Z$d;w2QeIL?fo`5_2e(Yg$3lq@Xnue|; zDvBbxB0sVSl=^v}fD8>as)XHy7GOv&7`eSodCKADK#Ouf8By{XhzLg%qXmFT6(t>I za6B+S{-%$-u#Qb4?y_6;&~`jCzQ{`EA4s?E<|iW>MmN-x;FO`PmTQ~ zXa)Y}+|s&*P~tDo7uqk3^C@{b*NWszA|gFWqmVkamgjNm)LI_8VFRg=3$NJ3`OPUI z7St`oU$vW$Ka7VzO(cXuSFc&MbsdiT#6&>MJD6K?7Mok7$vgZAYWz_<)dxM%>EP#i z`9g#|ZU$7Llb=F&n(FkQXpTk)_wY{vTJ>VQ`C#qcfaf3yyxj|irzwf&&UnG9lf|0l z7xRBpjGg$mwDoJ$%}$Nio&IRNCXzcbx1ZzJL~{OLPgf# zUIJN>jQ8X4zNaR#secz90pS?F74HfrS0wS~qdWJ43o`wPMm?%o^_=sx6Z8MWZI_ID zMFu7u{4X{k$d4;>PFBy@V8y5Zi>;`_ds^xx6ZPaJjpvW{pynUv#8Uj=3Wrjd6};La zS`kVJbNn!9+-LlcQZ^kP6}W=&$S7Ti<^F%NinjwVHLmXI*?@N<)T`I=xV+oLrSHv# zj`7VDs`~%% zRToaG_%lm_yA`GU%p{GoEIyx8&&ouA9&+3dr5bGF(>HZ)e7dHOip+fUTm4&%=SlDZ ztmogj@d>4Rp2nxsxl=aQug><=nKGX^t7m@ibEXXWZK5yN{`)*F``^$zpKtB*9vXXE z$aXHk!S6zR=gXJv;NR%6C)G1ae%{zq(6hg%Uv)TrW&6>6L+FQ93}FBcNB7`Pc8dvbcD!NRPF+OUJ>1pjqlHbUOEDR zGU=;rHz@f;h;}rjGwEJXV%uFnxT6R0E&4QJxEj*&ffNMURyv9=>&oF<^l2hmQ$z|{ z#(u?zZPUp?@56X3?IhBK`}c!&Y1ACjqah+sGz?HPiBrX#jk*!72BJqHTN`y?kZVpG zgTZFa3_T~;HR;7r6P?Cd;C4?<`64G>XPc9m4$eFyHG?nd4U+_;@Q-!S4P9w?wn)Ql zt_+YnH#Jl0eQ{E;s^{T)znfGrrCut6%CLVHE;Z0tzQ~|9T+x%jm=o3Y6l!>al$wJ} z{SrNj5wYggSIyT`bHSNcZ_cl%b)@D2kCvdwQ&h`J%?I1c>n+dgMIf1aUZq}91Bq#9 z_q!<9(Qfn%)XGEwJYx$0N1?!OXj*!vMnkFE0>hJDkY{+EE8YVQ&Qu*rQ}qD8+Dl!j z1|Zq>=+V4%OI|c@^+J@Dq_>}nEuASh-F4FfW?eX7rW25gViJ4m+?G3FO4nQ|46CR9 zLoZdhh7k%{xg+4Ha|Rr#MzCMBa@^4x(lB3jdKX>x09C+1^wpG*$Mg({`Ozh;MXRCc zQXr{i0G`@)R_aV3-t-7J%o24=(7CgK_?%H_wn~D!bg3c~h1+U_hV;ayUb3PqAo`-E zO#H%2WmcVKDZ^ArjRqI%w2hH&Rdh8N(Q^x2N3@9<95LRPBEIy!&}F3`2Xv*H zxnvCkbjzynzC_L%_94+MT6O_NPJ^e{>CqNQ)I0T5E3P#aHJ4>D)jc_m*@*zMM}Xtn zRCy6v^yoTB>S|Vf8%>78QH({ceh^(Z)2|nEyv&)VLndU)ApXttSreR^v>rah6>(u{ zTv$YpBIbCW>$I6o$4Aec2iBoVDTOm@4YGelwE*o*FSu>kxTPwio)(mpKr{*-7^=Q*UUqpLIL;*dD){&Vo5iMLWvn;P} zr8>Yogq&sSPKCp#rfxZ-F$gHL^ij5M3^8cVs}Ii~LsXh?S(l+avjw&1<+y~CUF{fR zvRGyh%XEnhmy9i+Ts*+VTcwywq858X`W%H7$OW7m_wvxg{!%Tv4Wgd(c6fXue|oE; z-C#$z17J2>h|B0j040#W7+1^f%3Ol0GkPgtScD~sTjgfJ9K*~gHh7RN>Ve?A9G659 zL$&1&H#~c^xjW$KhEn*&vuu^E-QBHg+&3nyPkpCT@;iux%0Wm%wq+uZF5TDBZ6J7Fi-Rcp4}O$ZTXUj&_bb>N|6 zwm{Gl4>PZ6JF7&|cDsUf8?0P2_fbLgwA>YRDmMqDf6Zc3&|O^>?d68t>Gu{!3yG>0 zjQf}omqLA%122j-jSIdwQ4rmQav3mNeZa+z&NX*iliKm*+wr9TX>vQBqINv`C7ZoF^$`eKQsO9CgwuaXT2YnmuBNPLr zB+m92DFmv=yZl4o2eoJt#JtfIpl@^tu*5wv56aD;!$LitJGkt{1)gk@VdPL1LV%2P zz~PG4!;WYgfAl9tyK$&cvAUubQI;Og8+49`Y4i; znK}dlN^7;yp)sHcH(4JK zC2hN_E~&@vFnS$0x`<$|ag>{u%AlrrCSZo6Gte*HlwNe7B*ab&m?05zhr3JMuGOQM zZPE8Y6`aNE|6kxM3{@b5scd;H^ANUPrZvj2ewra|v+{GJ??s(4w5I%7Q))z;P=hzJ zRSf{AEpdd)ty&{gZUwA*r=Q2ZR`;7;%UN%PYMr+HAyk5ThINst>J@(?!5Go^u>(eV zOH*%D)cTcO4!;?liMD-WU5Tr1Mo89e*W{_x)Lo+1^I$$za7UVWg6)t+jU74>1v^yE zYu@hW6_E!|hN3!py%NLZm0&@tD`cD<&9F|Ap34fFGQJhj8(8zD z0Is?Vl-<*$3EL^ou{r2%ts{CP%iRQknw)0sR^7!dx{_xtCe{_ zN|obM8$hRS0quuBt7^4yEP5-bnt#)Ib7$2phH@U&@DjtU!;YDmHQ@0y6e4{I5}x8m zKfpDykyml_9$b-IuP|+(sgK~oduNl}nES}0fecnk+>d`01!HGRHwxSMBzNT>P%tmY zCD8!<91Q|^t+BhnKGee8Bf#o$Q*bw~&gjP&BKok-TA6!5@Pbe~cFttYDK7muO6TRc zBwE2T2(;q;Hg+pk<`W{rB~kcaweEAJJ_&Yc@%o@qWp5^p^x}e#BpeL6O;oXDxI&@m zr`VQ%1PB@uDPx~z9nsG)`#yktEMtoD(OIC#g>u8qhdZ^N%x57w&l+1b+19;m>*u&A z$0fY0SGFGdypl&UF76w4PYJ~)b1N#vx^8m{Eo}iZZDz!Cf=@*vOIxc*W3r{N)UTSCF z5<@nRVYR^sbL=<>(SVtRHCD#4lUlXng)%@O2)AwR9^gYHX&swB(?3pg?wu`o*NEO%&jE&j? z)_mNMgApV3Wv*UWSu8^iN{rN3Sn_;0L&btF{zmu)^zinz4%KHl_|SpFRr0Nkj74CD zr*_5|)+Jm+W3%MyAA@C}E!l|JIZ5fn)cYGc(`^W6=uB^rYbf0zSATk|T&?tWzOtbl z7vF;|{F(jP^~YBg0baW^Uq!ESyce3i;Nlxs6@iJ;8C-h&z^$ui|EA-iRSHa$a<^3J zH-7iY)w3_Yam{MpE;~qs zi@?)Qwc9rHTBy$adeJweh(P9R=!?e@+M=hv4$ND7jx+TL5T9dx{Q{Q}Jqo-oVvRkD ztC{&{Kv(n|0v`iRJr3XqZ?Rm0In|lh*Ix2C>nx{z7?;2ID5Le;WT;Bx z!Tw#3ev?&w3n2O=0M2lc@#PFxUBi3|hg2L=SB#G(2hd1g0sR?FSWPsfKMbuE714iT zz23au^nDXtB>zRwWYv4p-vw8WxiH>*2D%0QKspMHeH*pIOUk2-5Ulgb!Oo}$=EyN2 zv|2CKW8XWorLe$WZWR_-i&wb9x~a1;LBBdfn@-NTRF4?fxV>|W+pB8Oa0ZE1Y~{5D zJg*YffuH%EUdcHUgHQ<6(bCkI? zv+ZJLaJeu90YfBNLXa9g+JqKDZy9oS2b_@o0-5iiMUR!<=EU*ycY)8h(t`zDx1z|# z7EXp|MpDm6>C8hml6n@j=(-ZRke&lZdbm*2(A6&NIj&@+-(6gQ^}|Zp1^-Z7zy{*> z^{tQ}(IxtoNk;m?VzD(4^YRV&ehhYzSh%JEdo6eqz0_*vmy!`}15xu<6xjyZlU%d7XJF3H>2Cl2-{TRxHunMt&S;n!j zm@j_w5atr3Utk3d&y?k7CI`2GyzKkhVh7N>l*64L!qt1n~KqK5I_=nGnF}#yv?uEjjPKmj0*8bOHZVzkud!onCa$Qe!!WKhWbe#F?Z9W@gNVuegztHBhHHJiu z;)iWMn`21u2wyxhL+8_HtL7yYoVsd91+mhtB3bEE;jDB>NY}i_9!#DKJa#-6c&Ix3 zVDQj%aMvT`UPaNJ+>pAr+o{Pu2lsYAM~@7F(U*Z{UIFlgytOzc>GtA=k;@m(a4M07 zm3V0ID%Vh#=vJt#|3S!CYr4YSK9>@AZeQ5wQWb!np(}29yKB3utBMM=Qu0Ws+)+CV zhp`SNWM4DCKx?Ug1qe98-F!CTZ2Ylq+vS8x-u7E1hHYShgU5Gbdbs;CJh0^ia@GQPFwQA=A9Xk($V&CL(XEMKp=)4?rt!N(@E*ya@wCp|vtVvzSlSM{a;q81} z5?b_>#kVQS-&jH6Z!FBVzio$m;bNZ^?si>x{TR}8iPWv_b3%o)c8fb;xh@PiyKy{i zd!*Rs(^1SdA#iJ6GTeQujA3o*L?L+L?vDmUXTTX+_$gFV7B2i?sN96pm%j(|K!=lL zTy3|@nADwNf?pM!SvECReX0s#0xM7y-wrv|)4> z&cP_3o4~PFZS^~Z;ppy!#x z;Fh?pLzp$D+YawTm`bMGaEVV2ZDwMW%SXAYdsIqCnVmX;aH*T?DCSLclcx~Ri{&?< zAp@BEE$b>Yr%pR}11rZSCR_37uS`bYh_^mKmSW$t6WPw}8}fPyKF|_zha+14(tRUZZm6Ec6LB1WY;1 z<{VjOId#L?Exx^5@}=4M8~Z-fp!tvw7Uq0-yQj7iEr@XHH4Yoys=;LRSjNJCt*mLt zB+znh8O-1lAK9tQwh!-WW*Dh|Lk(P`5#IA&J{z}c&FU6?;)s;bLGc)drGLxPYmCu1 za1BNO9k2pR@(g42Ke(V#-7+gFNPn4``9|NR%TcjD+Qbe*b8-SD_PpOP_0blv<*267 zFlv2@HdAjxMdo+Kdj5@tu{E9~%(N2LN7n)`zFl{tsS0(c-oQ^btlTnhaHXP~_9>e-+%Rho@Cl}N;)OJ@` zqb#G3a%>_ruY=&kgOYlqx>lQ(IS0&=+Fh!N3Vl@O##`2VrpV%G_%vJgY$fYe+I>!! zya#~$XWlo6PvwXwY>Ru-o!IVW{s_YlxC=c#6MkUhWTC(oLwaZ^3LRS0`K~pZ{pOVq z^yNz%7Bwtcv}ExL5O_Ob0I(`TGzW>`DqM%Efp=!piRA7Kis((q-R=T!YbU*Lje5^* z&ibuwh#$HW^zCIRTQiW_$-?;PSU&drXng)n5qyaj`6QVNta{{qi_ zK8vpKHkVrTO%KDX1RfE%Lipzj{tFLl*diM01%G$tLBB;QX!B63^Ln>MZ@EtZzrW;q zx1X-8^tyxe9i!Y4qW6b-Ah|Ypq{5=JTwKcqzC~A-9`t+Yh4Sm+0z7YnhO>q9@yb71 z7KI=aq?v$zI&OGT`k=vee_h6wyagK$Qx}qzMGxr*{eG$rdZA~1nCpHPlBdx>8}rL7 z3cI;^`~Q<5_JukZ@@eN+dE zEelq!z#Y;;I-kZt%?~iOgkp3U)cxq~Dq2eYWXvj~QlyGiw2Uc~eGq2_Qh1!ocS5b>i%@fI1+E=7y9gP&Va300V3$*XfWMt~ebdJIj5O zX3>5Z!(j)*<>fbN9l6I!59l4auM0d@$@sqs{GPz~mK@M$(G9M-?pbte#ZB5Q+{8rj z2x?np9pIYErGT?4R{{Pdv<}b}X4qTW278wK4(MmnbmvC*nY7i<@NEafO9bu`_$Pti zc5j58>nvDheal?uz92W^=mgv)@NI#Y2)x$M{7;v5!k&fJHo*G?-exhU!m0%96Zp8m z1Av`5SH(JHj9+-?qV+-ZBH$kC0en@v3~-Hc5bzx0tg=!22;e^BOgIljRx4Ab*5HfTd{n^)wS-I1u zmIhlfE3-mfQ`Hk}!>qhks9Ri*l-GDnx>2Z`T~C!S1oc6oo*=FIAXdju+LWuZA5_yU z!sj-+*S8EU-)>W{InMF8=pmchR@SI_=$_e1a!9DIIZWM*C*R9F9{QzC^;Vzb@zH-O zswy6A)JmvoE=xW^e?qn{p^Q*$HB&bgs3uPd{YFW;T72vAy3=2Tx>l*S@(2s-~ zr-ha9JT0$Pk}a-LPZfPrsGD7Xu1x9G^dq}$W97J~8e2Uj+3vsAGmT1klL4I}|1F-G z)NWHhExpq-hh7nCoF0Xp^XNy5Sn>(F2Qf6CjtMnRcULh*iS(`B{b$LqJ@xdIP&dnHHqd95Dani7Z+IH0?o6gsStGRz zb(`yhE}yrNI#rqLlddvQ7b%LiRW)Ku*K1R+2bdbLscC^q?-Kf|O&zXi)Rt1+S<(Vs zih4`wTASKd74lw${-W3$GsUJJe1vS&Abl=(D zv*`?*D)F5QYLQKS+P5rq%?ro-9h5Ci|=kig{8ag4=HT0p% z%e*aQoGm><|Mq#;5kGW66u5dTukgN$mZ~yWgX19HP+w_ND;-y(Y^_Z_;yQ>maf3~L z%XPK)e7eAoClD@9W zWOnSOe-TP$x4rb7P>1nkw9~hjeqvLvYZv;`^s-I08~wfv{o1D1(y%W}?iOzEFm-F= zzF`X3)VOht?{b=LQzhojzI{|Frr3Z-m0K#m{;ZR#KNr#)9wrbzM{I%tg*r@|%%?r?CO+2Sakv!JwZJrT zyC&cZA;zEbGThHMpz_hMcbLH6<(R;kpO?P@dUWiwrq=xc^yqX=IrACU=<+J&tj0aE zL5~7A?G_gU*XRn7{{U`nmAo#k8^on1eW1t^Ze16?A$(=0jy(?7ea2)st$M;*#+8gl zpBDYfekHHq1F&DGhhT$23&J{D{3rA`-$yRhXmKga>=9qRhEkno;Sj;VerZBd`B1@s zLQge%MEZ4x)GEgB6HA^c(;@SkLlp&bzCEr1?c09b|%55pA#n+3KB>=3wJU<^>B1YjlY6*w;Zn*lB1 zoJO;>o?ry`dd#^4a6WwsP@{*1b6mT)ay6aMG*>Gi@0_ng-8R5i%Qw**+MVtT=}%gH zC`Ovj8r=H#{QbahbtHjnPMk0h@09PLKLrlr*~3xqyMdoDZ^8qhKV#KAoihI0s9I0B zKL@-g_#iFN?=Cw^i}jAw5h_hkgRiGvR}DT&u7C891K| zzeq8ydxc-4qx3*XRlJ_9a#s1>twl1!WX~(ra(0^PzES7VB zUyyzI3*yf=1YV~nst2*x;?k7@j{{z(9j8akn*g0vH)(G8_7>p08*iom2z^0ohs;s! z4*lw?C$uiXFSa>vR+Z^T==!QY{fK@%)DQR=_S8r8g}#jL)-JC)fYN1L3i(6&{rYtO z)q0;@_w)V_gMXxiOIsmRjds5bc+_ze(B&_qYHeNFL7YMM1aHG#n6hDs`-Fbb#&4*s zHtx{(hSwWc!JlKs`^29EG~c+*7@(gRA2N=RJJg4Eoz_QzpMzH3CnNH({$2m4jT55h z4Z7F7-*|z3i!r(%v2oP6)#i@`equbS>%LcUs=$7DgAU`K`%b&{pH%(UIHIR~Zu5Tq zH{sJww>DUPKz~>t3oS4o*4wHY0JDOB*WX|+rOW(FAz2YR8}KFU{h!tMS8V|P&w;be zBf39$f%$@di~ABYrrlDNFpujs!4&XcxD)0*(&C8hD&Npw@EyL*&XZ}h5 zS;?b#3O1wko2F)LcRX)a8?TrA)LdZv!2G#+g66p_I!;%(e`6le_J-#;RxVGfM2zHcSGe0$76y&CGh*=ffw{e;oBWNMrG7t+!?;d5i>N`r%?BU zxF2kmS^1*KH*2>99gb#=@23++Pw;z=?}LwfVJZEMz?CxN_8Rpe%NjB6beC99>TTs= z>ki`m>>b3r(-?Ff(%+Q2|0T1=)Ogm68+t{Db)8Y?zsO2JW`{Ly9P#%9z8~;S+V03& zcc9%ex=!nH9k8y`zEgD=oH_pMtox1goVQvF^glX3YaP)Z3Vz-4LBmn)S+W0F&Efok z#qdRIMD73>KVem9J)u7W9&zf<(*-|W@Cxi)%fMkcB6c1JUgdnjI9k%E%@xVHB3UPR zo#5ALv#J+6>!j9Fjic*?!7;NEHfYqUakR7pzl~OE+?Ot~uuGfg`h@dh!TSUb2)q-c z9CBr~hpUiJbbt5^*KzG9$Y}>Po(q36epq&n>!8Ln;`?=Rk zrR^?1ewT19U>O|&te`IeR?*`wALhaL0hiMYfGg=Gz$SsM^fK`C=$C*)^qR{>x6{7^ z-bsG|yo>$|@E+2z#rh=Shkx`L@&Z0ZWjHUNrTx1rNb>+AIG=m8WFGCIxwylwU=9v0>>9rD5KTKQ7<_Ld| z@LP1&-zuf8Qrat=!va4h@EZbO7HAkOKSyATz+Qoe1%6E6QG+e~hTyLl+^=zyYdvPZ zmo6=P1#oW}IeK9gIhb!cm>&^NL^w@?HwnH&U`{wW!N&z25qK2*=Sm(G{J21}n4?*& zd6vbk&l0>zV6$+V1>YfX9GtnHBLa^~-A4sKE>LrFX~fAoBhHV}k4k1a*^{$`-z+c& zj_!{Mo^x`$<~QW;?{G*$q8p%IClwt zMDU}69}`G!@v57(Y3{>R?l;}+frxNs0iWxcE1V|bG`rtRGyToNj|mh;U{Jr%5X-uG2puY6@gUB@~x%J zIV$)ufmFtPvy9t~2tHTvR)Kv29}{>?N+SW5ZxXnlK1wI(OzjS>Ro`xW!uX1D%qTOP z%=eggm~WV0aJ=Zyt<$a9)=k#Ot%t1dS}$AYIB#@**!g*9sjJ0hVE=7lFONxy(^0hx zCp@uv1$R2@eJ21F{&wZ-z(3>q55U(Pe*m22WB6nDe*s?W#NjRNu5qh>@mp6NJ7G6qBQmpw{Obk25~sZyo_CZ0U(HXA@T8>_coXgn zG(2Ys0AGXJI?kag0XN{CV~sWf;+_`w4H{0Cs)2XVG~inRb?U@3CXKoP8~B+KIkb>A zqK#|+sC`Mp&;RxJ>Cfsv)(vCGxYl^i_*dg{^W$cTqsFnrvEOmC<1xo^#~Y5btR}0? zdcyjx<#Nt));KSA_Bk`o1J0|Q4?3T9{?<9&wZvuS_>M-;-yGw*4Ibdf)A`S4*PFOs zV<{&so8Npu@n6M#81srh1Gq<;TK5R<{-*La1y1J8$2}O|{S|+HhCBVKbw7gp=Bd29 zai+lUuvqZ1h4tEkm+>qa-!Q&a7?)~{!D;wT!}oN2r{g;V-(`5>zLy%by_CS3meoE% zztrxdW%{G^IsHlcCcaPW-vjV9wi;m z(>0(U!S!j!`P#36n~?dl<4O9WrD=~q=5goq^mBYQ*Uz-9a^K39fmAZSdM>S8y`rb5 zaZ%4ATDdtL-!v3YuHKnvtnTgUX-#B?24W*E1F=kIsfbC1#ay8^wJnkCOYLh(CA0Bl zHX}8qWXo_eiT4#Yr22%pRG3^*kmlsb1@UwykxI7o$CA6_eOytAOlfgRBe&BzJUAFj zkE~7NC5X6cWeM|7vhP&PCCkLrbZp<6;cPaQ%&gws)1%OXs*LRtA?ajd*9e; zvn_K$B9quT5SQ*Qmo}kciD0Tj?WtH_ypMa|mFkRLE|RKzsz#g3^;fVamY#54dm@wN zR#aIl`p^;|7+{VqDs9+w^FU&Ea(y};?~@L&f1nmayLjr*f^kZhfC0kTH=OL(bdi#5N~nB2_B)qmPwa&D-n{mSkjXTl>3BNPD@r!UdiTV3 z$J_cS1CsV(u$@IW@Rfjf>0E&cJ{Jv&qT z+haT91E4o0n|piXL)r7=BZ#m}2CPIf6HjMZ`4AFyU*21NVj)j9Wu3x?lj72NpIsou z8&WK^B|a!u&M0eA`#Vzui9W<}zALNuAVY#cbgfIo2T&v`PG+}dPyng*aQ4Zi8&Any zmm27cr%zqhnT=&np{QBSg_EqZ7m<@&?u=)%Fe`IX$(F?6wp1EDTAR!}k=9`i!+c3+ z;tidtVc^{L-I!fKGwd|P53O9Zx~HcT9b!VwO~w#c{kRO`%Q*vW_Mi{RzF4}CI(yTJ zopEZ*aMIe8-qxSU#yb%%aq1jM0CH%yWm*%vcE!{2Bp3XHTkv=E zXw#5fIKEf_?LuqcM!s|p4#?H^gy2{v;a@IYyFZZ?gl!W9hN^})AfOP=G-zM#H7CC; z#}JW7beE_0KuaOdQXB6EQW;LO=$E|=66$6XgYmABq4;^|JIY`^Q|r>HL7UT++PxbE z>`%K5wKA?tfIEPmaR}KiK_~5)lDSqKWYo=pBXWZ9Tr6-VB1`HOlU&J8AGWoGRbVHw z4{c3iNm!fQ&A7_2u$+c7c{}aZg0}TznEpo6YDqz)O(yA@_%0@RkT<3K63N&=+wLSd zEwN0TBzqxkvop~u#~yX1vBV+#fy9!T0mitpObO|a!kD*N{pm%;;Dov#&cp%Cp?$nq3ChN_ zVeX(^Rm2IiHNI=pF0zvgrvSxh$K*OMp4i==MX4lN@TZ7jYGwPu#HLvdC+8KuiI_5R z8|*0HlCIcp+&!pwqX`K`UXspdVro-&C%nk9FuNwTTSYkf983516T68s&t~ox<#!>< zmx{+(mnC@1a2~dm@wDR0jN)xYF%wNd2RhQ*ui93!_D(|)2xe{yoyB|v*bqza*_P-7 z)|(m_9!x5ny}M7i2}2L^cX2e|!H^sJ!|Mfuwj zT01y|%{6XN@*{@TNlab|Wbe62ZWJ(ZNm>&}m#8Z-v;{~iDGl2llD6|W`v$$qlRP;$#FB|!@k}<)Yr$?Z z8>CUu)xJ1Ve>RP@!0l&x)42QCoXSYzKXo>@6eQXG$IYf4J z!82L34n`MzluZm#KFid%boR&6L&STUAvA}a){D+5Q&VYKJknV%QX6axTLWh7-aIWh zk1{LX8B0^vzUF6KbF!}mqmstHGA5(RlLq^y!t7A%PgkmlqjpCkn5O&$>|)ZC%3^iqZ3jknTO!-v)`v-v*p&binI?v9HtyQu>7f*_ zt9HUrJCk(YT?LYtUs)bG=sB56j6qX3&%hW`S>7x)W=u{vj$IH>OLj<8$1vtyr=%LJ zaxj2)sf*-5j086GX_;VZ(*VM5lQAfI$NM9hb>|_Y;-%3Og1|ChksqY9Fp@ z{@hRsNr$R(FUl+DZENJLxLl z76^MrAqMUIiU65Nt}vYMT-A9V)`CPby8#d zVWhE_LLau^V1`n|S-Ec7c^Q+PxZlSm&7d{jn@`Qf=|lB^Fgwr@8i?;w4NPpTNO?o; zvLvS=Q`?liz zGsxqd=DQqh38n9LXi-lOT!#cqE%C%aR|<&7(!xw7WIci&H+- z;eC2vb9%R;dWX}fusMbsXVqHEa9VIUj|WgD#X8`!v~YxXOMgdxZXBki#a%G3C?=Q4971yHofh^62tf*ft_bd4yv=KY1nolX3g-*iO?E zJDIInxl7A$!;p-yyUvctQFYrc*%RYI(ZY-Pop0&`OyG@+LHvFyi}xM+@fWvJl(y+C z#erW!5&S`fPT*Pmmmq9qQKKC-c7iv6-#D)YkEsFB7f2msN9al7eT;t4{GNrXM=OR= z_hhLQS{#CoG|F1>MZyD~0FPT_P1Ic6$~Gy@Tf@KG%0QB}Om6=?=!&72+z(o?&#r}I z6L7L9d55;#gLXMp4?-(@DKF1{ilH^CY!kgnv2Yixlr~B)mutTNRJs+m@4+{PUz4F1 zjsZv#ehIJ>oMF_Z>J8AB1a}u|axWV2SKJ5LuQOXjW&rI(L_-!=_7L|7W8yfQ;hewi zQL4?L4qGmBlKm zumOR-;<(xdskHQywRNEs*@xSkmA63EY?pf3VhKltqIfL2fM{MbEaFk;zPzLSGF39w zf|Db=2hXMZq#x+pivNDCEoglZK4L3S^Y3Vn+pue`|L?aqdDMzMj@H-y_w{tX^T?k6 z_iK5_7;s1M%h1(`P0heFbo|zsdz=O>@rqxxf_v8A(;m+_&P6?lmg3&G!<%Y$^9<#A ziU>S|XH62bDSp2W4mmYy1q+oZe?_U*&F5BdB{fwh?bJUcc@k9daUvt2&s zC&vT&oV)i_se)aQgeehBa+OXwVK>m!1|lLCq2gv#x+_kf=RnV(m=v)GG@s-$DA@-c z1Nd(uQE^-RGM`F3eQ1|*_~{8}y~!DEA}jy31~|J%;1ym^Ce$_n@0!Bn?qItFxdB80 zQ*5Ae26rFPx9wDROk~z~?&dDU6t6O;vIKf7o6%+h@xkMc$6Aoxhd&M7b@;;?i}W}p*O8)V;BD8SrtcHy@0k%Kt(36mDs>@973~q%ES47v#sjM7UP;lJM?UhrxR_3(JgWH3b z`2E3{18?Armw3=tWpF$I8Pf`mS2LK77J}Ph1_WlAfR{l1=`ax5f$2V{Ss9GO#NY<8 zSoh=iGw9C4U~zZ&ig#mK2JiPMQK%^=Z<%IlWi!Y>gLK0KtC52&rvYmc7UAOOE(XV! zsis#5u?igmZ$)r?m6J@C@uRA4m1#6x+@oALu3WiQ4=i_C?7#6g-O?TCyw6wW)M3@Q z7HqOFY$TKDjLQxDUOlifu$+ZYubNe+v-HF8O72to3~h$$nOyGGXW%c!;~za?@iM6> zQi0{nD4oo3%`Gzw9unwK8jEy@D3s}b4`k4gQsJ8E^!hNQV2x{)f!uZA`+b3qK!-1o zyB`0;V*D)-MA!IMIFHdl?rOho`J53GJ@tX3Yk>~GU)hq^P+q3#tn^{k9qxzE<0-Vj zJ$JdRMR-kW201(&Z1AzmmA7EBHoY3!CX^NF)GH@7o3DOa-la3xsYUWLw4g`NOXaVD zGLA^}zu@_h)AFJ80|k4PQ%XcCUkwYn$dpsbBw52MCww)RI3|$0AJfH$Scb3M5d*#o z4hM%#Cni#WTOVa@NupK1tnDBR`7oKo=udjiXSPCd{Cr`&;o{Rj`LAOZdtN;>>H z`e1V;BaD}lBRGCFLLfM-`vc?Gak>faWe?np_b4&3ZVry$!D&q|cbdU&83YcHaZXOx zbCzM0r`Wx))%4f~j~{WPm#UD(Uo-qFzWn}ar;3l)j2Yybp?Odr_FV@pJl^-JaPj+f zw;jGy_3D-xxLlbB2LXhvnR`4i z{;Z_zAE^BI16ZR6@Mr9EU*q*=Iskv2PCb~wK(Wf7a-S0p#I@g#H)ZuQe;Eday&T-2 z!{lJ@1atxV)x~fil9ua=I2~q=zlv~|G(By5H*7wdcjm1WSE_W9g9;kWE;3D1Y z#QbtiXW;T;Y7Mwt!6BDR$C#G0T^f&R)=t_SVXBrLR!rC#K3^7WDzRgW3r{KztSl}L zti<1Nn-0_Ata7-9V?||oEP|RE>KSmA3T-$aOSK;X=?Ly;-N+d$U7tU<=Ze4&JZCrj zedixlYD;+XF+jzsP67Pth9|#2_D?Rv9l0}Lho z`F8U$f0;5hdl`^z%t^&RshQyVu)7}e&c>@m3Sw*{4~NoP~|RMC3Q)%`C6HdNZ@4-I!LYg z$W?h6*rErA%XBxEUy1eQ*c_s*h&D6F`2-X;af%8oN6SbOC%6(%3eKjd`%tqC6A=G% znla%#tcp;A^rq@bI%DPN2UL0?!^w^Rs~{wOj=kaxQ%T6oQ<<*mSSTh*72MCN&p_x; zDYYX*yiy2(5W7I>&W|2fFH=qOrc~uP{JSG|*5mYm9Nfg2mN$kad;w1-5FfH^fxs~G z-bjKRzVZ99q0cXXK=W%S+<-9iM)E~|Eg(Z za)cT#^5LtSQNoykwcLo3jxvk_Fux0bmx+TNJ7%0Tbm^gOc)q?dl`K5G@9O7AJ{sgW zc@UkeQFX({wO#xo7~avWv!7?Jx_t4X2HXpE`6~)zK{_ATXh1dFDNjyMI+Fii+;-`dSAJl^RsV-g2=Y^r zypz>aJy>z)|6wa?@b;N{MMgc~$>3qIJ*b7JMzJ)%x`m^%e86!O;zza|TPolTevCt< z#jLj_u6gkV7m|ot{56oNANkQv5!1Y5f|$X0WP&cn^8W_1;^A3fx^-Y+1K#JL!Avja zYh2!D;?lS0LC4f)idFqTj%(66{Lm>kxg(M)2nppRKFghlZ}(k9?Yu>@Kkdd+U>D%* ztOsx{PT)Fm;W z9-4fX$aXHq8EhlIi{#68@Gt(@lj?aRKT8}a>e=7ZuLhh=@-Z}juyR6gJ5CsPi$?a` z5PZg--;HxgJbRO=d8xF|PcxX$qr&ay{lXNVkN$oBEw)3A+h*wJCltdti{-QZyw<-@ zFZXK^;eMDV>znH32_F-Fsn6rljELkj@j-b`JA$6Sa~&R=|IMG9kXpGfEB-gP^Z(WV IU+IDW0fp`I-2eap literal 32256 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 diff --git a/Source/ModMismatchFormatter/UI/ModMismatchWindow.cs b/Source/ModMismatchFormatter/UI/ModMismatchWindow.cs index 7b58122..5ea44a8 100644 --- a/Source/ModMismatchFormatter/UI/ModMismatchWindow.cs +++ b/Source/ModMismatchFormatter/UI/ModMismatchWindow.cs @@ -12,7 +12,7 @@ namespace Madeline.ModMismatchFormatter.UI; /// Shows the save's mod list and the running mod list side by side, aligned row by row. public sealed class ModMismatchWindow : Window { - private const float Gap = 10f; + private const float Gap = 6f; private const float ColumnGap = 16f; private const float ToolbarHeight = 24f; private const float HeaderHeight = 24f; @@ -22,7 +22,6 @@ public sealed class ModMismatchWindow : Window private readonly Action loadGame; private readonly bool showVersions; private readonly QuickSearchWidget search = new(); - private readonly Dictionary truncateCache = []; private IReadOnlyList rows = []; private List visibleRows = []; @@ -60,14 +59,27 @@ public override void DoWindowContents(Rect inRect) { float y = inRect.y; + // Title with the summary on the same line; the summary wraps below when it does not fit. + string title = "ModsMismatchWarningTitle".Translate(); Text.Font = GameFont.Medium; float titleHeight = Text.LineHeight; - Widgets.Label(new Rect(inRect.x, y, inRect.width, titleHeight), "ModsMismatchWarningTitle".Translate()); + float titleWidth = Text.CalcSize(title).x; + Widgets.Label(new Rect(inRect.x, y, titleWidth, titleHeight), title); Text.Font = GameFont.Small; - y += titleHeight + 4f; - DrawSummary(new Rect(inRect.x, y, inRect.width, Text.LineHeight)); - y += Text.LineHeight + Gap; + var summary = SummaryEntries(); + float summaryWidth = summary.Sum(entry => SwatchSize + 4f + Text.CalcSize(entry.Label).x + 18f); + if (titleWidth + 24f + summaryWidth <= inRect.width) + { + DrawSummary(new Rect(inRect.x + titleWidth + 24f, y + (titleHeight - Text.LineHeight) / 2f, summaryWidth, Text.LineHeight), summary); + y += titleHeight + Gap; + } + else + { + y += titleHeight; + DrawSummary(new Rect(inRect.x, y, inRect.width, Text.LineHeight), summary); + y += Text.LineHeight + Gap; + } DrawToolbar(new Rect(inRect.x, y, inRect.width, ToolbarHeight)); y += ToolbarHeight + Gap; @@ -83,8 +95,10 @@ public override void DoWindowContents(Rect inRect) DrawButtons(buttons); } + private const float SwatchSize = 12f; + // Colored counts that double as the legend. - private void DrawSummary(Rect rect) + private List<(RowState State, string Label)> SummaryEntries() { // A moved mod has one MovedFrom and one MovedTo row; count it once. (RowState State, string Key)[] entries = @@ -95,16 +109,19 @@ private void DrawSummary(Rect rect) (RowState.VersionChanged, "ModMismatchFormatter_VersionChanged"), ]; + return entries + .Where(entry => showVersions || entry.State != RowState.VersionChanged) + .Select(entry => (entry.State, (string)entry.Key.Translate(counts.TryGetValue(entry.State, out int count) ? count : 0))) + .ToList(); + } + + private static void DrawSummary(Rect rect, List<(RowState State, string Label)> entries) + { float x = rect.x; - foreach (var (state, key) in entries) + foreach (var (state, label) in entries) { - if (state == RowState.VersionChanged && !showVersions) - continue; - - string label = key.Translate(counts.TryGetValue(state, out int count) ? count : 0); float width = Text.CalcSize(label).x; - - var swatch = new Rect(x, rect.y + (rect.height - 12f) / 2f, 12f, 12f); + var swatch = new Rect(x, rect.y + (rect.height - SwatchSize) / 2f, SwatchSize, SwatchSize); Widgets.DrawBoxSolid(swatch, Palette.For(state)); Widgets.Label(new Rect(swatch.xMax + 4f, rect.y, width, rect.height), label); x = swatch.xMax + 4f + width + 18f; @@ -155,9 +172,8 @@ private bool IsVisible(AlignedRow row) mod is not null && (search.filter.Matches(mod.Name) || search.filter.Matches(mod.PackageId))); } - private float RowHeight => showVersions - ? Text.LineHeightOf(GameFont.Small) + Text.LineHeightOf(GameFont.Tiny) - : Text.LineHeightOf(GameFont.Small) + 2f; + // One line per row; the version sits at the right end of the same line. + private static float RowHeight => Text.LineHeightOf(GameFont.Small) + 2f; private void DrawRows(Rect outRect, float columnWidth) { @@ -198,18 +214,28 @@ private void DrawCell(Rect rect, ModEntry? mod, AlignedRow row, bool isSavedSide Widgets.DrawBoxSolid(rect.ContractedBy(0f, 1f), Palette.For(state)); Widgets.DrawHighlightIfMouseover(rect); - float nameHeight = Text.LineHeightOf(GameFont.Small); - var nameRect = new Rect(rect.x + 4f + MarkWidth, rect.y, rect.width - MarkWidth - 8f, nameHeight); + // [mark][name ............ version] + string? versionLabel = showVersions ? VersionLabel(mod, row, isSavedSide) : null; + float versionWidth = 0f; + if (versionLabel is not null) + { + Text.Font = GameFont.Tiny; + versionWidth = Text.CalcSize(versionLabel).x + 6f; + Text.Font = GameFont.Small; + } + + var nameRect = new Rect(rect.x + 4f + MarkWidth, rect.y, rect.width - MarkWidth - 8f - versionWidth, rect.height); Text.Anchor = TextAnchor.MiddleLeft; - Widgets.Label(new Rect(rect.x + 4f, rect.y, MarkWidth, nameHeight), Palette.Mark(state)); - Widgets.Label(nameRect, mod.Name.Truncate(nameRect.width, truncateCache)); + Widgets.Label(new Rect(rect.x + 4f, rect.y, MarkWidth, rect.height), Palette.Mark(state)); + Widgets.Label(nameRect, mod.Name.Truncate(nameRect.width)); - if (showVersions && VersionLabel(mod, row, isSavedSide) is { } versionLabel) + if (versionLabel is not null) { Text.Font = GameFont.Tiny; + Text.Anchor = TextAnchor.MiddleRight; GUI.color = state == RowState.Unchanged ? Color.gray : Palette.TextOnState; - Widgets.Label(new Rect(nameRect.x, nameRect.yMax, nameRect.width, rect.height - nameHeight), versionLabel); + Widgets.Label(new Rect(nameRect.xMax, rect.y, versionWidth, rect.height), versionLabel); GUI.color = Color.white; Text.Font = GameFont.Small; } @@ -221,8 +247,8 @@ private void DrawCell(Rect rect, ModEntry? mod, AlignedRow row, bool isSavedSide // Nothing for unknown versions: saves made without version data would otherwise repeat "Unknown" on every row. private static string? VersionLabel(ModEntry mod, AlignedRow row, bool isSavedSide) => mod.Version is null ? null - : row.State == RowState.VersionChanged && !isSavedSide ? $"ver : {row.Saved!.Version} -> {mod.Version}" - : $"ver : {mod.Version}"; + : row.State == RowState.VersionChanged && !isSavedSide ? $"{row.Saved!.Version} -> {mod.Version}" + : mod.Version; private static string Tooltip(ModEntry mod, RowState state) {