diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml
deleted file mode 100644
index f75f773..0000000
--- a/.github/workflows/installer.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-name: Installer
-on:
- push:
- paths:
- - 'installer/**'
- pull_request:
- paths:
- - 'installer/**'
-jobs:
- job:
- defaults:
- run:
- working-directory: ./installer
- strategy:
- fail-fast: false
- matrix:
- os: [windows-latest, ubuntu-22.04, macos-13]
- name: Build on ${{ matrix.os }}
- runs-on: ${{ matrix.os }}
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-python@v5
- with:
- python-version: '3.11'
- - name: Install requirements
- run: pip install -r requirements.txt
- - name: Build
- run: python build.py
- - uses: actions/upload-artifact@v4
- with:
- name: ${{ matrix.os }}
- path: installer/dist/
- merge:
- name: Merge artifacts
- runs-on: ubuntu-latest
- needs: job
- steps:
- - uses: actions/upload-artifact/merge@v4
- with:
- name: artifacts
- delete-merged: true
- separate-directories: true
-
diff --git a/PolyMod.csproj b/PolyMod.csproj
index d17309e..4ace07a 100644
--- a/PolyMod.csproj
+++ b/PolyMod.csproj
@@ -11,7 +11,7 @@
IL2CPP
PolyMod
- 1.2.18
+ 2.0.0-pre
2.17.3.16375
PolyModdingTeam
The Battle of Polytopia's mod loader.
@@ -23,7 +23,7 @@
-
+
all
diff --git a/README.md b/README.md
index 2772d50..408cd22 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
| | x86 | ARM |
|:-------:|:---:|:---:|
| Windows | ✅ | ❌ |
-| Linux | ⏺️ | ❌ |
-| MacOS | ⏺️ | ❌ |
+| Linux | ✅ | ❌ |
+| MacOS | ❌ | ❌ |
| Android | ➖ | 🚧 |
| IOS | ➖ | ❌ |
diff --git a/installer/build.py b/installer/build.py
deleted file mode 100644
index 8b802ae..0000000
--- a/installer/build.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import os
-import PyInstaller.__main__
-
-ROOT = os.path.abspath(os.getcwd())
-
-PyInstaller.__main__.run(
- [
- "--noconfirm",
- "--onefile",
- "--windowed",
- "--icon",
- ROOT + "/icon.ico",
- "--name",
- "PolyMod",
- "--add-data",
- ROOT + "/icon.ico:.",
- ROOT + "/main.py",
- ]
-)
diff --git a/installer/icon.ico b/installer/icon.ico
deleted file mode 100644
index f52643a..0000000
Binary files a/installer/icon.ico and /dev/null differ
diff --git a/installer/main.py b/installer/main.py
deleted file mode 100644
index e1fad1b..0000000
--- a/installer/main.py
+++ /dev/null
@@ -1,154 +0,0 @@
-import os
-import io
-import sys
-import shutil
-import zipfile
-import requests
-import threading
-import subprocess
-import customtkinter
-import CTkMessagebox as messagebox
-
-OS = {
- "linux": "linux",
- "linux2": "linux",
- "win32": "win",
- "darwin": "macos",
-}[sys.platform]
-BEPINEX = "https://polymod.dev/data/bepinex.txt"
-POLYMOD = "https://api.github.com/repos/PolyModdingTeam/PolyMod/releases"
-
-
-def resource_path(path):
- try:
- base_path = sys._MEIPASS
- except Exception:
- base_path = os.path.abspath(".")
- return os.path.join(base_path, path)
-
-
-def to_zip(request: requests.Response):
- return zipfile.ZipFile(io.BytesIO(request.content))
-
-
-def browse():
- global path_entry
- path_entry.delete(0, customtkinter.END)
- path_entry.insert(0, customtkinter.filedialog.askdirectory())
-
-
-def prepare(target):
- global progress_bar
- path = path_entry.get()
- try:
- if "Polytopia_Data" not in os.listdir(path):
- raise FileNotFoundError
- except FileNotFoundError:
- messagebox.CTkMessagebox(
- title="Error",
- message="The folder does not exist or is not valid!",
- icon="cancel",
- width=100,
- height=50
- )
- return
- path_entry.configure(state=customtkinter.DISABLED)
- browse_button.configure(state=customtkinter.DISABLED)
- prerelease_checkbox.destroy()
- install_button.destroy()
- uninstall_button.destroy()
- progress_bar = customtkinter.CTkProgressBar(app, determinate_speed=50 / 2)
- progress_bar.grid(column=0, row=1, columnspan=2, padx=5, pady=5)
- progress_bar.set(0)
- threading.Thread(target=target, daemon=True, args=(path, )).start()
-
-
-def install(path):
- to_zip(
- requests.get(
- requests.get(BEPINEX).text.strip().replace("{os}", OS)
- )
- ).extractall(path)
- progress_bar.step()
-
- for release in requests.get(POLYMOD).json():
- if release["prerelease"] and not prerelease_checkbox.get(): continue
- latest = release
- break
- open(path + "/BepInEx/plugins/PolyMod.dll", "wb").write(
- requests.get(latest["assets"][0]["browser_download_url"]).content
- )
- progress_bar.step()
-
- customtkinter.CTkButton(app, text="Launch", command=lambda: launch(path)).grid(
- column=0, row=2, columnspan=2, padx=5, pady=5
- )
-
-
-def uninstall(path):
- dirs = [
- "BepInEx",
- "dotnet",
- ]
- files = [
- ".doorstop_version",
- "changelog.txt",
- "doorstop_config.ini",
- "winhttp.dll", # windows
- "libdoorstop.so", # linux
- "libdoorstop.dylib", # mac
- "run_bepinex.sh", # linux + mac
- ]
- for dir in dirs:
- shutil.rmtree(path + "/" + dir, True)
- progress_bar.step()
- for file in files:
- try:
- os.remove(path + "/" + file)
- except FileNotFoundError:
- ...
- progress_bar.step()
- customtkinter.CTkButton(app, text="Quit", command=quit).grid(
- column=0, row=2, columnspan=2, padx=5, pady=5
- )
-
-
-def launch(path):
- if sys.platform != "win32":
- subprocess.check_call(f"chmod +x {path}/run_bepinex.sh", shell=True)
- subprocess.check_call(f"{path}/run_bepinex.sh {path}/Polytopia.*", shell=True)
- subprocess.check_call(f"xdg-open https://docs.bepinex.dev/articles/advanced/steam_interop.html", shell=True)
- else:
- subprocess.check_call(f"start steam://rungameid/874390", shell=True)
- quit()
-
-
-def quit():
- app.destroy()
- sys.exit()
-
-
-app = customtkinter.CTk()
-app.title("PolyMod")
-if OS != "linux":
- app.iconbitmap(default=resource_path("icon.ico"))
-app.resizable(False, False)
-
-path_entry = customtkinter.CTkEntry(
- app, placeholder_text="Game path", width=228)
-browse_button = customtkinter.CTkButton(
- app, text="Browse", command=browse, width=1)
-prerelease_checkbox = customtkinter.CTkCheckBox(
- app, text="Prerelease", width=1)
-install_button = customtkinter.CTkButton(
- app, text="Install", command=lambda: prepare(install))
-uninstall_button = customtkinter.CTkButton(
- app, text="Uninstall", command=lambda: prepare(uninstall))
-
-path_entry.grid(column=0, row=0, padx=5, pady=5)
-browse_button.grid(column=1, row=0, padx=(0, 5), pady=5)
-prerelease_checkbox.grid(column=0, row=1, columnspan=2, padx=5, pady=5)
-install_button.grid(column=0, row=2, columnspan=2, padx=5, pady=5)
-uninstall_button.grid(column=0, row=3, columnspan=2, padx=5, pady=5)
-
-app.mainloop()
diff --git a/installer/requirements.txt b/installer/requirements.txt
deleted file mode 100644
index c396099..0000000
--- a/installer/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-requests
-pyinstaller
-customtkinter
-CTkMessagebox
\ No newline at end of file
diff --git a/resources/localization.json b/resources/localization.json
index 3b19f1e..b1219e2 100644
--- a/resources/localization.json
+++ b/resources/localization.json
@@ -218,18 +218,14 @@
"Russian": "{0} задан на {1}!",
"Elyrion": "{0} iȓ ȓ∑Ŧ Ŧȱ {1}!"
},
- "polymod_hub_spriteinfo_update": {
- "English": "UPDATE SPRITES",
- "Russian": "ОБНОВИТЬ СПРАЙТЫ",
- "Elyrion": "₺πΔaŦ∑ ȓπriŦ∑ȓ"
- },
- "polymod_spriteinfo_updated": {
- "English": "Sprite info for mod {0} updated!",
- "Russian": "Данные спрайтов обновлены для мода {0}!",
- "Elyrion": "ȓπriŦ∑ iŋ₼ȱ ₼ȱr mȱΔ {0} ₺πΔaŦ∑Δ!"
- },
- "polymod_spriteinfo_notupdated": {
- "English": "No sprite infos were found!",
+ "polymod_hub_hotreload": {
+ "English": "HOT RELOAD"
+ },
+ "polymod_hotreload_success": {
+ "English": "Hot reload completed for the mod {0}!"
+ },
+ "polymod_hotreload_failure": {
+ "English": "No mods for hot reload!",
"Russian": "Данные спрайтов не найдены!",
"Elyrion": "ŋȱ ȓπriŦ∑ iŋ₼ȱȓ ~∑r∑ ₼ȱ₺ŋΔ!"
},
@@ -242,5 +238,17 @@
"English": "PROCEED",
"Russian": "ПРОДОЛЖИТЬ",
"Elyrion": "πrȱ#∑∑Δ"
+ },
+ "polymod_title_invalid": {
+ "English": "Invalid Launch",
+ "Russian": "Неверный Запуск"
+ },
+ "polymod_description_invalid": {
+ "English": "PolyMod launch was invalid. Please, open the game via the PolyMod Launcher or proceed to our site to install it :)",
+ "Russian": "PolyMod запущен неккоректно. Пожалуйста, откройте игру через PolyMod Launcher или перейдите на наш сайт и установите его :)"
+ },
+ "buttons_download_launcher": {
+ "English": "OUR SITE",
+ "Russian": "НАШ САЙТ"
}
-}
+}
\ No newline at end of file
diff --git a/src/Json/EnumNameJson.cs b/src/Json/EnumNameJson.cs
new file mode 100644
index 0000000..953ec68
--- /dev/null
+++ b/src/Json/EnumNameJson.cs
@@ -0,0 +1,29 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace PolyMod.Json;
+
+///
+/// Converts fixed (non-extensible) enums to and from their plain string names.
+/// Use this for enums that are not registered in EnumCache.
+///
+public class EnumNameJson : JsonConverter where EnumType : struct, Enum
+{
+ public override EnumType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ string? enumName = reader.GetString();
+
+ if (enumName != null && Enum.TryParse(enumName, true, out EnumType parsedValue))
+ {
+ return parsedValue;
+ }
+
+ string validNames = string.Join(", ", Enum.GetNames(typeof(EnumType)));
+ throw new JsonException($"Unknown {typeof(EnumType).Name} value '{enumName}'. Valid values: {validNames}");
+ }
+
+ public override void Write(Utf8JsonWriter writer, EnumType value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+}
\ No newline at end of file
diff --git a/src/Loader.cs b/src/Loader.cs
index 69fe92b..419e3bf 100644
--- a/src/Loader.cs
+++ b/src/Loader.cs
@@ -8,7 +8,6 @@
using Polytopia.Data;
using PolytopiaBackendBase.Game;
using System.Diagnostics;
-using System.Globalization;
using System.IO.Compression;
using System.Reflection;
using System.Text;
@@ -126,7 +125,7 @@ out TribeAbility.Type tribeAbilityType
}
if (token["preview"] != null)
{
- Visual.PreviewTile[] preview = JsonSerializer.Deserialize(token["preview"].ToString())!;
+ TribePreviews.PreviewTile[] preview = JsonSerializer.Deserialize(token["preview"].ToString())!;
Registry.tribePreviews[Util.GetJTokenName(token)] = preview;
}
}
@@ -135,19 +134,8 @@ out TribeAbility.Type tribeAbilityType
[typeof(UnitData.Type)] = new List>() {
new((token, duringEnumCacheCreation) =>
{
- if(duringEnumCacheCreation)
+ if(!duringEnumCacheCreation)
{
- if (token["prefab"] == null)
- {
- Registry.prefabNames.Add((int)token["idx"], EnumCache.GetName(UnitData.Type.Scout).ToLowerInvariant());
- }
- }
- else
- {
- if (token["prefab"] != null)
- {
- Registry.prefabNames.Add((int)token["idx"], token["prefab"].ToString().ToLowerInvariant());
- }
if (token["embarksTo"] != null)
{
string unitId = Util.GetJTokenName(token);
@@ -170,34 +158,31 @@ out TribeAbility.Type tribeAbilityType
new((token, duringEnumCacheCreation) =>
{
if (duringEnumCacheCreation)
+ return;
+
+ if (token["prefab"] != null && (bool)token["prefab"])
{
- ImprovementData.Type improvementPrefabType = ImprovementData.Type.CustomsHouse;
- if (token["prefab"] != null)
+ bool levels = false;
+ if(token["maxLevel"] != null && (int)token["maxLevel"] > 0)
{
- string prefabId = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(token["prefab"]!.ToString());
- if (Enum.TryParse(prefabId, out ImprovementData.Type parsedType))
- improvementPrefabType = parsedType;
+ levels = true;
}
- if(token["idx"] != null)
- PrefabManager.improvements.TryAdd((ImprovementData.Type)(int)token["idx"], PrefabManager.improvements[improvementPrefabType]);
+ Prefabs.improvementPrefabInfosToCreate[(ImprovementData.Type)(int)token["idx"]] = levels;
}
- else
+ string improvementId = Util.GetJTokenName(token);
+ if (token["attractsResource"] != null)
{
- string improvementId = Util.GetJTokenName(token);
- if (token["attractsResource"] != null)
- {
- string attractsId = token["attractsResource"].ToString();
- Main.attractsResourceNames[improvementId] = attractsId;
- }
- if (token["attractsToTerrain"] != null)
- {
- string attractsId = token["attractsToTerrain"].ToString();
- Main.attractsTerrainNames[improvementId] = attractsId;
- }
- if(token["infoOverride"] != null)
- {
- Loc.buildingsInfoOverrides[improvementId] = Loc.ReplaceDashesWithDots(token["infoOverride"].ToString());
- }
+ string attractsId = token["attractsResource"].ToString();
+ Main.attractsResourceNames[improvementId] = attractsId;
+ }
+ if (token["attractsToTerrain"] != null)
+ {
+ string attractsId = token["attractsToTerrain"].ToString();
+ Main.attractsTerrainNames[improvementId] = attractsId;
+ }
+ if(token["infoOverride"] != null)
+ {
+ Loc.buildingsInfoOverrides[improvementId] = Loc.ReplaceDashesWithDots(token["infoOverride"].ToString());
}
})
},
@@ -206,16 +191,11 @@ out TribeAbility.Type tribeAbilityType
new((token, duringEnumCacheCreation) =>
{
if (duringEnumCacheCreation)
+ return;
+
+ if (token["prefab"] != null && (bool)token["prefab"])
{
- ResourceData.Type resourcePrefabType = ResourceData.Type.Game;
- if (token["prefab"] != null)
- {
- string prefabId = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(token["prefab"]!.ToString());
- if (Enum.TryParse(prefabId, out ResourceData.Type parsedType))
- resourcePrefabType = parsedType;
- }
- if(token["idx"] != null)
- PrefabManager.resources.TryAdd((ResourceData.Type)(int)token["idx"], PrefabManager.resources[resourcePrefabType]);
+ Prefabs.resourcePrefabsInfosToCreate.Add((ResourceData.Type)(int)token["idx"]);
}
})
},
@@ -428,7 +408,6 @@ internal static void LoadMods(Dictionary mods, out bool dependencyC
}
}
Compatibility.HashSignatures(checksumString);
-
}
private static void CheckDependencies(Dictionary mods)
{
@@ -624,45 +603,11 @@ public static void LoadLanguageFile(Mod mod, Mod.File file, string languageName)
///
/// The mod the sprite file belongs to.
/// The sprite file to load.
- public static void LoadSpriteFile(Mod mod, Mod.File file)
+ public static void LoadTextureFile(Mod mod, Mod.File file)
{
string name = Path.GetFileNameWithoutExtension(file.name);
- Vector2 pivot = name.Split("_")[0] switch
- {
- "field" => new(0.5f, 0.0f),
- "mountain" => new(0.5f, -0.375f),
- _ => new(0.5f, 0.5f),
- };
- float pixelsPerUnit = 2112f;
- if (Registry.spriteInfos.ContainsKey(name))
- {
- Visual.SpriteInfo spriteData = Registry.spriteInfos[name];
- pivot = spriteData.pivot ?? pivot;
- pixelsPerUnit = spriteData.pixelsPerUnit ?? pixelsPerUnit;
- }
- Sprite sprite = Visual.BuildSprite(file.bytes, pivot, pixelsPerUnit);
- GameManager.GetSpriteAtlasManager().cachedSprites.TryAdd("Heads", new());
- GameManager.GetSpriteAtlasManager().cachedSprites["Heads"].Add(name, sprite);
- Registry.sprites.Add(name, sprite);
- }
-
- ///
- /// Updates a sprite with new information.
- ///
- /// The name of the sprite to update.
- public static void UpdateSprite(string name)
- {
- if (Registry.spriteInfos.ContainsKey(name) && Registry.sprites.ContainsKey(name))
- {
- Visual.SpriteInfo spriteData = Registry.spriteInfos[name];
- Sprite sprite = Visual.BuildSpriteWithTexture(
- Registry.sprites[name].texture,
- spriteData.pivot,
- spriteData.pixelsPerUnit
- );
- GameManager.GetSpriteAtlasManager().cachedSprites["Heads"][name] = sprite;
- Registry.sprites[name] = sprite;
- }
+ Texture2D texture2D = Visual.BuildTexture(file.bytes);
+ Registry.textures[name] = new KeyValuePair(texture2D, false);
}
///
@@ -715,160 +660,94 @@ public static void LoadAudioFile(Mod mod, Mod.File file)
// TODO: issue #71
}
- ///
- /// Loads a prefab info file from a mod and creates a new unit prefab.
- ///
- /// The mod the prefab info file belongs to.
- /// The prefab info file to load.
- public static void LoadPrefabInfoFile(Mod mod, Mod.File file)
+ public static void LoadPrefabsFile(Mod mod, Mod.File file)
{
try
{
- var prefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions
+ var prefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions
{
Converters = { new Vector2Json() },
PropertyNameCaseInsensitive = true,
});
- if (prefab == null || prefab.type != Visual.PrefabType.Unit || prefab.visualParts.Count == 0)
- return;
-
- if (Registry.unitPrefabs.Keys.Any(pref => pref.name == prefab.name))
- {
- Plugin.logger.LogInfo($"Prefab {prefab.name} already exists, skipping.");
- }
- var baseUnit = PrefabManager.GetPrefab(UnitData.Type.Warrior, TribeType.Imperius, SkinType.Default);
- if (baseUnit == null)
- return;
-
- var unitInstance = GameObject.Instantiate(baseUnit);
- if (unitInstance == null)
+ if (prefab == null)
return;
- var spriteContainer = unitInstance.transform.GetChild(0);
- var material = ClearExistingPartsAndExtractMaterial(spriteContainer);
-
- var visualParts = ApplyVisualParts(prefab, spriteContainer, material);
-
- Transform? headPositionMarker = null;
- foreach (var vp in visualParts)
- {
- if (vp.visualPart.gameObject.name == prefab.headPositionMarker)
- {
- headPositionMarker = vp.visualPart.gameObject.transform;
- break;
- }
- }
-
- unitInstance.headPositionMarker = headPositionMarker ?? visualParts[0].visualPart.transform;
-
- var svr = unitInstance.GetComponent();
- svr.visualParts = visualParts.ToArray();
+ Prefabs.unitInfos.AddRange(prefab.units);
+ Prefabs.improvementInfos.AddRange(prefab.improvements);
+ Prefabs.resourceInfos.AddRange(prefab.resources);
- GameObject.DontDestroyOnLoad(unitInstance.gameObject);
- Registry.unitPrefabs.Add(prefab, unitInstance.GetComponent());
-
- Plugin.logger.LogInfo($"Registered prefab {prefab.name} info from {mod.id} mod.");
+ Plugin.logger.LogInfo($"Registered prefabs {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod.");
}
catch (Exception e)
{
- Plugin.logger.LogError($"Error on loading prefab {Path.GetFileNameWithoutExtension(file.name)} info from {mod.id} mod: {e.StackTrace}.");
+ Plugin.logger.LogError($"Error on loading prefabs {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod: {e.StackTrace}.");
}
}
- ///
- /// Clears existing visual parts from a prefab and extracts the material.
- ///
- /// The transform containing the sprite parts.
- /// The material of the original sprite parts.
- private static Material? ClearExistingPartsAndExtractMaterial(Transform spriteContainer)
+ public static void LoadUnitPrefabFile(Mod mod, Mod.File file)
{
- Material? material = null;
- for (int i = 0; i < spriteContainer.childCount; i++)
+ try
{
- var child = spriteContainer.GetChild(i);
- if (child.gameObject.name == "Head")
+ var unitPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions
{
- var renderer = child.GetComponent();
- if (renderer != null)
- material = renderer.material;
- }
- GameObject.Destroy(child.gameObject);
+ Converters = { new Vector2Json() },
+ PropertyNameCaseInsensitive = true,
+ });
+ if (unitPrefab == null)
+ return;
+
+ Prefabs.unitInfos.Add(unitPrefab);
+
+ Plugin.logger.LogInfo($"Registered unit prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod.");
+ }
+ catch (Exception e)
+ {
+ Plugin.logger.LogError($"Error on loading unit prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod: {e.StackTrace}.");
}
- return material;
}
- ///
- /// Applies new visual parts to a prefab.
- ///
- /// A list of visual part information.
- /// The transform to add the parts to.
- /// The material to use for the parts.
- /// A list of the created visual parts.
- private static List ApplyVisualParts(
- Visual.PrefabInfo prefab,
- Transform spriteContainer,
- Material? material)
+ public static void LoadImprovementPrefabFile(Mod mod, Mod.File file)
{
- if (prefab.visualParts.Count == 0)
- return new List();
+ try
+ {
+ var improvementPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions
+ {
+ Converters = { new Vector2Json() },
+ PropertyNameCaseInsensitive = true,
+ });
+ if (improvementPrefab == null)
+ return;
- List parts = new();
+ Prefabs.improvementInfos.Add(improvementPrefab);
- foreach (var info in prefab.visualParts)
+ Plugin.logger.LogInfo($"Registered improvement prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod.");
+ }
+ catch (Exception e)
{
- parts.Add(CreateVisualPart(info, spriteContainer, material));
+ Plugin.logger.LogError($"Error on loading improvement prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod: {e.StackTrace}.");
}
-
- return parts;
}
- ///
- /// Creates a single visual part for a prefab.
- ///
- /// The information for the visual part.
- /// The parent transform for the part.
- /// The material to use for the part.
- /// The created visual part.
- private static SkinVisualsReference.VisualPart CreateVisualPart(
- Visual.VisualPartInfo info,
- Transform parent,
- Material? material)
+ public static void LoadResourcePrefabFile(Mod mod, Mod.File file)
{
- var visualPartObj = new GameObject(info.gameObjectName);
- visualPartObj.transform.SetParent(parent);
- visualPartObj.transform.position = info.coordinates;
- visualPartObj.transform.localScale = info.scale;
- visualPartObj.transform.rotation = Quaternion.Euler(0f, 0f, info.rotation);
-
- var outlineObj = new GameObject("Outline");
- outlineObj.transform.SetParent(visualPartObj.transform);
- outlineObj.transform.position = info.coordinates;
- outlineObj.transform.localScale = info.scale;
- outlineObj.transform.rotation = Quaternion.Euler(0f, 0f, info.rotation);
-
- var visualPart = new SkinVisualsReference.VisualPart
+ try
{
- DefaultSpriteName = info.baseName,
- visualPart = visualPartObj,
- outline = outlineObj,
- tintable = info.tintable
- };
-
- var renderer = visualPartObj.AddComponent();
- renderer.material = material;
- renderer.sortingLayerName = "Units";
- renderer.sortingOrder = info.tintable ? 0 : 1;
-
- visualPart.renderer = new SkinVisualsReference.RendererUnion { spriteRenderer = renderer };
-
- var outlineRenderer = outlineObj.AddComponent();
- outlineRenderer.material = material;
- outlineRenderer.sortingLayerName = "Units";
- outlineRenderer.sortingOrder = -1;
+ var resourcePrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions
+ {
+ Converters = { new Vector2Json() },
+ PropertyNameCaseInsensitive = true,
+ });
+ if (resourcePrefab == null)
+ return;
- visualPart.outlineRenderer = new SkinVisualsReference.RendererUnion { spriteRenderer = outlineRenderer };
+ Prefabs.resourceInfos.Add(resourcePrefab);
- return visualPart;
+ Plugin.logger.LogInfo($"Registered resource prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod.");
+ }
+ catch (Exception e)
+ {
+ Plugin.logger.LogError($"Error on loading resource prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod: {e.StackTrace}.");
+ }
}
///
@@ -916,7 +795,6 @@ internal static void ProcessGameLogicData(GameLogicData gameLogicData, JObject r
{
CreateMappings(rootObject);
ProcessCustomTribes();
- ProcessPrefabs();
ProcessEmbarkOverrides();
ProcessAttractOverrides();
}
@@ -1028,36 +906,47 @@ internal static void CreateMappings(JObject rootObject)
///
/// Processes the prefab registry and populates the PrefabManager with custom prefabs.
///
- internal static void ProcessPrefabs()
+ internal static void ProcessFilesQueue()
{
- Plugin.logger.LogInfo($"Processing prefabs, count: {Registry.prefabNames.Count}.");
- foreach (KeyValuePair item in Registry.prefabNames)
+ try
{
- UnitData.Type unitPrefabType = UnitData.Type.Scout;
- string prefabId = item.Value;
- int hashKey = PrefabManager.GetSkinnedHashKey((UnitData.Type)item.Key, TribeType.None, SkinType.Default);
- if (!Enum.TryParse(CultureInfo.CurrentCulture.TextInfo.ToTitleCase(prefabId), out unitPrefabType)) // Existing unit prefabs
+ foreach (KeyValuePair keyValuePair in Registry.filesQueueToBeProcessedAfterGLDMerge)
{
- KeyValuePair prefabInfo = Registry.unitPrefabs.FirstOrDefault(
- kv => kv.Key.name == prefabId
- );
- if (!EqualityComparer.Default.Equals(prefabInfo.Key, default))
+ Mod mod = keyValuePair.Key;
+ Mod.File file = keyValuePair.Value;
+ Match prefabsMatch = Regex.Match(Path.GetFileName(file.name), @"^prefabs(?:_(.*))?\.json$");
+ if (prefabsMatch.Success)
{
- Visual.UnitPrefabInfo unitPrefabInfo = new(
- EnumCache.GetName((UnitData.Type)item.Key),
- EnumCache.GetName(TribeType.None),
- EnumCache.GetName(SkinType.Default)
- );
- Visual.customPrefabs.Add(unitPrefabInfo, prefabInfo.Value);
- Plugin.logger.LogInfo($"Using custom prefab {prefabId} for unit {item.Key}.");
+ LoadPrefabsFile(mod, file);
+ continue;
+ }
+
+ Match unitPrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^unitPrefab(?:_(.*))?\.json$");
+ if (unitPrefabMatch.Success)
+ {
+ LoadUnitPrefabFile(mod, file);
continue;
}
+
+ Match improvementPrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^improvementPrefab(?:_(.*))?\.json$");
+ if (improvementPrefabMatch.Success)
+ {
+ LoadImprovementPrefabFile(mod, file);
+ continue;
+ }
+
+ Match resourcePrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^resourcePrefab(?:_(.*))?\.json$");
+ if (resourcePrefabMatch.Success)
+ {
+ LoadResourcePrefabFile(mod, file);
+ continue;
+ }
+
}
- Plugin.logger.LogInfo($"Using existing prefab {EnumCache.GetName(unitPrefabType)} for unit {item.Key}.");
- PrefabManager.units.TryAdd(
- hashKey,
- PrefabManager.units[PrefabManager.GetSkinnedHashKey(unitPrefabType, TribeType.None, SkinType.Default)]
- );
+ }
+ catch (Exception e)
+ {
+ Plugin.logger.LogError($"Error on processing post gld merge files queue : {e.StackTrace}");
}
}
diff --git a/src/Managers/Audio.cs b/src/Managers/Audio.cs
index 100cec7..d6fb90e 100644
--- a/src/Managers/Audio.cs
+++ b/src/Managers/Audio.cs
@@ -1,5 +1,4 @@
using HarmonyLib;
-using Polytopia.Data;
using UnityEngine;
using PolytopiaBackendBase.Common;
@@ -17,7 +16,7 @@ public static class Audio
[HarmonyPatch(typeof(MusicData), nameof(MusicData.GetNatureAudioClip))]
private static bool MusicData_GetNatureAudioClip(ref AudioClip __result, TribeType type, SkinType skinType)
{
- AudioClip? audioClip = Registry.GetAudioClip("nature", Util.GetStyle(type, skinType));
+ AudioClip? audioClip = Registry.GetAudioClip("nature", type, skinType);
if (audioClip != null)
{
__result = audioClip;
@@ -33,7 +32,7 @@ private static bool MusicData_GetNatureAudioClip(ref AudioClip __result, TribeTy
[HarmonyPatch(typeof(MusicData), nameof(MusicData.GetMusicAudioClip))]
private static bool MusicData_GetMusicAudioClip(ref AudioClip __result, TribeType type, SkinType skinType)
{
- AudioClip? audioClip = Registry.GetAudioClip("music", Util.GetStyle(type, skinType));
+ AudioClip? audioClip = Registry.GetAudioClip("music", type, skinType);
if (audioClip != null)
{
__result = audioClip;
@@ -51,7 +50,7 @@ private static bool AudioSFXData_GetClip(ref AudioClip __result, SFXTypes id, Sk
{
AudioClip? audioClip = Registry.GetAudioClip(
EnumCache.GetName(id),
- EnumCache.GetName(skinType)
+ skin: skinType
);
if (audioClip != null)
{
diff --git a/src/Managers/AutoUpdate.cs b/src/Managers/AutoUpdate.cs
deleted file mode 100644
index 6384616..0000000
--- a/src/Managers/AutoUpdate.cs
+++ /dev/null
@@ -1,182 +0,0 @@
-using System.Diagnostics;
-using System.IO.Compression;
-using System.Text.Json;
-using HarmonyLib;
-using UnityEngine;
-
-namespace PolyMod.Managers;
-
-///
-/// Manages the automatic update process for PolyMod.
-///
-internal static class AutoUpdate
-{
- ///
- /// Checks for updates when the start screen is shown.
- ///
- [HarmonyPostfix]
- [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.OnShow))]
- private static void StartScreen_UI2_OnShow()
- {
- if (!Plugin.config.autoUpdate) return;
- if (Environment.GetEnvironmentVariable("WINEPREFIX") != null)
- {
- Plugin.logger.LogError("Autoupdate is not supported on Wine!");
- return;
- }
- HttpClient client = new();
- client.DefaultRequestHeaders.Add("User-Agent", "PolyMod");
- try
- {
- // Fetch release information from GitHub API
- var json = JsonDocument.Parse(
- client.GetAsync("https://api.github.com/repos/PolyModdingTeam/PolyMod/releases").UnwrapAsync()
- .Content.ReadAsStringAsync().UnwrapAsync()
- );
- JsonElement? latest = null;
- for (int i = 0; i < json.RootElement.GetArrayLength(); i++)
- {
- var release = json.RootElement[i];
- if (release.GetProperty("prerelease").GetBoolean() && !Plugin.config.updatePrerelease) continue;
- latest = release;
- break;
- }
- string newVersion = latest?.GetProperty("tag_name").GetString()!.TrimStart('v')!;
- if (newVersion.IsVersionOlderOrEqual(Plugin.VERSION)) return;
-
- // Determine the correct BepInEx URL for the current OS
- string os = Application.platform switch
- {
- RuntimePlatform.WindowsPlayer => "win",
- RuntimePlatform.LinuxPlayer => "linux",
- RuntimePlatform.OSXPlayer => "macos",
- _ => "unknown",
- };
- if (os == "unknown")
- {
- Plugin.logger.LogError("Unsupported platform for autoupdate!");
- return;
- }
- string bepinex_url = client
- .GetAsync("https://polymod.dev/data/bepinex.txt").UnwrapAsync()
- .Content.ReadAsStringAsync().UnwrapAsync()
- .Replace("{os}", os);
-
- // The actual update logic
- void Update()
- {
- Time.timeScale = 0;
-
- // Download new PolyMod DLL and BepInEx files
- File.WriteAllBytes(
- Path.Combine(Plugin.BASE_PATH, "PolyMod.new.dll"),
- client.GetAsync(latest?.GetProperty("assets")[0].GetProperty("browser_download_url").GetString()!).UnwrapAsync()
- .Content.ReadAsByteArrayAsync().UnwrapAsync()
- );
- using ZipArchive bepinex = new(client.GetAsync(bepinex_url).UnwrapAsync().Content.ReadAsStream());
- bepinex.ExtractToDirectory(Path.Combine(Plugin.BASE_PATH, "New"), overwriteFiles: true);
-
- // Create and run the appropriate update script for the OS
- ProcessStartInfo info = new()
- {
- WorkingDirectory = Path.Combine(Plugin.BASE_PATH),
- CreateNoWindow = true,
- };
- if (Application.platform == RuntimePlatform.WindowsPlayer)
- {
- string batchPath = Path.Combine(Plugin.BASE_PATH, "update.bat");
- File.WriteAllText(batchPath, $@"
- @echo off
- echo Waiting for Polytopia.exe to exit...
- :waitloop
- tasklist | findstr /I ""Polytopia.exe"" >nul
- if not errorlevel 1 (
- timeout /T 1 >nul
- goto waitloop
- )
-
- echo Updating...
- robocopy ""New"" . /E /MOVE /NFL /NDL /NJH /NJS /NP >nul
- rmdir /S /Q ""New""
- del /F /Q ""BepInEx\plugins\PolyMod.dll""
- move /Y ""PolyMod.new.dll"" ""BepInEx\plugins\PolyMod.dll""
-
- echo Launching game...
- start steam://rungameid/874390
- timeout /T 3 /NOBREAK >nul
- exit
- ");
- info.FileName = "cmd.exe";
- info.Arguments = $"/C start \"\" \"{batchPath}\"";
- info.WorkingDirectory = Plugin.BASE_PATH;
- info.CreateNoWindow = true;
- info.UseShellExecute = false;
- }
- if (Application.platform == RuntimePlatform.LinuxPlayer || Application.platform == RuntimePlatform.OSXPlayer)
- {
- string bashPath = Path.Combine(Plugin.BASE_PATH, "update.sh");
- File.WriteAllText(bashPath, $@"
- #!/bin/bash
-
- echo ""Waiting for Polytopia to exit...""
- while pgrep -x ""Polytopia"" > /dev/null; do
- sleep 1
- done
-
- echo ""Updating...""
- mv New/* . && rm -rf New
- rm -f BepInEx/plugins/PolyMod.dll
- mv -f PolyMod.new.dll BepInEx/plugins/PolyMod.dll
-
- echo ""Launching game...""
- xdg-open steam://rungameid/874390 &
-
- sleep 3
- exit 0
- ");
-
- System.Diagnostics.Process chmod = new System.Diagnostics.Process();
- chmod.StartInfo.FileName = "chmod";
- chmod.StartInfo.Arguments = $"+x \"{bashPath}\"";
- chmod.StartInfo.UseShellExecute = false;
- chmod.StartInfo.CreateNoWindow = true;
- chmod.Start();
- chmod.WaitForExit();
-
- info.FileName = "/bin/bash";
- info.Arguments = $"\"{bashPath}\"";
- info.WorkingDirectory = Plugin.BASE_PATH;
- info.CreateNoWindow = true;
- info.UseShellExecute = false;
- }
- Process.Start(info);
- Application.Quit();
- }
-
- // Show a popup to the user asking if they want to update
- PopupManager.GetBasicPopupWithData(new(
- Localization.Get("polymod.autoupdate"),
- Localization.Get("polymod.autoupdate.description"),
- new(new PopupBase.PopupButtonData[] {
- new(
- "polymod.autoupdate.update",
- PopupBase.PopupButtonData.States.None,
- (Il2CppSystem.Action)Update
- )
- }))
- ).Show();
- }
- catch (Exception e)
- {
- Plugin.logger.LogError($"Failed to check updates: {e.Message}");
- }
- }
-
- ///
- /// Initializes the AutoUpdate manager by patching the necessary methods.
- ///
- internal static void Init()
- {
- Harmony.CreateAndPatchAll(typeof(AutoUpdate));
- }
-}
\ No newline at end of file
diff --git a/src/Managers/Compatibility.cs b/src/Managers/Compatibility.cs
index 1d55bb7..c2d2498 100644
--- a/src/Managers/Compatibility.cs
+++ b/src/Managers/Compatibility.cs
@@ -1,4 +1,5 @@
using HarmonyLib;
+using Il2CppInterop.Runtime;
using System.Text;
using UnityEngine;
using UnityEngine.EventSystems;
@@ -20,6 +21,12 @@ internal static class Compatibility
///
internal static bool shouldResetSettings = false;
private static bool sawSignatureWarning;
+ private const string downloadLink = "https://polymod.dev/download/";
+
+ // TODO: rewrite all of this bullshit! each mod should have a checksum. and then using per mod checksum, we should calculate overall checksum
+ // why? launcher will let you install a mod if u dont have it. if u have a mod with this id, but it doesnt match any checksum of any ver of
+ // this mod on polymod.dev, then we just leave it as "install". if there is a new ver, we can simply put "update"
+ // also add a client mod check or whatever
///
/// Hashes the signatures of all loaded mods to create a checksum.
@@ -87,6 +94,36 @@ private static bool CheckSignatures(Action action, Il2CppSystem.Guid gameId)
[HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.OnShow))]
private static void StartScreen_UI2_OnShow()
{
+ if(!Plugin.ValidLaunch && Application.platform != RuntimePlatform.Android)
+ {
+ PopupManager.GetBasicPopupWithData(
+ new(
+ Localization.Get("polymod.title.invalid"),
+ Localization.Get("polymod.description.invalid"),
+ new PopupBase.PopupButtonData[] {
+ new(
+ "buttons.exitgame",
+ PopupBase.PopupButtonData.States.None,
+ callback: (Il2CppSystem.Action)Application.Quit,
+ closesPopup: false
+ ),
+ new(
+ "buttons.download.launcher",
+ PopupBase.PopupButtonData.States.None,
+ callback: DelegateSupport.ConvertDelegate(OpenSite),
+ closesPopup: false
+ )
+ }
+ )
+ ).Show();
+
+ void OpenSite()
+ {
+ NativeHelpers.OpenURL(downloadLink, false);
+ Application.Quit();
+ }
+ return;
+ }
string lastChecksum = checksum;
try
{
diff --git a/src/Managers/Hub.cs b/src/Managers/Hub.cs
index 57da0b6..22ab356 100644
--- a/src/Managers/Hub.cs
+++ b/src/Managers/Hub.cs
@@ -3,15 +3,12 @@
using HarmonyLib;
using I2.Loc;
using Il2CppInterop.Runtime;
-using TMPro;
using UnityEngine;
-using UnityEngine.EventSystems;
-using UnityEngine.UI;
using static PopupBase;
using PolytopiaBackendBase.Common;
using System.Text.Encodings.Web;
using Il2CppSystem.Linq;
-using System.Text.RegularExpressions;
+using PolyMod.Json;
namespace PolyMod.Managers;
@@ -148,35 +145,36 @@ private static void GameManager_Update()
///
/// Updates sprite information from dumped files.
///
- internal static void UpdateSpriteInfos()
+ internal static void HotReload()
{
- string message = string.Empty;
- Directory.CreateDirectory(Plugin.DUMPED_DATA_PATH);
-
- foreach (var file in Directory.GetFiles(Plugin.DUMPED_DATA_PATH))
- {
- string? name = Path.GetFileNameWithoutExtension(file);
- List subnames = new();
- if (name.Contains("sprites_"))
- {
- subnames = name.Split('_').ToList();
- Mod.File spriteInfo = new(Path.GetFileNameWithoutExtension(file), File.ReadAllBytes(file));
- Dictionary? deserialized = Loader.LoadSpriteInfoFile(Registry.mods[subnames[1]], spriteInfo);
- if (deserialized != null)
- {
- foreach (var kvp in deserialized)
- {
- Loader.UpdateSprite(kvp.Key);
- }
- message += Localization.Get("polymod.spriteinfo.updated", new Il2CppSystem.Object[] { subnames[1] });
- }
- }
- }
- if (message == string.Empty)
- {
- message = Localization.Get("polymod.spriteinfo.notupdated");
- }
- NotificationManager.Notify(message);
+ NotificationManager.Notify("FAILED: wip");
+ // string message = string.Empty;
+ // Directory.CreateDirectory(Plugin.DUMPED_DATA_PATH);
+
+ // foreach (var file in Directory.GetFiles(Plugin.DUMPED_DATA_PATH))
+ // {
+ // string? name = Path.GetFileNameWithoutExtension(file);
+ // List subnames = new();
+ // if (name.Contains("sprites_"))
+ // {
+ // subnames = name.Split('_').ToList();
+ // Mod.File spriteInfo = new(Path.GetFileNameWithoutExtension(file), File.ReadAllBytes(file));
+ // Dictionary? deserialized = Loader.LoadSpriteInfoFile(Registry.mods[subnames[1]], spriteInfo);
+ // if (deserialized != null)
+ // {
+ // foreach (var kvp in deserialized)
+ // {
+ // Loader.UpdateSprite(kvp.Key);
+ // }
+ // message += Localization.Get("polymod.hotreload.success", new Il2CppSystem.Object[] { subnames[1] });
+ // }
+ // }
+ // }
+ // if (message == string.Empty)
+ // {
+ // message = Localization.Get("polymod.hotreload.failure");
+ // }
+ // NotificationManager.Notify(message);
}
///
@@ -185,16 +183,38 @@ internal static void UpdateSpriteInfos()
internal static void DumpData()
{
Directory.CreateDirectory(Plugin.DUMPED_DATA_PATH);
+ DumpLogic();
+ DumpLocalization();
+ DumpSprites();
+ DumpTribePreviewLegacy();
+ DumpPrefabs();
+ NotificationManager.Notify(Localization.Get("polymod.hub.dumped"));
+ }
+
+ internal static void DumpLogic()
+ {
Directory.CreateDirectory(Plugin.LOGIC_DUMP_PATH);
+ for (int version = 1; version <= VersionManager.GameLogicDataVersion; version++)
+ {
+ File.WriteAllTextAsync(
+ Path.Combine(Plugin.LOGIC_DUMP_PATH, $"GameLogicData{version}.json"),
+ PolytopiaDataManager.provider.LoadGameLogicData(version)
+ );
+ }
+ for (int version = 1; version <= VersionManager.AvatarDataVersion; version++)
+ {
+ File.WriteAllTextAsync(
+ Path.Combine(Plugin.LOGIC_DUMP_PATH, $"AvatarData{version}.json"),
+ PolytopiaDataManager.provider.LoadAvatarData(version)
+ );
+ }
+ }
+
+ internal static void DumpLocalization()
+ {
Directory.CreateDirectory(Plugin.LOCALIZATION_DUMP_PATH);
- File.WriteAllTextAsync(
- Path.Combine(Plugin.LOGIC_DUMP_PATH, "gameLogicData.json"),
- PolytopiaDataManager.provider.LoadGameLogicData(VersionManager.GameLogicDataVersion)
- );
- File.WriteAllTextAsync(
- Path.Combine(Plugin.LOGIC_DUMP_PATH, "avatarData.json"),
- PolytopiaDataManager.provider.LoadAvatarData(1337)
- );
+
+ // Languages dump
var source = LocalizationManager.Sources[0];
foreach (var language in source.GetLanguages())
{
@@ -221,29 +241,52 @@ internal static void DumpData()
json
);
}
+ // Full loc dump
foreach (var category in source.GetCategories())
File.WriteAllTextAsync(
Path.Combine(Plugin.LOCALIZATION_DUMP_PATH, $"localization_{category}.csv"),
source.Export_CSV(category)
);
- foreach (KeyValuePair entry in Registry.mods)
+ }
+
+ private static void DumpSprites()
+ {
+ Directory.CreateDirectory(Plugin.SPRITES_DUMP_PATH);
+ SpriteAtlasManager spriteAtlasManager = GameManager.GetSpriteAtlasManager();
+
+ for (int i = 0; i < AtlasData.raw.Count; i++)
{
- foreach (Mod.File file in entry.Value.files)
+ if (spriteAtlasManager.loadedAtlases[i] == null) continue; // not loaded, skip
+
+ string[] row = AtlasData.raw[i];
+ string atlasName = row[0];
+ string dir = Path.Combine(Plugin.SPRITES_DUMP_PATH, atlasName);
+ Directory.CreateDirectory(dir);
+
+ Dictionary atlasSpriteInfo = new();
+ for (int j = 1; j < row.Length; j++)
{
- Match spritesMatch = Regex.Match(Path.GetFileName(file.name), @"^sprites(?:_(.*))?\.json$");
- if (spritesMatch.Success)
- File.WriteAllBytes(
- Path.Combine(
- Plugin.DUMPED_DATA_PATH,
- $"{Path.GetFileNameWithoutExtension(file.name)}_{entry.Key}.json"
- ),
- file.bytes
- );
+ string spriteName = row[j];
+ Sprite sprite = spriteAtlasManager.GetSprite(spriteName, atlasName);
+ if (sprite == null) continue;
+
+ (byte[] bytes, Visual.SpriteInfo spriteInfo) = Visual.SpriteToPng(sprite);
+ File.WriteAllBytesAsync(Path.Combine(dir, spriteName + ".png"), bytes);
+ atlasSpriteInfo.Add(spriteName, spriteInfo);
}
+ File.WriteAllTextAsync(
+ Path.Combine(dir, $"sprites_{atlasName}.json"),
+ JsonSerializer.Serialize(atlasSpriteInfo, new JsonSerializerOptions { WriteIndented = true, Converters = { new PolyMod.Json.Vector2Json() }})
+ );
}
+ }
+
+ internal static void DumpTribePreviewLegacy()
+ {
+ // TODO legacy tribe preview dump. i have to update that
foreach (TribeType type in Enum.GetValues(typeof(TribeType)))
{
- List previewTiles = new();
+ List previewTiles = new();
SelectTribePopup popup = PopupManager.GetSelectTribePopup();
for (int x = -3; x <= 3; x++)
{
@@ -252,7 +295,7 @@ internal static void DumpData()
Vector2Int pos = new Vector2Int(x, y);
if (popup.UIWorldPreview.worldPreviewData.TryGetData(pos, type, out UITileData tileData))
{
- Visual.PreviewTile previewTile = new Visual.PreviewTile
+ TribePreviews.PreviewTile previewTile = new TribePreviews.PreviewTile
{
x = tileData.Position.x,
y = tileData.Position.y,
@@ -270,7 +313,104 @@ internal static void DumpData()
JsonSerializer.Serialize(previewTiles, new JsonSerializerOptions { WriteIndented = true })
);
}
- NotificationManager.Notify(Localization.Get("polymod.hub.dumped"));
+ }
+
+ internal static void DumpPrefabs() // TODO
+ {
+ Directory.CreateDirectory(Plugin.PREFABS_DUMP_PATH);
+ PrefabsInfo prefabsInfo = new();
+ foreach (KeyValuePair keyValuePairs in Prefabs.unitsNew)
+ {
+ var unit = keyValuePairs.Value;
+ SkinVisualsReference skinVisualsReference = unit.GetComponent();
+ if (skinVisualsReference == null || skinVisualsReference.visualParts == null)
+ {
+ Plugin.logger.LogWarning($"Dump skipped {keyValuePairs.Key.unitType}: no SkinVisualsReference");
+ continue;
+ }
+ UnitPrefabInfo unitPrefabInfo = new()
+ {
+ headPositionMarker = unit.headPositionMarker.gameObject.name,
+ skinnedKey = keyValuePairs.Key,
+ visualParts = new()
+ };
+ foreach (var visualPart in skinVisualsReference.visualParts)
+ {
+ if (visualPart == null || visualPart.visualPart == null)
+ {
+ continue;
+ }
+
+ var visualPartInfo = new VisualPartInfo { };
+ visualPartInfo.CopyFromVisualPart(visualPart);
+ unitPrefabInfo.visualParts.Add(visualPartInfo);
+
+ }
+ prefabsInfo.units.Add(unitPrefabInfo);
+ }
+
+ foreach (Il2CppSystem.Collections.Generic.KeyValuePair keyValuePairs in PrefabManager.improvements)
+ {
+ var unit = keyValuePairs.Value;
+ SkinVisualsReference skinVisualsReference = unit.GetComponent();
+ if (skinVisualsReference == null || skinVisualsReference.visualParts == null)
+ {
+ Plugin.logger.LogWarning($"Dump skipped {keyValuePairs.Key}: no SkinVisualsReference");
+ continue;
+ }
+ ImprovementPrefabInfo improvementPrefabInfo = new()
+ {
+ improvementType = keyValuePairs.Key,
+ visualParts = new()
+ };
+ foreach (var visualPart in skinVisualsReference.visualParts)
+ {
+ if (visualPart == null || visualPart.visualPart == null)
+ {
+ continue;
+ }
+
+ var visualPartInfo = new VisualPartInfo { };
+ visualPartInfo.CopyFromVisualPart(visualPart);
+ improvementPrefabInfo.visualParts.Add(visualPartInfo);
+
+ }
+ prefabsInfo.improvements.Add(improvementPrefabInfo);
+ }
+
+ foreach (Il2CppSystem.Collections.Generic.KeyValuePair keyValuePairs in PrefabManager.resources)
+ {
+ var unit = keyValuePairs.Value;
+ SkinVisualsReference skinVisualsReference = unit.GetComponent();
+ if (skinVisualsReference == null || skinVisualsReference.visualParts == null)
+ {
+ Plugin.logger.LogWarning($"Dump skipped {keyValuePairs.Key}: no SkinVisualsReference");
+ continue;
+ }
+ ResourcePrefabInfo resourcePrefabInfo = new()
+ {
+ resourceType = keyValuePairs.Key,
+ visualParts = new()
+ };
+ foreach (var visualPart in skinVisualsReference.visualParts)
+ {
+ if (visualPart == null || visualPart.visualPart == null)
+ {
+ continue;
+ }
+
+ var visualPartInfo = new VisualPartInfo { };
+ visualPartInfo.CopyFromVisualPart(visualPart);
+ resourcePrefabInfo.visualParts.Add(visualPartInfo);
+
+ }
+ prefabsInfo.resources.Add(resourcePrefabInfo);
+ }
+
+ File.WriteAllTextAsync(
+ Path.Combine(Plugin.PREFABS_DUMP_PATH, $"prefabs.json"),
+ JsonSerializer.Serialize(prefabsInfo, new JsonSerializerOptions { WriteIndented = true, Converters = { new Vector2Json() },})
+ );
}
///
@@ -278,11 +418,11 @@ internal static void DumpData()
///
internal static void ShowConfigPopup()
{
- BasicPopupLegacy polymodPopup = Visual.GetBasicPopupLegacy();
+ BasicPopup polymodPopup = PopupManager.GetBasicPopup();
polymodPopup.Header = Localization.Get("polymod.hub.config");
polymodPopup.Description = "";
polymodPopup.buttonData = CreateConfigPopupButtonData();
- polymodPopup.ShowSetWidth(POPUP_WIDTH);
+ polymodPopup.Show();
}
internal static void ShowPolyModHub()
@@ -335,8 +475,8 @@ void OpenDiscord()
closesPopup: false
));
popupButtons.Add(new(
- "polymod.hub.spriteinfo.update",
- callback: DelegateSupport.ConvertDelegate(UpdateSpriteInfos),
+ "polymod.hub.hotreload",
+ callback: DelegateSupport.ConvertDelegate(HotReload),
closesPopup: false
));
}
@@ -357,7 +497,7 @@ internal static PopupButtonData[] CreateConfigPopupButtonData()
if (GameManager.Instance.isLevelLoaded)
{
- popupButtons.Add(new PopupButtonData(Localization.Get("polymod.hub.spriteinfo.update"), PopupButtonData.States.None, DelegateSupport.ConvertDelegate(OnUpdateSpritesButtonClicked), -1, true, null));
+ popupButtons.Add(new PopupButtonData(Localization.Get("polymod.hub.hotreload"), PopupButtonData.States.None, DelegateSupport.ConvertDelegate(OnUpdateSpritesButtonClicked), -1, true, null));
}
else
{
@@ -366,25 +506,13 @@ internal static PopupButtonData[] CreateConfigPopupButtonData()
new Il2CppSystem.Object[] { Localization.Get("polymod.debug",
new Il2CppSystem.Object[]{}).ToUpperInvariant() }
);
- string autoUpdateButtonName = Localization.Get(
- Plugin.config.autoUpdate ? "polymod.hub.config.disable" : "polymod.hub.config.enable",
- new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate",
- new Il2CppSystem.Object[]{}).ToUpperInvariant() }
- );
- string includeAlphasButtonName = Localization.Get(
- Plugin.config.updatePrerelease ? "polymod.hub.config.disable" : "polymod.hub.config.enable",
- new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate.alpha",
- new Il2CppSystem.Object[]{}).ToUpperInvariant() }
- );
popupButtons.Add(new PopupButtonData(debugButtonName, PopupButtonData.States.None, DelegateSupport.ConvertDelegate(OnDebugButtonClicked), -1, true, null));
- popupButtons.Add(new PopupButtonData(autoUpdateButtonName, PopupButtonData.States.None, DelegateSupport.ConvertDelegate(OnAutoUpdateButtonClicked), -1, true, null));
- popupButtons.Add(new PopupButtonData(includeAlphasButtonName, Plugin.config.autoUpdate ? PopupButtonData.States.None : PopupButtonData.States.Disabled, DelegateSupport.ConvertDelegate(OnIncludeAlphasButtonClicked), -1, true, null));
}
return popupButtons.ToArray();
void OnDebugButtonClicked()
{
- Plugin.config = new(debug: !Plugin.config.debug, autoUpdate: Plugin.config.autoUpdate, updatePrerelease: Plugin.config.updatePrerelease);
+ Plugin.config = new(debug: !Plugin.config.debug);
Plugin.WriteConfig();
Plugin.UpdateConsole();
NotificationManager.Notify(Localization.Get(
@@ -395,35 +523,9 @@ void OnDebugButtonClicked()
isConfigPopupActive = false;
}
- void OnAutoUpdateButtonClicked()
- {
- Plugin.config = new(debug: Plugin.config.debug, autoUpdate: !Plugin.config.autoUpdate, updatePrerelease: Plugin.config.updatePrerelease);
- Plugin.WriteConfig();
- Plugin.UpdateConsole();
- NotificationManager.Notify(Localization.Get(
- "polymod.config.setto",
- new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate",
- new Il2CppSystem.Object[]{}), Plugin.config.autoUpdate }
- ));
- isConfigPopupActive = false;
- }
-
- void OnIncludeAlphasButtonClicked()
- {
- Plugin.config = new(debug: Plugin.config.debug, autoUpdate: Plugin.config.autoUpdate, updatePrerelease: !Plugin.config.updatePrerelease);
- Plugin.WriteConfig();
- Plugin.UpdateConsole();
- NotificationManager.Notify(Localization.Get(
- "polymod.config.setto",
- new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate.alpha",
- new Il2CppSystem.Object[]{}), Plugin.config.updatePrerelease }
- ));
- isConfigPopupActive = false;
- }
-
void OnUpdateSpritesButtonClicked()
{
- UpdateSpriteInfos();
+ HotReload();
isConfigPopupActive = false;
}
@@ -440,4 +542,4 @@ internal static void Init()
{
Harmony.CreateAndPatchAll(typeof(Hub));
}
-}
+}
\ No newline at end of file
diff --git a/src/Managers/Main.cs b/src/Managers/Main.cs
index 6bead52..188544b 100644
--- a/src/Managers/Main.cs
+++ b/src/Managers/Main.cs
@@ -334,17 +334,37 @@ private static void StartTurnAction_Execute(StartTurnAction __instance, GameStat
}
///
- /// Patches the unit creation method to handle custom unit prefabs.
+ /// Serialization fixes for custom (modded) content.
+ /// GamePlayerSummary stores the tribe as a single byte, which custom tribe ids (>= 1000) overflow into garbage.
+ /// Instead the tribe is clamped to None before the untouched native serializer runs and menu summaries show a generic icon for custom tribes, nothing more.
///
[HarmonyPrefix]
- [HarmonyPatch(typeof(Unit), nameof(Unit.CreateUnit))]
- private static bool Unit_CreateUnit(Unit __instance, UnitData unitData, TribeType tribe, SkinType unitSkin)
+ [HarmonyPatch(typeof(GamePlayerSummary), nameof(GamePlayerSummary.Serialize))]
+ private static bool GamePlayerSummary_Serialize(GamePlayerSummary __instance)
{
- Unit unit = PrefabManager.GetPrefab(unitData.type, tribe, unitSkin);
- if (unit == null) Console.Write("THIS FUCKING SHIT IS NULL WHAT THE FUCK");
+ if ((int)__instance.TribeType >= byte.MaxValue)
+ {
+ __instance.TribeType = TribeType.None;
+ }
+
return true;
}
+ ///
+ /// Serialization fixes for custom (modded) content.
+ /// PlayerState stores the play climate as a single byte, which custom climate ids (>= 1000) overflow into garbage.
+ /// Instead the climate is overriden by the tribe value, as those are effectively the same.
+ ///
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(PlayerState), nameof(PlayerState.Deserialize))]
+ private static void PlayerState_Deserialize(PlayerState __instance, Il2CppSystem.IO.BinaryReader reader, int version)
+ {
+ if ((int)__instance.tribe >= Plugin.AUTOIDX_STARTS_FROM)
+ {
+ __instance.climate = __instance.tribe;
+ }
+ }
+
///
/// Initializes the Main manager.
///
@@ -412,20 +432,39 @@ internal static void Load(GameLogicData gameLogicData, JObject json)
);
continue;
}
- Match prefabMatch = Regex.Match(Path.GetFileName(file.name), @"^prefab(?:_(.*))?\.json$");
- if (prefabMatch.Success)
+
+ Match prefabsMatch = Regex.Match(Path.GetFileName(file.name), @"^prefabs(?:_(.*))?\.json$");
+ if (prefabsMatch.Success)
{
- Loader.LoadPrefabInfoFile(
- mod,
- file
- );
+ Registry.filesQueueToBeProcessedAfterGLDMerge.Add(mod, file);
+ continue;
+ }
+
+ Match unitPrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^unitPrefab(?:_(.*))?\.json$");
+ if (unitPrefabMatch.Success)
+ {
+ Registry.filesQueueToBeProcessedAfterGLDMerge.Add(mod, file);
+ continue;
+ }
+
+ Match improvementPrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^improvementPrefab(?:_(.*))?\.json$");
+ if (improvementPrefabMatch.Success)
+ {
+ Registry.filesQueueToBeProcessedAfterGLDMerge.Add(mod, file);
+ continue;
+ }
+
+ Match resourcePrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^resourcePrefab(?:_(.*))?\.json$");
+ if (resourcePrefabMatch.Success)
+ {
+ Registry.filesQueueToBeProcessedAfterGLDMerge.Add(mod, file);
continue;
}
switch (Path.GetExtension(file.name))
{
case ".png":
- Loader.LoadSpriteFile(mod, file);
+ Loader.LoadTextureFile(mod, file);
break;
case ".wav":
Loader.LoadAudioFile(mod, file);
@@ -442,6 +481,9 @@ internal static void Load(GameLogicData gameLogicData, JObject json)
TechItem.techTierFirebaseId.Add($"tech_research_{i}");
}
Loader.ProcessGameLogicData(gameLogicData, json);
+ Loader.ProcessFilesQueue();
+ Visual.CreateSprites();
+ Prefabs.CreatePrefabs();
stopwatch.Stop();
Plugin.logger.LogInfo($"Loaded all mods in {stopwatch.ElapsedMilliseconds}ms");
}
diff --git a/src/Managers/Prefabs.cs b/src/Managers/Prefabs.cs
new file mode 100644
index 0000000..343aa8c
--- /dev/null
+++ b/src/Managers/Prefabs.cs
@@ -0,0 +1,552 @@
+using HarmonyLib;
+using Polytopia.Data;
+using PolytopiaBackendBase.Common;
+using UnityEngine;
+using static PrefabManager;
+using static SkinVisualsReference;
+
+namespace PolyMod.Managers;
+
+public static class Prefabs
+{
+ internal static Dictionary unitsNew = new();
+ internal static Dictionary> unitOverridesNew = new(); // maybe in the future will implement
+ internal static Dictionary unitUIOverridesNew = new();
+
+ internal static Material? cachedUnitMaterial;
+ internal static Material? cachedSharedMaterial;
+ internal const string CACHED_SHARED_MATERIAL_NAME = "SpriteMaterial";
+
+ public static List unitInfos = new();
+
+ public static List improvementInfos = new();
+
+ public static List resourceInfos = new();
+
+ internal static Dictionary improvementPrefabInfosToCreate = new();
+ internal static List resourcePrefabsInfosToCreate = new();
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(PrefabManager), nameof(PrefabManager.Awake))]
+ private static void PrefabManager_Awake_Postfix(PrefabManager __instance)
+ {
+ unitsNew.Clear();
+ unitOverridesNew.Clear();
+ unitUIOverridesNew.Clear();
+
+ foreach (UnitPrefabData unitPrefabData in __instance.unitPrefabs)
+ {
+ if(cachedUnitMaterial == null)
+ {
+ SkinVisualsReference skinVisualsReference = unitPrefabData.prefab.GetComponent();
+ if (skinVisualsReference != null && skinVisualsReference.visualParts != null)
+ {
+ foreach (VisualPart currentPart in skinVisualsReference.visualParts)
+ {
+ if (currentPart.visualPart == null)
+ {
+ continue;
+ }
+
+ SpriteRenderer headSpriteRenderer = currentPart.visualPart.GetComponent();
+ if (headSpriteRenderer != null)
+ {
+ cachedUnitMaterial = headSpriteRenderer.sharedMaterial;
+ break;
+ }
+ }
+ }
+ }
+ unitsNew[new SkinnedKey(unitPrefabData.type, unitPrefabData.tribe, unitPrefabData.skin)] = unitPrefabData.prefab;
+ }
+
+ foreach (UnitOverridePrefabData unitOverridePrefabData in __instance.unitOverridePrefabs)
+ {
+ if (!unitOverridesNew.TryGetValue(unitOverridePrefabData.overrideCondition, out Dictionary? overridesForCondition))
+ {
+ overridesForCondition = new Dictionary();
+ unitOverridesNew[unitOverridePrefabData.overrideCondition] = overridesForCondition;
+ }
+ overridesForCondition[unitOverridePrefabData.type] = unitOverridePrefabData.prefab;
+ }
+
+ foreach (UnitPrefabData unitUIOverridePrefabData in __instance.unitUIOverridePrefabs)
+ {
+ unitUIOverridesNew[new SkinnedKey(unitUIOverridePrefabData.type, unitUIOverridePrefabData.tribe, unitUIOverridePrefabData.skin)] = unitUIOverridePrefabData.prefab;
+ }
+
+ Plugin.logger.LogInfo($"Built lookups: {unitsNew.Count} units, {unitUIOverridesNew.Count} UI overrides");
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(PrefabManager), nameof(PrefabManager.HasUIUnitOverride))]
+ public static bool HasUIUnitOverride(ref bool __result, UnitData.Type unitType, TribeType tribe, SkinType skin)
+ {
+ __result = unitUIOverridesNew.ContainsKey(new SkinnedKey(unitType, tribe, skin));
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(PrefabManager), nameof(PrefabManager.GetUnitOrUiOverride))]
+ public static bool GetUnitOrUiOverride(ref Unit __result, UnitData.Type unitType, TribeType tribe, SkinType skin)
+ {
+ if (unitUIOverridesNew.TryGetValue(new SkinnedKey(unitType, tribe, skin), out Unit? uiOverridePrefab))
+ {
+ __result = uiOverridePrefab;
+ }
+ else
+ {
+ __result = ResolveUnitPrefab(unitType, tribe, skin);
+ }
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(PrefabManager), nameof(PrefabManager.GetPrefab), new Type[] { typeof(UnitData.Type), typeof(TribeType), typeof(SkinType) })]
+ public static bool PrefabManager_GetPrefab(ref Unit __result, UnitData.Type type, TribeType tribe, SkinType skin)
+ {
+ __result = ResolveUnitPrefab(type, tribe, skin);
+ return false;
+ }
+
+ [HarmonyPostfix] // prolly remove later
+ [HarmonyPatch(typeof(SkinVisualsReference), nameof(SkinVisualsReference.SetupVisualPart))]
+ public static void SetupVisualPart(ref VisualPart visualSetting, bool isImprovement)
+ {
+ bool flag = false;
+ flag = visualSetting.outline != null && visualSetting.outline.transform.parent == visualSetting.visualPart.transform;
+ if (!flag && visualSetting.visualPart.transform.childCount > 0)
+ {
+ visualSetting.outline = visualSetting.visualPart.transform.GetChild(0).gameObject;
+ flag = true;
+ }
+ bool flag2 = false;
+ if (flag)
+ {
+ string defaultSpriteName = visualSetting.DefaultSpriteName;
+ if (visualSetting.outline != null && visualSetting.outline.name == defaultSpriteName + "_Outline")
+ {
+ flag2 = true;
+ }
+ }
+ visualSetting.hasValidOutline = flag2;
+ }
+
+ private static Unit ResolveUnitPrefab(UnitData.Type unitType, TribeType tribeType, SkinType skinType)
+ {
+ if (SeasonManager.IsChristmas()
+ && unitOverridesNew.TryGetValue(OverrideCondition.Christmas, out Dictionary? christmasOverrides)
+ && christmasOverrides.TryGetValue(unitType, out Unit? christmasPrefab)
+ && christmasPrefab != null)
+ {
+ return christmasPrefab;
+ }
+
+ SkinnedKey exactKey = new SkinnedKey(unitType, tribeType, skinType);
+ if (unitsNew.TryGetValue(exactKey, out Unit? exactPrefab))
+ {
+ return exactPrefab;
+ }
+
+ if (skinType != SkinType.Default || tribeType != TribeType.None)
+ {
+ SkinnedKey noTribeKey = new SkinnedKey(unitType, TribeType.None, skinType);
+ SkinnedKey defaultSkinKey = new SkinnedKey(unitType, tribeType, SkinType.Default);
+ SkinnedKey noTribeDefaultSkinKey = new SkinnedKey(unitType, TribeType.None, SkinType.Default);
+
+ if (unitsNew.TryGetValue(noTribeKey, out Unit? fallbackPrefab)
+ || unitsNew.TryGetValue(defaultSkinKey, out fallbackPrefab)
+ || unitsNew.TryGetValue(noTribeDefaultSkinKey, out fallbackPrefab))
+ {
+ unitsNew[exactKey] = fallbackPrefab;
+ return fallbackPrefab;
+ }
+ }
+
+ Plugin.logger.LogInfo($"Couldn't find prefab for type: {unitType}");
+ unitsNew.TryGetValue(new SkinnedKey(default, TribeType.None, SkinType.Default), out Unit? lastResortPrefab);
+ return lastResortPrefab!;
+ }
+
+ internal static void CreatePrefabs()
+ {
+ // get materials
+ Material[] allLoadedMaterials = Resources.FindObjectsOfTypeAll();
+ foreach (Material loadedMaterial in allLoadedMaterials)
+ {
+ // Plugin.logger.LogInfo($"Material: {loadedMaterial.name}, shader: {loadedMaterial.shader.name}");
+
+ if(loadedMaterial.name == CACHED_SHARED_MATERIAL_NAME)
+ {
+ cachedSharedMaterial = loadedMaterial;
+ GameObject.DontDestroyOnLoad(cachedSharedMaterial);
+ break;
+ }
+ }
+
+ // creates dummy improvements. those have one visual part, and default name is the enum cache name of the type
+ foreach (KeyValuePair improvementInfo in improvementPrefabInfosToCreate)
+ {
+ ImprovementData.Type improvementType = improvementInfo.Key;
+ improvementInfos.Add(
+ new ImprovementPrefabInfo()
+ {
+ improvementType = improvementType,
+ visualParts = new()
+ {
+ new()
+ {
+ defaultSpriteName = EnumCache.GetName(improvementType),
+ levels = improvementInfo.Value,
+ }
+ }
+ }
+ );
+ }
+
+ // creates dummy resources. those have one visual part, and default name is the enum cache name of the type
+ foreach (ResourceData.Type resourceType in resourcePrefabsInfosToCreate)
+ {
+ resourceInfos.Add(
+ new ResourcePrefabInfo()
+ {
+ resourceType = resourceType,
+ visualParts = new()
+ {
+ new()
+ {
+ defaultSpriteName = EnumCache.GetName(resourceType),
+ }
+ }
+ }
+ );
+ }
+
+ foreach (var unitPrefabInfo in unitInfos)
+ {
+ ProcessUnitPrefabInfo(unitPrefabInfo);
+ }
+
+ foreach (var improvementPrefabInfo in improvementInfos)
+ {
+ ProcessImprovementPrefabInfo(improvementPrefabInfo);
+ }
+
+ foreach (var resourcePrefabInfo in resourceInfos)
+ {
+ ProcessResourcePrefabInfo(resourcePrefabInfo);
+ }
+ }
+
+ #region Units
+ internal static void ProcessUnitPrefabInfo(UnitPrefabInfo unitPrefabInfo)
+ {
+ // maybe just init new unit? hmmmmm will try later
+ var baseUnit = PrefabManager.GetPrefab(UnitData.Type.Warrior, TribeType.Imperius, SkinType.Default);
+ if (baseUnit == null)
+ return;
+
+ var unitInstance = GameObject.Instantiate(baseUnit);
+ if (unitInstance == null)
+ return;
+
+ GameObject.Destroy(unitInstance.spriteContainer.gameObject);
+
+ var spriteContainerObj = new GameObject("SpriteContainer");
+ spriteContainerObj.transform.SetParent(unitInstance.transform, false);
+ var spriteContainer = spriteContainerObj.transform;
+ unitInstance.spriteContainer = spriteContainer;
+
+ var visualParts = ApplyUnitVisualParts(unitPrefabInfo, spriteContainer);
+
+ Transform? headPositionMarker = null;
+ foreach (var vp in visualParts)
+ {
+ if (vp.visualPart.gameObject.name == unitPrefabInfo.headPositionMarker)
+ {
+ headPositionMarker = vp.visualPart.gameObject.transform;
+ break;
+ }
+ }
+
+ unitInstance.headPositionMarker = headPositionMarker ?? visualParts[0].visualPart.transform;
+
+ var svr = unitInstance.GetComponent();
+ svr.visualParts = visualParts.ToArray();
+
+ GameObject.DontDestroyOnLoad(unitInstance.gameObject);
+
+ if(unitPrefabInfo.uiOverride)
+ {
+ unitUIOverridesNew[unitPrefabInfo.skinnedKey] = unitInstance;
+ return;
+ }
+ unitsNew[unitPrefabInfo.skinnedKey] = unitInstance;
+ }
+
+ private static List ApplyUnitVisualParts(
+ UnitPrefabInfo unitPrefabInfo,
+ Transform spriteContainer)
+ {
+ List parts = new();
+ List visualPartInfos = unitPrefabInfo.visualParts;
+
+ if (visualPartInfos.Count == 0)
+ return parts;
+
+ int nextPartSortingOrder = 0;
+ int nextOutlineSortingOrder = -visualPartInfos.Count;
+ var newVisualInfo = visualPartInfos;
+ newVisualInfo.Reverse();
+ foreach (var info in newVisualInfo)
+ {
+ parts.Add(CreateUnitVisualPart(info, spriteContainer, info.sortingOrderOverride ?? nextPartSortingOrder, info.outlineSortingOrderOverride ?? nextOutlineSortingOrder));
+ if(nextOutlineSortingOrder < -1)
+ {
+ nextOutlineSortingOrder++;
+ }
+ nextPartSortingOrder++;
+ }
+
+ return parts;
+ }
+
+ private static SkinVisualsReference.VisualPart CreateUnitVisualPart(
+ VisualPartInfo info,
+ Transform parent,
+ int visualPartSortingOrder,
+ int outlineSortingOrder)
+ {
+ var visualPartObj = new GameObject(info.defaultSpriteName);
+ visualPartObj.transform.SetParent(parent);
+
+ var outlineObj = new GameObject($"{info.defaultSpriteName}_Outline");
+ outlineObj.transform.SetParent(visualPartObj.transform);
+
+ visualPartObj.transform.position = info.coordinates;
+ outlineObj.transform.position = info.coordinates;
+ visualPartObj.transform.rotation = outlineObj.transform.rotation = Quaternion.Euler(0f, 0f, info.rotation);
+
+ // this bs was hunting me for so long. that is the thing which makes outline hidden in ui
+ outlineObj.SetActive(false);
+
+ var visualPart = info.ToVisualPart(false);
+ visualPart.visualPart = visualPartObj;
+ visualPart.outline = outlineObj;
+
+ var renderer = visualPartObj.AddComponent();
+ renderer.material = cachedUnitMaterial;
+ renderer.sortingOrder = visualPartSortingOrder;
+ renderer.sortingLayerName = "Units";
+ visualPart.renderer = new SkinVisualsReference.RendererUnion { spriteRenderer = renderer };
+
+ var outlineRenderer = outlineObj.AddComponent();
+ outlineRenderer.material = cachedUnitMaterial;
+ outlineRenderer.sortingOrder = outlineSortingOrder;
+ outlineRenderer.sortingLayerName = "Units";
+ visualPart.outlineRenderer = new SkinVisualsReference.RendererUnion { spriteRenderer = outlineRenderer };
+
+ return visualPart;
+ }
+
+ #endregion
+ #region Improvements
+ internal static void ProcessImprovementPrefabInfo(ImprovementPrefabInfo improvementPrefabInfo)
+ {
+ if(improvementPrefabInfo.improvementType == null)
+ return;
+
+ var baseBuilding = PrefabManager.GetPrefab(ImprovementData.Type.LumberHut);
+ if (baseBuilding == null)
+ return;
+
+ var buildingInstance = GameObject.Instantiate(baseBuilding);
+ if (buildingInstance == null)
+ return;
+
+ Transform buildingTransform = buildingInstance.transform;
+ for (int childIndex = buildingTransform.childCount - 1; childIndex >= 0; childIndex--)
+ {
+ GameObject.Destroy(buildingTransform.GetChild(childIndex).gameObject);
+ }
+
+ var visualParts = ApplyBuildingVisualParts(improvementPrefabInfo, buildingTransform);
+
+ var svr = buildingInstance.GetComponent();
+ svr.visualParts = visualParts.ToArray();
+
+ GameObject.DontDestroyOnLoad(buildingInstance.gameObject);
+
+ PrefabManager.improvements[(Polytopia.Data.ImprovementData.Type)improvementPrefabInfo.improvementType] = buildingInstance;
+ }
+
+ private static List ApplyBuildingVisualParts(
+ ImprovementPrefabInfo improvementPrefabInfo,
+ Transform spriteContainer)
+ {
+ List parts = new();
+ List visualPartInfos = improvementPrefabInfo.visualParts;
+
+ if (visualPartInfos.Count == 0)
+ return parts;
+
+ int nextPartSortingOrder = 0;
+ int nextOutlineSortingOrder = -visualPartInfos.Count;
+ foreach (var info in visualPartInfos)
+ {
+ parts.Add(CreateBuildingVisualPart(info, spriteContainer, nextPartSortingOrder, nextOutlineSortingOrder, improvementPrefabInfo.needsOutlines));
+ if(nextOutlineSortingOrder < -1)
+ {
+ nextOutlineSortingOrder++;
+ }
+ nextPartSortingOrder++;
+ }
+
+ return parts;
+ }
+
+ private static SkinVisualsReference.VisualPart CreateBuildingVisualPart(
+ VisualPartInfo info,
+ Transform parent,
+ int visualPartSortingOrder,
+ int outlineSortingOrder,
+ bool needsOutline = false)
+ {
+ var visualPartObj = new GameObject(info.defaultSpriteName);
+ visualPartObj.transform.SetParent(parent);
+
+ var outlineObj = new GameObject($"{info.defaultSpriteName}_Outline");
+ outlineObj.transform.SetParent(visualPartObj.transform);
+
+ visualPartObj.transform.position = outlineObj.transform.position = info.coordinates;
+ visualPartObj.transform.rotation = outlineObj.transform.rotation = Quaternion.Euler(0f, 0f, info.rotation);
+
+ outlineObj.SetActive(false);
+
+ var visualPart = info.ToVisualPart(true);
+ visualPart.visualPart = visualPartObj;
+ visualPart.outline = outlineObj;
+
+ var renderer = AddPolytopiaSpriteRenderer(visualPartObj, visualPartSortingOrder);
+ visualPart.renderer = new SkinVisualsReference.RendererUnion { polytopiaSpriteRenderer = renderer };
+
+ if(needsOutline)
+ {
+ var outlineRenderer = AddPolytopiaSpriteRenderer(outlineObj, outlineSortingOrder);
+ visualPart.outlineRenderer = new SkinVisualsReference.RendererUnion { polytopiaSpriteRenderer = outlineRenderer };
+ }
+
+ return visualPart;
+ }
+
+ private static PolytopiaSpriteRenderer AddPolytopiaSpriteRenderer(GameObject targetGameObject, int sortingOrder, string sortingLayerName = "Terrain")
+ {
+ MeshFilter meshFilter = targetGameObject.AddComponent();
+ MeshRenderer meshRenderer = targetGameObject.AddComponent();
+ meshRenderer.receiveShadows = false;
+ meshRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
+ meshRenderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
+ meshRenderer.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
+
+ PolytopiaSpriteRenderer polytopiaSpriteRenderer = targetGameObject.AddComponent();
+ polytopiaSpriteRenderer.meshFilter = meshFilter;
+ polytopiaSpriteRenderer.meshRenderer = meshRenderer;
+
+ polytopiaSpriteRenderer.SortingLayer = SortingLayer.NameToID(sortingLayerName);
+ polytopiaSpriteRenderer.SortingOrder = sortingOrder;
+ if (cachedSharedMaterial != null)
+ {
+ polytopiaSpriteRenderer.SharedMaterial = cachedSharedMaterial;
+ }
+ return polytopiaSpriteRenderer;
+ }
+
+ #endregion
+ #region Resources
+
+ internal static void ProcessResourcePrefabInfo(ResourcePrefabInfo resourcePrefabInfo)
+ {
+ if(resourcePrefabInfo.resourceType == null)
+ return;
+
+ var baseResource = PrefabManager.GetPrefab(ResourceData.Type.Game);
+ if (baseResource == null)
+ return;
+
+ var resourceInstance = GameObject.Instantiate(baseResource);
+ if (resourceInstance == null)
+ return;
+
+ Transform resourceTransform = resourceInstance.transform;
+ for (int childIndex = resourceTransform.childCount - 1; childIndex >= 0; childIndex--)
+ {
+ GameObject.Destroy(resourceTransform.GetChild(childIndex).gameObject);
+ }
+
+ var visualParts = ApplyResourceVisualParts(resourcePrefabInfo, resourceTransform);
+
+ var svr = resourceInstance.GetComponent();
+ svr.visualParts = visualParts.ToArray();
+
+ GameObject.DontDestroyOnLoad(resourceInstance.gameObject);
+
+ PrefabManager.resources[(Polytopia.Data.ResourceData.Type)resourcePrefabInfo.resourceType] = resourceInstance;
+ }
+
+ private static List ApplyResourceVisualParts(
+ ResourcePrefabInfo resourcePrefabInfo,
+ Transform spriteContainer)
+ {
+ List parts = new();
+ List visualPartInfos = resourcePrefabInfo.visualParts;
+
+ if (visualPartInfos.Count == 0)
+ return parts;
+
+ int nextPartSortingOrder = 0;
+ int nextOutlineSortingOrder = -visualPartInfos.Count;
+ foreach (var info in visualPartInfos)
+ {
+ parts.Add(CreateResourceVisualPart(info, spriteContainer, nextPartSortingOrder, nextOutlineSortingOrder));
+ if(nextOutlineSortingOrder < -1)
+ {
+ nextOutlineSortingOrder++;
+ }
+ nextPartSortingOrder++;
+ }
+
+ return parts;
+ }
+
+ private static SkinVisualsReference.VisualPart CreateResourceVisualPart(
+ VisualPartInfo info,
+ Transform parent,
+ int visualPartSortingOrder,
+ int outlineSortingOrder)
+ {
+ var visualPartObj = new GameObject(info.defaultSpriteName);
+ visualPartObj.transform.SetParent(parent);
+
+ var outlineObj = new GameObject($"{info.defaultSpriteName}_Outline");
+ outlineObj.transform.SetParent(visualPartObj.transform);
+
+ visualPartObj.transform.position = outlineObj.transform.position = info.coordinates;
+ visualPartObj.transform.rotation = outlineObj.transform.rotation = Quaternion.Euler(0f, 0f, info.rotation);
+
+ outlineObj.SetActive(false);
+
+ var visualPart = info.ToVisualPart(false);
+ visualPart.visualPart = visualPartObj;
+ visualPart.outline = outlineObj;
+
+ var renderer = AddPolytopiaSpriteRenderer(visualPartObj, visualPartSortingOrder);
+ visualPart.renderer = new SkinVisualsReference.RendererUnion { polytopiaSpriteRenderer = renderer };
+
+ var outlineRenderer = AddPolytopiaSpriteRenderer(outlineObj, outlineSortingOrder);
+ visualPart.outlineRenderer = new SkinVisualsReference.RendererUnion { polytopiaSpriteRenderer = outlineRenderer };
+
+ return visualPart;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Managers/TribePreviews.cs b/src/Managers/TribePreviews.cs
new file mode 100644
index 0000000..f3ab13d
--- /dev/null
+++ b/src/Managers/TribePreviews.cs
@@ -0,0 +1,223 @@
+using HarmonyLib;
+using Polytopia.Data;
+using UnityEngine;
+using Il2CppSystem.Linq;
+using PolyMod.Json;
+using System.Text.Json.Serialization;
+using PolytopiaBackendBase.Common;
+using TMPro;
+
+namespace PolyMod.Managers;
+
+public static class TribePreviews
+{
+ ///
+ /// Represents a tile in a tribe preview.
+ ///
+ public class PreviewTile
+ {
+ /// The x-coordinate of the tile.
+ [JsonInclude]
+ public int? x = null;
+ /// The y-coordinate of the tile.
+ [JsonInclude]
+ public int? y = null;
+ /// The terrain type of the tile.
+ [JsonInclude]
+ [JsonConverter(typeof(EnumCacheJson))]
+ public Polytopia.Data.TerrainData.Type terrainType = Polytopia.Data.TerrainData.Type.None;
+ /// The resource type on the tile.
+ [JsonInclude]
+ [JsonConverter(typeof(EnumCacheJson))]
+ public ResourceData.Type? resourceType = null;
+ /// The unit type on the tile.
+ [JsonInclude]
+ [JsonConverter(typeof(EnumCacheJson))]
+ public UnitData.Type? unitType = null;
+ /// The improvement type on the tile.
+ [JsonInclude]
+ [JsonConverter(typeof(EnumCacheJson))]
+ public ImprovementData.Type? improvementType = null;
+ }
+
+ public record PreviewInfo(int arrayIdx, SaveStateData saveStateData, PreviewTile[] customPreview);
+
+ private static bool isTakingSnapshot = false;
+ private static float? baseOrthographicCameraSize = null;
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(TribePreviewRegistry), nameof(TribePreviewRegistry.PreloadTribePreviews))]
+ public static void PreloadTribePreviews()
+ {
+ Dictionary previewSaveStateDatas = new();
+
+ SaveStateData fallbackSaveStateData = TribePreviewRegistry.saveStateDatas[TribePreviewRegistry.fallbackStateIndex];
+ foreach(string tribe in Registry.tribePreviews.Keys)
+ {
+ string previewId = GetTribePreviewName(tribe);
+ PreviewTile[] customPreview = Registry.tribePreviews[tribe];
+ previewSaveStateDatas[previewId] = new(TribePreviewRegistry.fallbackStateIndex, fallbackSaveStateData, customPreview);
+ }
+
+ for (int i = 0; i < TribePreviewRegistry.saveStateDatas.Length; i++)
+ {
+ SaveStateData saveStateData = TribePreviewRegistry.saveStateDatas[i];
+ if(previewSaveStateDatas.ContainsKey(saveStateData.name))
+ {
+ previewSaveStateDatas[saveStateData.name] = new(i, saveStateData, previewSaveStateDatas[saveStateData.name].customPreview);
+ }
+ }
+
+ foreach (string previewId in previewSaveStateDatas.Keys)
+ {
+ var data = previewSaveStateDatas[previewId];
+ SaveStateData? customSaveStateData = ApplyCustomPreview(previewId, data.saveStateData, data.customPreview);
+ if(customSaveStateData == null)
+ continue;
+
+ if(data.arrayIdx == TribePreviewRegistry.fallbackStateIndex)
+ {
+ List saveStateDatas = TribePreviewRegistry.saveStateDatas.ToList();
+ saveStateDatas.Add(customSaveStateData);
+ TribePreviewRegistry.saveStateDatas = saveStateDatas.ToArray();
+ }
+ else
+ {
+ TribePreviewRegistry.saveStateDatas[data.arrayIdx] = customSaveStateData;
+ }
+ }
+ }
+
+ private static string GetTribePreviewName(string tribeType)
+ {
+ return "worldpreview_" + tribeType;
+ }
+
+ private static SaveStateData? ApplyCustomPreview(string previewId, SaveStateData originalPreview, PreviewTile[] preview)
+ {
+ SaveStateData? saveStateData = null;
+ if (DiskSerializationHelpers.FromLZ4CompressedByteArray(
+ originalPreview.byteArray, out ClientSerializationWrapper clientSerializationWrapper, out int version))
+ {
+ GameState currentGameState = clientSerializationWrapper.GetCurrentGameState();
+ foreach (var previewTile in preview)
+ {
+ if(previewTile.x == null || previewTile.y == null)
+ continue;
+
+ WorldCoordinates coordinates = new((int)previewTile.x, (int)previewTile.y);
+ TileData? tileData = currentGameState.Map.GetTile(coordinates);
+ if(tileData == null)
+ continue;
+
+ if(previewTile.terrainType != Polytopia.Data.TerrainData.Type.None)
+ tileData.terrain = previewTile.terrainType;
+
+ if(previewTile.resourceType != null)
+ {
+ ResourceState? resourceState = null;
+ ResourceData.Type resourceType = (ResourceData.Type)previewTile.resourceType;
+ if(resourceType != ResourceData.Type.None)
+ {
+ resourceState = new ResourceState { type = resourceType };
+ }
+ tileData.resource = resourceState;
+ }
+
+ if(previewTile.unitType != null)
+ {
+ tileData.unit = null;
+ if(previewTile.unitType != Polytopia.Data.UnitData.Type.None &&
+ currentGameState.TryGetPlayer(currentGameState.CurrentPlayer, out PlayerState playerState) &&
+ currentGameState.GameLogicData.TryGetData((UnitData.Type)previewTile.unitType, out UnitData unitData))
+ {
+ UnitState unitState = ActionUtils.TrainUnit(currentGameState, playerState, tileData, unitData);
+ unitState.moved = false;
+ unitState.attacked = false;
+ }
+ }
+
+
+ if(previewTile.improvementType != null)
+ {
+ tileData.improvement = null;
+ ImprovementData.Type improvementType = (ImprovementData.Type)previewTile.improvementType;
+ if(
+ previewTile.improvementType != ImprovementData.Type.None &&
+ currentGameState.GameLogicData.TryGetData(
+ improvementType,
+ out ImprovementData improvementData)
+ )
+ {
+ tileData.improvement = new ImprovementState
+ {
+ type = improvementData.type,
+ borderSize = (ushort)improvementData.borderSize,
+ level = 0,
+ xp = 0,
+ production = 1,
+ founded = (ushort)currentGameState.CurrentTurn,
+ baseScore = (ushort)improvementData.GetScoreReward(),
+ founder = currentGameState.CurrentPlayer
+ };
+ }
+ }
+ }
+
+ saveStateData = ScriptableObject.CreateInstance();
+ saveStateData.name = previewId;
+ saveStateData.byteArray = DiskSerializationHelpers.ToLZ4CompressedByteArray(clientSerializationWrapper, version);
+ }
+
+ return saveStateData;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(SpriteCamera), nameof(SpriteCamera.TakeSnapshotOfMapState))]
+ private static bool SpriteCamera_TakeSnapshotOfMapState(ref Texture2D __result, SpriteCamera.SnapshotMapData mapSnapshot,
+ float w, float h, TribeType tribe, SkinType skin, TribeType climate, int color, bool clearMapAfterwards)
+ {
+ if(!Plugin.config.debug)
+ return true;
+
+ var instance = SpriteCamera.instance;
+ if(baseOrthographicCameraSize == null)
+ baseOrthographicCameraSize = instance.spriteCamera.orthographicSize;
+
+ instance.spriteCamera.orthographicSize = (float)baseOrthographicCameraSize * 2;
+ isTakingSnapshot = true;
+
+ return true;
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(SpriteCamera), nameof(SpriteCamera.TakeSnapshotOfMapState))]
+ private static void SpriteCamera_TakeSnapshotOfMapState_Postfix(ref Texture2D __result, SpriteCamera.SnapshotMapData mapSnapshot,
+ float w, float h, TribeType tribe, SkinType skin, TribeType climate, int color, bool clearMapAfterwards)
+ {
+ if(isTakingSnapshot)
+ isTakingSnapshot = false;
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(Tile), nameof(Tile.RenderDebug))]
+ public static void Tile_RenderDebug(Tile __instance)
+ {
+ if(!isTakingSnapshot)
+ return;
+
+ GameObject textObj = new GameObject("CoordinateText");
+ textObj.transform.SetParent(__instance.transform);
+ textObj.transform.localPosition = new Vector3(0, 0.0f, 0);
+
+ TextMeshPro textMesh = textObj.AddComponent();
+ textMesh.text = __instance.Data.coordinates.ToString();
+ textMesh.fontSize = 3;
+ textMesh.alignment = TextAlignmentOptions.Center;
+ textMesh.color = Color.white;
+
+ MeshRenderer renderer = textObj.GetComponent();
+ renderer.sortingLayerID = MeshCache.TERRAIN_LAYER_ID;
+ renderer.sortingOrder = __instance.Depth + 10;
+ }
+}
\ No newline at end of file
diff --git a/src/Managers/Visual.cs b/src/Managers/Visual.cs
index bccc5fc..c4ebd69 100644
--- a/src/Managers/Visual.cs
+++ b/src/Managers/Visual.cs
@@ -1,15 +1,11 @@
-using System.Reflection;
using HarmonyLib;
using Polytopia.Data;
using UnityEngine;
-using UnityEngine.U2D;
-using UnityEngine.UI;
using Il2CppSystem.Linq;
-using PolyMod.Json;
-using System.Text.Json.Serialization;
using PolytopiaBackendBase.Common;
using Il2CppInterop.Runtime;
-using TMPro;
+using System.Text.Json.Serialization;
+using PolyMod.Json;
namespace PolyMod.Managers;
@@ -18,109 +14,57 @@ namespace PolyMod.Managers;
///
public static class Visual
{
- ///
- /// Represents a tile in a tribe preview.
- ///
- public class PreviewTile
+ /// Represents information about a sprite, such as its pivot and pixels per unit.
+ public class SpriteInfo
{
- /// The x-coordinate of the tile.
- [JsonInclude]
- public int? x = null;
- /// The y-coordinate of the tile.
[JsonInclude]
- public int? y = null;
- /// The terrain type of the tile.
+ public float? pixelsPerUnit;
+
[JsonInclude]
- [JsonConverter(typeof(EnumCacheJson))]
- public Polytopia.Data.TerrainData.Type terrainType = Polytopia.Data.TerrainData.Type.None;
- /// The resource type on the tile.
+ public Vector2? pivot;
+
[JsonInclude]
- [JsonConverter(typeof(EnumCacheJson))]
- public ResourceData.Type? resourceType = null;
- /// The unit type on the tile.
+ public SpriteOrigin? spriteOrigin = null;
+
+ public SpriteInfo(float? pixelsPerUnit, Vector2? pivot, SpriteOrigin? spriteOrigin = null)
+ {
+ this.pixelsPerUnit = pixelsPerUnit;
+ this.pivot = pivot;
+ this.spriteOrigin = spriteOrigin;
+ }
+ }
+
+ public class SpriteOrigin
+ {
[JsonInclude]
- [JsonConverter(typeof(EnumCacheJson))]
- public UnitData.Type? unitType = null;
- /// The improvement type on the tile.
+ public string? name;
+
[JsonInclude]
- [JsonConverter(typeof(EnumCacheJson))]
- public ImprovementData.Type? improvementType = null;
+ [JsonConverter(typeof(EnumNameJson))]
+ public OriginType originType = OriginType.Outer;
}
- /// Represents information about a sprite, such as its pivot and pixels per unit.
- public record SpriteInfo(float? pixelsPerUnit, Vector2? pivot);
+ public enum OriginType
+ {
+ Outer, // Outer means sprite comes from a mod, and not from game's files
+ Inner // Inner means sprite is to be fetched from game's files
+ }
/// Represents information about a custom skin.
public record SkinInfo(int idx, string id, SkinData? skinData);
/// A dictionary of custom widths for basic popups.
public static Dictionary basicPopupWidths = new();
- /// Represents information about a unit prefab.
- public struct UnitPrefabInfo
- {
- public UnitPrefabInfo(string type, string tribe, string skin)
- {
- this.type = type;
- this.tribe = tribe;
- this.skin = skin;
- }
- public string type;
- public string tribe;
- public string skin;
- }
+
private const int BASIC_POPUP_LEGACY_ID = 29;
- public static Dictionary customPrefabs = new();
- private static UnitData.Type currentUnitTypeUI = UnitData.Type.None;
- private static TribeType attackerTribe = TribeType.None;
///
/// A dictionary of skin types of tribes, which can flood tiles, keyed by custom flood tile effect.
///
internal static Dictionary customFloodingSkins = new();
- private static bool isTakingSnapshot = false;
- private static float? baseOrthographicCameraSize = null;
-
- /// The type of a custom prefab.
- public enum PrefabType
- {
- Unit,
- Improvement,
- Resource
- }
-
- /// Represents information about a custom prefab.
- public record PrefabInfo
- {
- public PrefabType type { get; init; }
- public string name { get; init; }
- public List visualParts { get; init; }
- public string headPositionMarker { get; init; }
-
- [JsonConstructor]
- public PrefabInfo(
- PrefabType type,
- List visualParts,
- string name = "",
- string headPositionMarker = "")
- {
- this.type = type;
- this.name = name.ToLowerInvariant();
- this.visualParts = visualParts;
- this.headPositionMarker = headPositionMarker;
- }
- }
-
- /// Represents information about a visual part of a prefab.
- public record VisualPartInfo(
- string gameObjectName,
- string baseName,
- float rotation = 0f,
- Vector2 coordinates = new Vector2(),
- Vector2 scale = new Vector2(),
- bool tintable = false
- );
- private static bool enableOutlines = false;
private static bool seenWarningWCPopup = false;
- public record PreviewInfo(int arrayIdx, SaveStateData saveStateData, PreviewTile[] customPreview);
+
+ private static Dictionary? cachedSpriteDataReverse = null;
+ private static Dictionary cachedReversedNames = new();
#region General
@@ -131,241 +75,199 @@ private static void TechItem_SetupComplete()
{
}
- /// Patches the sprite atlas manager to load custom sprites.
- [HarmonyPrefix]
- [HarmonyPatch(typeof(SpriteAtlasManager), nameof(SpriteAtlasManager.LoadSprite), typeof(string), typeof(string), typeof(SpriteCallback))]
- private static bool SpriteAtlasManager_LoadSprite(SpriteAtlasManager __instance, string atlas, string sprite, SpriteCallback completion)
- {
- bool found = false;
- __instance.LoadSpriteAtlas(atlas, (Il2CppSystem.Action)GetAtlas);
+ /// Patches the sprite atlas manager to look up custom sprites.
+ internal static void CacheSpriteNames()
+ {
+ cachedSpriteDataReverse = new();
+ foreach (Polytopia.Data.TerrainData.Type terrainType in Enum.GetValues())
+ {
+ string result = SpriteData.TerrainToString(terrainType);
+ if(terrainType == Polytopia.Data.TerrainData.Type.Wetland)
+ {
+ cachedSpriteDataReverse.TryAdd(result, EnumCache.GetName(Polytopia.Data.TerrainData.Type.Field) + "_flooded");
+ continue;
+ }
+
+ if(result == SpriteData.TILE_UNKNOWN)
+ continue;
+
+ cachedSpriteDataReverse.TryAdd(result, EnumCache.GetName(terrainType));
+ }
+
+ foreach (ResourceData.Type resourceType in Enum.GetValues())
+ {
+ string result = SpriteData.ResourceToString(resourceType);
+ if(result == SpriteData.IMPROVEMENT_PLACEHOLDER)
+ continue;
+
+ cachedSpriteDataReverse.TryAdd(result, EnumCache.GetName(resourceType));
+ }
+
+ foreach (ImprovementData.Type improvementType in Enum.GetValues())
+ {
+ string result = SpriteData.ImprovementToString(improvementType);
+ if(result == SpriteData.IMPROVEMENT_PLACEHOLDER)
+ continue;
+
+ cachedSpriteDataReverse.TryAdd(result, EnumCache.GetName(improvementType));
+ }
+ }
+ internal static string FormatSpriteName(string baseName)
+ {
+ if(cachedSpriteDataReverse == null)
+ CacheSpriteNames();
- return !found;
+ if(cachedReversedNames.ContainsKey(baseName))
+ return cachedReversedNames[baseName];
- void GetAtlas(SpriteAtlas spriteAtlas)
+ string formattedName = baseName;
+ foreach (var valuePair in cachedSpriteDataReverse!)
{
- if (spriteAtlas != null)
- {
- List names = sprite.Split('_').ToList();
- List filteredNames = new List(names);
- string style = "";
- foreach (string item in names)
- {
- string upperitem = char.ToUpper(item[0]) + item[1..];
- if (EnumCache.TryGetType(item, out TribeType tribe) || EnumCache.TryGetType(item, out SkinType skin)
- || EnumCache.TryGetType(upperitem, out TribeType tribeUpper) || EnumCache.TryGetType(upperitem, out SkinType skinUpper))
- {
- filteredNames.Remove(item);
- style = item;
- continue;
- }
- }
- string name = string.Join("_", filteredNames);
- Sprite? newSprite = Registry.GetSprite(name, style);
- if (newSprite != null)
- {
- completion?.Invoke(atlas, sprite, newSprite);
- found = true;
- }
- }
+ formattedName = formattedName.Replace(valuePair.Key, valuePair.Value);
}
+ cachedReversedNames[baseName] = formattedName;
+ return formattedName;
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(SpriteAtlasManager), nameof(SpriteAtlasManager.TrySolveSpriteNameForLevelSprites))]
+ private static void SpriteAtlasManager_TrySolveSpriteNameForLevelSprites(
+ ref bool __result,
+ SpriteAtlasManager __instance,
+ Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStringArray spriteNameVariants, int level, ref string result)
+ {
+ if(!__result) __result = true; // will crash original lookup if false....
}
- /// Patches the sprite atlas manager to look up custom sprites.
[HarmonyPostfix]
[HarmonyPatch(typeof(SpriteAtlasManager), nameof(SpriteAtlasManager.DoSpriteLookup))]
- private static void SpriteAtlasManager_DoSpriteLookup(ref SpriteAtlasManager.SpriteLookupResult __result, SpriteAtlasManager __instance, string baseName, TribeType tribe, SkinType skin, int level)
+ private static void SpriteAtlasManager_DoSpriteLookup(
+ ref SpriteAtlasManager.SpriteLookupResult __result,
+ SpriteAtlasManager __instance,
+ string baseName, TribeType tribe, SkinType skin, int level)
{
- baseName = Util.FormatSpriteName(baseName);
+ ApplyRegisteredSprites(ref __result, baseName, tribe, skin, level);
- Sprite? sprite = Registry.GetSprite(baseName, Util.GetStyle(tribe, skin), level);
- if (sprite != null)
- __result.sprite = sprite;
+ string formattedName = FormatSpriteName(baseName);
+ if (formattedName != baseName)
+ {
+ ApplyRegisteredSprites(ref __result, formattedName, tribe, skin, level);
+ }
}
- #endregion
- #region Units
-
- /// Enables outlines when the interaction bar is shown.
- [HarmonyPrefix]
- [HarmonyPatch(typeof(InteractionBar), nameof(InteractionBar.Show))]
- private static bool InteractionBar_Show(InteractionBar __instance, bool instant, bool force)
+ private static void ApplyRegisteredSprites(
+ ref SpriteAtlasManager.SpriteLookupResult lookupResult,
+ string spriteName, TribeType tribe, SkinType skin, int level)
{
- enableOutlines = true;
- return true;
+ Sprite? registeredSprite = Registry.GetSprite(spriteName, tribe, skin, level);
+ if (registeredSprite != null)
+ lookupResult.sprite = registeredSprite;
+
+ Sprite? registeredOutlineSprite = Registry.GetSprite(spriteName + "_outline", tribe, skin, level);
+ if (registeredOutlineSprite != null)
+ lookupResult.outlineSprite = registeredOutlineSprite;
}
- /// Prevents outlines from being created when they are disabled.
- [HarmonyPrefix]
- [HarmonyPatch(typeof(UISpriteDuplicator), nameof(UISpriteDuplicator.CreateImage), typeof(SpriteRenderer), typeof(Transform), typeof(Transform), typeof(float), typeof(Vector2), typeof(bool))]
- private static bool UISpriteDuplicator_CreateImage(SpriteRenderer spriteRenderer, Transform source, Transform destination, float scale, Vector2 offset, bool forceFullAlpha)
+ [HarmonyPrefix] // its obsolete so may be removed soon by midjiwan
+ [HarmonyPatch(typeof(SpriteAtlasManager), nameof(SpriteAtlasManager.LoadSprite), new Type[]{ typeof(string), typeof(string), typeof(SpriteCallback) })]
+ private static bool LoadSprite(string atlas, string sprite, SpriteCallback completion)
{
- return !(spriteRenderer.sortingOrder == -1 && !enableOutlines);
+ string formattedName = FormatSpriteName(sprite);
+
+ Sprite? resolvedSprite = Registry.GetSprite(formattedName);
+
+ if (resolvedSprite == null && formattedName != sprite)
+ {
+ resolvedSprite = Registry.GetSprite(sprite);
+ }
+
+ if (resolvedSprite == null)
+ {
+ return true;
+ }
+
+ completion.Invoke(atlas, sprite, resolvedSprite);
+ return false;
}
- /// Disables outlines after the interaction bar is shown.
[HarmonyPostfix]
- [HarmonyPatch(typeof(InteractionBar), nameof(InteractionBar.Show))]
- private static void InteractionBar_Show_Postfix(InteractionBar __instance, bool instant, bool force)
+ [HarmonyPatch(typeof(SpriteData), nameof(SpriteData.TerrainToString))]
+ private static void SpriteData_TerrainToString(ref string __result, Polytopia.Data.TerrainData.Type terrain)
{
- enableOutlines = false;
+ if((int)terrain >= Plugin.AUTOIDX_STARTS_FROM)
+ __result = EnumCache.GetName(terrain);
}
- /// Sets the current unit type being rendered in the UI.
- [HarmonyPrefix]
- [HarmonyPatch(typeof(UIUnitRenderer), nameof(UIUnitRenderer.CreateUnit))]
- private static bool UIUnitRenderer_CreateUnit_Prefix(UIUnitRenderer __instance)
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(SpriteData), nameof(SpriteData.ResourceToString))]
+ private static void SpriteData_ResourceToString(ref string __result, ResourceData.Type resource)
{
- currentUnitTypeUI = __instance.unitType;
- return true;
+ if((int)resource >= Plugin.AUTOIDX_STARTS_FROM)
+ __result = EnumCache.GetName(resource);
}
- /// Resets the current unit type being rendered in the UI.
[HarmonyPostfix]
- [HarmonyPatch(typeof(UIUnitRenderer), nameof(UIUnitRenderer.CreateUnit))]
- private static void UIUnitRenderer_CreateUnit_Postfix(UIUnitRenderer __instance)
+ [HarmonyPatch(typeof(SpriteData), nameof(SpriteData.ImprovementToString))]
+ private static void SpriteData_ImprovementToString(ref string __result, ImprovementData.Type improvement)
{
- currentUnitTypeUI = UnitData.Type.None;
+ if((int)improvement >= Plugin.AUTOIDX_STARTS_FROM)
+ __result = EnumCache.GetName(improvement);
}
- /// Skins the visual parts of a unit with custom sprites.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(SkinVisualsRenderer), nameof(SkinVisualsRenderer.SkinWorldObject))]
- private static void SkinVisualsRenderer_SkinWorldObject(
- SkinVisualsRenderer.SkinWorldType type,
- SkinVisualsReference skinVisuals,
- SkinVisualsTransientData transientSkinData,
- bool checkOutlines,
- int level)
- {
- if (type != SkinVisualsRenderer.SkinWorldType.Unit || skinVisuals == null ||
- skinVisuals.visualParts == null || transientSkinData == null)
- return;
-
- Unit unit = skinVisuals.gameObject.GetComponent();
-
- string unitTypeName = EnumCache.GetName(UnitData.Type.Warrior);
-
- if(unit != null && unit.UnitData != null)
- unitTypeName = EnumCache.GetName(unit.unitData.type);
- if (currentUnitTypeUI != UnitData.Type.None)
- unitTypeName = EnumCache.GetName(currentUnitTypeUI);
-
- string style = Util.GetStyle(transientSkinData.unitSettings.tribe, transientSkinData.unitSettings.skin);
+ #endregion
+ #region Level
- foreach (var visualPart in skinVisuals.visualParts)
+ private static string GetLandSpriteName(TileData tileData)
+ {
+ if (!tileData.HasEffect(TileData.EffectType.Flooded))
{
- if(visualPart.visualPart != null)
- UpdateVisualPart(visualPart, $"{visualPart.visualPart.name}_{unitTypeName}", style);
+ return SpriteData.TerrainToString(Polytopia.Data.TerrainData.Type.Field);
}
- }
- [HarmonyPostfix]
- [HarmonyPatch(typeof(PrefabManager), nameof(PrefabManager.GetPrefab),
- typeof(UnitData.Type), typeof(TribeType), typeof(SkinType)
- )]
- private static void PrefabManager_GetPrefab(ref Unit __result, UnitData.Type type, TribeType tribe, SkinType skin)
- {
- UnitPrefabInfo unitPrefabInfo = new(
- EnumCache.GetName(type),
- EnumCache.GetName(TribeType.None),
- EnumCache.GetName(SkinType.Default)
- );
- if(customPrefabs.ContainsKey(
- unitPrefabInfo
- ))
+ string wetlandSpriteName = SpriteData.TerrainToString(Polytopia.Data.TerrainData.Type.Wetland);
+
+ if (tileData.HasEffect(TileData.EffectType.Swamped))
{
- __result = customPrefabs[unitPrefabInfo];
+ return wetlandSpriteName + "_swamp";
}
- }
-
- #endregion
- #region Level
- /// Updates the visual parts of a resource with custom sprites.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(Resource), nameof(Resource.UpdateObject), typeof(MapRenderContext), typeof(SkinVisualsTransientData))]
- private static void Resource_UpdateObject(Resource __instance, MapRenderContext ctx, SkinVisualsTransientData transientSkinData)
- {
- if (__instance.data != null)
+ foreach (TileData.EffectType effectType in tileData.effects)
{
- string style = Util.GetStyle(__instance.tile.data.climate, __instance.tile.data.Skin);
- string name = EnumCache.GetName(__instance.tile.data.resource.type);
-
- foreach (SkinVisualsReference.VisualPart visualPart in __instance.GetSkinVisualsReference().visualParts)
+ if (customFloodingSkins.TryGetValue(effectType, out SkinType skinType))
{
- UpdateVisualPart(visualPart, name, style);
+ return $"{wetlandSpriteName}_{EnumCache.GetName(skinType)}";
}
}
- }
- /// Updates a building with a custom sprite.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(Building), nameof(Building.UpdateObject), typeof(MapRenderContext), typeof(SkinVisualsTransientData))]
- private static void Building_UpdateObject(Building __instance, MapRenderContext ctx, SkinVisualsTransientData transientSkinData)
- {
- string style = Util.GetStyle(transientSkinData.foundingTribeSettings.tribe, transientSkinData.foundingTribeSettings.skin);
- string name = EnumCache.GetName(__instance.tile.data.improvement.type);
- Sprite? sprite = Registry.GetSprite(name, style, __instance.Level);
- if (sprite != null)
- {
- __instance.Sprite = sprite;
- }
+ return wetlandSpriteName;
}
/// Updates the terrain graphics with custom sprites.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(TerrainRenderer), nameof(TerrainRenderer.UpdateGraphics))]
- private static void TerrainRenderer_UpdateGraphics(TerrainRenderer __instance, Tile tile)
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(TerrainRenderer), nameof(TerrainRenderer.UpdateGraphics))] // Why prefix? I really hate repeating the logic. I would have to do the same bs 2 times.... anyway reviewers tell me ok?
+ private static bool TerrainRenderer_UpdateGraphics(TerrainRenderer __instance, Tile tile, TribeType climate, SkinType skin, bool shouldDesaturate)
{
- string terrain = EnumCache.GetName(tile.data.terrain) ?? string.Empty;
- TribeType tribe = tile.data.climate;
- SkinType skinType = tile.data.Skin;
+ string spriteName = tile.Data.terrain switch
+ {
+ Polytopia.Data.TerrainData.Type.Water => __instance.waterSprites.GetSpriteName(tile),
+ Polytopia.Data.TerrainData.Type.Ocean => __instance.oceanSprites.GetSpriteName(tile),
+ Polytopia.Data.TerrainData.Type.Ice => __instance.iceSprites.GetSpriteName(tile),
+ _ => GetLandSpriteName(tile.Data),
+ };
- string flood = "";
- if (
- tile.data.effects.Contains(TileData.EffectType.Flooded) ||
- (tribe == TribeType.Aquarion && tile.data.terrain == Polytopia.Data.TerrainData.Type.Mountain)
- ) // TODO: Check all tribes with flooded abil instead of Aquarion only.
+ SpriteAtlasManager.SpriteLookupResult spriteLookupResult = GameManager.GetSpriteAtlasManager().DoSpriteLookup(spriteName, climate, skin);
+ if (spriteLookupResult.HasSprite())
{
- foreach (var effect in tile.data.effects)
- {
- if(customFloodingSkins.ContainsKey(effect))
- {
- skinType = customFloodingSkins[effect];
- break;
- }
- }
- flood = "_flooded";
+ __instance.spriteRenderer.Sprite = spriteLookupResult.sprite;
}
- if (tile.data.terrain is Polytopia.Data.TerrainData.Type.Forest or Polytopia.Data.TerrainData.Type.Mountain)
+ if (shouldDesaturate != __instance.isDesaturated)
{
- string propertyName = terrain.ToLower();
- terrain = "field";
-
- PropertyInfo? rendererProperty = tile.GetType().GetProperty(propertyName + "Renderer",
- BindingFlags.Public | BindingFlags.Instance);
-
- if (rendererProperty != null)
- {
- PolytopiaSpriteRenderer? renderer = (PolytopiaSpriteRenderer?)rendererProperty.GetValue(tile);
- if (renderer != null)
- {
- Sprite? additionalSprite = Registry.GetSprite(propertyName + flood, Util.GetStyle(tribe, skinType));
- if (additionalSprite != null)
- {
- renderer.Sprite = additionalSprite;
- rendererProperty.SetValue(tile, renderer);
- }
- }
- }
+ TerrainMaterialHelper.SetSpriteSaturated(__instance.SpriteRenderer, shouldDesaturate);
+ __instance.isDesaturated = shouldDesaturate;
}
- Sprite? sprite = Registry.GetSprite(terrain + flood, Util.GetStyle(tribe, skinType));
- if (sprite != null)
- {
- __instance.spriteRenderer.Sprite = sprite;
- }
+ return false;
}
/// Adds custom flood tile effect when tile is flooded for custom aqua skins.
@@ -401,366 +303,13 @@ public static void GetTileTipHeader(ref string __result, TileData tileData, Skin
[HarmonyPatch(typeof(PolytopiaSpriteRenderer), nameof(PolytopiaSpriteRenderer.ForceUpdateMesh))]
private static void PolytopiaSpriteRenderer_ForceUpdateMesh(PolytopiaSpriteRenderer __instance)
{
- if (__instance.sprite != null && string.IsNullOrEmpty(__instance.atlasName))
- {
- MaterialPropertyBlock materialPropertyBlock = new();
- materialPropertyBlock.SetVector("_Flip", new Vector4(1f, 1f, 0f, 0f));
- materialPropertyBlock.SetTexture("_MainTex", __instance.sprite.texture);
- __instance.meshRenderer.SetPropertyBlock(materialPropertyBlock);
- }
- }
-
- [HarmonyPostfix]
- [HarmonyPatch(typeof(Tile), nameof(Tile.Render), typeof(MapRenderContext), typeof(SkinVisualsTransientData))]
- private static void Tile_Render(Tile __instance, MapRenderContext ctx, SkinVisualsTransientData transientSkinningData)
- {
- Sprite? sprite = Registry.GetSprite("hidden");
- if(sprite != null)
- {
- __instance.fogOfWarRenderer.atlasName = "";
- __instance.fogOfWarRenderer.sprite = sprite;
- __instance.fogOfWarRenderer.spriteRenderer.sprite = sprite;
- }
- }
-
- #endregion
- #region Tribe Preview
-
- [HarmonyPostfix]
- [HarmonyPatch(typeof(TribePreviewRegistry), nameof(TribePreviewRegistry.PreloadTribePreviews))]
- public static void PreloadTribePreviews()
- {
- Dictionary previewSaveStateDatas = new();
-
- SaveStateData fallbackSaveStateData = TribePreviewRegistry.saveStateDatas[TribePreviewRegistry.fallbackStateIndex];
- foreach(string tribe in Registry.tribePreviews.Keys)
- {
- string previewId = GetTribePreviewName(tribe);
- PreviewTile[] customPreview = Registry.tribePreviews[tribe];
- previewSaveStateDatas[previewId] = new(TribePreviewRegistry.fallbackStateIndex, fallbackSaveStateData, customPreview);
- }
-
- for (int i = 0; i < TribePreviewRegistry.saveStateDatas.Length; i++)
- {
- SaveStateData saveStateData = TribePreviewRegistry.saveStateDatas[i];
- if(previewSaveStateDatas.ContainsKey(saveStateData.name))
- {
- previewSaveStateDatas[saveStateData.name] = new(i, saveStateData, previewSaveStateDatas[saveStateData.name].customPreview);
- }
- }
-
- foreach (string previewId in previewSaveStateDatas.Keys)
- {
- var data = previewSaveStateDatas[previewId];
- SaveStateData? customSaveStateData = ApplyCustomPreview(previewId, data.saveStateData, data.customPreview);
- if(customSaveStateData == null)
- continue;
-
- if(data.arrayIdx == TribePreviewRegistry.fallbackStateIndex)
- {
- List saveStateDatas = TribePreviewRegistry.saveStateDatas.ToList();
- saveStateDatas.Add(customSaveStateData);
- TribePreviewRegistry.saveStateDatas = saveStateDatas.ToArray();
- }
- else
- {
- TribePreviewRegistry.saveStateDatas[data.arrayIdx] = customSaveStateData;
- }
- }
- }
-
- private static string GetTribePreviewName(string tribeType)
- {
- return "worldpreview_" + tribeType;
- }
-
- private static SaveStateData? ApplyCustomPreview(string previewId, SaveStateData originalPreview, PreviewTile[] preview)
- {
- SaveStateData? saveStateData = null;
- if (DiskSerializationHelpers.FromLZ4CompressedByteArray(
- originalPreview.byteArray, out ClientSerializationWrapper clientSerializationWrapper, out int version))
- {
- GameState currentGameState = clientSerializationWrapper.GetCurrentGameState();
- foreach (var previewTile in preview)
- {
- if(previewTile.x == null || previewTile.y == null)
- continue;
-
- WorldCoordinates coordinates = new((int)previewTile.x, (int)previewTile.y);
- TileData? tileData = currentGameState.Map.GetTile(coordinates);
- if(tileData == null)
- continue;
-
- if(previewTile.terrainType != Polytopia.Data.TerrainData.Type.None)
- tileData.terrain = previewTile.terrainType;
-
- if(previewTile.resourceType != null)
- {
- ResourceState? resourceState = null;
- ResourceData.Type resourceType = (ResourceData.Type)previewTile.resourceType;
- if(resourceType != ResourceData.Type.None)
- {
- resourceState = new ResourceState { type = resourceType };
- }
- tileData.resource = resourceState;
- }
-
- if(previewTile.unitType != null)
- {
- tileData.unit = null;
- if(previewTile.unitType != Polytopia.Data.UnitData.Type.None &&
- currentGameState.TryGetPlayer(currentGameState.CurrentPlayer, out PlayerState playerState) &&
- currentGameState.GameLogicData.TryGetData((UnitData.Type)previewTile.unitType, out UnitData unitData))
- {
- UnitState unitState = ActionUtils.TrainUnit(currentGameState, playerState, tileData, unitData);
- unitState.moved = false;
- unitState.attacked = false;
- }
- }
-
-
- if(previewTile.improvementType != null)
- {
- tileData.improvement = null;
- ImprovementData.Type improvementType = (ImprovementData.Type)previewTile.improvementType;
- if(
- previewTile.improvementType != ImprovementData.Type.None &&
- currentGameState.GameLogicData.TryGetData(
- improvementType,
- out ImprovementData improvementData)
- )
- {
- tileData.improvement = new ImprovementState
- {
- type = improvementData.type,
- borderSize = (ushort)improvementData.borderSize,
- level = 0,
- xp = 0,
- production = 1,
- founded = (ushort)currentGameState.CurrentTurn,
- baseScore = (ushort)improvementData.GetScoreReward(),
- founder = currentGameState.CurrentPlayer
- };
- }
- }
- }
-
- saveStateData = ScriptableObject.CreateInstance();
- saveStateData.name = previewId;
- saveStateData.byteArray = DiskSerializationHelpers.ToLZ4CompressedByteArray(clientSerializationWrapper, version);
- }
-
- return saveStateData;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(SpriteCamera), nameof(SpriteCamera.TakeSnapshotOfMapState))]
- private static bool SpriteCamera_TakeSnapshotOfMapState(ref Texture2D __result, SpriteCamera.SnapshotMapData mapSnapshot,
- float w, float h, TribeType tribe, SkinType skin, TribeType climate, int color, bool clearMapAfterwards)
- {
- if(!Plugin.config.debug)
- return true;
-
- var instance = SpriteCamera.instance;
- if(baseOrthographicCameraSize == null)
- baseOrthographicCameraSize = instance.spriteCamera.orthographicSize;
-
- instance.spriteCamera.orthographicSize = (float)baseOrthographicCameraSize * 2;
- isTakingSnapshot = true;
-
- return true;
- }
-
- [HarmonyPostfix]
- [HarmonyPatch(typeof(SpriteCamera), nameof(SpriteCamera.TakeSnapshotOfMapState))]
- private static void SpriteCamera_TakeSnapshotOfMapState_Postfix(ref Texture2D __result, SpriteCamera.SnapshotMapData mapSnapshot,
- float w, float h, TribeType tribe, SkinType skin, TribeType climate, int color, bool clearMapAfterwards)
- {
- if(isTakingSnapshot)
- isTakingSnapshot = false;
- }
-
- [HarmonyPostfix]
- [HarmonyPatch(typeof(Tile), nameof(Tile.RenderDebug))]
- public static void Tile_RenderDebug(Tile __instance)
- {
- if(!isTakingSnapshot)
+ if (__instance.sprite == null || !string.IsNullOrEmpty(__instance.atlasName))
return;
- GameObject textObj = new GameObject("CoordinateText");
- textObj.transform.SetParent(__instance.transform);
- textObj.transform.localPosition = new Vector3(0, 0.0f, 0);
-
- TextMeshPro textMesh = textObj.AddComponent();
- textMesh.text = __instance.Data.coordinates.ToString();
- textMesh.fontSize = 3;
- textMesh.alignment = TextAlignmentOptions.Center;
- textMesh.color = Color.white;
-
- MeshRenderer renderer = textObj.GetComponent();
- renderer.sortingLayerID = MeshCache.TERRAIN_LAYER_ID;
- renderer.sortingOrder = __instance.Depth + 10;
- }
-
- #endregion
- #region UI
-
- /// Provides custom sprites for improvements in the UI.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(UIUtils), nameof(UIUtils.GetImprovementSprite), typeof(ImprovementData.Type), typeof(TribeType), typeof(SkinType), typeof(SpriteAtlasManager))]
- private static void UIUtils_GetImprovementSprite(ref Sprite __result, ImprovementData.Type improvement, TribeType tribe, SkinType skin, SpriteAtlasManager atlasManager)
- {
- Sprite? sprite = Registry.GetSprite(EnumCache.GetName(improvement), Util.GetStyle(tribe, skin));
- if (sprite != null)
- {
- __result = sprite;
- }
- }
-
- /// Provides custom sprites for improvements in the UI.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(UIUtils), nameof(UIUtils.GetImprovementSprite), typeof(SkinVisualsTransientData), typeof(ImprovementData.Type), typeof(SpriteAtlasManager))]
- private static void UIUtils_GetImprovementSprite_2(ref Sprite __result, SkinVisualsTransientData data, ImprovementData.Type improvement, SpriteAtlasManager atlasManager)
- {
- UIUtils_GetImprovementSprite(ref __result, improvement, data.foundingTribeSettings.tribe, data.foundingTribeSettings.skin, atlasManager);
- }
-
- /// Provides custom sprites for resources in the UI.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(UIUtils), nameof(UIUtils.GetResourceSprite))]
- private static void UIUtils_GetResourceSprite(ref Sprite __result, SkinVisualsTransientData data, ResourceData.Type resource, SpriteAtlasManager atlasManager)
- {
- Sprite? sprite = Registry.GetSprite(EnumCache.GetName(resource), Util.GetStyle(data.tileClimateSettings.tribe, data.tileClimateSettings.skin));
- if (sprite != null)
- {
- __result = sprite;
- }
- }
-
- #endregion
- #region Houses
-
- /// Provides custom sprites for houses in the city view.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(CityRenderer), nameof(CityRenderer.GetHouse))]
- private static void CityRenderer_GetHouse(ref PolytopiaSpriteRenderer __result, CityRenderer __instance, TribeType tribe, int type, SkinType skinType)
- {
- PolytopiaSpriteRenderer polytopiaSpriteRenderer = __result;
-
- if (type != __instance.HOUSE_WORKSHOP && type != __instance.HOUSE_PARK)
- {
- Sprite? sprite = Registry.GetSprite("house", Util.GetStyle(tribe, skinType), type);
- if (sprite != null)
- {
- polytopiaSpriteRenderer.Sprite = sprite;
- TerrainMaterialHelper.SetSpriteSaturated(polytopiaSpriteRenderer, __instance.IsEnemyCity);
- __result = polytopiaSpriteRenderer;
- }
- }
- }
-
- /// Provides custom sprites for houses in the UI city renderer.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(UICityRenderer), nameof(UICityRenderer.GetResource))]
- private static void UICityRenderer_GetResource(ref GameObject __result, string baseName, TribeType tribe, SkinType skin)
- {
- Image imageComponent = __result.GetComponent();
- string[] tokens = baseName.Split('_');
- if (tokens.Length > 0)
- {
- if (tokens[0] == "House")
- {
- int level = 0;
- if (tokens.Length > 1)
- {
- _ = int.TryParse(tokens[1], out level);
- }
-
- Sprite? sprite = Registry.GetSprite("house", Util.GetStyle(tribe, skin), level);
- if (sprite == null)
- {
- return;
- }
- imageComponent.sprite = sprite;
- imageComponent.SetNativeSize();
- }
- }
- }
-
- #endregion
- #region Icons
-
- /// Provides custom sprites for UI icons.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(UIIconData), nameof(UIIconData.GetImage))]
- private static void UIIconData_GetImage(ref Image __result, string id)
- {
- Sprite? sprite;
- if (GameManager.LocalPlayer != null)
- {
- sprite = Registry.GetSprite(id, Util.GetStyle(GameManager.LocalPlayer.tribe, GameManager.LocalPlayer.skinType));
- }
- else
- {
- sprite = Registry.GetSprite(id);
- }
- if (sprite != null)
- {
- __result.sprite = sprite;
- __result.useSpriteMesh = true;
- __result.SetNativeSize();
- }
- }
-
- /// Provides custom sprites for face icons in the game info row.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(GameInfoRow), nameof(GameInfoRow.LoadFaceIcon), typeof(TribeType), typeof(SkinType))]
- private static void GameInfoRow_LoadFaceIcon(GameInfoRow __instance, TribeType type, SkinType skinType)
- {
- string style = EnumCache.GetName(type);
-
- if (style == "None")
- {
- for (int i = 0; i < 20; i++)
- {
- type += byte.MaxValue + 1;
- style = EnumCache.GetName(type);
-
- if (style != "None")
- {
- break;
- }
- }
- }
-
- Sprite? sprite = Registry.GetSprite("head", Util.GetStyle(type, skinType));
-
- if (sprite != null)
- {
- __instance.SetFaceIcon(sprite);
- }
-
- if (__instance.icon.sprite == null)
- {
- __instance.LoadFaceIcon(SpriteData.SpecialFaceIcon.neutral);
- }
- }
-
- /// Provides custom sprites for player info icons.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(PlayerInfoIcon), nameof(PlayerInfoIcon.SetData), typeof(TribeType), typeof(SkinType), typeof(SpriteData.SpecialFaceIcon), typeof(Color), typeof(DiplomacyRelationState), typeof(PlayerInfoIcon.Mood))]
- private static void PlayerInfoIcon_SetData(PlayerInfoIcon __instance, TribeType tribe, SkinType skin, SpriteData.SpecialFaceIcon face, Color color, DiplomacyRelationState diplomacyState, PlayerInfoIcon.Mood mood)
- {
- if (face == SpriteData.SpecialFaceIcon.tribe)
- {
- Sprite? sprite = Registry.GetSprite("head", Util.GetStyle(tribe, skin));
- if (sprite != null)
- {
- __instance.HeadImage.sprite = sprite;
- Vector2 size = sprite.rect.size;
- __instance.HeadImage.rectTransform.sizeDelta = size * __instance.rectTransform.GetHeight() / 512f;
- }
- }
+ MaterialPropertyBlock materialPropertyBlock = new();
+ materialPropertyBlock.SetVector("_Flip", new Vector4(1f, 1f, 0f, 0f));
+ materialPropertyBlock.SetTexture("_MainTex", __instance.sprite.texture);
+ __instance.meshRenderer.SetPropertyBlock(materialPropertyBlock);
}
#endregion
@@ -781,34 +330,6 @@ private static void UpdateWidth(PopupBase __instance)
__instance.rectTransform.SetWidth(basicPopupWidths[id]);
}
- /// Sets the attacker's tribe before a unit attacks.
- [HarmonyPrefix]
- [HarmonyPatch(typeof(Unit), nameof(Unit.Attack))]
- private static bool Unit_Attack(Unit __instance, WorldCoordinates target, bool moveToTarget, Il2CppSystem.Action onComplete)
- {
- if (__instance.Owner != null)
- {
- attackerTribe = __instance.Owner.tribe;
- }
- return true;
- }
-
- /// Sets the skin of a weapon's graphics, using custom sprites if available.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(WeaponGFX), nameof(WeaponGFX.SetSkin))]
- private static void WeaponGFX_SetSkin(WeaponGFX __instance, SkinType skinType)
- {
- if (attackerTribe != TribeType.None)
- {
- Sprite? sprite = Registry.GetSprite(__instance.defaultSprite.name, Util.GetStyle(attackerTribe, skinType));
- if (sprite != null)
- {
- __instance.spriteRenderer.sprite = sprite;
- }
- attackerTribe = TribeType.None;
- }
- }
-
/// Removes a popup's custom width when it is hidden.
[HarmonyPostfix]
[HarmonyPatch(typeof(PopupBase), nameof(PopupBase.Hide))]
@@ -817,6 +338,13 @@ private static void PopupBase_Hide(PopupBase __instance)
basicPopupWidths.Remove(__instance.GetInstanceID());
}
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.OnShow))]
+ private static void StartScreen_UI2_OnShow()
+ {
+ CreateInnerOriginSprites();
+ }
+
[HarmonyPrefix]
[HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.OnWeeklyChallengeClicked))]
private static bool StartScreen_OnWeeklyChallengeClicked(StartScreen_UI2 __instance)
@@ -855,24 +383,6 @@ public static void ShowSetWidth(this BasicPopupLegacy self, int width)
self.Show();
}
- // [HarmonyPrefix]
- // [HarmonyPatch(typeof(BasicPopup), nameof(BasicPopup.SetButtonData))]
- // public static bool SetButtonData(BasicPopup __instance, PopupBase.PopupButtonData[] buttonDatas, bool updateOmniCursor)
- // {
- // if (buttonDatas == null)
- // {
- // __instance.SetDefaultOkbutton();
- // return false;
- // }
- // foreach (var buttonData in buttonDatas)
- // {
- // var MainButton = __instance.createButton(buttonData.text, buttonData.callback, UIButtonBase_UI2.ButtonStyle.Suggested);
- // MainButton.OnClickedSignal.Add(DelegateSupport.ConvertDelegate(__instance.Hide));
- // }
- // __instance.RunLayout();
- // return false;
- // }
-
#endregion
internal static BasicPopupLegacy GetBasicPopupLegacy()
@@ -889,31 +399,13 @@ internal static BasicPopupLegacy GetBasicPopupLegacy()
return basicPopupLegacy;
}
- /// Updates a visual part with a custom sprite.
- private static void UpdateVisualPart(SkinVisualsReference.VisualPart? visualPart, string name, string style)
+ public static Texture2D BuildTexture(byte[] data)
{
- if(visualPart == null || visualPart.visualPart == null)
- return;
-
- Sprite? sprite = Registry.GetSprite(name, style) ?? Registry.GetSprite(visualPart.visualPart.name, style);
-
- if (sprite != null && visualPart.renderer != null)
- {
- if (visualPart.renderer.spriteRenderer != null)
- visualPart.renderer.spriteRenderer.sprite = sprite;
- else if (visualPart.renderer.polytopiaSpriteRenderer != null)
- visualPart.renderer.polytopiaSpriteRenderer.sprite = sprite;
- }
-
- Sprite? outlineSprite = Registry.GetSprite($"{name}_outline", style) ?? Registry.GetSprite($"{visualPart.visualPart.name}_outline", style);
-
- if (outlineSprite != null && visualPart.outlineRenderer != null)
- {
- if (visualPart.outlineRenderer.spriteRenderer != null)
- visualPart.outlineRenderer.spriteRenderer.sprite = outlineSprite;
- else if (visualPart.outlineRenderer.polytopiaSpriteRenderer != null)
- visualPart.outlineRenderer.polytopiaSpriteRenderer.sprite = outlineSprite;
- }
+ Texture2D texture = new(1, 1, TextureFormat.RGBA32, true);
+ texture.LoadImage(data);
+ texture.filterMode = FilterMode.Trilinear;
+ texture.Apply();
+ return texture;
}
/// Builds a sprite from raw byte data.
@@ -923,17 +415,7 @@ private static void UpdateVisualPart(SkinVisualsReference.VisualPart? visualPart
/// The created sprite.
public static Sprite BuildSprite(byte[] data, Vector2? pivot = null, float pixelsPerUnit = 2112f)
{
- Texture2D texture = new(1, 1, TextureFormat.RGBA32, true);
- texture.LoadImage(data);
- Color[] pixels = texture.GetPixels();
- for (int i = 0; i < pixels.Length; i++)
- {
- pixels[i] = new Color(pixels[i].r, pixels[i].g, pixels[i].b, pixels[i].a);
- }
- texture.SetPixels(pixels);
- texture.filterMode = FilterMode.Trilinear;
- texture.Apply();
- return BuildSpriteWithTexture(texture, pivot, pixelsPerUnit);
+ return BuildSpriteWithTexture(BuildTexture(data), pivot, pixelsPerUnit);
}
/// Builds a sprite from a texture.
@@ -951,9 +433,194 @@ public static Sprite BuildSpriteWithTexture(Texture2D texture, Vector2? pivot =
);
}
+ internal static List> pendingInnerOriginSpriteInfos = new();
+
+ private static Vector2 GetPivotPreset(string spriteName)
+ {
+ return spriteName.Split("_")[0] switch
+ {
+ "field" => new Vector2(0.5f, 0.0f),
+ "mountain" => new Vector2(0.5f, -0.375f),
+ _ => new Vector2(0.5f, 0.5f),
+ };
+ }
+
+ internal static void RegisterSprite(SpriteAtlasManager spriteAtlasManager, string spriteName, Sprite sprite)
+ {
+ spriteAtlasManager.cachedSprites.TryAdd("Heads", new());
+ spriteAtlasManager.cachedSprites["Heads"][spriteName] = sprite;
+ Registry.sprites[spriteName] = sprite;
+ }
+
+ // Runs early: only touches your own textures, nothing from SpriteAtlasManager's lookup state.
+ public static void CreateSprites()
+ {
+ pendingInnerOriginSpriteInfos.Clear();
+
+ SpriteAtlasManager spriteAtlasManager = GameManager.GetSpriteAtlasManager();
+ const float defaultPixelsPerUnit = 2112f;
+
+ foreach (KeyValuePair spriteInfoPair in Registry.spriteInfos)
+ {
+ string spriteName = spriteInfoPair.Key;
+ SpriteInfo spriteInfo = spriteInfoPair.Value;
+
+ bool isInnerOrigin = spriteInfo.spriteOrigin != null
+ && spriteInfo.spriteOrigin.originType == OriginType.Inner;
+
+ if (isInnerOrigin)
+ {
+ pendingInnerOriginSpriteInfos.Add(spriteInfoPair);
+ continue;
+ }
+
+ KeyValuePair textureEntry;
+ if (!Registry.textures.TryGetValue(spriteName, out textureEntry)) // search texture
+ {
+ if (spriteInfo.spriteOrigin == null ||
+ spriteInfo.spriteOrigin.originType != OriginType.Outer ||
+ spriteInfo.spriteOrigin.name == null ||
+ !Registry.textures.TryGetValue(spriteInfo.spriteOrigin.name, out textureEntry)) // search texture in outer sprite origin
+ {
+ continue;
+ }
+ }
+
+
+ Vector2 pivot = spriteInfo.pivot ?? GetPivotPreset(spriteName);
+ float pixelsPerUnit = spriteInfo.pixelsPerUnit ?? defaultPixelsPerUnit;
+
+ Sprite sprite = Visual.BuildSpriteWithTexture(textureEntry.Key, pivot, pixelsPerUnit);
+ Registry.textures[spriteName] = new KeyValuePair(textureEntry.Key, true);
+ RegisterSprite(spriteAtlasManager, spriteName, sprite);
+ }
+
+ foreach (KeyValuePair> texturePair in Registry.textures)
+ {
+ if (texturePair.Value.Value)
+ {
+ continue;
+ }
+
+ string spriteName = texturePair.Key;
+ Sprite sprite = Visual.BuildSpriteWithTexture(texturePair.Value.Key, GetPivotPreset(spriteName), defaultPixelsPerUnit);
+ RegisterSprite(spriteAtlasManager, spriteName, sprite);
+ }
+ }
+
+ internal static (byte[], Visual.SpriteInfo) SpriteToPng(Sprite sprite)
+ {
+ Texture2D bakedTexture = BakeSpriteToTexture(sprite);
+
+ byte[] pngBytes = ImageConversion.EncodeToPNG(bakedTexture);
+ UnityEngine.Object.Destroy(bakedTexture);
+
+ return (pngBytes, new Visual.SpriteInfo(sprite.pixelsPerUnit, sprite.pivot));
+ }
+
+ private static Texture2D BakeSpriteToTexture(Sprite sourceSprite)
+ {
+ Texture2D atlasTexture = sourceSprite.texture;
+ int spriteWidth = (int)sourceSprite.rect.width;
+ int spriteHeight = (int)sourceSprite.rect.height;
+ float sourcePixelsPerUnit = sourceSprite.pixelsPerUnit;
+ Vector2 sourcePivotInPixels = sourceSprite.pivot;
+
+ var meshVertices = sourceSprite.vertices;
+ var meshUvs = sourceSprite.uv;
+ var meshTriangles = sourceSprite.triangles;
+
+ RenderTexture renderTexture = RenderTexture.GetTemporary(
+ spriteWidth, spriteHeight, 0,
+ RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
+ RenderTexture previousActiveRenderTexture = RenderTexture.active;
+ RenderTexture.active = renderTexture;
+ GL.Clear(true, true, Color.clear);
+
+ Material spriteMaterial = new Material(Shader.Find("Sprites/Default"));
+ spriteMaterial.mainTexture = atlasTexture;
+ spriteMaterial.SetPass(0);
+
+ GL.PushMatrix();
+ GL.LoadPixelMatrix(0, spriteWidth, 0, spriteHeight);
+ GL.Begin(GL.TRIANGLES);
+ for (int triangleCornerIndex = 0; triangleCornerIndex < meshTriangles.Length; triangleCornerIndex++)
+ {
+ int vertexIndex = meshTriangles[triangleCornerIndex];
+ Vector2 uv = meshUvs[vertexIndex];
+ Vector2 vertex = meshVertices[vertexIndex];
+
+ GL.TexCoord(new Vector3(uv.x, uv.y, 0));
+ GL.Vertex3(
+ vertex.x * sourcePixelsPerUnit + sourcePivotInPixels.x,
+ vertex.y * sourcePixelsPerUnit + sourcePivotInPixels.y,
+ 0);
+ }
+ GL.End();
+ GL.PopMatrix();
+
+ Texture2D bakedTexture = new Texture2D(spriteWidth, spriteHeight, TextureFormat.RGBA32, false);
+ bakedTexture.ReadPixels(new Rect(0, 0, spriteWidth, spriteHeight), 0, 0);
+ bakedTexture.Apply();
+ bakedTexture.wrapMode = TextureWrapMode.Clamp;
+
+ RenderTexture.active = previousActiveRenderTexture;
+ RenderTexture.ReleaseTemporary(renderTexture);
+ UnityEngine.Object.Destroy(spriteMaterial);
+
+ return bakedTexture;
+ }
+
+ public static void CreateInnerOriginSprites()
+ {
+ SpriteAtlasManager spriteAtlasManager = GameManager.GetSpriteAtlasManager();
+
+ foreach (KeyValuePair spriteInfoPair in pendingInnerOriginSpriteInfos)
+ {
+ string spriteName = spriteInfoPair.Key;
+ SpriteInfo spriteData = spriteInfoPair.Value;
+
+ Sprite originalSprite = spriteAtlasManager
+ .DoSpriteLookup(spriteData.spriteOrigin!.name, TribeType.None, SkinType.Default)
+ .sprite;
+
+ if (originalSprite == null)
+ {
+ Plugin.logger.LogWarning($"Inner origin sprite '{spriteData.spriteOrigin.name}' not found for '{spriteName}'");
+ continue;
+ }
+
+ bool hasPivotOverride = spriteData.pivot != null;
+ bool hasPixelsPerUnitOverride = spriteData.pixelsPerUnit != null;
+
+ Sprite sprite = originalSprite;
+
+ if (hasPivotOverride || hasPixelsPerUnitOverride)
+ {
+ Vector2 normalizedOriginalPivot = new(
+ originalSprite.pivot.x / originalSprite.rect.width,
+ originalSprite.pivot.y / originalSprite.rect.height
+ );
+
+ Vector2 finalPivot = hasPivotOverride ? spriteData.pivot!.Value : normalizedOriginalPivot;
+ float finalPixelsPerUnit = hasPixelsPerUnitOverride ? spriteData.pixelsPerUnit!.Value : originalSprite.pixelsPerUnit;
+
+ Texture2D bakedTexture = BakeSpriteToTexture(originalSprite);
+ sprite = BuildSpriteWithTexture(bakedTexture, finalPivot, finalPixelsPerUnit);
+ sprite.name = spriteName;
+ }
+
+ RegisterSprite(spriteAtlasManager, spriteName, sprite);
+ }
+
+ pendingInnerOriginSpriteInfos.Clear();
+ }
+
/// Initializes the Visual manager by patching the necessary methods.
internal static void Init()
{
Harmony.CreateAndPatchAll(typeof(Visual));
+ Harmony.CreateAndPatchAll(typeof(Prefabs));
+ Harmony.CreateAndPatchAll(typeof(TribePreviews));
}
}
diff --git a/src/NullableFix.cs b/src/NullableFix.cs
index adc6cdf..1c7e549 100644
--- a/src/NullableFix.cs
+++ b/src/NullableFix.cs
@@ -1,5 +1,5 @@
#nullable disable
-
+// verify whether it is needed at all bruv, i tested one time quickly and it easily compiled
namespace System.Runtime.CompilerServices
{
///
diff --git a/src/Plugin.cs b/src/Plugin.cs
index 96ae942..5cc311d 100644
--- a/src/Plugin.cs
+++ b/src/Plugin.cs
@@ -1,10 +1,10 @@
-using System.Reflection;
+using System.IO.Pipes;
+using System.Reflection;
using System.Text.Json;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using PolyMod.Managers;
-using UnityEngine;
namespace PolyMod;
@@ -14,18 +14,22 @@ namespace PolyMod;
[BepInPlugin("com.polymod", "PolyMod", VERSION)]
public partial class Plugin : BepInEx.Unity.IL2CPP.BasePlugin
{
+ internal static string? Token { get; set; }
+ internal static bool ValidLaunch { get; set; } = false;
+ internal static bool Disabled { get; set; } = false;
+
///
/// Represents the configuration for PolyMod.
///
/// Whether to enable debug mode.
- /// Whether to automatically update PolyMod.
- /// Whether to include pre-release versions when updating.
internal record PolyConfig(
bool debug = false,
- bool autoUpdate = true,
- bool updatePrerelease = false,
- bool allowUnsafeIndexes = false
- );
+ bool allowUnsafeIndexes = false,
+ string overrideDeviceId = ""
+ )
+ {
+ public List disabledMods { get; init; } = new();
+ };
///
/// The starting index for automatically assigned IDs.
@@ -38,6 +42,8 @@ internal record PolyConfig(
internal const string INCOMPATIBILITY_WARNING_LAST_VERSION_KEY
= "INCOMPATIBILITY_WARNING_LAST_VERSION";
+ internal const int LAUNCH_DATA_TIMEOUT = 2000;
+
///
/// The base path for PolyMod files.
///
@@ -63,6 +69,16 @@ internal const string INCOMPATIBILITY_WARNING_LAST_VERSION_KEY
///
public static readonly string LOCALIZATION_DUMP_PATH = Path.Combine(DUMPED_DATA_PATH, "Localization");
+ ///
+ /// The path to the directory where sprites and sprite infos are dumped.
+ ///
+ public static readonly string SPRITES_DUMP_PATH = Path.Combine(DUMPED_DATA_PATH, "Sprites");
+
+ ///
+ /// The path to the directory where prefabs datas are dumped.
+ ///
+ public static readonly string PREFABS_DUMP_PATH = Path.Combine(DUMPED_DATA_PATH, "Prefabs");
+
///
/// The path to the PolyMod configuration file.
///
@@ -122,10 +138,12 @@ public override void Load()
logger = Log;
ConfigFile.CoreConfig[new("Logging.Disk", "WriteUnityLog")].BoxedValue = true;
- AutoUpdate.Init();
+ CaptureLaunchData();
- Compatibility.Init();
+ if(Disabled)
+ return;
+ Compatibility.Init();
Audio.Init();
Loc.Init();
Visual.Init();
@@ -134,6 +152,31 @@ public override void Load()
Main.Init();
}
+ internal static void CaptureLaunchData()
+ {
+ try
+ {
+ using var client = new NamedPipeClientStream(
+ ".", "PolyMod.Launcher", PipeDirection.In, PipeOptions.CurrentUserOnly);
+ client.Connect(LAUNCH_DATA_TIMEOUT);
+
+ using var reader = new StreamReader(client);
+ var line = reader.ReadLine();
+ if (string.IsNullOrEmpty(line)) return;
+ using var doc = JsonDocument.Parse(line);
+ var root = doc.RootElement;
+
+ ValidLaunch = root.GetProperty("validLaunch").GetBoolean();
+ Disabled = root.GetProperty("disabled").GetBoolean();
+ Token = root.GetProperty("token").GetString() ?? "";
+ }
+ catch
+ {
+ ValidLaunch = false;
+ }
+ ValidLaunch = true;
+ }
+
///
/// Gets a resource stream from the assembly.
///
@@ -163,9 +206,10 @@ internal static void UpdateConsole()
{
ConsoleManager.CreateConsole();
}
- else
+ else if (OperatingSystem.IsWindows())
{
+ // BepInEx's Unix console driver throws unsupported on detach. Off-Windows there is no separate console window, so there is nothing to detach.
ConsoleManager.DetachConsole();
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Prefabs/OtherPrefabInfo.cs b/src/Prefabs/OtherPrefabInfo.cs
new file mode 100644
index 0000000..fd2eecf
--- /dev/null
+++ b/src/Prefabs/OtherPrefabInfo.cs
@@ -0,0 +1,47 @@
+
+using System.Text.Json.Serialization;
+using PolyMod.Json;
+using Polytopia.Data;
+
+namespace PolyMod;
+
+public class BasePrefabInfo
+{
+ [JsonInclude]
+ public List visualParts = new();
+}
+public class UnitPrefabInfo : BasePrefabInfo
+{
+ [JsonInclude]
+ public SkinnedKey skinnedKey;
+
+ [JsonInclude]
+ public string headPositionMarker = "";
+
+ [JsonInclude]
+ public bool uiOverride = false;
+}
+
+public class ImprovementPrefabInfo : BasePrefabInfo
+{
+ [JsonInclude]
+ [JsonConverter(typeof(EnumCacheJson))]
+ public ImprovementData.Type? improvementType = null;
+
+ [JsonInclude]
+ public bool needsOutlines = false;
+}
+
+public class ResourcePrefabInfo : BasePrefabInfo
+{
+ [JsonInclude]
+ [JsonConverter(typeof(EnumCacheJson))]
+ public ResourceData.Type? resourceType = null;
+}
+
+public class WeaponPrefabInfo
+{
+ [JsonInclude]
+ [JsonConverter(typeof(EnumCacheJson))]
+ public UnitData.WeaponEnum? weaponEnum = null;
+}
\ No newline at end of file
diff --git a/src/Prefabs/PrefabsInfo.cs b/src/Prefabs/PrefabsInfo.cs
new file mode 100644
index 0000000..d0b4ab3
--- /dev/null
+++ b/src/Prefabs/PrefabsInfo.cs
@@ -0,0 +1,16 @@
+
+using System.Text.Json.Serialization;
+
+namespace PolyMod;
+
+public class PrefabsInfo
+{
+ [JsonInclude]
+ public List units = new();
+
+ [JsonInclude]
+ public List improvements = new();
+
+ [JsonInclude]
+ public List resources = new();
+}
\ No newline at end of file
diff --git a/src/Prefabs/SkinnedKey.cs b/src/Prefabs/SkinnedKey.cs
new file mode 100644
index 0000000..080b6df
--- /dev/null
+++ b/src/Prefabs/SkinnedKey.cs
@@ -0,0 +1,46 @@
+using System.Text.Json.Serialization;
+using PolyMod.Json;
+using Polytopia.Data;
+using PolytopiaBackendBase.Common;
+
+namespace PolyMod;
+
+public readonly struct SkinnedKey : IEquatable
+{
+ [JsonInclude]
+ [JsonConverter(typeof(EnumCacheJson))]
+ public UnitData.Type unitType { get; }
+
+ [JsonInclude]
+ [JsonConverter(typeof(EnumCacheJson))]
+ public TribeType tribeType { get; }
+
+ [JsonInclude]
+ [JsonConverter(typeof(EnumCacheJson))]
+ public SkinType skinType { get; }
+
+ [JsonConstructor]
+ public SkinnedKey(UnitData.Type unitType, TribeType tribeType, SkinType skinType)
+ {
+ this.unitType = unitType;
+ this.tribeType = tribeType;
+ this.skinType = skinType;
+ }
+
+ public bool Equals(SkinnedKey other)
+ {
+ return unitType == other.unitType
+ && tribeType == other.tribeType
+ && skinType == other.skinType;
+ }
+
+ public override bool Equals(object? other)
+ {
+ return other is SkinnedKey otherKey && Equals(otherKey);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine((int)unitType, (int)tribeType, (int)skinType);
+ }
+}
\ No newline at end of file
diff --git a/src/Prefabs/VisualPartInfo.cs b/src/Prefabs/VisualPartInfo.cs
new file mode 100644
index 0000000..27105a2
--- /dev/null
+++ b/src/Prefabs/VisualPartInfo.cs
@@ -0,0 +1,81 @@
+using System.Text.Json.Serialization;
+using PolyMod.Json;
+using UnityEngine;
+
+namespace PolyMod;
+
+public class VisualPartInfo
+{
+ [JsonInclude]
+ public string? defaultSpriteName;
+
+ [JsonInclude]
+ [JsonConverter(typeof(EnumNameJson))]
+ public SkinVisualsReference.SkinningLogic skinningLogic = SkinVisualsReference.SkinningLogic.UseTribe;
+
+ [JsonInclude]
+ [JsonConverter(typeof(EnumNameJson))]
+ public SkinVisualsReference.ImprovementSkinningLogic improvementSkinningLogic = SkinVisualsReference.ImprovementSkinningLogic.FoundingTribe; // improvement specific
+
+ [JsonInclude]
+ public Vector2 coordinates = new Vector2(0, 0);
+
+ [JsonInclude]
+ public float rotation = 0f;
+
+ [JsonInclude]
+ public bool tintable = false;
+
+ [JsonInclude]
+ public bool levels = false; // improvement specific
+
+ [JsonInclude]
+ public int? sortingOrderOverride = null;
+
+ [JsonInclude]
+ public int? outlineSortingOrderOverride = null;
+
+ public SkinVisualsReference.VisualPart ToVisualPart(bool isImprovement)
+ {
+ return new SkinVisualsReference.VisualPart
+ {
+ DefaultSpriteName = defaultSpriteName,
+ tintable = tintable,
+ skinLogic = skinningLogic,
+ improvementSkinLogic = improvementSkinningLogic,
+ levels = levels,
+ isImprovement = isImprovement,
+ };
+ }
+
+ public void CopyFromVisualPart(SkinVisualsReference.VisualPart visualPart)
+ {
+ int? sortingOrderOverride = null;
+ SpriteRenderer spriteRenderer = visualPart.visualPart.GetComponent();
+ if (spriteRenderer != null)
+ {
+ sortingOrderOverride = spriteRenderer.sortingOrder;
+ }
+
+ int? outlineSortingOrderOverride = null;
+
+ if (visualPart.outline != null)
+ {
+ spriteRenderer = visualPart.outline.GetComponent();
+ if (spriteRenderer != null)
+ {
+ outlineSortingOrderOverride = spriteRenderer.sortingOrder;
+ }
+ }
+
+ coordinates = visualPart.visualPart.transform.position;
+ rotation = visualPart.visualPart.transform.rotation.z;
+ defaultSpriteName = visualPart.DefaultSpriteName;
+ tintable = visualPart.tintable;
+ skinningLogic = visualPart.skinLogic;
+ improvementSkinningLogic = visualPart.improvementSkinLogic;
+ levels = visualPart.levels;
+ this.sortingOrderOverride = sortingOrderOverride;
+ this.outlineSortingOrderOverride = outlineSortingOrderOverride;
+ }
+}
\ No newline at end of file
diff --git a/src/Registry.cs b/src/Registry.cs
index 3a3d36a..3943b5d 100644
--- a/src/Registry.cs
+++ b/src/Registry.cs
@@ -21,6 +21,8 @@ public static class Registry
///
public static Dictionary sprites = new();
+ public static Dictionary> textures = new(); // string = texture name, bool, whether texture was processed or not. reviwers, pls tell me how to improve
+
///
/// A dictionary of all loaded audio clips, keyed by their name.
///
@@ -34,33 +36,13 @@ public static class Registry
///
/// A dictionary of tribe previews, keyed by tribe name.
///
- public static Dictionary tribePreviews = new();
+ public static Dictionary tribePreviews = new();
///
/// A dictionary of sprite information, keyed by sprite name.
///
public static Dictionary spriteInfos = new();
- ///
- /// A dictionary of prefab names, keyed by their enum value.
- ///
- public static Dictionary prefabNames = new();
-
- ///
- /// A dictionary of custom unit prefabs, keyed by their prefab info.
- ///
- public static Dictionary unitPrefabs = new();
-
- ///
- /// A dictionary of custom resource prefabs, keyed by their prefab info.
- ///
- public static Dictionary resourcePrefabs = new();
-
- ///
- /// A dictionary of custom improvement prefabs, keyed by their prefab info.
- ///
- public static Dictionary improvementsPrefabs = new();
-
///
/// A dictionary of loaded asset bundles, keyed by their name.
///
@@ -86,24 +68,47 @@ public static class Registry
///
public static int gameModesAutoidx = Enum.GetValues(typeof(GameMode)).Length;
+ public static Dictionary filesQueueToBeProcessedAfterGLDMerge = new(); // I dont know whether I should do sum abt it
+
+ public static Sprite? GetSprite(string baseName, TribeType tribe = TribeType.None, SkinType skin = SkinType.Default, int level = -1)
+ {
+ string tribeText = (tribe == TribeType.None) ? "" : EnumCache.GetName(tribe);
+ string skinText = (skin == SkinType.Default) ? "" : EnumCache.GetName(skin);
+ return GetSpriteByText(baseName, tribeText, skinText, level);
+ }
///
/// Gets a sprite from the registry, trying to find the best match.
///
- /// The name of the sprite.
- /// The style of the sprite (e.g., tribe or skin).
- /// The level of the sprite (e.g., for cities).
- /// The requested sprite, or null if not found. The lookup priority is: name_style_level, name__level, name_style_, name__.
- public static Sprite? GetSprite(string name, string style = "", int level = 0)
+ public static Sprite? GetSpriteByText(string baseName, string tribeText = "", string skinText = "", int level = -1)
{
Sprite? sprite = null;
- name = name.ToLower();
- style = style.ToLower();
- // This looks backwards, but GetOrDefault returns the provided default value if the key isn't found.
- // This means `sprite` is updated sequentially, and the last, most-specific match is the one that is kept.
- sprite = sprites.GetOrDefault($"{name}__", sprite);
- sprite = sprites.GetOrDefault($"{name}_{style}_", sprite);
- sprite = sprites.GetOrDefault($"{name}__{level}", sprite);
- sprite = sprites.GetOrDefault($"{name}_{style}_{level}", sprite);
+ baseName = baseName.ToLower();
+ tribeText = tribeText.ToLower();
+ skinText = skinText.ToLower();
+
+ List spriteNameVariants = new();
+
+ if (level >= 0)
+ {
+ string levelText = level.ToString();
+ spriteNameVariants.Add(baseName + "_" + levelText);
+ spriteNameVariants.Add(baseName + "_" + levelText + "_" + tribeText);
+ spriteNameVariants.Add(baseName + "_" + levelText + "_" + skinText);
+ spriteNameVariants.Add(baseName + "_" + levelText + "_" + tribeText + "_" + skinText);
+ }
+ else
+ {
+ spriteNameVariants.Add(baseName);
+ spriteNameVariants.Add(baseName + "_" + tribeText);
+ spriteNameVariants.Add(baseName + "_" + skinText);
+ spriteNameVariants.Add(baseName + "_" + tribeText + "_" + skinText);
+ }
+
+ foreach(string spriteName in spriteNameVariants)
+ {
+ sprite = sprites.GetOrDefault(spriteName, sprite);
+ }
+
return sprite;
}
@@ -113,12 +118,15 @@ public static class Registry
/// The name of the audio clip.
/// The style of the audio clip.
/// The requested audio clip, or null if not found.
- public static AudioClip? GetAudioClip(string name, string style)
+ public static AudioClip? GetAudioClip(string name, TribeType tribe = TribeType.None, SkinType skin = SkinType.Default)
{
AudioSource? audioSource = null;
name = name.ToLower();
- style = style.ToLower();
- audioSource = audioClips.GetOrDefault($"{name}_{style}", audioSource);
+ string tribeText = (tribe == TribeType.None) ? "" : EnumCache.GetName(tribe);
+ string skinText = (skin == SkinType.Default) ? "" : EnumCache.GetName(skin);
+ tribeText = tribeText.ToLower();
+ skinText = skinText.ToLower();
+ audioSource = audioClips.GetOrDefault($"{name}_{tribeText}_{skinText}", audioSource);
if (audioSource == null) return null;
return audioSource.clip;
}
diff --git a/src/Util.cs b/src/Util.cs
index 3396965..d1fd591 100644
--- a/src/Util.cs
+++ b/src/Util.cs
@@ -3,8 +3,6 @@
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.Injection;
using Newtonsoft.Json.Linq;
-using Polytopia.Data;
-using PolytopiaBackendBase.Common;
namespace PolyMod;
@@ -100,74 +98,4 @@ internal static bool IsVersionOlderOrEqual(this string version1, string version2
return string.Compare(pre1, pre2, StringComparison.Ordinal) <= 0;
}
-
- ///
- /// Gets the style name for a tribe and skin combination.
- ///
- /// The tribe.
- /// The skin.
- /// The style name.
- internal static string GetStyle(TribeType tribe, SkinType skin)
- {
- return skin != SkinType.Default ? EnumCache.GetName(skin) : EnumCache.GetName(tribe);
- }
-
- ///
- /// Formats a sprite name to match the enum naming convention.
- /// This is a workaround for inconsistencies in the game's sprite naming.
- ///
- /// The original sprite name.
- /// The formatted sprite name.
- internal static string FormatSpriteName(string baseName) // I cant believe i had to do this shit #MIDJIWANFIXYOURSHITCODE
- {
- // This method is necessary because the sprite names in the game's data do not always
- // match the names in the corresponding enums. This method replaces the hardcoded sprite
- // names with the enum names to ensure consistency.
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_AQUA_FARM, EnumCache.GetName(ImprovementData.Type.Aquafarm));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_ATOLL, EnumCache.GetName(ImprovementData.Type.Atoll));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_BURN_FOREST, EnumCache.GetName(ImprovementData.Type.BurnForest));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_CLEAR_FOREST, EnumCache.GetName(ImprovementData.Type.ClearForest));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_CUSTOMS_HOUSE, EnumCache.GetName(ImprovementData.Type.CustomsHouse));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_FARM, EnumCache.GetName(ImprovementData.Type.Farm));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_FOREST_TEMPLE, EnumCache.GetName(ImprovementData.Type.ForestTemple));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_FORGE, EnumCache.GetName(ImprovementData.Type.Forge));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_GROW_FOREST, EnumCache.GetName(ImprovementData.Type.GrowForest));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_ICE_BANK, EnumCache.GetName(ImprovementData.Type.IceBank));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_ICE_PORT, EnumCache.GetName(ImprovementData.Type.Outpost));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_ICE_TEMPLE, EnumCache.GetName(ImprovementData.Type.IceTemple));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_LUMBER_HUT, EnumCache.GetName(ImprovementData.Type.LumberHut));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_MARKET, EnumCache.GetName(ImprovementData.Type.Market));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_MINE, EnumCache.GetName(ImprovementData.Type.Mine));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_MOUNTAIN_TEMPLE, EnumCache.GetName(ImprovementData.Type.MountainTemple));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_PORT, EnumCache.GetName(ImprovementData.Type.Port));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_ROAD, EnumCache.GetName(ImprovementData.Type.Road));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_RUIN, EnumCache.GetName(ImprovementData.Type.Ruin));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_SANCTUARY, EnumCache.GetName(ImprovementData.Type.Sanctuary));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_SAWMILL, EnumCache.GetName(ImprovementData.Type.Sawmill));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_TEMPLE, EnumCache.GetName(ImprovementData.Type.Temple));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_WATER_TEMPLE, EnumCache.GetName(ImprovementData.Type.WaterTemple));
- baseName = baseName.Replace(SpriteData.IMPROVEMENT_WINDMILL, EnumCache.GetName(ImprovementData.Type.Windmill));
-
- baseName = baseName.Replace(SpriteData.RESOURCE_AQUACROP, EnumCache.GetName(ResourceData.Type.AquaCrop));
- baseName = baseName.Replace(SpriteData.RESOURCE_CROP, EnumCache.GetName(ResourceData.Type.Crop));
- baseName = baseName.Replace(SpriteData.RESOURCE_FISH, EnumCache.GetName(ResourceData.Type.Fish));
- baseName = baseName.Replace(SpriteData.RESOURCE_FRUIT, EnumCache.GetName(ResourceData.Type.Fruit));
- baseName = baseName.Replace(SpriteData.RESOURCE_GAME, EnumCache.GetName(ResourceData.Type.Game));
- baseName = baseName.Replace(SpriteData.RESOURCE_METAL, EnumCache.GetName(ResourceData.Type.Metal));
- baseName = baseName.Replace(SpriteData.RESOURCE_SPORES, EnumCache.GetName(ResourceData.Type.Spores));
- baseName = baseName.Replace(SpriteData.RESOURCE_STARFISH, EnumCache.GetName(ResourceData.Type.Starfish));
- baseName = baseName.Replace(SpriteData.RESOURCE_WHALE, EnumCache.GetName(ResourceData.Type.Whale));
-
- baseName = baseName.Replace(SpriteData.TILE_FIELD, EnumCache.GetName(TerrainData.Type.Field));
- baseName = baseName.Replace(SpriteData.TILE_FOREST, EnumCache.GetName(TerrainData.Type.Forest));
- baseName = baseName.Replace(SpriteData.TILE_ICE, EnumCache.GetName(TerrainData.Type.Ice));
- baseName = baseName.Replace(SpriteData.TILE_MOUNTAIN, EnumCache.GetName(TerrainData.Type.Mountain));
- baseName = baseName.Replace(SpriteData.TILE_OCEAN, EnumCache.GetName(TerrainData.Type.Ocean));
- baseName = baseName.Replace(SpriteData.TILE_UNKNOWN, EnumCache.GetName(TerrainData.Type.Field));
- baseName = baseName.Replace(SpriteData.TILE_WATER, EnumCache.GetName(TerrainData.Type.Water));
- baseName = baseName.Replace(SpriteData.TILE_WETLAND, EnumCache.GetName(TerrainData.Type.Field) + "_flooded");
-
- baseName = baseName.Replace("UI_", "");
- return baseName;
- }
}