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; - } }