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