├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── cicd.yml │ └── installer.yml ├── .gitignore ├── LICENSE ├── PolyMod.csproj ├── PolyMod.sln ├── README.md ├── installer ├── build.py ├── icon.ico ├── main.py └── requirements.txt ├── resources ├── intro.mp4 ├── localization.json └── polymod_icon.png └── src ├── Json ├── EnumCacheJson.cs ├── Vector2Json.cs └── VersionJson.cs ├── Loader.cs ├── Managers ├── Audio.cs ├── Compatibility.cs ├── Hub.cs ├── Loc.cs ├── Main.cs └── Visual.cs ├── Mod.cs ├── NullableFix.cs ├── Plugin.cs ├── Registry.cs └── Util.cs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: polymoddingteam 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Logs** 27 | Attach `BepInEx/LogOutput.log` relative to the game root folder file. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | -------------------------------------------------------------------------------- /.github/workflows/cicd.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | on: [push, pull_request, workflow_dispatch] 3 | jobs: 4 | job: 5 | permissions: write-all 6 | name: CI/CD 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-dotnet@v4 11 | with: 12 | dotnet-version: '8.0.x' 13 | - name: Cache NuGet packages 14 | uses: actions/cache@v4 15 | with: 16 | path: ~/.nuget/packages 17 | key: nuget-${{ hashFiles('**/*.csproj') }} 18 | - name: Build 19 | run: dotnet build -warnaserror 20 | - name: Build nuget 21 | if: github.event_name == 'workflow_dispatch' 22 | run: dotnet pack PolyMod.csproj -o nuget 23 | - name: Deploy nuget 24 | if: github.event_name == 'workflow_dispatch' 25 | run: dotnet nuget push -s https://polymod.dev/nuget/v3/index.json -k ${{ secrets.KEY }} nuget/*.nupkg 26 | - name: Get version 27 | if: github.event_name == 'workflow_dispatch' 28 | id: version 29 | uses: kzrnm/get-net-sdk-project-versions-action@v2 30 | with: 31 | proj-path: PolyMod.csproj 32 | - name: Deploy release 33 | if: github.event_name == 'workflow_dispatch' 34 | run: | 35 | if [[ "${{ steps.version.outputs.version }}" == *"-"* ]]; then 36 | gh release create v${{ steps.version.outputs.version }} bin/IL2CPP/net6.0/PolyMod.dll -p -t v${{ steps.version.outputs.version }} 37 | else 38 | gh release create v${{ steps.version.outputs.version }} bin/IL2CPP/net6.0/PolyMod.dll -t v${{ steps.version.outputs.version }} 39 | fi 40 | env: 41 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/installer.yml: -------------------------------------------------------------------------------- 1 | name: Installer 2 | on: 3 | push: 4 | paths: 5 | - 'installer/**' 6 | pull_request: 7 | paths: 8 | - 'installer/**' 9 | jobs: 10 | job: 11 | defaults: 12 | run: 13 | working-directory: ./installer 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [windows-latest, ubuntu-22.04, macos-13] 18 | name: Build on ${{ matrix.os }} 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-python@v5 23 | with: 24 | python-version: '3.11' 25 | - name: Install requirements 26 | run: pip install -r requirements.txt 27 | - name: Build 28 | run: python build.py 29 | - uses: actions/upload-artifact@v4 30 | with: 31 | name: ${{ matrix.os }} 32 | path: installer/dist/ 33 | merge: 34 | name: Merge artifacts 35 | runs-on: ubuntu-latest 36 | needs: job 37 | steps: 38 | - uses: actions/upload-artifact/merge@v4 39 | with: 40 | name: artifacts 41 | delete-merged: true 42 | separate-directories: true 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/csharp 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=csharp 3 | 4 | ### Csharp ### 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | ## 8 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 9 | 10 | # User-specific files 11 | *.rsuser 12 | *.suo 13 | *.user 14 | *.userosscache 15 | *.sln.docstates 16 | 17 | # User-specific files (MonoDevelop/Xamarin Studio) 18 | *.userprefs 19 | 20 | # Mono auto generated files 21 | mono_crash.* 22 | 23 | # Build results 24 | [Dd]ebug/ 25 | [Dd]ebugPublic/ 26 | [Rr]elease/ 27 | [Rr]eleases/ 28 | x64/ 29 | x86/ 30 | [Ww][Ii][Nn]32/ 31 | [Aa][Rr][Mm]/ 32 | [Aa][Rr][Mm]64/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | [Ll]ogs/ 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Visual Studio 2017 auto generated files 45 | Generated\ Files/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUnit 52 | *.VisualState.xml 53 | TestResult.xml 54 | nunit-*.xml 55 | 56 | # Build Results of an ATL Project 57 | [Dd]ebugPS/ 58 | [Rr]eleasePS/ 59 | dlldata.c 60 | 61 | # Benchmark Results 62 | BenchmarkDotNet.Artifacts/ 63 | 64 | # .NET Core 65 | project.lock.json 66 | project.fragment.lock.json 67 | artifacts/ 68 | 69 | # ASP.NET Scaffolding 70 | ScaffoldingReadMe.txt 71 | 72 | # StyleCop 73 | StyleCopReport.xml 74 | 75 | # Files built by Visual Studio 76 | *_i.c 77 | *_p.c 78 | *_h.h 79 | *.ilk 80 | *.meta 81 | *.obj 82 | *.iobj 83 | *.pch 84 | *.pdb 85 | *.ipdb 86 | *.pgc 87 | *.pgd 88 | *.rsp 89 | *.sbr 90 | *.tlb 91 | *.tli 92 | *.tlh 93 | *.tmp 94 | *.tmp_proj 95 | *_wpftmp.csproj 96 | *.log 97 | *.tlog 98 | *.vspscc 99 | *.vssscc 100 | .builds 101 | *.pidb 102 | *.svclog 103 | *.scc 104 | 105 | # Chutzpah Test files 106 | _Chutzpah* 107 | 108 | # Visual C++ cache files 109 | ipch/ 110 | *.aps 111 | *.ncb 112 | *.opendb 113 | *.opensdf 114 | *.sdf 115 | *.cachefile 116 | *.VC.db 117 | *.VC.VC.opendb 118 | 119 | # Visual Studio profiler 120 | *.psess 121 | *.vsp 122 | *.vspx 123 | *.sap 124 | 125 | # Visual Studio Trace Files 126 | *.e2e 127 | 128 | # TFS 2012 Local Workspace 129 | $tf/ 130 | 131 | # Guidance Automation Toolkit 132 | *.gpState 133 | 134 | # ReSharper is a .NET coding add-in 135 | _ReSharper*/ 136 | *.[Rr]e[Ss]harper 137 | *.DotSettings.user 138 | 139 | # TeamCity is a build add-in 140 | _TeamCity* 141 | 142 | # DotCover is a Code Coverage Tool 143 | *.dotCover 144 | 145 | # AxoCover is a Code Coverage Tool 146 | .axoCover/* 147 | !.axoCover/settings.json 148 | 149 | # Coverlet is a free, cross platform Code Coverage Tool 150 | coverage*.json 151 | coverage*.xml 152 | coverage*.info 153 | 154 | # Visual Studio code coverage results 155 | *.coverage 156 | *.coveragexml 157 | 158 | # NCrunch 159 | _NCrunch_* 160 | .*crunch*.local.xml 161 | nCrunchTemp_* 162 | 163 | # MightyMoose 164 | *.mm.* 165 | AutoTest.Net/ 166 | 167 | # Web workbench (sass) 168 | .sass-cache/ 169 | 170 | # Installshield output folder 171 | [Ee]xpress/ 172 | 173 | # DocProject is a documentation generator add-in 174 | DocProject/buildhelp/ 175 | DocProject/Help/*.HxT 176 | DocProject/Help/*.HxC 177 | DocProject/Help/*.hhc 178 | DocProject/Help/*.hhk 179 | DocProject/Help/*.hhp 180 | DocProject/Help/Html2 181 | DocProject/Help/html 182 | 183 | # Click-Once directory 184 | publish/ 185 | 186 | # Publish Web Output 187 | *.[Pp]ublish.xml 188 | *.azurePubxml 189 | # Note: Comment the next line if you want to checkin your web deploy settings, 190 | # but database connection strings (with potential passwords) will be unencrypted 191 | *.pubxml 192 | *.publishproj 193 | 194 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 195 | # checkin your Azure Web App publish settings, but sensitive information contained 196 | # in these scripts will be unencrypted 197 | PublishScripts/ 198 | 199 | # NuGet Packages 200 | *.nupkg 201 | # NuGet Symbol Packages 202 | *.snupkg 203 | # The packages folder can be ignored because of Package Restore 204 | **/[Pp]ackages/* 205 | # except build/, which is used as an MSBuild target. 206 | !**/[Pp]ackages/build/ 207 | # Uncomment if necessary however generally it will be regenerated when needed 208 | #!**/[Pp]ackages/repositories.config 209 | # NuGet v3's project.json files produces more ignorable files 210 | *.nuget.props 211 | *.nuget.targets 212 | 213 | # Microsoft Azure Build Output 214 | csx/ 215 | *.build.csdef 216 | 217 | # Microsoft Azure Emulator 218 | ecf/ 219 | rcf/ 220 | 221 | # Windows Store app package directories and files 222 | AppPackages/ 223 | BundleArtifacts/ 224 | Package.StoreAssociation.xml 225 | _pkginfo.txt 226 | *.appx 227 | *.appxbundle 228 | *.appxupload 229 | 230 | # Visual Studio cache files 231 | # files ending in .cache can be ignored 232 | *.[Cc]ache 233 | # but keep track of directories ending in .cache 234 | !?*.[Cc]ache/ 235 | 236 | # Others 237 | ClientBin/ 238 | ~$* 239 | *~ 240 | *.dbmdl 241 | *.dbproj.schemaview 242 | *.jfm 243 | *.pfx 244 | *.publishsettings 245 | orleans.codegen.cs 246 | 247 | # Including strong name files can present a security risk 248 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 249 | #*.snk 250 | 251 | # Since there are multiple workflows, uncomment next line to ignore bower_components 252 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 253 | #bower_components/ 254 | 255 | # RIA/Silverlight projects 256 | Generated_Code/ 257 | 258 | # Backup & report files from converting an old project file 259 | # to a newer Visual Studio version. Backup files are not needed, 260 | # because we have git ;-) 261 | _UpgradeReport_Files/ 262 | Backup*/ 263 | UpgradeLog*.XML 264 | UpgradeLog*.htm 265 | ServiceFabricBackup/ 266 | *.rptproj.bak 267 | 268 | # SQL Server files 269 | *.mdf 270 | *.ldf 271 | *.ndf 272 | 273 | # Business Intelligence projects 274 | *.rdl.data 275 | *.bim.layout 276 | *.bim_*.settings 277 | *.rptproj.rsuser 278 | *- [Bb]ackup.rdl 279 | *- [Bb]ackup ([0-9]).rdl 280 | *- [Bb]ackup ([0-9][0-9]).rdl 281 | 282 | # Microsoft Fakes 283 | FakesAssemblies/ 284 | 285 | # GhostDoc plugin setting file 286 | *.GhostDoc.xml 287 | 288 | # Node.js Tools for Visual Studio 289 | .ntvs_analysis.dat 290 | node_modules/ 291 | 292 | # Visual Studio 6 build log 293 | *.plg 294 | 295 | # Visual Studio 6 workspace options file 296 | *.opt 297 | 298 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 299 | *.vbw 300 | 301 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 302 | *.vbp 303 | 304 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 305 | *.dsw 306 | *.dsp 307 | 308 | # Visual Studio 6 technical files 309 | 310 | # Visual Studio LightSwitch build output 311 | **/*.HTMLClient/GeneratedArtifacts 312 | **/*.DesktopClient/GeneratedArtifacts 313 | **/*.DesktopClient/ModelManifest.xml 314 | **/*.Server/GeneratedArtifacts 315 | **/*.Server/ModelManifest.xml 316 | _Pvt_Extensions 317 | 318 | # Paket dependency manager 319 | .paket/paket.exe 320 | paket-files/ 321 | 322 | # FAKE - F# Make 323 | .fake/ 324 | 325 | # CodeRush personal settings 326 | .cr/personal 327 | 328 | # Python Tools for Visual Studio (PTVS) 329 | __pycache__/ 330 | *.pyc 331 | 332 | # Cake - Uncomment if you are using it 333 | # tools/** 334 | # !tools/packages.config 335 | 336 | # Tabs Studio 337 | *.tss 338 | 339 | # Telerik's JustMock configuration file 340 | *.jmconfig 341 | 342 | # BizTalk build output 343 | *.btp.cs 344 | *.btm.cs 345 | *.odx.cs 346 | *.xsd.cs 347 | 348 | # OpenCover UI analysis results 349 | OpenCover/ 350 | 351 | # Azure Stream Analytics local run output 352 | ASALocalRun/ 353 | 354 | # MSBuild Binary and Structured Log 355 | *.binlog 356 | 357 | # NVidia Nsight GPU debugger configuration file 358 | *.nvuser 359 | 360 | # MFractors (Xamarin productivity tool) working folder 361 | .mfractor/ 362 | 363 | # Local History for Visual Studio 364 | .localhistory/ 365 | 366 | # Visual Studio History (VSHistory) files 367 | .vshistory/ 368 | 369 | # BeatPulse healthcheck temp database 370 | healthchecksdb 371 | 372 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 373 | MigrationBackup/ 374 | 375 | # Ionide (cross platform F# VS Code tools) working folder 376 | .ionide/ 377 | 378 | # Fody - auto-generated XML schema 379 | FodyWeavers.xsd 380 | 381 | # VS Code files for those working on multiple tools 382 | .vscode/* 383 | !.vscode/settings.json 384 | !.vscode/tasks.json 385 | # !.vscode/launch.json 386 | !.vscode/extensions.json 387 | *.code-workspace 388 | 389 | # Local History for Visual Studio Code 390 | .history/ 391 | 392 | # Windows Installer files from build outputs 393 | *.cab 394 | *.msi 395 | *.msix 396 | *.msm 397 | *.msp 398 | 399 | # JetBrains Rider 400 | *.sln.iml 401 | 402 | # Python 403 | build/ 404 | dist/ 405 | *.spec 406 | 407 | # End of https://www.toptal.com/developers/gitignore/api/csharp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | PolyMod is licensed under CC BY-NC-ND 4.0 -------------------------------------------------------------------------------- /PolyMod.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | enable 5 | enable 6 | 7 | https://api.nuget.org/v3/index.json; 8 | https://nuget.bepinex.dev/v3/index.json; 9 | https://nuget.samboy.dev/v3/index.json; 10 | https://polymod.dev/nuget/v3/index.json; 11 | 12 | IL2CPP 13 | 1.1.8 14 | 2.13.0.14218 15 | PolyModdingTeam 16 | The Battle of Polytopia's mod loader. 17 | IDE0130 18 | NU5104 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | $(IntermediateOutputPath)Props.cs 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /PolyMod.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33205.214 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PolyMod", "PolyMod.csproj", "{58B09361-FD7A-48F1-82E1-E2359ADA512F}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | IL2CPP|Any CPU = IL2CPP|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {58B09361-FD7A-48F1-82E1-E2359ADA512F}.IL2CPP|Any CPU.ActiveCfg = IL2CPP|Any CPU 14 | {58B09361-FD7A-48F1-82E1-E2359ADA512F}.IL2CPP|Any CPU.Build.0 = IL2CPP|Any CPU 15 | EndGlobalSection 16 | GlobalSection(SolutionProperties) = preSolution 17 | HideSolutionNode = FALSE 18 | EndGlobalSection 19 | GlobalSection(ExtensibilityGlobals) = postSolution 20 | SolutionGuid = {85E2543B-5FCB-481F-AA67-C05EB7CF843F} 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [PolyMod](https://polymod.dev) 2 | 3 | ![](https://img.shields.io/github/downloads/PolyModdingTeam/PolyMod/total) 4 | ![](https://img.shields.io/codefactor/grade/github/PolyModdingTeam/PolyMod) 5 | ![](https://img.shields.io/github/actions/workflow/status/PolyModdingTeam/PolyMod/cicd.yml) 6 | 7 | | | x86 | ARM | 8 | |:-------:|:---:|:---:| 9 | | Windows | ✅ | ❌ | 10 | | Linux | ⏺️ | ❌ | 11 | | MacOS | ⏺️ | ❌ | 12 | | Android | ➖ | 🚧 | 13 | | IOS | ➖ | ❌ | 14 | -------------------------------------------------------------------------------- /installer/build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import PyInstaller.__main__ 3 | 4 | ROOT = os.path.abspath(os.getcwd()) 5 | 6 | PyInstaller.__main__.run( 7 | [ 8 | "--noconfirm", 9 | "--onefile", 10 | "--windowed", 11 | "--icon", 12 | ROOT + "/icon.ico", 13 | "--name", 14 | "PolyMod", 15 | "--add-data", 16 | ROOT + "/icon.ico:.", 17 | ROOT + "/main.py", 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /installer/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolyModdingTeam/PolyMod/7774308849c7833efde6fdbe8ceac073f14e80b1/installer/icon.ico -------------------------------------------------------------------------------- /installer/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import io 3 | import sys 4 | import shutil 5 | import zipfile 6 | import requests 7 | import threading 8 | import subprocess 9 | import customtkinter 10 | import CTkMessagebox as messagebox 11 | 12 | OS = { 13 | "linux": "linux", 14 | "linux2": "linux", 15 | "win32": "win", 16 | "darwin": "macos", 17 | }[sys.platform] 18 | BEPINEX = f"733/BepInEx-Unity.IL2CPP-{OS}-x64-6.0.0-be.733%2B995f049" 19 | POLYMOD = "https://github.com/PolyModdingTeam/PolyMod/releases/latest/download/PolyMod.dll" 20 | 21 | 22 | def resource_path(path): 23 | try: 24 | base_path = sys._MEIPASS 25 | except Exception: 26 | base_path = os.path.abspath(".") 27 | return os.path.join(base_path, path) 28 | 29 | 30 | def to_zip(request: requests.Response): 31 | return zipfile.ZipFile(io.BytesIO(request.content)) 32 | 33 | 34 | def browse(): 35 | global path_entry 36 | path_entry.delete(0, customtkinter.END) 37 | path_entry.insert(0, customtkinter.filedialog.askdirectory()) 38 | 39 | 40 | def prepare(target): 41 | global progress_bar 42 | path = path_entry.get() 43 | try: 44 | if "Polytopia_Data" not in os.listdir(path): 45 | raise FileNotFoundError 46 | except FileNotFoundError: 47 | messagebox.CTkMessagebox( 48 | title="Error", 49 | message="The folder does not exist or is not valid!", 50 | icon="cancel", 51 | width=100, 52 | height=50 53 | ) 54 | return 55 | path_entry.configure(state=customtkinter.DISABLED) 56 | browse_button.configure(state=customtkinter.DISABLED) 57 | install_button.destroy() 58 | uninstall_button.destroy() 59 | progress_bar = customtkinter.CTkProgressBar(app, determinate_speed=50 / 2) 60 | progress_bar.grid(column=0, row=1, columnspan=2, padx=5, pady=5) 61 | progress_bar.set(0) 62 | threading.Thread(target=target, daemon=True, args=(path, )).start() 63 | 64 | 65 | def install(path): 66 | to_zip( 67 | requests.get( 68 | f"https://builds.bepinex.dev/projects/bepinex_be/{BEPINEX}.zip" 69 | ) 70 | ).extractall(path) 71 | progress_bar.step() 72 | 73 | open(path + "/BepInEx/plugins/PolyMod.dll", "wb").write( 74 | requests.get(POLYMOD).content 75 | ) 76 | progress_bar.step() 77 | 78 | customtkinter.CTkButton(app, text="Launch", command=lambda: launch(path)).grid( 79 | column=0, row=2, columnspan=2, padx=5, pady=5 80 | ) 81 | 82 | 83 | def uninstall(path): 84 | dirs = [ 85 | "BepInEx", 86 | "dotnet", 87 | ] 88 | files = [ 89 | ".doorstop_version", 90 | "changelog.txt", 91 | "doorstop_config.ini", 92 | "winhttp.dll", # windows 93 | "libdoorstop.so", # linux 94 | "libdoorstop.dylib", # mac 95 | "run_bepinex.sh", # linux + mac 96 | ] 97 | for dir in dirs: 98 | shutil.rmtree(path + "/" + dir, True) 99 | progress_bar.step() 100 | for file in files: 101 | try: 102 | os.remove(path + "/" + file) 103 | except FileNotFoundError: 104 | ... 105 | progress_bar.step() 106 | customtkinter.CTkButton(app, text="Quit", command=quit).grid( 107 | column=0, row=2, columnspan=2, padx=5, pady=5 108 | ) 109 | 110 | 111 | def launch(path): 112 | if sys.platform != "win32": 113 | subprocess.check_call(f"chmod +x {path}/run_bepinex.sh", shell=True) 114 | subprocess.check_call(f"{path}/run_bepinex.sh {path}/Polytopia.*", shell=True) 115 | subprocess.check_call(f"xdg-open https://docs.bepinex.dev/articles/advanced/steam_interop.html", shell=True) 116 | else: 117 | subprocess.check_call(f"start steam://rungameid/874390", shell=True) 118 | quit() 119 | 120 | 121 | def quit(): 122 | app.destroy() 123 | sys.exit() 124 | 125 | 126 | app = customtkinter.CTk() 127 | app.title("PolyMod") 128 | if OS != "linux": 129 | app.iconbitmap(default=resource_path("icon.ico")) 130 | app.resizable(False, False) 131 | 132 | path_entry = customtkinter.CTkEntry( 133 | app, placeholder_text="Game path", width=228) 134 | browse_button = customtkinter.CTkButton( 135 | app, text="Browse", command=browse, width=1) 136 | install_button = customtkinter.CTkButton( 137 | app, text="Install", command=lambda: prepare(install)) 138 | uninstall_button = customtkinter.CTkButton( 139 | app, text="Uninstall", command=lambda: prepare(uninstall)) 140 | 141 | path_entry.grid(column=0, row=0, padx=5, pady=5) 142 | browse_button.grid(column=1, row=0, padx=(0, 5), pady=5) 143 | install_button.grid(column=0, row=1, columnspan=2, padx=5, pady=5) 144 | uninstall_button.grid(column=0, row=2, columnspan=2, padx=5, pady=5) 145 | 146 | app.mainloop() 147 | -------------------------------------------------------------------------------- /installer/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pyinstaller 3 | customtkinter 4 | CTkMessagebox -------------------------------------------------------------------------------- /resources/intro.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolyModdingTeam/PolyMod/7774308849c7833efde6fdbe8ceac073f14e80b1/resources/intro.mp4 -------------------------------------------------------------------------------- /resources/localization.json: -------------------------------------------------------------------------------- 1 | { 2 | "polymod_hub": { 3 | "English": "PolyMod Hub", 4 | "Russian": "Центр PolyMod", 5 | "Turkish": "PolyMod Merkezi", 6 | "Spanish (Mexico)": "Repositorio de PolyMod", 7 | "French (France)": "Centre PolyMod", 8 | "Polish": "Centrum PolyMod", 9 | "Portuguese (Brazil)": "Hub do PolyMod", 10 | "Elyrion": "πȱ∫ỹmȱΔ ţ₺o", 11 | "German (Germany)": "PolyMod Zentrum" 12 | }, 13 | "polymod_hub_discord": { 14 | "English": "OUR DISCORD", 15 | "Russian": "НАШ ДИСКОРД", 16 | "Turkish": "DİSCORD'UMUZ", 17 | "Spanish (Mexico)": "NUESTRO DISCORD", 18 | "French (France)": "NOTRE DISCORD", 19 | "Polish": "NASZ DISCORD", 20 | "Portuguese (Brazil)": "NOSSO DISCORD", 21 | "Elyrion": "Δi^#ȱrΔ", 22 | "German (Germany)": "UNSER DISCORD" 23 | }, 24 | "polymod_hub_footer": { 25 | "English": "Join our discord! Feel free to discuss mods, create them and ask for help!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", 26 | "Russian": "Присоединяйтесь к нашему дискорду! Не стесняйтесь обсуждать моды, создавать их и просить о помощи!\n\n{0}Особая благодарность{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", 27 | "Turkish": "Discord sunucumuza katıl! Orada modlar oluşturabilir, tartışabilir ve yardım isteyebilirsin!\n\n{0}Hepinize çok teşekkür ederim:{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", 28 | "Spanish (Mexico)": "Unete a nuestro discord! Aqui se puede discutir sobre la modificacion del juego, guias para crear su propio, preguntar a los creadores, y mas!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", 29 | "French (France)": "Rejoignez notre discord! N'hésitez pas à discuter des mods, à en créer et à demander de l'aide!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", 30 | "Polish": "Dołącz do naszego discorda! Zachęcamy do omawiania modów, tworzenia ich lub proszenia o pomoc!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", 31 | "Portuguese (Brazil)": "Entre no nosso Discord! Sinta-se à vontade para discutir sobre os mods, criar novos mods e pedir ajuda!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", 32 | "Elyrion": "§ii∫ Δi^#ȱrΔ! Δi^#₺^^ mȱΔ#, ȱrrȱ ỹ a^š ỹȱπ!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", 33 | "German (Germany)": "Tritt unserem Discord bei, um Hilfe zu bekommen, Mods zu diskutieren oder sogar selbst zu erstellen!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon" 34 | }, 35 | "polymod_hub_header": { 36 | "English": "{0}Welcome!{1}\nHere you can see the list of all currently loaded mods:", 37 | "Russian": "{0}Добро пожаловать!{1}\nЗдесь вы можете увидеть список всех загруженных на данный момент модов:", 38 | "Turkish": "{0}Hoş geldin!{1}\nBurada yüklenmiş modların listesini bulabilirsin:", 39 | "Spanish (Mexico)": "{0}Bienvenidos!{1}\nAqui puedes ver un compilacion actualicado de todo los mods cargados:", 40 | "French (France)": "{0}Bienvenue!{1}\nIci vous pouvez voir tous les mods chargés:", 41 | "Polish": "{0}Witaj!{1}\nTutaj możesz zobaczyć listę wszystkich aktualnie załadowanych modów:", 42 | "Portuguese (Brazil)": "{0}Bem vindo!{1}\nAqui você pode ver a lista dos mods carregados:", 43 | "Elyrion": "{0}þr∑∑Ŧ!{1}\niii ŋ∑ƒ ƒ§§π mȱΔ#:", 44 | "German (Germany)": "{0} Willkommen! {1}\nDiese Mods sind gerade eben geladen:" 45 | }, 46 | "polymod_hub_mod": { 47 | "English": "Name: {0}\nStatus: {1}\nAuthors: {2}\nVersion: {3}", 48 | "Russian": "Имя: {0}\nСтатус: {1}\nАвторы: {2}\nВерсия: {3}", 49 | "Turkish": "İsim: {0}\nDurum: {1}\nYaratıcılar: {2}\nSürüm: {3}", 50 | "Spanish (Mexico)": "Titulo: {0}\nEstado: {1}\nPublicador: {2}\nVersion: {3}", 51 | "French (France)": "Titre: {0}\nEtat: {1}\nAuteurs: {2}\nVersion: {3}", 52 | "Polish": "Nazwa: {0}\nStatus: {1}\nAutorzy: {2}\nWersja: {3}", 53 | "Portuguese (Brazil)": "Nome: {0}\nStatus: {1}\nAutores: {2}\nVersão: {3}", 54 | "Elyrion": "r₼: {0}\n^ŦaŦ₺^: {1}\na₺Ŧţȱr: {2}\nƒƒƒƒƒƒƒ: {3}", 55 | "German (Germany)": "Name: {0}\nStatus: {1}\nAutoren: {2}\nVersion: {3}" 56 | }, 57 | "polymod_hub_dump": { 58 | "English": "DUMP DATA", 59 | "Russian": "ЗАДАМПИТЬ ДАННЫЕ", 60 | "German (Germany)": "DATENDUMP" 61 | }, 62 | "polymod_hub_dumped": { 63 | "English": "Dump completed successfully!", 64 | "Russian": "Дамп завершен успешно!", 65 | "German (Germany)": "Dump erfolgreich beendet!" 66 | }, 67 | "polymod_cycle": { 68 | "English": "Dependency cycle detected", 69 | "Russian": "Обнаружен цикл зависимостей" 70 | }, 71 | "polymod_cycle_description": { 72 | "English": "Unable to determine mod loading order!", 73 | "Russian": "Невозможно определить порядок загрузки модов!", 74 | "German (Germany)": "Mod - ladereihenfolge konnte nicht bestimmt werden!" 75 | }, 76 | "polymod_hub_mod_status_error": { 77 | "English": "had loading error", 78 | "Russian": "произошла ошибка загрузки", 79 | "Turkish": "yükleme hatasıyla karşılaştı", 80 | "Spanish (Mexico)": "error en cargando", 81 | "French (France)": "a eu une erreur de chargement", 82 | "Polish": "wystąpił błąd ładowania", 83 | "Portuguese (Brazil)": "houve um erro de carregamento", 84 | "Elyrion": "ƒ§§π ∑rrȱr", 85 | "German (Germany)": "Ladefehler" 86 | }, 87 | "polymod_hub_mod_status_success": { 88 | "English": "loaded successfully", 89 | "Russian": "успешно загружено", 90 | "Turkish": "başarıyla yüklendi", 91 | "Spanish (Mexico)": "cargado exitosamente", 92 | "French (France)": "chargé avec succès", 93 | "Polish": "załadowano pomyślnie", 94 | "Portuguese (Brazil)": "carregado com êxito", 95 | "Elyrion": "ƒ§§π ^₺##∑^^₼₺∫∫ỹ", 96 | "German (Germany)": "Erfolgreich geladen" 97 | }, 98 | "polymod_hub_mod_status_dependenciesunsatisfied": { 99 | "English": "required dependencies not found", 100 | "Russian": "необходимые зависимости не найдены", 101 | "Turkish": "gerekli bağımlılıklar bulunamadı", 102 | "Spanish (Mexico)": "no se han encontrado las dependencias necesarias", 103 | "French (France)": "les dépendances requises n'ont pas été trouvé", 104 | "Polish": "wymagane zależności nie zostały znalezione", 105 | "Portuguese (Brazil)": "as dependências requisitadas não foram encontradas", 106 | "Elyrion": "r∑¦₺i∑r∑Δ Δ∑π∑ŋΔ∑ŋ#i∑# ∑∫!ỹr", 107 | "German (Germany)": "Benötigte Dependenzen nicht aufgefunden" 108 | }, 109 | "polymod_signature_incompatible": { 110 | "English": "Current mods are not compatible with original mods!", 111 | "Russian": "Текущие моды несовместимы с оригинальными модами!", 112 | "Turkish": "Mevcut modlar orijinal modlarla uyumsuz!", 113 | "Spanish (Mexico)": "Falta de compatibilidad entre los mods concurrientes!", 114 | "French (France)": "Les mods actuels ne sont pas compatibles avec les mods originaux!", 115 | "Polish": "Obecne mody nie są kompatybilne z oryginalnymi modami!", 116 | "Portuguese (Brazil)": "Incompatibilidade entre os mods carregados!", 117 | "Elyrion": "#₺rr∑ŋŦ mȱΔ# ₼π₺þþţ∫ ȱriþiŋa∫ mȱΔ#!", 118 | "German (Germany)": "Einige aktuelle mods sind nicht mit Älteren kompatibel!" 119 | }, 120 | "polymod_signature_maybe_incompatible": { 121 | "English": "Current mods may not be compatible with original mods!", 122 | "Russian": "Текущие моды могут быть несовместимы с оригинальными модами!", 123 | "Turkish": "Mevcut modlar orijinal modlarla uyumlu olmayabilir!", 124 | "Spanish (Mexico)": "Es posible que este mod no sea compatibile con el original!", 125 | "French (France)": "Les mods actuels peuvent ne pas être compatibles avec les mods originaux!", 126 | "Polish": "Obecne mody mogą nie być kompatybilne z oryginalnymi modami!", 127 | "Portuguese (Brazil)": "É possível que os mods carregados não sejam compatíveis com os mods originais", 128 | "Elyrion": "#₺rr∑ŋŦ mȱΔ# maỹ ₼π₺þþţ∫ ȱriþiŋa∫ mȱΔ#!", 129 | "German (Germany)": "Einige aktuelle mods könnten mit Älteren inkompatibel sein!" 130 | }, 131 | "polymod_signature_mismatch": { 132 | "English": "Signature mismatch", 133 | "Russian": "Несоответствие подписи", 134 | "Turkish": "İmza uyumsuz", 135 | "Spanish (Mexico)": "Falta de coincidencia en la firma", 136 | "French (France)": "Les signatures ne correspondent pas", 137 | "Polish": "Niezgodność podpisu", 138 | "Portuguese (Brazil)": "As assinaturas não correspondem", 139 | "Elyrion": "^iþŋaŦ₺r∑ ₼π₺þþţ∫", 140 | "German (Germany)": "Signaturen passen nicht zusammen!" 141 | }, 142 | "polymod_version_mismatch": { 143 | "English": "Version mismatch", 144 | "Russian": "Несовпадение версий", 145 | "Turkish": "Sürüm uyumusuz", 146 | "Spanish (Mexico)": "Falta de coincidencia de version", 147 | "French (France)": "Les versions ne correspondent pas", 148 | "Polish": "Niezgodność wersji", 149 | "Portuguese (Brazil)": "As versões não correspondem", 150 | "Elyrion": "ƒƒƒƒƒƒƒ ₼π₺þþţ∫", 151 | "German (Germany)": "Versionen passen nicht zusammen!" 152 | }, 153 | "polymod_version_mismatch_description": { 154 | "English": "This version of PolyMod is not designed for the current version of the application and may not work correctly!", 155 | "Russian": "PolyMod этой версии не предназначен для текущей версии приложения и может работать некорректно!", 156 | "Turkish": "PolyMod'un bu sürümü mevcut uygulama sürümü için tasarlanmamıştır ve düzgün çalışmayabilir!", 157 | "Spanish (Mexico)": "¡El version de PolyMod no es diseñado para este version de la aplicación y es posible que no funcione correctamente!", 158 | "French (France)": "Cette version de PolyMod n'est pas conçu pour la version actuelle de l'application et peut ne pas fonctionner correctement!", 159 | "Polish": "Ta wersja PolyMod nie jest przeznaczona dla bieżącej wersji aplikacji i może nie działać poprawnie!", 160 | "Portuguese (Brazil)": "Essa versão do PolyMod não foi desenvolvida para a versão atual do aplicativo e pode não funcionar corretamente!", 161 | "Elyrion": "πȱ∫ỹmȱδ ƒƒƒƒƒƒƒ ŋȱŧ ȱrrȱ #₺rr∑ŋŧ ƒƒƒƒƒƒƒ ỹ maỹ ŋȱŧ ~ȱr§ #ȱrr∑#ŧ∫ỹ!", 162 | "German (Germany)": "Diese Version von PolyMod ist nicht für die aktuelle Version der Anwendung ausgelegt und könnte nicht funktionieren!" 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /resources/polymod_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolyModdingTeam/PolyMod/7774308849c7833efde6fdbe8ceac073f14e80b1/resources/polymod_icon.png -------------------------------------------------------------------------------- /src/Json/EnumCacheJson.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace PolyMod.Json; 5 | internal class EnumCacheJson : JsonConverter where T : struct, Enum 6 | { 7 | public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 8 | { 9 | return EnumCache.GetType(reader.GetString()); 10 | } 11 | 12 | public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) 13 | { 14 | writer.WriteStringValue(EnumCache.GetName(value)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Json/Vector2Json.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using UnityEngine; 4 | 5 | namespace PolyMod.Json; 6 | internal class Vector2Json : JsonConverter 7 | { 8 | public override Vector2 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | List values = new(); 11 | if (reader.TokenType == JsonTokenType.StartArray) 12 | { 13 | while (reader.Read()) 14 | { 15 | if (reader.TokenType == JsonTokenType.EndArray) break; 16 | if (reader.TokenType != JsonTokenType.Number) throw new JsonException(); 17 | values.Add(reader.GetSingle()); 18 | } 19 | } 20 | if (values.Count != 2) throw new JsonException(); 21 | return new(values[0], values[1]); 22 | } 23 | 24 | public override void Write(Utf8JsonWriter writer, Vector2 value, JsonSerializerOptions options) 25 | { 26 | writer.WriteStartArray(); 27 | writer.WriteNumberValue(value.x); 28 | writer.WriteNumberValue(value.y); 29 | writer.WriteEndArray(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Json/VersionJson.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace PolyMod.Json; 5 | internal class VersionJson : JsonConverter 6 | { 7 | public override Version? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 8 | { 9 | return new(reader.GetString()!); 10 | } 11 | 12 | public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) 13 | { 14 | writer.WriteStringValue(value.ToString()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Loader.cs: -------------------------------------------------------------------------------- 1 | using BepInEx.Logging; 2 | using Cpp2IL.Core.Extensions; 3 | using Il2CppSystem.Linq; 4 | using MonoMod.Utils; 5 | using Newtonsoft.Json.Linq; 6 | using PolyMod.Json; 7 | using PolyMod.Managers; 8 | using Polytopia.Data; 9 | using System.Data; 10 | using System.Diagnostics; 11 | using System.Globalization; 12 | using System.IO.Compression; 13 | using System.Reflection; 14 | using System.Text.Json; 15 | using System.Text.RegularExpressions; 16 | using UnityEngine; 17 | 18 | namespace PolyMod; 19 | public static class Loader 20 | { 21 | internal static Dictionary typeMappings = new() 22 | { 23 | { "tribeData", typeof(TribeData.Type) }, 24 | { "techData", typeof(TechData.Type) }, 25 | { "unitData", typeof(UnitData.Type) }, 26 | { "improvementData", typeof(ImprovementData.Type) }, 27 | { "terrainData", typeof(Polytopia.Data.TerrainData.Type) }, 28 | { "resourceData", typeof(ResourceData.Type) }, 29 | { "taskData", typeof(TaskData.Type) }, 30 | { "tribeAbility", typeof(TribeAbility.Type) }, 31 | { "unitAbility", typeof(UnitAbility.Type) }, 32 | { "improvementAbility", typeof(ImprovementAbility.Type) }, 33 | { "playerAbility", typeof(PlayerAbility.Type) } 34 | }; 35 | 36 | public static void AddPatchDataType(string typeId, Type type) 37 | { 38 | if (!typeMappings.ContainsKey(typeId)) 39 | typeMappings.Add(typeId, type); 40 | } 41 | 42 | internal static void LoadMods(Dictionary mods) 43 | { 44 | Directory.CreateDirectory(Plugin.MODS_PATH); 45 | string[] modContainers = Directory.GetDirectories(Plugin.MODS_PATH) 46 | .Union(Directory.GetFiles(Plugin.MODS_PATH, "*.polymod")) 47 | .Union(Directory.GetFiles(Plugin.MODS_PATH, "*.zip")) 48 | .ToArray(); 49 | foreach (var modContainer in modContainers) 50 | { 51 | Mod.Manifest? manifest = null; 52 | List files = new(); 53 | 54 | if (Directory.Exists(modContainer)) 55 | { 56 | foreach (var file in Directory.GetFiles(modContainer)) 57 | { 58 | if (Path.GetFileName(file) == "manifest.json") 59 | { 60 | manifest = JsonSerializer.Deserialize( 61 | File.ReadAllBytes(file), 62 | new JsonSerializerOptions() 63 | { 64 | Converters = { new VersionJson() }, 65 | } 66 | ); 67 | continue; 68 | } 69 | files.Add(new(Path.GetFileName(file), File.ReadAllBytes(file))); 70 | } 71 | } 72 | else 73 | { 74 | foreach (var entry in new ZipArchive(File.OpenRead(modContainer)).Entries) 75 | { 76 | if (entry.FullName == "manifest.json") 77 | { 78 | manifest = JsonSerializer.Deserialize( 79 | entry.ReadBytes(), 80 | new JsonSerializerOptions() 81 | { 82 | Converters = { new VersionJson() }, 83 | } 84 | ); 85 | continue; 86 | } 87 | files.Add(new(entry.FullName, entry.ReadBytes())); 88 | } 89 | } 90 | 91 | if (manifest != null 92 | && manifest.id != null 93 | && Regex.IsMatch(manifest.id, @"^(?!polytopia$)[a-z_]+$") 94 | && manifest.version != null 95 | && manifest.authors != null 96 | && manifest.authors.Length != 0 97 | ) 98 | { 99 | if (mods.ContainsKey(manifest.id)) 100 | { 101 | Plugin.logger.LogError($"Mod {manifest.id} already exists"); 102 | continue; 103 | } 104 | mods.Add(manifest.id, new( 105 | manifest, 106 | Mod.Status.Success, 107 | files 108 | )); 109 | Plugin.logger.LogInfo($"Registered mod {manifest.id}"); 110 | } 111 | else 112 | { 113 | Plugin.logger.LogError("An invalid mod manifest was found or not found at all"); 114 | } 115 | } 116 | 117 | foreach (var (id, mod) in mods) 118 | { 119 | foreach (var dependency in mod.dependencies ?? Array.Empty()) 120 | { 121 | string? message = null; 122 | if (!mods.ContainsKey(dependency.id)) 123 | { 124 | message = $"Dependency {dependency.id} not found"; 125 | } 126 | else 127 | { 128 | Version version = mods[dependency.id].version; 129 | if ( 130 | (dependency.min != null && version < dependency.min) 131 | || 132 | (dependency.max != null && version > dependency.max) 133 | ) 134 | { 135 | message = $"Need dependency {dependency.id} version {dependency.min} - {dependency.max} found {version}"; 136 | } 137 | } 138 | if (message != null) 139 | { 140 | if (dependency.required) 141 | { 142 | Plugin.logger.LogError(message); 143 | mod.status = Mod.Status.DependenciesUnsatisfied; 144 | } 145 | else 146 | { 147 | Plugin.logger.LogWarning(message); 148 | } 149 | } 150 | } 151 | } 152 | } 153 | 154 | internal static bool SortMods(Dictionary mods) 155 | { 156 | Stopwatch s = new(); 157 | Dictionary> graph = new(); 158 | Dictionary inDegree = new(); 159 | Dictionary successfulMods = new(); 160 | Dictionary unsuccessfulMods = new(); 161 | foreach (var (id, mod) in mods) 162 | { 163 | if (mod.status == Mod.Status.Success) successfulMods.Add(id, mod); 164 | else unsuccessfulMods.Add(id, mod); 165 | } 166 | foreach (var (id, _) in successfulMods) 167 | { 168 | graph[id] = new(); 169 | inDegree[id] = 0; 170 | } 171 | foreach (var (id, mod) in successfulMods) 172 | { 173 | foreach (var dependency in mod.dependencies ?? Array.Empty()) 174 | { 175 | graph[dependency.id].Add(id); 176 | inDegree[id]++; 177 | } 178 | } 179 | Queue queue = new(); 180 | foreach (var (id, _) in successfulMods) 181 | { 182 | if (inDegree[id] == 0) 183 | { 184 | queue.Enqueue(id); 185 | } 186 | } 187 | Dictionary sorted = new(); 188 | while (queue.Count > 0) 189 | { 190 | var id = queue.Dequeue(); 191 | var mod = successfulMods[id]; 192 | sorted.Add(id, mod); 193 | foreach (var neighbor in graph[id]) 194 | { 195 | inDegree[neighbor]--; 196 | if (inDegree[neighbor] == 0) 197 | { 198 | queue.Enqueue(neighbor); 199 | } 200 | } 201 | } 202 | if (sorted.Count != successfulMods.Count) 203 | { 204 | return false; 205 | } 206 | mods.Clear(); 207 | mods.AddRange(sorted); 208 | mods.AddRange(unsuccessfulMods); 209 | 210 | return true; 211 | } 212 | 213 | public static void LoadAssemblyFile(Mod mod, Mod.File file) 214 | { 215 | try 216 | { 217 | Assembly assembly = Assembly.Load(file.bytes); 218 | foreach (Type type in assembly.GetTypes()) 219 | { 220 | MethodInfo? loadWithLogger = type.GetMethod("Load", new Type[] { typeof(ManualLogSource) }); 221 | if (loadWithLogger != null) 222 | { 223 | loadWithLogger.Invoke(null, new object[] 224 | { 225 | BepInEx.Logging.Logger.CreateLogSource($"PolyMod] [{mod.id}") 226 | }); 227 | Plugin.logger.LogInfo($"Invoked Load method with logger from {type.FullName} from {mod.id} mod"); 228 | } 229 | MethodInfo? load = type.GetMethod("Load", Array.Empty()); 230 | if (load != null) 231 | { 232 | load.Invoke(null, null); 233 | Plugin.logger.LogInfo($"Invoked Load method from {type.FullName} from {mod.id} mod"); 234 | } 235 | } 236 | } 237 | catch (TargetInvocationException exception) 238 | { 239 | if (exception.InnerException != null) 240 | { 241 | Plugin.logger.LogError($"Error on loading assembly from {mod.id} mod: {exception.InnerException.Message}"); 242 | mod.status = Mod.Status.Error; 243 | } 244 | } 245 | } 246 | 247 | public static void LoadLocalizationFile(Mod mod, Mod.File file) 248 | { 249 | try 250 | { 251 | Loc.BuildAndLoadLocalization(JsonSerializer 252 | .Deserialize>>(file.bytes)!); 253 | Plugin.logger.LogInfo($"Registried localization from {mod.id} mod"); 254 | } 255 | catch (Exception e) 256 | { 257 | Plugin.logger.LogError($"Error on loading locatization from {mod.id} mod: {e.Message}"); 258 | } 259 | } 260 | 261 | public static void LoadSpriteFile(Mod mod, Mod.File file) 262 | { 263 | string name = Path.GetFileNameWithoutExtension(file.name); 264 | Vector2 pivot = name.Split("_")[0] switch 265 | { 266 | "field" => new(0.5f, 0.0f), 267 | "mountain" => new(0.5f, -0.375f), 268 | _ => new(0.5f, 0.5f), 269 | }; 270 | float pixelsPerUnit = 2112f; 271 | if (Registry.spriteInfos.ContainsKey(name)) 272 | { 273 | Visual.SpriteInfo spriteData = Registry.spriteInfos[name]; 274 | pivot = spriteData.pivot ?? pivot; 275 | pixelsPerUnit = spriteData.pixelsPerUnit ?? pixelsPerUnit; 276 | } 277 | Sprite sprite = Visual.BuildSprite(file.bytes, pivot, pixelsPerUnit); 278 | GameManager.GetSpriteAtlasManager().cachedSprites.TryAdd("Heads", new()); 279 | GameManager.GetSpriteAtlasManager().cachedSprites["Heads"].Add(name, sprite); 280 | Registry.sprites.Add(name, sprite); 281 | } 282 | 283 | public static void LoadSpriteInfoFile(Mod mod, Mod.File file) 284 | { 285 | try 286 | { 287 | Registry.spriteInfos = Registry.spriteInfos 288 | .Concat(JsonSerializer.Deserialize>( 289 | file.bytes, 290 | new JsonSerializerOptions() 291 | { 292 | Converters = { new Vector2Json() }, 293 | } 294 | )!) 295 | .ToDictionary(e => e.Key, e => e.Value); 296 | Plugin.logger.LogInfo($"Registried sprite data from {mod.id} mod"); 297 | } 298 | catch (Exception e) 299 | { 300 | Plugin.logger.LogError($"Error on loading sprite data from {mod.id} mod: {e.Message}"); 301 | } 302 | } 303 | 304 | public static void LoadAudioFile(Mod mod, Mod.File file) 305 | { 306 | // AudioSource audioSource = new GameObject().AddComponent(); 307 | // GameObject.DontDestroyOnLoad(audioSource); 308 | // audioSource.clip = Managers.Audio.BuildAudioClip(file.bytes); 309 | // Registry.audioClips.Add(Path.GetFileNameWithoutExtension(file.name), audioSource); 310 | // TODO: issue #71 311 | } 312 | 313 | public static void LoadGameLogicDataPatch(Mod mod, JObject gld, JObject patch) 314 | { 315 | try 316 | { 317 | HandleSkins(gld, patch); 318 | foreach (JToken jtoken in patch.SelectTokens("$.*.*").ToArray()) 319 | { 320 | JObject? token = jtoken.TryCast(); 321 | if (token != null) 322 | { 323 | if (token["idx"] != null && (int)token["idx"] == -1) 324 | { 325 | string id = Util.GetJTokenName(token); 326 | string dataType = Util.GetJTokenName(token, 2); 327 | token["idx"] = Registry.autoidx; 328 | if (typeMappings.TryGetValue(dataType, out Type? targetType)) 329 | { 330 | MethodInfo? methodInfo = typeof(EnumCache<>).MakeGenericType(targetType).GetMethod("AddMapping"); 331 | if (methodInfo != null) 332 | { 333 | methodInfo.Invoke(null, new object[] { id, Registry.autoidx }); 334 | methodInfo.Invoke(null, new object[] { id, Registry.autoidx }); 335 | if (targetType == typeof(TribeData.Type)) 336 | { 337 | Registry.customTribes.Add((TribeData.Type)Registry.autoidx); 338 | token["style"] = Registry.climateAutoidx; 339 | token["climate"] = Registry.climateAutoidx; 340 | Registry.climateAutoidx++; 341 | } 342 | else if (targetType == typeof(UnitData.Type)) 343 | { 344 | UnitData.Type unitPrefabType = UnitData.Type.Scout; 345 | if (token["prefab"] != null) 346 | { 347 | TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo; 348 | string prefabId = textInfo.ToTitleCase(token["prefab"].ToString()); 349 | if (Enum.TryParse(prefabId, out UnitData.Type parsedType)) 350 | { 351 | unitPrefabType = parsedType; 352 | } 353 | } 354 | PrefabManager.units.TryAdd((int)(UnitData.Type)Registry.autoidx, PrefabManager.units[(int)unitPrefabType]); 355 | } 356 | else if (targetType == typeof(ImprovementData.Type)) 357 | { 358 | ImprovementData.Type improvementPrefabType = ImprovementData.Type.CustomsHouse; 359 | if (token["prefab"] != null) 360 | { 361 | TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo; 362 | string prefabId = textInfo.ToTitleCase(token["prefab"].ToString()); 363 | if (Enum.TryParse(prefabId, out ImprovementData.Type parsedType)) 364 | { 365 | improvementPrefabType = parsedType; 366 | } 367 | } 368 | PrefabManager.improvements.TryAdd((ImprovementData.Type)Registry.autoidx, PrefabManager.improvements[improvementPrefabType]); 369 | } 370 | else if (targetType == typeof(ResourceData.Type)) 371 | { 372 | ResourceData.Type resourcePrefabType = ResourceData.Type.Game; 373 | if (token["prefab"] != null) 374 | { 375 | TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo; 376 | string prefabId = textInfo.ToTitleCase(token["prefab"].ToString()); 377 | if (Enum.TryParse(prefabId, out ResourceData.Type parsedType)) 378 | { 379 | resourcePrefabType = parsedType; 380 | } 381 | } 382 | PrefabManager.resources.TryAdd((ResourceData.Type)Registry.autoidx, PrefabManager.resources[resourcePrefabType]); 383 | } 384 | Plugin.logger.LogInfo("Created mapping for " + targetType.ToString() + " with id " + id + " and index " + Registry.autoidx); 385 | Registry.autoidx++; 386 | } 387 | } 388 | } 389 | } 390 | } 391 | foreach (JToken jtoken in patch.SelectTokens("$.tribeData.*").ToArray()) 392 | { 393 | JObject token = jtoken.Cast(); 394 | 395 | if (token["preview"] != null) 396 | { 397 | Visual.PreviewTile[] preview = JsonSerializer.Deserialize(token["preview"].ToString())!; 398 | Registry.tribePreviews[Util.GetJTokenName(token)] = preview; 399 | } 400 | } 401 | gld.Merge(patch, new() { MergeArrayHandling = MergeArrayHandling.Replace, MergeNullValueHandling = MergeNullValueHandling.Merge }); 402 | Plugin.logger.LogInfo($"Registried patch from {mod.id} mod"); 403 | } 404 | catch (Exception e) 405 | { 406 | Plugin.logger.LogError($"Error on loading patch from {mod.id} mod: {e.Message}"); 407 | mod.status = Mod.Status.Error; 408 | } 409 | } 410 | 411 | public static void HandleSkins(JObject gld, JObject patch) 412 | { 413 | foreach (JToken jtoken in patch.SelectTokens("$.tribeData.*").ToArray()) 414 | { 415 | JObject token = jtoken.Cast(); 416 | 417 | if (token["skins"] != null) 418 | { 419 | JArray skins = token["skins"].Cast(); 420 | List skinsToRemove = new(); 421 | List skinValues = skins._values.ToArray().ToList(); 422 | foreach (var skin in skinValues) 423 | { 424 | string skinValue = skin.ToString(); 425 | if (skinValue.StartsWith('-') && Enum.TryParse(skinValue.Substring(1), out _)) 426 | { 427 | skinsToRemove.Add(skinValue.Substring(1)); 428 | } 429 | else if (!Enum.TryParse(skinValue, out _)) 430 | { 431 | EnumCache.AddMapping(skinValue.ToLowerInvariant(), (SkinType)Registry.autoidx); 432 | EnumCache.AddMapping(skinValue.ToLowerInvariant(), (SkinType)Registry.autoidx); 433 | Registry.skinInfo.Add(new Visual.SkinInfo(Registry.autoidx, skinValue, null)); 434 | Plugin.logger.LogInfo("Created mapping for skinType with id " + skinValue + " and index " + Registry.autoidx); 435 | Registry.autoidx++; 436 | } 437 | } 438 | foreach (var skin in Registry.skinInfo) 439 | { 440 | if (skins._values.Contains(skin.id)) 441 | { 442 | skins._values.Remove(skin.id); 443 | skins._values.Add(skin.idx); 444 | } 445 | } 446 | JToken originalSkins = gld.SelectToken(skins.Path, false); 447 | if (originalSkins != null) 448 | { 449 | skins.Merge(originalSkins); 450 | foreach (var skin in skinsToRemove) 451 | { 452 | skins._values.Remove(skin); 453 | skins._values.Remove("-" + skin); 454 | } 455 | } 456 | } 457 | } 458 | foreach (JToken jtoken in patch.SelectTokens("$.skinData.*").ToArray()) 459 | { 460 | JObject token = jtoken.Cast(); 461 | string id = Util.GetJTokenName(token); 462 | int index = Registry.skinInfo.FindIndex(t => t.id == id); 463 | if (Registry.skinInfo.ElementAtOrDefault(index) != null) 464 | { 465 | SkinData skinData = new(); 466 | if (token["color"] != null) 467 | { 468 | skinData.color = (int)token["color"]; 469 | } 470 | if (token["language"] != null) 471 | { 472 | skinData.language = token["language"].ToString(); 473 | } 474 | Registry.skinInfo[index] = new Visual.SkinInfo(Registry.skinInfo[index].idx, Registry.skinInfo[index].id, skinData); 475 | } 476 | } 477 | patch.Remove("skinData"); 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /src/Managers/Audio.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Polytopia.Data; 3 | using UnityEngine; 4 | using UnityEngine.Networking; 5 | 6 | namespace PolyMod.Managers; 7 | public static class Audio 8 | { 9 | [HarmonyPostfix] 10 | [HarmonyPatch(typeof(AudioManager), nameof(AudioManager.SetupData))] 11 | private static void AudioManager_SetupData() 12 | { 13 | foreach (var item in Registry.customTribes) 14 | { 15 | if (PolytopiaDataManager.GetLatestGameLogicData().TryGetData(item, out TribeData data)) 16 | { 17 | AudioManager.instance.climateTribeMap.Add(data.climate, item); 18 | } 19 | } 20 | } 21 | 22 | [HarmonyPrefix] 23 | [HarmonyPatch(typeof(MusicData), nameof(MusicData.GetNatureAudioClip))] 24 | private static bool MusicData_GetNatureAudioClip(ref AudioClip __result, TribeData.Type type, SkinType skinType) 25 | { 26 | AudioClip? audioClip = Registry.GetAudioClip("nature", Util.GetStyle(type, skinType)); 27 | if (audioClip != null) 28 | { 29 | __result = audioClip; 30 | return false; 31 | } 32 | return true; 33 | } 34 | 35 | [HarmonyPrefix] 36 | [HarmonyPatch(typeof(MusicData), nameof(MusicData.GetMusicAudioClip))] 37 | private static bool MusicData_GetMusicAudioClip(ref AudioClip __result, TribeData.Type type, SkinType skinType) 38 | { 39 | AudioClip? audioClip = Registry.GetAudioClip("music", Util.GetStyle(type, skinType)); 40 | if (audioClip != null) 41 | { 42 | __result = audioClip; 43 | return false; 44 | } 45 | return true; 46 | } 47 | 48 | [HarmonyPrefix] 49 | [HarmonyPatch(typeof(AudioSFXData), nameof(AudioSFXData.GetClip))] 50 | private static bool AudioSFXData_GetClip(ref AudioClip __result, SFXTypes id, SkinType skinType) 51 | { 52 | AudioClip? audioClip = Registry.GetAudioClip( 53 | EnumCache.GetName(id), 54 | EnumCache.GetName(skinType) 55 | ); 56 | if (audioClip != null) 57 | { 58 | __result = audioClip; 59 | return false; 60 | } 61 | return true; 62 | } 63 | 64 | public static AudioClip BuildAudioClip(byte[] data) 65 | { 66 | return new AudioClip(new()); 67 | } 68 | 69 | internal static void Init() 70 | { 71 | Harmony.CreateAndPatchAll(typeof(Audio)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Managers/Compatibility.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System.Text; 3 | using UnityEngine; 4 | using UnityEngine.EventSystems; 5 | 6 | namespace PolyMod.Managers; 7 | internal static class Compatibility 8 | { 9 | internal static string signature = string.Empty; 10 | internal static string looseSignature = string.Empty; 11 | private static bool sawSignatureWarning; 12 | 13 | public static void HashSignatures(StringBuilder looseSignatureString, StringBuilder signatureString) 14 | { 15 | looseSignature = Util.Hash(looseSignatureString); 16 | signature = Util.Hash(signatureString); 17 | } 18 | 19 | private static bool CheckSignatures(Action action, int id, BaseEventData eventData, Il2CppSystem.Guid gameId) 20 | { 21 | if (sawSignatureWarning) 22 | { 23 | sawSignatureWarning = false; 24 | return true; 25 | } 26 | 27 | string[] signatures = { string.Empty, string.Empty }; 28 | try 29 | { 30 | signatures = File.ReadAllLines(Path.Combine(Application.persistentDataPath, $"{gameId}.signatures")); 31 | } 32 | catch { } 33 | if (signatures[0] == string.Empty && signatures[1] == string.Empty) return true; 34 | if (Plugin.config.debug) return true; 35 | if (looseSignature != signatures[0]) 36 | { 37 | PopupManager.GetBasicPopup(new( 38 | Localization.Get("polymod.signature.mismatch"), 39 | Localization.Get("polymod.signature.incompatible"), 40 | new(new PopupBase.PopupButtonData[] { 41 | new("OK") 42 | }) 43 | )).Show(); 44 | return false; 45 | } 46 | if (signature != signatures[1]) 47 | { 48 | PopupManager.GetBasicPopup(new( 49 | Localization.Get("polymod.signature.mismatch"), 50 | Localization.Get("polymod.signature.maybe.incompatible"), 51 | new(new PopupBase.PopupButtonData[] { 52 | new( 53 | "OK", 54 | callback: (UIButtonBase.ButtonAction)((int _, BaseEventData _) => { 55 | sawSignatureWarning = true; 56 | action(id, eventData); 57 | }) 58 | ) 59 | }) 60 | )).Show(); 61 | return false; 62 | } 63 | return true; 64 | } 65 | 66 | [HarmonyPostfix] 67 | [HarmonyPatch(typeof(StartScreen), nameof(StartScreen.Start))] 68 | private static void StartScreen_Start() 69 | { 70 | Version incompatibilityWarningLastVersion = Plugin.POLYTOPIA_VERSION.CutRevision(); 71 | try 72 | { 73 | incompatibilityWarningLastVersion = new(File.ReadAllText(Plugin.INCOMPATIBILITY_WARNING_LAST_VERSION_PATH)); 74 | } 75 | catch (FileNotFoundException) { } 76 | if (VersionManager.SemanticVersion.Cast().CutRevision() > incompatibilityWarningLastVersion) 77 | { 78 | File.WriteAllText( 79 | Plugin.INCOMPATIBILITY_WARNING_LAST_VERSION_PATH, 80 | VersionManager.SemanticVersion.Cast().CutRevision().ToString() 81 | ); 82 | PopupManager.GetBasicPopup(new( 83 | Localization.Get("polymod.version.mismatch"), 84 | Localization.Get("polymod.version.mismatch.description"), 85 | new(new PopupBase.PopupButtonData[] { 86 | new("buttons.stay", customColorStates: ColorConstants.redButtonColorStates), 87 | new( 88 | "buttons.exitgame", 89 | PopupBase.PopupButtonData.States.None, 90 | (Il2CppSystem.Action)Application.Quit, 91 | closesPopup: false 92 | ) 93 | })) 94 | ).Show(); 95 | } 96 | } 97 | 98 | [HarmonyPrefix] 99 | [HarmonyPatch(typeof(GameInfoPopup), nameof(GameInfoPopup.OnMainButtonClicked))] 100 | private static bool GameInfoPopup_OnMainButtonClicked(GameInfoPopup __instance, int id, BaseEventData eventData) 101 | { 102 | return CheckSignatures(__instance.OnMainButtonClicked, id, eventData, __instance.gameId); 103 | } 104 | 105 | [HarmonyPrefix] 106 | [HarmonyPatch(typeof(StartScreen), nameof(StartScreen.OnResumeButtonClick))] 107 | private static bool StartScreen_OnResumeButtonClick(StartScreen __instance, int id, BaseEventData eventData) 108 | { 109 | return CheckSignatures(__instance.OnResumeButtonClick, id, eventData, ClientBase.GetSinglePlayerSessions()[0]); 110 | } 111 | 112 | [HarmonyPostfix] 113 | [HarmonyPatch(typeof(GameInfoPopup), nameof(GameInfoPopup.DeletePaPGame))] 114 | private static void ClientBase_DeletePassAndPlayGame(GameInfoPopup __instance) 115 | { 116 | File.Delete(Path.Combine(Application.persistentDataPath, $"{__instance.gameId}.signatures")); 117 | } 118 | 119 | [HarmonyPrefix] 120 | [HarmonyPatch(typeof(ClientBase), nameof(ClientBase.DeleteSinglePlayerGames))] 121 | private static void ClientBase_DeleteSinglePlayerGames() 122 | { 123 | foreach (var gameId in ClientBase.GetSinglePlayerSessions()) 124 | { 125 | File.Delete(Path.Combine(Application.persistentDataPath, $"{gameId}.signatures")); 126 | } 127 | } 128 | 129 | [HarmonyPrefix] 130 | [HarmonyPatch(typeof(GameManager), nameof(GameManager.MatchEnded))] 131 | private static void GameManager_MatchEnded(bool localPlayerIsWinner, ScoreDetails scoreDetails, byte winnerId) 132 | { 133 | File.Delete(Path.Combine(Application.persistentDataPath, $"{GameManager.Client.gameId}.signatures")); 134 | } 135 | 136 | [HarmonyPostfix] 137 | [HarmonyPatch(typeof(ClientBase), nameof(ClientBase.CreateSession), typeof(GameSettings), typeof(Il2CppSystem.Guid))] 138 | private static void ClientBase_CreateSession(GameSettings settings, Il2CppSystem.Guid gameId) 139 | { 140 | File.WriteAllLinesAsync( 141 | Path.Combine(Application.persistentDataPath, $"{gameId}.signatures"), 142 | new string[] { looseSignature, signature } 143 | ); 144 | } 145 | 146 | internal static void Init() 147 | { 148 | Harmony.CreateAndPatchAll(typeof(Compatibility)); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Managers/Hub.cs: -------------------------------------------------------------------------------- 1 | using Cpp2IL.Core.Extensions; 2 | using HarmonyLib; 3 | using Il2CppInterop.Runtime; 4 | using TMPro; 5 | using UnityEngine; 6 | using UnityEngine.EventSystems; 7 | using UnityEngine.UI; 8 | 9 | namespace PolyMod.Managers; 10 | internal static class Hub 11 | { 12 | private const string HEADER_PREFIX = ""; 13 | private const string HEADER_POSTFIX = ""; 14 | 15 | [HarmonyPrefix] 16 | [HarmonyPatch(typeof(SplashController), nameof(SplashController.LoadAndPlayClip))] 17 | private static bool SplashController_LoadAndPlayClip(SplashController __instance) 18 | { 19 | string name = "intro.mp4"; 20 | string path = Path.Combine(Application.persistentDataPath, name); 21 | File.WriteAllBytesAsync(path, Plugin.GetResource(name).ReadBytes()); 22 | __instance.lastPlayTime = Time.realtimeSinceStartup; 23 | __instance.videoPlayer.url = path; 24 | __instance.videoPlayer.Play(); 25 | return false; 26 | } 27 | 28 | [HarmonyPostfix] 29 | [HarmonyPatch(typeof(PopupButtonContainer), nameof(PopupButtonContainer.SetButtonData))] 30 | private static void PopupButtonContainer_SetButtonData(PopupButtonContainer __instance) 31 | { 32 | int num = __instance.buttons.Length; 33 | for (int i = 0; i < num; i++) 34 | { 35 | UITextButton uitextButton = __instance.buttons[i]; 36 | Vector2 vector = new((num == 1) ? 0.5f : (i / (num - 1.0f)), 0.5f); 37 | uitextButton.rectTransform.anchorMin = vector; 38 | uitextButton.rectTransform.anchorMax = vector; 39 | uitextButton.rectTransform.pivot = vector; 40 | } 41 | } 42 | 43 | [HarmonyPrefix] 44 | [HarmonyPatch(typeof(StartScreen), nameof(StartScreen.Start))] 45 | private static void StartScreen_Start() 46 | { 47 | Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppReferenceArray allLocalizers = GameObject.FindObjectsOfTypeAll(Il2CppType.From(typeof(TMPLocalizer))); 48 | 49 | foreach (UnityEngine.Object item in allLocalizers) 50 | { 51 | TMPLocalizer? localizer = item.TryCast(); 52 | if (localizer == null) 53 | { 54 | continue; 55 | } 56 | 57 | Transform? parent = localizer?.gameObject?.transform?.parent; 58 | if (parent == null) 59 | { 60 | continue; 61 | } 62 | 63 | string parentName = parent.name; 64 | 65 | if (parentName == "SettingsButton") 66 | { 67 | Transform? textTransform = parent.FindChild("DescriptionText"); 68 | if (textTransform == null) 69 | { 70 | return; 71 | } 72 | 73 | GameObject originalText = textTransform.gameObject; 74 | GameObject text = GameObject.Instantiate(originalText, originalText.transform.parent.parent.parent); 75 | text.name = "PolyModVersion"; 76 | 77 | RectTransform rect = text.GetComponent(); 78 | rect.anchoredPosition = new Vector2(265, 40); 79 | rect.sizeDelta = new Vector2(500, rect.sizeDelta.y); 80 | rect.anchorMax = Vector2.zero; 81 | rect.anchorMin = Vector2.zero; 82 | 83 | TextMeshProUGUI textComponent = text.GetComponent(); 84 | textComponent.fontSize = 18; 85 | textComponent.alignment = TextAlignmentOptions.BottomLeft; 86 | 87 | text.GetComponent().Text = $"PolyMod {Plugin.VERSION}"; 88 | text.AddComponent().ignoreLayout = true; 89 | } 90 | else if (parentName == "NewsButton") 91 | { 92 | GameObject originalButton = parent.gameObject; 93 | GameObject button = GameObject.Instantiate(originalButton, originalButton.transform.parent); 94 | button.name = "PolyModHubButton"; 95 | button.transform.position = originalButton.transform.position - new Vector3(90, 0, 0); 96 | 97 | UIRoundButton buttonComponent = button.GetComponent(); 98 | buttonComponent.bg.sprite = Visual.BuildSprite(Plugin.GetResource("polymod_icon.png").ReadBytes()); 99 | buttonComponent.bg.transform.localScale = new Vector3(1.2f, 1.2f, 0); 100 | buttonComponent.bg.color = Color.white; 101 | 102 | GameObject.Destroy(buttonComponent.icon.gameObject); 103 | GameObject.Destroy(buttonComponent.outline.gameObject); 104 | 105 | buttonComponent.OnClicked += (UIButtonBase.ButtonAction)PolyModHubButtonClicked; 106 | } 107 | } 108 | 109 | static void PolyModHubButtonClicked(int buttonId, BaseEventData eventData) 110 | { 111 | BasicPopup popup = PopupManager.GetBasicPopup(); 112 | popup.Header = Localization.Get("polymod.hub"); 113 | popup.Description = Localization.Get("polymod.hub.header", new Il2CppSystem.Object[] { 114 | HEADER_PREFIX, 115 | HEADER_POSTFIX 116 | }) + "\n\n"; 117 | foreach (var mod in Registry.mods.Values) 118 | { 119 | popup.Description += Localization.Get("polymod.hub.mod", new Il2CppSystem.Object[] { 120 | mod.name, 121 | Localization.Get("polymod.hub.mod.status." 122 | + Enum.GetName(typeof(Mod.Status), mod.status)!.ToLower()), 123 | string.Join(", ", mod.authors), 124 | mod.version.ToString() 125 | }); 126 | popup.Description += "\n\n"; 127 | } 128 | popup.Description += Localization.Get("polymod.hub.footer", new Il2CppSystem.Object[] { 129 | HEADER_PREFIX, 130 | HEADER_POSTFIX 131 | }); 132 | List popupButtons = new() 133 | { 134 | new("buttons.back"), 135 | new( 136 | "polymod.hub.discord", 137 | callback: (UIButtonBase.ButtonAction)((int _, BaseEventData _) => 138 | NativeHelpers.OpenURL(Plugin.DISCORD_LINK, false)) 139 | ) 140 | }; 141 | if (Plugin.config.debug) 142 | popupButtons.Add(new( 143 | "polymod.hub.dump", 144 | callback: (UIButtonBase.ButtonAction)((int _, BaseEventData _) => 145 | { 146 | Directory.CreateDirectory(Plugin.DUMPED_DATA_PATH); 147 | File.WriteAllTextAsync( 148 | Path.Combine(Plugin.DUMPED_DATA_PATH, $"gameLogicData.json"), 149 | PolytopiaDataManager.provider.LoadGameLogicData(VersionManager.GameLogicDataVersion) 150 | ); 151 | File.WriteAllTextAsync( 152 | Path.Combine(Plugin.DUMPED_DATA_PATH, $"avatarData.json"), 153 | PolytopiaDataManager.provider.LoadAvatarData(1337) 154 | ); 155 | NotificationManager.Notify(Localization.Get("polymod.hub.dumped")); 156 | }), 157 | closesPopup: false 158 | )); 159 | popup.buttonData = popupButtons.ToArray(); 160 | popup.ShowSetWidth(1000); 161 | } 162 | 163 | if (Main.dependencyCycle) 164 | { 165 | var popup = PopupManager.GetBasicPopup(new( 166 | Localization.Get("polymod.cycle"), 167 | Localization.Get("polymod.cycle.description"), 168 | new(new PopupBase.PopupButtonData[] { 169 | new( 170 | "buttons.exitgame", 171 | PopupBase.PopupButtonData.States.None, 172 | (Il2CppSystem.Action)Application.Quit, 173 | closesPopup: false, 174 | customColorStates: ColorConstants.redButtonColorStates 175 | ) 176 | }) 177 | )); 178 | popup.IsUnskippable = true; 179 | popup.Show(); 180 | } 181 | } 182 | 183 | internal static void Init() 184 | { 185 | Harmony.CreateAndPatchAll(typeof(Hub)); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Managers/Loc.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using I2.Loc; 3 | using Il2CppInterop.Runtime.InteropTypes.Arrays; 4 | using System.Reflection; 5 | using LibCpp2IL; 6 | using Polytopia.Data; 7 | 8 | namespace PolyMod.Managers; 9 | public static class Loc 10 | { 11 | [HarmonyPostfix] 12 | [HarmonyPatch(typeof(SelectTribePopup), nameof(SelectTribePopup.SetDescription))] 13 | private static void SetDescription(SelectTribePopup __instance) 14 | { 15 | if ((int)__instance.SkinType >= Plugin.AUTOIDX_STARTS_FROM) 16 | { 17 | string description = Localization.Get(__instance.SkinType.GetLocalizationDescriptionKey()); 18 | if (description == __instance.SkinType.GetLocalizationDescriptionKey()) 19 | { 20 | description = Localization.Get(__instance.tribeData.description, new Il2CppSystem.Object[] 21 | { 22 | Localization.Get(__instance.tribeData.displayName), 23 | }); 24 | } 25 | __instance.Description = description + "\n\n" + Localization.GetSkinned(__instance.SkinType, __instance.tribeData.description2, new Il2CppSystem.Object[] 26 | { 27 | __instance.tribeName, 28 | Localization.Get(__instance.startTechSid, Array.Empty()) 29 | }); 30 | } 31 | } 32 | 33 | [HarmonyPrefix] 34 | [HarmonyPatch(typeof(Localization), nameof(Localization.Get), typeof(string), typeof(Il2CppReferenceArray))] 35 | private static bool Localization_Get(ref string key, Il2CppReferenceArray args) 36 | { 37 | List keys = key.Split('.').ToList(); 38 | int? idx = null; 39 | string? name = null; 40 | foreach (string item in keys) 41 | { 42 | if (int.TryParse(item, out int parsedIdx)) 43 | { 44 | if(parsedIdx >= Plugin.AUTOIDX_STARTS_FROM) 45 | { 46 | idx = parsedIdx; 47 | } 48 | } 49 | } 50 | if (idx != null) 51 | { 52 | foreach (var targetType in Loader.typeMappings.Values) 53 | { 54 | MethodInfo? methodInfo = typeof(EnumCache<>).MakeGenericType(targetType).GetMethod("TryGetName"); 55 | if (methodInfo != null) 56 | { 57 | object?[] parameters = { idx, null }; 58 | object? methodInvokeResult = methodInfo.Invoke(null, parameters); 59 | if (methodInvokeResult != null) 60 | { 61 | if ((bool)methodInvokeResult) 62 | { 63 | name = (string?)parameters[1]; 64 | } 65 | } 66 | } 67 | } 68 | if (name != null && idx != null) 69 | { 70 | int index = keys.IndexOf(idx.ToString()!); 71 | keys[index] = name; 72 | key = string.Join(".", keys); 73 | } 74 | } 75 | return true; 76 | } 77 | 78 | public static void BuildAndLoadLocalization(Dictionary> localization) 79 | { 80 | foreach (var (key, data) in localization) 81 | { 82 | string name = key.Replace("_", "."); 83 | name = name.Replace("..", "_"); 84 | if (name.StartsWith("tribeskins")) name = "TribeSkins/" + name; 85 | TermData term = LocalizationManager.Sources[0].AddTerm(name); 86 | List strings = new(); 87 | foreach (string language in LocalizationManager.GetAllLanguages(false)) 88 | { 89 | strings.Add(data.GetOrDefault(language, data.GetOrDefault("English", term.Term))!); 90 | } 91 | term.Languages = new Il2CppStringArray(strings.ToArray()); 92 | } 93 | } 94 | 95 | internal static void Init() 96 | { 97 | Harmony.CreateAndPatchAll(typeof(Loc)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Managers/Main.cs: -------------------------------------------------------------------------------- 1 | using BepInEx.Unity.IL2CPP.Logging; 2 | using HarmonyLib; 3 | using Newtonsoft.Json.Linq; 4 | using Polytopia.Data; 5 | using System.Diagnostics; 6 | using System.Text; 7 | using System.Text.Json; 8 | using UnityEngine; 9 | 10 | namespace PolyMod.Managers; 11 | public static class Main 12 | { 13 | internal const int MAX_TECH_TIER = 100; 14 | internal static readonly Stopwatch stopwatch = new(); 15 | internal static bool fullyInitialized; 16 | internal static bool dependencyCycle; 17 | 18 | 19 | [HarmonyPrefix] 20 | [HarmonyPatch(typeof(GameLogicData), nameof(GameLogicData.AddGameLogicPlaceholders))] 21 | private static void GameLogicData_Parse(GameLogicData __instance, JObject rootObject) 22 | { 23 | if (!fullyInitialized) 24 | { 25 | Load(rootObject); 26 | foreach (Visual.SkinInfo skin in Registry.skinInfo) 27 | { 28 | if (skin.skinData != null) 29 | __instance.skinData[(SkinType)skin.idx] = skin.skinData; 30 | } 31 | fullyInitialized = true; 32 | } 33 | } 34 | 35 | [HarmonyPrefix] 36 | [HarmonyPatch(typeof(PurchaseManager), nameof(PurchaseManager.IsSkinUnlocked))] 37 | [HarmonyPatch(typeof(PurchaseManager), nameof(PurchaseManager.IsSkinUnlockedInternal))] 38 | private static bool PurchaseManager_IsSkinUnlockedInternal(ref bool __result, SkinType skinType) 39 | { 40 | __result = (int)skinType >= Plugin.AUTOIDX_STARTS_FROM && skinType != SkinType.Test; 41 | return !__result; 42 | } 43 | 44 | [HarmonyPostfix] 45 | [HarmonyPatch(typeof(PurchaseManager), nameof(PurchaseManager.IsTribeUnlocked))] 46 | private static void PurchaseManager_IsTribeUnlocked(ref bool __result, TribeData.Type type) 47 | { 48 | __result = (int)type >= Plugin.AUTOIDX_STARTS_FROM || __result; 49 | } 50 | 51 | [HarmonyPostfix] 52 | [HarmonyPatch(typeof(PurchaseManager), nameof(PurchaseManager.GetUnlockedTribes))] 53 | private static void PurchaseManager_GetUnlockedTribes( 54 | ref Il2CppSystem.Collections.Generic.List __result, 55 | bool forceUpdate = false 56 | ) 57 | { 58 | foreach (var tribe in Registry.customTribes) __result.Add(tribe); 59 | } 60 | 61 | [HarmonyPrefix] 62 | [HarmonyPatch(typeof(IL2CPPUnityLogSource), nameof(IL2CPPUnityLogSource.UnityLogCallback))] 63 | private static bool IL2CPPUnityLogSource_UnityLogCallback(string logLine, string exception, LogType type) 64 | { 65 | foreach (string stringToIgnore in Plugin.LOG_MESSAGES_IGNORE) 66 | { 67 | if (logLine.Contains(stringToIgnore)) 68 | return false; 69 | } 70 | return true; 71 | } 72 | 73 | internal static void Init() 74 | { 75 | stopwatch.Start(); 76 | Harmony.CreateAndPatchAll(typeof(Main)); 77 | Mod.Manifest polytopia = new( 78 | "polytopia", 79 | "The Battle of Polytopia", 80 | new(Application.version.ToString()), 81 | new string[] { "Midjiwan AB" }, 82 | Array.Empty() 83 | ); 84 | Registry.mods.Add(polytopia.id, new(polytopia, Mod.Status.Success, new())); 85 | Loader.LoadMods(Registry.mods); 86 | dependencyCycle = !Loader.SortMods(Registry.mods); 87 | if (dependencyCycle) return; 88 | 89 | StringBuilder looseSignatureString = new(); 90 | StringBuilder signatureString = new(); 91 | foreach (var (id, mod) in Registry.mods) 92 | { 93 | if (mod.status != Mod.Status.Success) continue; 94 | foreach (var file in mod.files) 95 | { 96 | if (Path.GetExtension(file.name) == ".dll") 97 | { 98 | Loader.LoadAssemblyFile(mod, file); 99 | } 100 | if (Path.GetFileName(file.name) == "sprites.json") 101 | { 102 | Loader.LoadSpriteInfoFile(mod, file); 103 | } 104 | } 105 | if (!mod.client && id != "polytopia") 106 | { 107 | looseSignatureString.Append(id); 108 | looseSignatureString.Append(mod.version.Major); 109 | 110 | signatureString.Append(id); 111 | signatureString.Append(mod.version.ToString()); 112 | } 113 | } 114 | Compatibility.HashSignatures(looseSignatureString, signatureString); 115 | 116 | stopwatch.Stop(); 117 | } 118 | 119 | internal static void Load(JObject gameLogicdata) 120 | { 121 | stopwatch.Start(); 122 | Loc.BuildAndLoadLocalization( 123 | JsonSerializer.Deserialize>>( 124 | Plugin.GetResource("localization.json") 125 | )! 126 | ); 127 | if (dependencyCycle) return; 128 | 129 | foreach (var (id, mod) in Registry.mods) 130 | { 131 | if (mod.status != Mod.Status.Success) continue; 132 | foreach (var file in mod.files) 133 | { 134 | if (Path.GetFileName(file.name) == "patch.json") 135 | { 136 | Loader.LoadGameLogicDataPatch(mod, gameLogicdata, JObject.Parse(new StreamReader(new MemoryStream(file.bytes)).ReadToEnd())); 137 | } 138 | if (Path.GetFileName(file.name) == "localization.json") 139 | { 140 | Loader.LoadLocalizationFile(mod, file); 141 | } 142 | if (Path.GetExtension(file.name) == ".png") 143 | { 144 | Loader.LoadSpriteFile(mod, file); 145 | } 146 | if (Path.GetExtension(file.name) == ".wav") 147 | { 148 | Loader.LoadAudioFile(mod, file); 149 | } 150 | } 151 | } 152 | TechItem.techTierFirebaseId.Clear(); 153 | for (int i = 0; i <= MAX_TECH_TIER; i++) 154 | { 155 | TechItem.techTierFirebaseId.Add($"tech_research_{i}"); 156 | } 157 | stopwatch.Stop(); 158 | Plugin.logger.LogInfo($"Loaded all mods in {stopwatch.ElapsedMilliseconds}ms"); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Managers/Visual.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using HarmonyLib; 3 | using Polytopia.Data; 4 | using UnityEngine; 5 | using UnityEngine.U2D; 6 | using UnityEngine.UI; 7 | using Il2CppSystem.Linq; 8 | using PolyMod.Json; 9 | using System.Text.Json.Serialization; 10 | 11 | namespace PolyMod.Managers; 12 | public static class Visual 13 | { 14 | public class PreviewTile 15 | { 16 | [JsonInclude] 17 | public int? x = null; 18 | [JsonInclude] 19 | public int? y = null; 20 | [JsonInclude] 21 | [JsonConverter(typeof(EnumCacheJson))] 22 | public Polytopia.Data.TerrainData.Type terrainType = Polytopia.Data.TerrainData.Type.Ocean; 23 | [JsonInclude] 24 | [JsonConverter(typeof(EnumCacheJson))] 25 | public ResourceData.Type resourceType = ResourceData.Type.None; 26 | [JsonInclude] 27 | [JsonConverter(typeof(EnumCacheJson))] 28 | public UnitData.Type unitType = UnitData.Type.None; 29 | [JsonInclude] 30 | [JsonConverter(typeof(EnumCacheJson))] 31 | public ImprovementData.Type improvementType = ImprovementData.Type.None; 32 | } 33 | public record SpriteInfo(float? pixelsPerUnit, Vector2? pivot); 34 | public record SkinInfo(int idx, string id, SkinData? skinData); 35 | public static Dictionary basicPopupWidths = new(); 36 | private static bool firstTimeOpeningPreview = true; 37 | private static UnitData.Type currentUnitTypeUI = UnitData.Type.None; 38 | 39 | #region General 40 | 41 | [HarmonyPostfix] 42 | [HarmonyPatch(typeof(TechItem), nameof(TechItem.SetupComplete))] 43 | private static void TechItem_SetupComplete() 44 | { 45 | } 46 | 47 | [HarmonyPrefix] 48 | [HarmonyPatch(typeof(StartScreen), nameof(StartScreen.Start))] 49 | private static void StartScreen_Start() 50 | { 51 | firstTimeOpeningPreview = true; 52 | } 53 | 54 | [HarmonyPrefix] 55 | [HarmonyPatch(typeof(SpriteAtlasManager), nameof(SpriteAtlasManager.LoadSprite), typeof(string), typeof(string), typeof(SpriteCallback))] 56 | private static bool SpriteAtlasManager_LoadSprite(SpriteAtlasManager __instance, string atlas, string sprite, SpriteCallback completion) 57 | { 58 | bool found = false; 59 | __instance.LoadSpriteAtlas(atlas, (Il2CppSystem.Action)GetAtlas); 60 | 61 | return !found; 62 | 63 | void GetAtlas(SpriteAtlas spriteAtlas) 64 | { 65 | if (spriteAtlas != null) 66 | { 67 | List names = sprite.Split('_').ToList(); 68 | List filteredNames = new List(names); 69 | string style = ""; 70 | foreach (string item in names) 71 | { 72 | string upperitem = char.ToUpper(item[0]) + item.Substring(1); 73 | if (EnumCache.TryGetType(item, out TribeData.Type tribe) || EnumCache.TryGetType(item, out SkinType skin) 74 | || EnumCache.TryGetType(upperitem, out TribeData.Type tribeUpper) || EnumCache.TryGetType(upperitem, out SkinType skinUpper)) 75 | { 76 | filteredNames.Remove(item); 77 | style = item; 78 | continue; 79 | } 80 | } 81 | string name = string.Join("_", filteredNames); 82 | Sprite? newSprite = Registry.GetSprite(name, style); 83 | if (newSprite != null) 84 | { 85 | completion?.Invoke(atlas, sprite, newSprite); 86 | found = true; 87 | } 88 | } 89 | } 90 | } 91 | 92 | [HarmonyPostfix] 93 | [HarmonyPatch(typeof(SpriteAtlasManager), nameof(SpriteAtlasManager.DoSpriteLookup))] 94 | private static void SpriteAtlasManager_DoSpriteLookup(ref SpriteAtlasManager.SpriteLookupResult __result, SpriteAtlasManager __instance, string baseName, TribeData.Type tribe, SkinType skin, bool checkForOutline, int level) 95 | { 96 | baseName = Util.FormatSpriteName(baseName); 97 | 98 | Sprite? sprite = Registry.GetSprite(baseName, Util.GetStyle(tribe, skin), level); 99 | if (sprite != null) 100 | __result.sprite = sprite; 101 | } 102 | 103 | #endregion 104 | #region Units 105 | 106 | [HarmonyPrefix] 107 | [HarmonyPatch(typeof(UIUnitRenderer), nameof(UIUnitRenderer.CreateUnit))] 108 | private static bool UIUnitRenderer_CreateUnit_Prefix(UIUnitRenderer __instance) 109 | { 110 | currentUnitTypeUI = __instance.unitType; 111 | return true; 112 | } 113 | 114 | [HarmonyPostfix] 115 | [HarmonyPatch(typeof(UIUnitRenderer), nameof(UIUnitRenderer.CreateUnit))] 116 | private static void UIUnitRenderer_CreateUnit_Postfix(UIUnitRenderer __instance) 117 | { 118 | currentUnitTypeUI = UnitData.Type.None; 119 | } 120 | 121 | [HarmonyPostfix] 122 | [HarmonyPatch(typeof(SkinVisualsRenderer), nameof(SkinVisualsRenderer.SkinWorldObject))] 123 | private static void SkinVisualsRenderer_SkinWorldObject( 124 | SkinVisualsRenderer.SkinWorldType type, 125 | SkinVisualsReference skinVisuals, 126 | SkinVisualsTransientData transientSkinData, 127 | bool checkOutlines, 128 | int level) 129 | { 130 | if (type != SkinVisualsRenderer.SkinWorldType.Unit || skinVisuals == null || transientSkinData == null) 131 | return; 132 | 133 | Unit unit = skinVisuals.gameObject.GetComponent(); 134 | string unitTypeName = unit?.unitData != null 135 | ? EnumCache.GetName(unit.unitData.type) 136 | : EnumCache.GetName(UnitData.Type.Warrior); 137 | if (currentUnitTypeUI != UnitData.Type.None) 138 | unitTypeName = EnumCache.GetName(currentUnitTypeUI); 139 | 140 | string style = Util.GetStyle(transientSkinData.unitSettings.tribe, transientSkinData.unitSettings.skin); 141 | 142 | foreach (var visualPart in skinVisuals.visualParts) 143 | { 144 | UpdateVisualPart(visualPart, $"{visualPart.visualPart.name}_{unitTypeName}", style); 145 | } 146 | } 147 | 148 | #endregion 149 | #region Level 150 | 151 | [HarmonyPostfix] 152 | [HarmonyPatch(typeof(Resource), nameof(Resource.UpdateObject), typeof(SkinVisualsTransientData))] 153 | private static void Resource_UpdateObject(Resource __instance, SkinVisualsTransientData transientSkinData) 154 | { 155 | if (__instance.data != null) 156 | { 157 | string style = Util.GetStyle(GameManager.GameState.GameLogicData.GetTribeTypeFromStyle(__instance.tile.data.climate), __instance.tile.data.Skin); 158 | string name = EnumCache.GetName(__instance.tile.data.resource.type); 159 | 160 | foreach (SkinVisualsReference.VisualPart visualPart in __instance.GetSkinVisualsReference().visualParts) 161 | { 162 | UpdateVisualPart(visualPart, name, style); 163 | } 164 | } 165 | } 166 | 167 | [HarmonyPostfix] 168 | [HarmonyPatch(typeof(Building), nameof(Building.UpdateObject), typeof(SkinVisualsTransientData))] 169 | private static void Building_UpdateObject(Building __instance, SkinVisualsTransientData transientSkinData) 170 | { 171 | string style = Util.GetStyle(transientSkinData.foundingTribeSettings.tribe, transientSkinData.foundingTribeSettings.skin); 172 | string name = EnumCache.GetName(__instance.tile.data.improvement.type); 173 | Sprite? sprite = Registry.GetSprite(name, style, __instance.Level); 174 | if (sprite != null) 175 | { 176 | __instance.Sprite = sprite; 177 | } 178 | } 179 | 180 | [HarmonyPostfix] 181 | [HarmonyPatch(typeof(TerrainRenderer), nameof(TerrainRenderer.UpdateGraphics))] 182 | private static void TerrainRenderer_UpdateGraphics(TerrainRenderer __instance, Tile tile) 183 | { 184 | string terrain = EnumCache.GetName(tile.data.terrain) ?? string.Empty; 185 | 186 | TribeData.Type tribe = GameManager.GameState.GameLogicData.GetTribeTypeFromStyle(tile.data.climate); 187 | SkinType skinType = tile.data.Skin; 188 | 189 | string flood = ""; 190 | if (tile.data.effects.Contains(TileData.EffectType.Flooded)) 191 | { 192 | flood = "_flooded"; 193 | } 194 | if (tile.data.terrain is Polytopia.Data.TerrainData.Type.Forest or Polytopia.Data.TerrainData.Type.Mountain) 195 | { 196 | string propertyName = terrain.ToLower(); 197 | terrain = "field"; 198 | 199 | PropertyInfo? rendererProperty = tile.GetType().GetProperty(propertyName + "Renderer", 200 | BindingFlags.Public | BindingFlags.Instance); 201 | 202 | if (rendererProperty != null) 203 | { 204 | PolytopiaSpriteRenderer? renderer = (PolytopiaSpriteRenderer?)rendererProperty.GetValue(tile); 205 | if (renderer != null) 206 | { 207 | Sprite? additionalSprite = Registry.GetSprite(propertyName + flood, Util.GetStyle(tribe, skinType)); 208 | if (additionalSprite != null) 209 | { 210 | renderer.Sprite = additionalSprite; 211 | rendererProperty.SetValue(tile, renderer); 212 | } 213 | } 214 | } 215 | } 216 | 217 | Sprite? sprite = Registry.GetSprite(terrain + flood, Util.GetStyle(tribe, skinType)); 218 | if (sprite != null) 219 | { 220 | __instance.spriteRenderer.Sprite = sprite; 221 | } 222 | } 223 | 224 | [HarmonyPostfix] 225 | [HarmonyPatch(typeof(PolytopiaSpriteRenderer), nameof(PolytopiaSpriteRenderer.ForceUpdateMesh))] 226 | private static void PolytopiaSpriteRenderer_ForceUpdateMesh(PolytopiaSpriteRenderer __instance) 227 | { 228 | if (__instance.sprite != null && string.IsNullOrEmpty(__instance.atlasName)) 229 | { 230 | MaterialPropertyBlock materialPropertyBlock = new(); 231 | materialPropertyBlock.SetVector("_Flip", new Vector4(1f, 1f, 0f, 0f)); 232 | materialPropertyBlock.SetTexture("_MainTex", __instance.sprite.texture); 233 | __instance.meshRenderer.SetPropertyBlock(materialPropertyBlock); 234 | } 235 | } 236 | 237 | #endregion 238 | #region TribePreview 239 | 240 | [HarmonyPostfix] 241 | [HarmonyPatch(typeof(UIWorldPreviewData), nameof(UIWorldPreviewData.TryGetData))] 242 | private static void UIWorldPreviewData_TryGetData(ref bool __result, UIWorldPreviewData __instance, Vector2Int position, TribeData.Type tribeType, ref UITileData uiTile) 243 | { 244 | PreviewTile[]? preview = null; 245 | if (Registry.tribePreviews.ContainsKey(EnumCache.GetName(tribeType).ToLower())) 246 | { 247 | preview = Registry.tribePreviews[EnumCache.GetName(tribeType).ToLower()]; 248 | } 249 | if (preview != null) 250 | { 251 | PreviewTile? previewTile = preview.FirstOrDefault(tileInPreview => tileInPreview.x == position.x && tileInPreview.y == position.y); 252 | if (previewTile != null) 253 | { 254 | uiTile = new UITileData 255 | { 256 | Position = position, 257 | terrainType = previewTile.terrainType, 258 | resourceType = previewTile.resourceType, 259 | unitType = previewTile.unitType, 260 | improvementType = previewTile.improvementType, 261 | tileEffects = new Il2CppSystem.Collections.Generic.List() 262 | }; 263 | __result = true; 264 | } 265 | } 266 | } 267 | 268 | [HarmonyPostfix] 269 | [HarmonyPatch(typeof(UIWorldPreview), nameof(UIWorldPreview.SetPreview), new Type[] { })] 270 | private static void UIWorldPreview_SetPreview(UIWorldPreview __instance) 271 | { 272 | if (Plugin.config.debug && UIManager.Instance.CurrentScreen == UIConstants.Screens.TribeSelector) 273 | { 274 | if (firstTimeOpeningPreview) 275 | { 276 | RectMask2D mask = __instance.gameObject.GetComponent(); 277 | GameObject.Destroy(mask); 278 | __instance.gameObject.transform.localScale = new Vector3(0.5f, 0.5f, 1f); 279 | __instance.gameObject.transform.position -= new Vector3(-5f, 40f, 0f); 280 | firstTimeOpeningPreview = false; 281 | } 282 | foreach (UITile tile in __instance.tiles) 283 | { 284 | tile.DebugText.gameObject.SetActive(true); 285 | } 286 | } 287 | } 288 | 289 | #endregion 290 | #region UI 291 | 292 | [HarmonyPostfix] 293 | [HarmonyPatch(typeof(UIUtils), nameof(UIUtils.GetImprovementSprite), typeof(ImprovementData.Type), typeof(TribeData.Type), typeof(SkinType), typeof(SpriteAtlasManager))] 294 | private static void UIUtils_GetImprovementSprite(ref Sprite __result, ImprovementData.Type improvement, TribeData.Type tribe, SkinType skin, SpriteAtlasManager atlasManager) 295 | { 296 | Sprite? sprite = Registry.GetSprite(EnumCache.GetName(improvement), Util.GetStyle(tribe, skin)); 297 | if (sprite != null) 298 | { 299 | __result = sprite; 300 | } 301 | } 302 | 303 | [HarmonyPostfix] 304 | [HarmonyPatch(typeof(UIUtils), nameof(UIUtils.GetImprovementSprite), typeof(SkinVisualsTransientData), typeof(ImprovementData.Type), typeof(SpriteAtlasManager))] 305 | private static void UIUtils_GetImprovementSprite_2(ref Sprite __result, SkinVisualsTransientData data, ImprovementData.Type improvement, SpriteAtlasManager atlasManager) 306 | { 307 | UIUtils_GetImprovementSprite(ref __result, improvement, data.foundingTribeSettings.tribe, data.foundingTribeSettings.skin, atlasManager); 308 | } 309 | 310 | [HarmonyPostfix] 311 | [HarmonyPatch(typeof(UIUtils), nameof(UIUtils.GetResourceSprite))] 312 | private static void UIUtils_GetResourceSprite(ref Sprite __result, SkinVisualsTransientData data, ResourceData.Type resource, SpriteAtlasManager atlasManager) 313 | { 314 | Sprite? sprite = Registry.GetSprite(EnumCache.GetName(resource), Util.GetStyle(data.tileClimateSettings.tribe, data.tileClimateSettings.skin)); 315 | if (sprite != null) 316 | { 317 | __result = sprite; 318 | } 319 | } 320 | 321 | #endregion 322 | #region Houses 323 | 324 | [HarmonyPostfix] 325 | [HarmonyPatch(typeof(CityRenderer), nameof(CityRenderer.GetHouse))] 326 | private static void CityRenderer_GetHouse(ref PolytopiaSpriteRenderer __result, CityRenderer __instance, TribeData.Type tribe, int type, SkinType skinType) 327 | { 328 | PolytopiaSpriteRenderer polytopiaSpriteRenderer = __result; 329 | 330 | if (type != __instance.HOUSE_WORKSHOP && type != __instance.HOUSE_PARK) 331 | { 332 | Sprite? sprite = Registry.GetSprite("house", Util.GetStyle(tribe, skinType), type); 333 | if (sprite != null) 334 | { 335 | polytopiaSpriteRenderer.Sprite = sprite; 336 | TerrainMaterialHelper.SetSpriteSaturated(polytopiaSpriteRenderer, __instance.IsEnemyCity); 337 | __result = polytopiaSpriteRenderer; 338 | } 339 | } 340 | } 341 | 342 | [HarmonyPostfix] 343 | [HarmonyPatch(typeof(UICityRenderer), nameof(UICityRenderer.GetResource))] 344 | private static void UICityRenderer_GetResource(ref GameObject __result, string baseName, Polytopia.Data.TribeData.Type tribe, Polytopia.Data.SkinType skin) 345 | { 346 | Image imageComponent = __result.GetComponent(); 347 | string[] tokens = baseName.Split('_'); 348 | if (tokens.Length > 0) 349 | { 350 | if (tokens[0] == "House") 351 | { 352 | int level = 0; 353 | if (tokens.Length > 1) 354 | { 355 | _ = int.TryParse(tokens[1], out level); 356 | } 357 | 358 | Sprite? sprite = Registry.GetSprite("house", Util.GetStyle(tribe, skin), level); 359 | if (sprite == null) 360 | { 361 | return; 362 | } 363 | imageComponent.sprite = sprite; 364 | imageComponent.SetNativeSize(); 365 | } 366 | } 367 | } 368 | 369 | #endregion 370 | #region Icons 371 | 372 | [HarmonyPostfix] 373 | [HarmonyPatch(typeof(UIIconData), nameof(UIIconData.GetImage))] 374 | private static void UIIconData_GetImage(ref Image __result, string id) 375 | { 376 | Sprite? sprite; 377 | if (GameManager.LocalPlayer != null) 378 | { 379 | sprite = Registry.GetSprite(id, Util.GetStyle(GameManager.LocalPlayer.tribe, GameManager.LocalPlayer.skinType)); 380 | } 381 | else 382 | { 383 | sprite = Registry.GetSprite(id); 384 | } 385 | if (sprite != null) 386 | { 387 | __result.sprite = sprite; 388 | __result.useSpriteMesh = true; 389 | __result.SetNativeSize(); 390 | } 391 | } 392 | 393 | [HarmonyPostfix] 394 | [HarmonyPatch(typeof(GameInfoRow), nameof(GameInfoRow.LoadFaceIcon), typeof(TribeData.Type), typeof(SkinType))] 395 | private static void GameInfoRow_LoadFaceIcon(GameInfoRow __instance, TribeData.Type type, SkinType skinType) 396 | { 397 | string style = EnumCache.GetName(type); 398 | 399 | if (style == "None") 400 | { 401 | for (int i = 0; i < 20; i++) 402 | { 403 | type += byte.MaxValue + 1; 404 | style = EnumCache.GetName(type); 405 | 406 | if (style != "None") 407 | { 408 | break; 409 | } 410 | } 411 | } 412 | 413 | Sprite? sprite = Registry.GetSprite("head", Util.GetStyle(type, skinType)); 414 | 415 | if (sprite != null) 416 | { 417 | __instance.SetFaceIcon(sprite); 418 | } 419 | 420 | if (__instance.icon.sprite == null) 421 | { 422 | __instance.LoadFaceIcon(SpriteData.SpecialFaceIcon.neutral); 423 | } 424 | } 425 | 426 | [HarmonyPostfix] 427 | [HarmonyPatch(typeof(PlayerInfoIcon), nameof(PlayerInfoIcon.SetData), typeof(TribeData.Type), typeof(SkinType), typeof(SpriteData.SpecialFaceIcon), typeof(Color), typeof(DiplomacyRelationState), typeof(PlayerInfoIcon.Mood))] 428 | private static void PlayerInfoIcon_SetData(PlayerInfoIcon __instance, TribeData.Type tribe, SkinType skin, SpriteData.SpecialFaceIcon face, Color color, DiplomacyRelationState diplomacyState, PlayerInfoIcon.Mood mood) 429 | { 430 | if (face == SpriteData.SpecialFaceIcon.tribe) 431 | { 432 | Sprite? sprite = Registry.GetSprite("head", Util.GetStyle(tribe, skin)); 433 | if (sprite != null) 434 | { 435 | __instance.HeadImage.sprite = sprite; 436 | Vector2 size = sprite.rect.size; 437 | __instance.HeadImage.rectTransform.sizeDelta = size * __instance.rectTransform.GetHeight() / 512f; 438 | } 439 | } 440 | } 441 | 442 | #endregion 443 | #region Popups 444 | 445 | [HarmonyPostfix] 446 | [HarmonyPatch(typeof(BasicPopup), nameof(BasicPopup.Update))] 447 | private static void BasicPopup_Update(BasicPopup __instance) 448 | { 449 | int id = __instance.GetInstanceID(); 450 | if (Visual.basicPopupWidths.ContainsKey(id)) 451 | __instance.rectTransform.SetWidth(basicPopupWidths[id]); 452 | } 453 | 454 | [HarmonyPostfix] 455 | [HarmonyPatch(typeof(PopupBase), nameof(PopupBase.Hide))] 456 | private static void PopupBase_Hide(PopupBase __instance) 457 | { 458 | basicPopupWidths.Remove(__instance.GetInstanceID()); 459 | } 460 | 461 | public static void ShowSetWidth(this BasicPopup self, int width) 462 | { 463 | basicPopupWidths.Add(self.GetInstanceID(), width); 464 | self.Show(); 465 | } 466 | 467 | #endregion 468 | 469 | private static void UpdateVisualPart(SkinVisualsReference.VisualPart visualPart, string name, string style) 470 | { 471 | Sprite? sprite = Registry.GetSprite(name, style) ?? Registry.GetSprite(visualPart.visualPart.name, style); 472 | if (sprite != null) 473 | { 474 | if (visualPart.renderer.spriteRenderer != null) 475 | visualPart.renderer.spriteRenderer.sprite = sprite; 476 | else if (visualPart.renderer.polytopiaSpriteRenderer != null) 477 | visualPart.renderer.polytopiaSpriteRenderer.sprite = sprite; 478 | } 479 | 480 | Sprite? outlineSprite = Registry.GetSprite($"{name}_outline", style) ?? Registry.GetSprite($"{visualPart.visualPart.name}_outline", style); 481 | if (outlineSprite != null) 482 | { 483 | if (visualPart.outlineRenderer.spriteRenderer != null) 484 | visualPart.outlineRenderer.spriteRenderer.sprite = outlineSprite; 485 | else if (visualPart.outlineRenderer.polytopiaSpriteRenderer != null) 486 | visualPart.outlineRenderer.polytopiaSpriteRenderer.sprite = outlineSprite; 487 | } 488 | } 489 | 490 | public static Sprite BuildSprite(byte[] data, Vector2? pivot = null, float pixelsPerUnit = 2112f) 491 | { 492 | Texture2D texture = new(1, 1, TextureFormat.RGBA32, true); 493 | texture.LoadImage(data); 494 | Color[] pixels = texture.GetPixels(); 495 | for (int i = 0; i < pixels.Length; i++) 496 | { 497 | pixels[i] = new Color(pixels[i].r, pixels[i].g, pixels[i].b, pixels[i].a); 498 | } 499 | texture.SetPixels(pixels); 500 | texture.filterMode = FilterMode.Trilinear; 501 | texture.Apply(); 502 | return Sprite.Create( 503 | texture, 504 | new(0, 0, texture.width, texture.height), 505 | pivot ?? new(0.5f, 0.5f), 506 | pixelsPerUnit 507 | ); 508 | } 509 | 510 | internal static void Init() 511 | { 512 | Harmony.CreateAndPatchAll(typeof(Visual)); 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /src/Mod.cs: -------------------------------------------------------------------------------- 1 | namespace PolyMod; 2 | public class Mod 3 | { 4 | public record Dependency(string id, Version min, Version max, bool required = true); 5 | public record Manifest(string id, string? name, Version version, string[] authors, Dependency[]? dependencies, bool client = false); 6 | public record File(string name, byte[] bytes); 7 | public enum Status 8 | { 9 | Success, 10 | Error, 11 | DependenciesUnsatisfied, 12 | } 13 | 14 | public string id; 15 | public string? name; 16 | public Version version; 17 | public string[] authors; 18 | public Dependency[]? dependencies; 19 | public bool client; 20 | public Status status; 21 | public List files; 22 | 23 | public Mod(Manifest manifest, Status status, List files) 24 | { 25 | id = manifest.id; 26 | name = manifest.name ?? manifest.id; 27 | version = manifest.version; 28 | authors = manifest.authors; 29 | dependencies = manifest.dependencies; 30 | client = manifest.client; 31 | this.status = status; 32 | this.files = files; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/NullableFix.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | namespace System.Runtime.CompilerServices 4 | { 5 | [System.AttributeUsage(System.AttributeTargets.All, AllowMultiple = true)] 6 | public sealed class NullableAttribute : System.Attribute 7 | { 8 | public NullableAttribute(byte b) { } 9 | public NullableAttribute(byte[] b) { } 10 | } 11 | 12 | [System.AttributeUsage(System.AttributeTargets.All, AllowMultiple = false)] 13 | public sealed class NullableContextAttribute : System.Attribute 14 | { 15 | public NullableContextAttribute(byte b) { } 16 | } 17 | 18 | [System.AttributeUsage(System.AttributeTargets.Module)] 19 | public sealed class NullablePublicOnlyAttribute : System.Attribute 20 | { 21 | public NullablePublicOnlyAttribute(bool b) { } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json; 3 | using BepInEx; 4 | using BepInEx.Configuration; 5 | using BepInEx.Logging; 6 | using PolyMod.Managers; 7 | 8 | namespace PolyMod; 9 | [BepInPlugin("com.polymod", "PolyMod", VERSION)] 10 | public partial class Plugin : BepInEx.Unity.IL2CPP.BasePlugin 11 | { 12 | internal record PolyConfig( 13 | bool debug = false 14 | ); 15 | 16 | internal const int AUTOIDX_STARTS_FROM = 1000; 17 | public static readonly string BASE_PATH = Path.Combine(BepInEx.Paths.BepInExRootPath, ".."); 18 | public static readonly string MODS_PATH = Path.Combine(BASE_PATH, "Mods"); 19 | public static readonly string DUMPED_DATA_PATH = Path.Combine(BASE_PATH, "DumpedData"); 20 | internal static readonly string CONFIG_PATH = Path.Combine(BASE_PATH, "PolyMod.json"); 21 | internal static readonly string INCOMPATIBILITY_WARNING_LAST_VERSION_PATH 22 | = Path.Combine(BASE_PATH, "IncompatibilityWarningLastVersion"); 23 | internal static readonly string DISCORD_LINK = "https://discord.gg/eWPdhWtfVy"; 24 | internal static readonly List LOG_MESSAGES_IGNORE = new() 25 | { 26 | "Failed to find atlas", 27 | "Could not find sprite", 28 | "Couldn't find prefab for type", 29 | "MARKET: id:", 30 | "Missing name for value", 31 | }; 32 | 33 | 34 | #pragma warning disable CS8618 35 | internal static PolyConfig config; 36 | internal static ManualLogSource logger; 37 | #pragma warning restore CS8618 38 | 39 | public override void Load() 40 | { 41 | try 42 | { 43 | config = JsonSerializer.Deserialize(File.ReadAllText(CONFIG_PATH))!; 44 | } 45 | catch 46 | { 47 | config = new(); 48 | File.WriteAllText(CONFIG_PATH, JsonSerializer.Serialize(config)); 49 | } 50 | if (!config.debug) ConsoleManager.DetachConsole(); 51 | logger = Log; 52 | ConfigFile.CoreConfig[new("Logging.Disk", "WriteUnityLog")].BoxedValue = true; 53 | 54 | Compatibility.Init(); 55 | 56 | Audio.Init(); 57 | Loc.Init(); 58 | Visual.Init(); 59 | Hub.Init(); 60 | 61 | Main.Init(); 62 | } 63 | 64 | internal static Stream GetResource(string id) 65 | { 66 | return Assembly.GetExecutingAssembly().GetManifestResourceStream( 67 | $"{typeof(Plugin).Namespace}.resources.{id}" 68 | )!; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Registry.cs: -------------------------------------------------------------------------------- 1 | using LibCpp2IL; 2 | using PolyMod.Managers; 3 | using Polytopia.Data; 4 | using UnityEngine; 5 | 6 | namespace PolyMod; 7 | public static class Registry 8 | { 9 | public static int autoidx = Plugin.AUTOIDX_STARTS_FROM; 10 | public static Dictionary sprites = new(); 11 | public static Dictionary audioClips = new(); 12 | internal static Dictionary mods = new(); 13 | public static Dictionary tribePreviews = new(); 14 | public static Dictionary spriteInfos = new(); 15 | public static List customTribes = new(); 16 | public static List skinInfo = new(); 17 | public static int climateAutoidx = (int)Enum.GetValues(typeof(TribeData.Type)).Cast().Last(); 18 | 19 | public static Sprite? GetSprite(string name, string style = "", int level = 0) 20 | { 21 | Sprite? sprite = null; 22 | name = name.ToLower(); 23 | style = style.ToLower(); 24 | sprite = sprites.GetOrDefault($"{name}__", sprite); 25 | sprite = sprites.GetOrDefault($"{name}_{style}_", sprite); 26 | sprite = sprites.GetOrDefault($"{name}__{level}", sprite); 27 | sprite = sprites.GetOrDefault($"{name}_{style}_{level}", sprite); 28 | return sprite; 29 | } 30 | 31 | public static AudioClip? GetAudioClip(string name, string style) 32 | { 33 | AudioSource? audioSource = null; 34 | name = name.ToLower(); 35 | style = style.ToLower(); 36 | audioSource = audioClips.GetOrDefault($"{name}_{style}", audioSource); 37 | if (audioSource == null) return null; 38 | return audioSource.clip; 39 | } 40 | } -------------------------------------------------------------------------------- /src/Util.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | using Il2CppInterop.Runtime; 4 | using Il2CppInterop.Runtime.Injection; 5 | using Newtonsoft.Json.Linq; 6 | using Polytopia.Data; 7 | 8 | namespace PolyMod; 9 | internal static class Util 10 | { 11 | internal static Il2CppSystem.Type WrapType() where T : class 12 | { 13 | if (!ClassInjector.IsTypeRegisteredInIl2Cpp()) 14 | ClassInjector.RegisterTypeInIl2Cpp(); 15 | return Il2CppType.From(typeof(T)); 16 | } 17 | 18 | internal static string Hash(object data) 19 | { 20 | return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(data.ToString()!))); 21 | } 22 | 23 | internal static string GetJTokenName(JToken token, int n = 1) 24 | { 25 | return token.Path.Split('.')[^n]; 26 | } 27 | 28 | internal static Version Cast(this Il2CppSystem.Version self) 29 | { 30 | return new(self.ToString()); 31 | } 32 | 33 | internal static Version CutRevision(this Version self) 34 | { 35 | return new(self.Major, self.Minor, self.Build); 36 | } 37 | 38 | internal static string GetStyle(TribeData.Type tribe, SkinType skin) 39 | { 40 | return skin != SkinType.Default ? EnumCache.GetName(skin) : EnumCache.GetName(tribe); 41 | } 42 | 43 | internal static string FormatSpriteName(string baseName) // I cant believe i had to do this shit #MIDJIWANFIXYOURSHITCODE 44 | { 45 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_AQUA_FARM, EnumCache.GetName(ImprovementData.Type.Aquafarm)); 46 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_ATOLL, EnumCache.GetName(ImprovementData.Type.Atoll)); 47 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_BURN_FOREST, EnumCache.GetName(ImprovementData.Type.BurnForest)); 48 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_CLEAR_FOREST, EnumCache.GetName(ImprovementData.Type.ClearForest)); 49 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_CUSTOMS_HOUSE, EnumCache.GetName(ImprovementData.Type.CustomsHouse)); 50 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_FARM, EnumCache.GetName(ImprovementData.Type.Farm)); 51 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_FOREST_TEMPLE, EnumCache.GetName(ImprovementData.Type.ForestTemple)); 52 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_FORGE, EnumCache.GetName(ImprovementData.Type.Forge)); 53 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_GROW_FOREST, EnumCache.GetName(ImprovementData.Type.GrowForest)); 54 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_ICE_BANK, EnumCache.GetName(ImprovementData.Type.IceBank)); 55 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_ICE_PORT, EnumCache.GetName(ImprovementData.Type.Outpost)); 56 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_ICE_TEMPLE, EnumCache.GetName(ImprovementData.Type.IceTemple)); 57 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_LUMBER_HUT, EnumCache.GetName(ImprovementData.Type.LumberHut)); 58 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_MARKET, EnumCache.GetName(ImprovementData.Type.Market)); 59 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_MINE, EnumCache.GetName(ImprovementData.Type.Mine)); 60 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_MOUNTAIN_TEMPLE, EnumCache.GetName(ImprovementData.Type.MountainTemple)); 61 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_PORT, EnumCache.GetName(ImprovementData.Type.Port)); 62 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_ROAD, EnumCache.GetName(ImprovementData.Type.Road)); 63 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_RUIN, EnumCache.GetName(ImprovementData.Type.Ruin)); 64 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_SANCTUARY, EnumCache.GetName(ImprovementData.Type.Sanctuary)); 65 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_SAWMILL, EnumCache.GetName(ImprovementData.Type.Sawmill)); 66 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_TEMPLE, EnumCache.GetName(ImprovementData.Type.Temple)); 67 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_WATER_TEMPLE, EnumCache.GetName(ImprovementData.Type.WaterTemple)); 68 | baseName = baseName.Replace(SpriteData.IMPROVEMENT_WINDMILL, EnumCache.GetName(ImprovementData.Type.Windmill)); 69 | 70 | baseName = baseName.Replace(SpriteData.RESOURCE_AQUACROP, EnumCache.GetName(ResourceData.Type.AquaCrop)); 71 | baseName = baseName.Replace(SpriteData.RESOURCE_CROP, EnumCache.GetName(ResourceData.Type.Crop)); 72 | baseName = baseName.Replace(SpriteData.RESOURCE_FISH, EnumCache.GetName(ResourceData.Type.Fish)); 73 | baseName = baseName.Replace(SpriteData.RESOURCE_FRUIT, EnumCache.GetName(ResourceData.Type.Fruit)); 74 | baseName = baseName.Replace(SpriteData.RESOURCE_GAME, EnumCache.GetName(ResourceData.Type.Game)); 75 | baseName = baseName.Replace(SpriteData.RESOURCE_METAL, EnumCache.GetName(ResourceData.Type.Metal)); 76 | baseName = baseName.Replace(SpriteData.RESOURCE_SPORES, EnumCache.GetName(ResourceData.Type.Spores)); 77 | baseName = baseName.Replace(SpriteData.RESOURCE_STARFISH, EnumCache.GetName(ResourceData.Type.Starfish)); 78 | baseName = baseName.Replace(SpriteData.RESOURCE_WHALE, EnumCache.GetName(ResourceData.Type.Whale)); 79 | 80 | baseName = baseName.Replace(SpriteData.TILE_FIELD, EnumCache.GetName(TerrainData.Type.Field)); 81 | baseName = baseName.Replace(SpriteData.TILE_FOREST, EnumCache.GetName(TerrainData.Type.Forest)); 82 | baseName = baseName.Replace(SpriteData.TILE_ICE, EnumCache.GetName(TerrainData.Type.Ice)); 83 | baseName = baseName.Replace(SpriteData.TILE_MOUNTAIN, EnumCache.GetName(TerrainData.Type.Mountain)); 84 | baseName = baseName.Replace(SpriteData.TILE_OCEAN, EnumCache.GetName(TerrainData.Type.Ocean)); 85 | baseName = baseName.Replace(SpriteData.TILE_UNKNOWN, EnumCache.GetName(TerrainData.Type.Field)); 86 | baseName = baseName.Replace(SpriteData.TILE_WATER, EnumCache.GetName(TerrainData.Type.Water)); 87 | baseName = baseName.Replace(SpriteData.TILE_WETLAND, EnumCache.GetName(TerrainData.Type.Field) + "_flooded"); 88 | 89 | baseName = baseName.Replace("UI_", ""); 90 | return baseName; 91 | } 92 | } 93 | --------------------------------------------------------------------------------