From 5c47bdb571090956a0ce7a2c3f14dc25e175cf11 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Mon, 12 Oct 2026 22:38:03 +0200 Subject: [PATCH 1/9] feat: rewrote dumping. added dump of ALL sprites and their sprite infos. added the launch pipe from 1.3 and other minor stuff. removed installer --- .github/workflows/installer.yml | 43 ------- PolyMod.csproj | 4 +- README.md | 4 +- installer/build.py | 19 ---- installer/icon.ico | Bin 116829 -> 0 bytes installer/main.py | 154 ------------------------- installer/requirements.txt | 4 - resources/localization.json | 34 +++--- src/Managers/AutoUpdate.cs | 182 ----------------------------- src/Managers/Compatibility.cs | 37 ++++++ src/Managers/Hub.cs | 196 +++++++++++++++++++++----------- src/Managers/Main.cs | 34 +++++- src/NullableFix.cs | 2 +- src/Plugin.cs | 68 +++++++++-- 14 files changed, 282 insertions(+), 499 deletions(-) delete mode 100644 .github/workflows/installer.yml delete mode 100644 installer/build.py delete mode 100644 installer/icon.ico delete mode 100644 installer/main.py delete mode 100644 installer/requirements.txt delete mode 100644 src/Managers/AutoUpdate.cs 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 f52643a3ebe810f2156f072e7a17e4d7c7bd2821..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116829 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�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 nc1Sy&CHsV}2K()>_6l00000NkvXXu0mjfcyCBQ 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/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..28d3601 100644 --- a/src/Managers/Hub.cs +++ b/src/Managers/Hub.cs @@ -12,6 +12,8 @@ using System.Text.Encodings.Web; using Il2CppSystem.Linq; using System.Text.RegularExpressions; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using UnityEngine.Experimental.U2D; namespace PolyMod.Managers; @@ -168,13 +170,13 @@ internal static void UpdateSpriteInfos() { Loader.UpdateSprite(kvp.Key); } - message += Localization.Get("polymod.spriteinfo.updated", new Il2CppSystem.Object[] { subnames[1] }); + message += Localization.Get("polymod.hotreload.success", new Il2CppSystem.Object[] { subnames[1] }); } } } if (message == string.Empty) { - message = Localization.Get("polymod.spriteinfo.notupdated"); + message = Localization.Get("polymod.hotreload.failure"); } NotificationManager.Notify(message); } @@ -185,16 +187,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,26 +245,102 @@ 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) = 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() }}) + ); + } + } + + private static (byte[], Visual.SpriteInfo) SpriteToPng(Sprite sprite) + { + Texture2D atlasTexture = sprite.texture; + int spriteWidth = (int)sprite.rect.width; + int spriteHeight = (int)sprite.rect.height; + float pixelsPerUnit = sprite.pixelsPerUnit; + Vector2 spritePivot = sprite.pivot; + + var meshVertices = sprite.vertices; + var meshUvs = sprite.uv; + var meshTriangles = sprite.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); + + var 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 * pixelsPerUnit + spritePivot.x, + vertex.y * pixelsPerUnit + spritePivot.y, 0); } + GL.End(); + GL.PopMatrix(); + + var croppedTexture = new Texture2D(spriteWidth, spriteHeight, TextureFormat.RGBA32, false); + croppedTexture.ReadPixels(new Rect(0, 0, spriteWidth, spriteHeight), 0, 0); + croppedTexture.Apply(); + + RenderTexture.active = previousActiveRenderTexture; + RenderTexture.ReleaseTemporary(renderTexture); + + byte[] pngBytes = ImageConversion.EncodeToPNG(croppedTexture); + UnityEngine.Object.Destroy(croppedTexture); + UnityEngine.Object.Destroy(spriteMaterial); + UnityEngine.Object.Destroy(sprite); + return (pngBytes, new Visual.SpriteInfo(pixelsPerUnit, spritePivot)); + } + + 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(); @@ -270,19 +370,21 @@ internal static void DumpData() JsonSerializer.Serialize(previewTiles, new JsonSerializerOptions { WriteIndented = true }) ); } - NotificationManager.Notify(Localization.Get("polymod.hub.dumped")); } + internal static void DumpPrefabs() // TODO + {} + /// /// Shows the configuration popup. /// 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,7 +437,7 @@ void OpenDiscord() closesPopup: false )); popupButtons.Add(new( - "polymod.hub.spriteinfo.update", + "polymod.hub.hotreload", callback: DelegateSupport.ConvertDelegate(UpdateSpriteInfos), closesPopup: false )); @@ -357,7 +459,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 +468,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,32 +485,6 @@ 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(); @@ -440,4 +504,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..bbc0b71 100644 --- a/src/Managers/Main.cs +++ b/src/Managers/Main.cs @@ -341,10 +341,42 @@ private static void StartTurnAction_Execute(StartTurnAction __instance, GameStat private static bool Unit_CreateUnit(Unit __instance, UnitData unitData, TribeType tribe, SkinType unitSkin) { Unit unit = PrefabManager.GetPrefab(unitData.type, tribe, unitSkin); - if (unit == null) Console.Write("THIS FUCKING SHIT IS NULL WHAT THE FUCK"); return true; } + + /// + /// 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(GamePlayerSummary), nameof(GamePlayerSummary.Serialize))] + private static bool GamePlayerSummary_Serialize(GamePlayerSummary __instance) + { + 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. /// 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 From 4211f85ceb1d9b1b9e3623a2acec17ae919113d9 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Tue, 13 Oct 2026 22:33:21 +0200 Subject: [PATCH 2/9] removed a lot of stuff which is not needed anymore. removed WHOLE prefab system for now --- src/Loader.cs | 231 +------------------ src/Managers/Audio.cs | 7 +- src/Managers/Main.cs | 49 ++-- src/Managers/PrefabFix.cs | 141 ++++++++++++ src/Managers/Visual.cs | 461 +++++++------------------------------- src/Registry.cs | 76 ++++--- src/Util.cs | 72 ------ 7 files changed, 309 insertions(+), 728 deletions(-) create mode 100644 src/Managers/PrefabFix.cs diff --git a/src/Loader.cs b/src/Loader.cs index 69fe92b..bfbbb1a 100644 --- a/src/Loader.cs +++ b/src/Loader.cs @@ -135,19 +135,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); @@ -169,19 +158,7 @@ out TribeAbility.Type tribeAbilityType [typeof(ImprovementData.Type)] = new List>() { new((token, duringEnumCacheCreation) => { - if (duringEnumCacheCreation) - { - ImprovementData.Type improvementPrefabType = ImprovementData.Type.CustomsHouse; - if (token["prefab"] != null) - { - string prefabId = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(token["prefab"]!.ToString()); - if (Enum.TryParse(prefabId, out ImprovementData.Type parsedType)) - improvementPrefabType = parsedType; - } - if(token["idx"] != null) - PrefabManager.improvements.TryAdd((ImprovementData.Type)(int)token["idx"], PrefabManager.improvements[improvementPrefabType]); - } - else + if (!duringEnumCacheCreation) { string improvementId = Util.GetJTokenName(token); if (token["attractsResource"] != null) @@ -204,20 +181,7 @@ out TribeAbility.Type tribeAbilityType [typeof(ResourceData.Type)] = new List>() { new((token, duringEnumCacheCreation) => - { - if (duringEnumCacheCreation) - { - 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]); - } - }) + { }) }, [typeof(SkinData)] = new List>() { @@ -715,161 +679,17 @@ 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) - { - try - { - 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) - return; - - var spriteContainer = unitInstance.transform.GetChild(0); - var material = ClearExistingPartsAndExtractMaterial(spriteContainer); + public static void LoadPrefabsFile(Mod mod, Mod.File file) + {} - var visualParts = ApplyVisualParts(prefab, spriteContainer, material); + public static void LoadUnitPrefabFile(Mod mod, Mod.File file) + {} - 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(); + public static void LoadImprovementPrefabFile(Mod mod, Mod.File file) + {} - GameObject.DontDestroyOnLoad(unitInstance.gameObject); - Registry.unitPrefabs.Add(prefab, unitInstance.GetComponent()); - - Plugin.logger.LogInfo($"Registered prefab {prefab.name} info 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}."); - } - } - - /// - /// 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) - { - Material? material = null; - for (int i = 0; i < spriteContainer.childCount; i++) - { - var child = spriteContainer.GetChild(i); - if (child.gameObject.name == "Head") - { - var renderer = child.GetComponent(); - if (renderer != null) - material = renderer.material; - } - GameObject.Destroy(child.gameObject); - } - 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) - { - if (prefab.visualParts.Count == 0) - return new List(); - - List parts = new(); - - foreach (var info in prefab.visualParts) - { - parts.Add(CreateVisualPart(info, spriteContainer, material)); - } - - 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) - { - 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 - { - 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; - - visualPart.outlineRenderer = new SkinVisualsReference.RendererUnion { spriteRenderer = outlineRenderer }; - - return visualPart; - } + public static void LoadResourcePrefabFile(Mod mod, Mod.File file) + {} /// /// Loads and applies a game logic data patch from a mod. @@ -1030,35 +850,6 @@ internal static void CreateMappings(JObject rootObject) /// internal static void ProcessPrefabs() { - Plugin.logger.LogInfo($"Processing prefabs, count: {Registry.prefabNames.Count}."); - foreach (KeyValuePair item in Registry.prefabNames) - { - 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 - { - KeyValuePair prefabInfo = Registry.unitPrefabs.FirstOrDefault( - kv => kv.Key.name == prefabId - ); - if (!EqualityComparer.Default.Equals(prefabInfo.Key, default)) - { - 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}."); - 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)] - ); - } } /// 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/Main.cs b/src/Managers/Main.cs index bbc0b71..0e36936 100644 --- a/src/Managers/Main.cs +++ b/src/Managers/Main.cs @@ -333,18 +333,6 @@ private static void StartTurnAction_Execute(StartTurnAction __instance, GameStat } } - /// - /// Patches the unit creation method to handle custom unit prefabs. - /// - [HarmonyPrefix] - [HarmonyPatch(typeof(Unit), nameof(Unit.CreateUnit))] - private static bool Unit_CreateUnit(Unit __instance, UnitData unitData, TribeType tribe, SkinType unitSkin) - { - Unit unit = PrefabManager.GetPrefab(unitData.type, tribe, unitSkin); - return true; - } - - /// /// Serialization fixes for custom (modded) content. /// GamePlayerSummary stores the tribe as a single byte, which custom tribe ids (>= 1000) overflow into garbage. @@ -444,10 +432,41 @@ 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.LoadPrefabsFile( + mod, + file + ); + continue; + } + + Match unitPrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^unitPrefab(?:_(.*))?\.json$"); + if (unitPrefabMatch.Success) + { + Loader.LoadUnitPrefabFile( + mod, + file + ); + continue; + } + + Match improvementPrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^improvementPrefab(?:_(.*))?\.json$"); + if (improvementPrefabMatch.Success) + { + Loader.LoadImprovementPrefabFile( + mod, + file + ); + continue; + } + + Match resourcePrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^resourcePrefab(?:_(.*))?\.json$"); + if (resourcePrefabMatch.Success) { - Loader.LoadPrefabInfoFile( + Loader.LoadResourcePrefabFile( mod, file ); diff --git a/src/Managers/PrefabFix.cs b/src/Managers/PrefabFix.cs new file mode 100644 index 0000000..23dc092 --- /dev/null +++ b/src/Managers/PrefabFix.cs @@ -0,0 +1,141 @@ +using HarmonyLib; +using PolyMod; +using Polytopia.Data; +using PolytopiaBackendBase.Common; +using static PrefabManager; + +public static class PrefabFix +{ + public readonly struct SkinnedKey : IEquatable + { + private readonly UnitData.Type unitType; + private readonly TribeType tribeType; + private readonly SkinType skinType; + + 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); + } + } + + private static readonly Dictionary unitsNew = new(); + private static readonly Dictionary> unitOverridesNew = new(); + private static readonly Dictionary unitUIOverridesNew = 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) + { + 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; + } + + 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!; + } +} \ No newline at end of file diff --git a/src/Managers/Visual.cs b/src/Managers/Visual.cs index bccc5fc..f048244 100644 --- a/src/Managers/Visual.cs +++ b/src/Managers/Visual.cs @@ -55,73 +55,21 @@ 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 /// A placeholder patch for the TechItem.SetupComplete method. @@ -131,9 +79,9 @@ 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))] + /// Patches the sprite atlas manager to load custom sprites. TODOOOO + // [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; @@ -160,7 +108,7 @@ void GetAtlas(SpriteAtlas spriteAtlas) } } string name = string.Join("_", filteredNames); - Sprite? newSprite = Registry.GetSprite(name, style); + Sprite? newSprite = null; // Registry.GetSprite(name, style); if (newSprite != null) { completion?.Invoke(atlas, sprite, newSprite); @@ -171,13 +119,70 @@ void GetAtlas(SpriteAtlas spriteAtlas) } /// 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(); + + if(cachedReversedNames.ContainsKey(baseName)) + return cachedReversedNames[baseName]; + + string formattedName = baseName; + foreach (var valuePair in cachedSpriteDataReverse!) + { + formattedName = formattedName.Replace(valuePair.Key, valuePair.Value); + } + cachedReversedNames[baseName] = formattedName; + return baseName; + } + [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); + Sprite? sprite = Registry.GetSprite(baseName, tribe, skin, level); + if (sprite != null) + __result.sprite = sprite; - Sprite? sprite = Registry.GetSprite(baseName, Util.GetStyle(tribe, skin), level); + string formattedName = FormatSpriteName(baseName); + sprite = Registry.GetSprite(formattedName, tribe, skin, level); if (sprite != null) __result.sprite = sprite; } @@ -190,7 +195,7 @@ private static void SpriteAtlasManager_DoSpriteLookup(ref SpriteAtlasManager.Spr [HarmonyPatch(typeof(InteractionBar), nameof(InteractionBar.Show))] private static bool InteractionBar_Show(InteractionBar __instance, bool instant, bool force) { - enableOutlines = true; + enableOutlines = true; // bro return true; } @@ -199,7 +204,7 @@ private static bool InteractionBar_Show(InteractionBar __instance, bool instant, [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) { - return !(spriteRenderer.sortingOrder == -1 && !enableOutlines); + return !(spriteRenderer.sortingOrder == -1 && !enableOutlines); // uhhh tf is this hack } /// Disables outlines after the interaction bar is shown. @@ -210,111 +215,12 @@ private static void InteractionBar_Show_Postfix(InteractionBar __instance, bool enableOutlines = false; } - /// 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) - { - currentUnitTypeUI = __instance.unitType; - return true; - } - - /// 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) - { - currentUnitTypeUI = UnitData.Type.None; - } - - /// 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); - - foreach (var visualPart in skinVisuals.visualParts) - { - if(visualPart.visualPart != null) - UpdateVisualPart(visualPart, $"{visualPart.visualPart.name}_{unitTypeName}", style); - } - } - - [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 - )) - { - __result = customPrefabs[unitPrefabInfo]; - } - } - #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) - { - 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) - { - UpdateVisualPart(visualPart, name, style); - } - } - } - - /// 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; - } - } - /// Updates the terrain graphics with custom sprites. [HarmonyPostfix] - [HarmonyPatch(typeof(TerrainRenderer), nameof(TerrainRenderer.UpdateGraphics))] + [HarmonyPatch(typeof(TerrainRenderer), nameof(TerrainRenderer.UpdateGraphics))] // recheck that private static void TerrainRenderer_UpdateGraphics(TerrainRenderer __instance, Tile tile) { string terrain = EnumCache.GetName(tile.data.terrain) ?? string.Empty; @@ -351,7 +257,7 @@ private static void TerrainRenderer_UpdateGraphics(TerrainRenderer __instance, T PolytopiaSpriteRenderer? renderer = (PolytopiaSpriteRenderer?)rendererProperty.GetValue(tile); if (renderer != null) { - Sprite? additionalSprite = Registry.GetSprite(propertyName + flood, Util.GetStyle(tribe, skinType)); + Sprite? additionalSprite = Registry.GetSprite(propertyName + flood, tribe, skinType); if (additionalSprite != null) { renderer.Sprite = additionalSprite; @@ -361,7 +267,7 @@ private static void TerrainRenderer_UpdateGraphics(TerrainRenderer __instance, T } } - Sprite? sprite = Registry.GetSprite(terrain + flood, Util.GetStyle(tribe, skinType)); + Sprite? sprite = Registry.GetSprite(terrain + flood, tribe, skinType); if (sprite != null) { __instance.spriteRenderer.Sprite = sprite; @@ -410,19 +316,6 @@ private static void PolytopiaSpriteRenderer_ForceUpdateMesh(PolytopiaSpriteRende } } - [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 @@ -604,13 +497,16 @@ public static void Tile_RenderDebug(Tile __instance) #endregion #region UI + // check if still needed /// 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)); + Sprite? sprite = Registry.GetSprite(EnumCache.GetName(improvement), tribe, skin); + Console.Write(EnumCache.GetName(improvement)); + Console.Write(sprite == null); if (sprite != null) { __result = sprite; @@ -630,139 +526,14 @@ private static void UIUtils_GetImprovementSprite_2(ref Sprite __result, SkinVisu [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)); + TribeAndSkin tileClimateSettings = data.tileClimateSettings; + Sprite? sprite = Registry.GetSprite(EnumCache.GetName(resource), tileClimateSettings.tribe, 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; - } - } - } - #endregion #region Popups @@ -781,34 +552,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))] @@ -855,24 +598,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,33 +614,6 @@ 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) - { - 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; - } - } - /// Builds a sprite from raw byte data. /// The raw byte data of the image. /// The pivot point of the sprite. @@ -955,5 +653,6 @@ public static Sprite BuildSpriteWithTexture(Texture2D texture, Vector2? pivot = internal static void Init() { Harmony.CreateAndPatchAll(typeof(Visual)); + Harmony.CreateAndPatchAll(typeof(PrefabFix)); } } diff --git a/src/Registry.cs b/src/Registry.cs index 3a3d36a..5bfd5f1 100644 --- a/src/Registry.cs +++ b/src/Registry.cs @@ -41,26 +41,6 @@ public static class Registry /// 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 +66,45 @@ public static class Registry /// public static int gameModesAutoidx = Enum.GetValues(typeof(GameMode)).Length; + 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 +114,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; - } } From 5b12ab229b8c382b2e2eeb8409237377e33d688a Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Wed, 14 Oct 2026 23:02:48 +0200 Subject: [PATCH 3/9] Rewrote whole prefabs system! (almost) --- src/Json/EnumNameJson.cs | 29 ++ src/Loader.cs | 110 ++++++- src/Managers/Hub.cs | 4 +- src/Managers/Visual.cs | 211 +------------ src/Managers/Visual/PrefabCreator.cs | 289 ++++++++++++++++++ .../PrefabRegistry.cs} | 44 +-- src/Managers/Visual/SkinnedKey.cs | 46 +++ src/Managers/Visual/TribePreviewManager.cs | 223 ++++++++++++++ src/Registry.cs | 2 +- 9 files changed, 705 insertions(+), 253 deletions(-) create mode 100644 src/Json/EnumNameJson.cs create mode 100644 src/Managers/Visual/PrefabCreator.cs rename src/Managers/{PrefabFix.cs => Visual/PrefabRegistry.cs} (78%) create mode 100644 src/Managers/Visual/SkinnedKey.cs create mode 100644 src/Managers/Visual/TribePreviewManager.cs 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 bfbbb1a..9e91445 100644 --- a/src/Loader.cs +++ b/src/Loader.cs @@ -126,7 +126,7 @@ out TribeAbility.Type tribeAbilityType } if (token["preview"] != null) { - Visual.PreviewTile[] preview = JsonSerializer.Deserialize(token["preview"].ToString())!; + TribePreviewManager.PreviewTile[] preview = JsonSerializer.Deserialize(token["preview"].ToString())!; Registry.tribePreviews[Util.GetJTokenName(token)] = preview; } } @@ -680,16 +680,114 @@ public static void LoadAudioFile(Mod mod, Mod.File file) } public static void LoadPrefabsFile(Mod mod, Mod.File file) - {} + { + try + { + if(!PrefabCreator.prefabsFilesToProcess.ContainsKey("all") || PrefabCreator.prefabsFilesToProcess["all"] == null) + PrefabCreator.prefabsFilesToProcess["all"] = new(); + + PrefabCreator.prefabsFilesToProcess["all"].Add(file.bytes); + + // var prefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + // { + // Converters = { new Vector2Json() }, + // PropertyNameCaseInsensitive = true, + // }); + // if (prefab == null) + // return; + + // PrefabCreator.unitInfos.AddRange(prefab.units); + // PrefabCreator.improvementInfos.AddRange(prefab.improvements); + // PrefabCreator.resourceInfos.AddRange(prefab.resources); + + Plugin.logger.LogInfo($"Registered prefabs {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod."); + } + catch (Exception e) + { + Plugin.logger.LogError($"Error on loading prefabs {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod: {e.StackTrace}."); + } + } public static void LoadUnitPrefabFile(Mod mod, Mod.File file) - {} + { + try + { + if(!PrefabCreator.prefabsFilesToProcess.ContainsKey("units") || PrefabCreator.prefabsFilesToProcess["units"] == null) + PrefabCreator.prefabsFilesToProcess["units"] = new(); + + PrefabCreator.prefabsFilesToProcess["units"].Add(file.bytes); + + // var unitPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + // { + // Converters = { new Vector2Json() }, + // PropertyNameCaseInsensitive = true, + // }); + // if (unitPrefab == null) + // return; + + // PrefabCreator.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}."); + } + } public static void LoadImprovementPrefabFile(Mod mod, Mod.File file) - {} + { + try + { + if(!PrefabCreator.prefabsFilesToProcess.ContainsKey("improvements") || PrefabCreator.prefabsFilesToProcess["improvements"] == null) + PrefabCreator.prefabsFilesToProcess["improvements"] = new(); + + PrefabCreator.prefabsFilesToProcess["improvements"].Add(file.bytes); + + // var improvementPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + // { + // Converters = { new Vector2Json() }, + // PropertyNameCaseInsensitive = true, + // }); + // if (improvementPrefab == null) + // return; + + // PrefabCreator.improvementInfos.Add(improvementPrefab); + + Plugin.logger.LogInfo($"Registered improvement prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod."); + } + catch (Exception e) + { + Plugin.logger.LogError($"Error on loading improvement prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod: {e.StackTrace}."); + } + } public static void LoadResourcePrefabFile(Mod mod, Mod.File file) - {} + { + try + { + if(!PrefabCreator.prefabsFilesToProcess.ContainsKey("resources") || PrefabCreator.prefabsFilesToProcess["resources"] == null) + PrefabCreator.prefabsFilesToProcess["resources"] = new(); + + PrefabCreator.prefabsFilesToProcess["resources"].Add(file.bytes); + + // var resourcePrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + // { + // Converters = { new Vector2Json() }, + // PropertyNameCaseInsensitive = true, + // }); + // if (resourcePrefab == null) + // return; + + // PrefabCreator.resourceInfos.Add(resourcePrefab); + + 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}."); + } + } /// /// Loads and applies a game logic data patch from a mod. @@ -736,6 +834,7 @@ internal static void ProcessGameLogicData(GameLogicData gameLogicData, JObject r { CreateMappings(rootObject); ProcessCustomTribes(); + // OKAY BASICALLY I need to find a way to load certain files at this stage (?) ProcessPrefabs(); ProcessEmbarkOverrides(); ProcessAttractOverrides(); @@ -850,6 +949,7 @@ internal static void CreateMappings(JObject rootObject) /// internal static void ProcessPrefabs() { + PrefabCreator.ProcessPrefabs(); } /// diff --git a/src/Managers/Hub.cs b/src/Managers/Hub.cs index 28d3601..553652c 100644 --- a/src/Managers/Hub.cs +++ b/src/Managers/Hub.cs @@ -343,7 +343,7 @@ 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++) { @@ -352,7 +352,7 @@ internal static void DumpTribePreviewLegacy() Vector2Int pos = new Vector2Int(x, y); if (popup.UIWorldPreview.worldPreviewData.TryGetData(pos, type, out UITileData tileData)) { - Visual.PreviewTile previewTile = new Visual.PreviewTile + TribePreviewManager.PreviewTile previewTile = new TribePreviewManager.PreviewTile { x = tileData.Position.x, y = tileData.Position.y, diff --git a/src/Managers/Visual.cs b/src/Managers/Visual.cs index f048244..82999a9 100644 --- a/src/Managers/Visual.cs +++ b/src/Managers/Visual.cs @@ -18,35 +18,6 @@ namespace PolyMod.Managers; /// public static class Visual { - /// - /// 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; - } - /// Represents information about a sprite, such as its pivot and pixels per unit. public record SpriteInfo(float? pixelsPerUnit, Vector2? pivot); @@ -61,11 +32,8 @@ public record SkinInfo(int idx, string id, SkinData? skinData); /// 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; 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(); @@ -319,182 +287,6 @@ private static void PolytopiaSpriteRenderer_ForceUpdateMesh(PolytopiaSpriteRende #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) - 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 // check if still needed @@ -653,6 +445,7 @@ public static Sprite BuildSpriteWithTexture(Texture2D texture, Vector2? pivot = internal static void Init() { Harmony.CreateAndPatchAll(typeof(Visual)); - Harmony.CreateAndPatchAll(typeof(PrefabFix)); + Harmony.CreateAndPatchAll(typeof(PrefabRegistry)); + Harmony.CreateAndPatchAll(typeof(TribePreviewManager)); } } diff --git a/src/Managers/Visual/PrefabCreator.cs b/src/Managers/Visual/PrefabCreator.cs new file mode 100644 index 0000000..46a186e --- /dev/null +++ b/src/Managers/Visual/PrefabCreator.cs @@ -0,0 +1,289 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using PolyMod.Json; +using Polytopia.Data; +using PolytopiaBackendBase.Common; +using UnityEngine; +using static PolyMod.Managers.PrefabRegistry; + +namespace PolyMod.Managers; + +public static class PrefabCreator +{ + + internal static Dictionary> prefabsFilesToProcess = new(); + + public static List unitInfos = new(); + + public static List improvementInfos = new(); + + public static List resourceInfos = new(); + + 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(); + + [JsonInclude] + public float rotation = 0f; + + [JsonInclude] + public bool tintable = false; + + [JsonInclude] + public bool levels = false; // improvement specific + } + + public class BasePrefabInfo + { + [JsonInclude] + public List visualParts = new(); + } + public class UnitPrefabInfo : BasePrefabInfo + { + [JsonInclude] + public SkinnedKey skinnedKey; + + [JsonInclude] + public string headPositionMarker = ""; + } + + public class ImprovementPrefabInfo : BasePrefabInfo + { + [JsonInclude] + [JsonConverter(typeof(EnumCacheJson))] + public ImprovementData.Type? improvementType = null; + } + + public class ResourcePrefabInfo : BasePrefabInfo + { + [JsonInclude] + [JsonConverter(typeof(EnumCacheJson))] + public ResourceData.Type? resourceType = null; + } + + public class PrefabsInfo + { + [JsonInclude] + public List units = new(); + + [JsonInclude] + public List improvements = new(); + + [JsonInclude] + public List resources = new(); + } + + internal static void ProcessPrefabs() + { + foreach (var keyValuePair in prefabsFilesToProcess) + { + if(keyValuePair.Key == "all") + { + foreach (var bytes in keyValuePair.Value) + { + var prefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (prefab == null) + return; + + unitInfos.AddRange(prefab.units); + improvementInfos.AddRange(prefab.improvements); + resourceInfos.AddRange(prefab.resources); + } + } + else if(keyValuePair.Key == "units") + { + foreach (var bytes in keyValuePair.Value) + { + var unitPrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (unitPrefab == null) + return; + + unitInfos.Add(unitPrefab); + } + } + else if(keyValuePair.Key == "improvements") + { + foreach (var bytes in keyValuePair.Value) + { + var improvementPrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (improvementPrefab == null) + return; + + improvementInfos.Add(improvementPrefab); + } + } + else if(keyValuePair.Key == "resources") + { + foreach (var bytes in keyValuePair.Value) + { + var resourcePrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (resourcePrefab == null) + return; + + PrefabCreator.resourceInfos.Add(resourcePrefab); + } + } + } + foreach (var unitPrefabInfo in unitInfos) + { + ProcessUnitPrefabInfo(unitPrefabInfo); + } + + foreach (var improvementPrefabInfo in improvementInfos) + { + ProcessImprovementPrefabInfo(improvementPrefabInfo); + } + + foreach (var resourcePrefabInfo in resourceInfos) + { + ProcessResourcePrefabInfo(resourcePrefabInfo); + } + } + + /// The material of the original sprite parts. + private static Material? ClearExistingPartsAndExtractMaterial(Transform spriteContainer) + { + Material? material = null; + for (int i = 0; i < spriteContainer.childCount; i++) + { + var child = spriteContainer.GetChild(i); + if (child.gameObject.name == "Head") + { + var renderer = child.GetComponent(); + if (renderer != null) + material = renderer.material; + } + GameObject.Destroy(child.gameObject); + } + return material; + } + + private static List ApplyVisualParts( + UnitPrefabInfo prefab, + Transform spriteContainer, + Material? material) + { + if (prefab.visualParts.Count == 0) + return new List(); + + List parts = new(); + + foreach (var info in prefab.visualParts) + { + parts.Add(CreateVisualPart(info, spriteContainer, material)); + } + + return parts; + } + + private static SkinVisualsReference.VisualPart CreateVisualPart( + VisualPartInfo info, + Transform parent, + Material? material) + { + var visualPartObj = new GameObject(info.defaultSpriteName); + visualPartObj.transform.SetParent(parent); + visualPartObj.transform.position = info.coordinates; + 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.rotation = Quaternion.Euler(0f, 0f, info.rotation); + + var visualPart = new SkinVisualsReference.VisualPart + { + DefaultSpriteName = info.defaultSpriteName, + 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; + + visualPart.outlineRenderer = new SkinVisualsReference.RendererUnion { spriteRenderer = outlineRenderer }; + + return visualPart; + } + + internal static void ProcessUnitPrefabInfo(UnitPrefabInfo unitPrefabInfo) + { + var baseUnit = PrefabManager.GetPrefab(UnitData.Type.Warrior, TribeType.Imperius, SkinType.Default); + if (baseUnit == null) + return; + + var unitInstance = GameObject.Instantiate(baseUnit); + if (unitInstance == null) + return; + + var spriteContainer = unitInstance.transform.GetChild(0); + var material = ClearExistingPartsAndExtractMaterial(spriteContainer); + + var visualParts = ApplyVisualParts(unitPrefabInfo, spriteContainer, material); + + 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); + + PrefabRegistry.unitsNew[unitPrefabInfo.skinnedKey] = unitInstance; + } + + internal static void ProcessImprovementPrefabInfo(ImprovementPrefabInfo unitPrefabInfo) + { + } + + internal static void ProcessResourcePrefabInfo(ResourcePrefabInfo unitPrefabInfo) + { + } +} \ No newline at end of file diff --git a/src/Managers/PrefabFix.cs b/src/Managers/Visual/PrefabRegistry.cs similarity index 78% rename from src/Managers/PrefabFix.cs rename to src/Managers/Visual/PrefabRegistry.cs index 23dc092..6b3f50e 100644 --- a/src/Managers/PrefabFix.cs +++ b/src/Managers/Visual/PrefabRegistry.cs @@ -1,45 +1,17 @@ +using System.Text.Json.Serialization; using HarmonyLib; -using PolyMod; +using PolyMod.Json; using Polytopia.Data; using PolytopiaBackendBase.Common; using static PrefabManager; -public static class PrefabFix -{ - public readonly struct SkinnedKey : IEquatable - { - private readonly UnitData.Type unitType; - private readonly TribeType tribeType; - private readonly SkinType skinType; - - 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; - } +namespace PolyMod.Managers; - 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); - } - } - - private static readonly Dictionary unitsNew = new(); - private static readonly Dictionary> unitOverridesNew = new(); - private static readonly Dictionary unitUIOverridesNew = new(); +public static class PrefabRegistry +{ + internal static Dictionary unitsNew = new(); + internal static Dictionary> unitOverridesNew = new(); + internal static Dictionary unitUIOverridesNew = new(); [HarmonyPostfix] [HarmonyPatch(typeof(PrefabManager), nameof(PrefabManager.Awake))] diff --git a/src/Managers/Visual/SkinnedKey.cs b/src/Managers/Visual/SkinnedKey.cs new file mode 100644 index 0000000..0f42c9c --- /dev/null +++ b/src/Managers/Visual/SkinnedKey.cs @@ -0,0 +1,46 @@ +using System.Text.Json.Serialization; +using PolyMod.Json; +using Polytopia.Data; +using PolytopiaBackendBase.Common; + +namespace PolyMod.Managers; + +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/Managers/Visual/TribePreviewManager.cs b/src/Managers/Visual/TribePreviewManager.cs new file mode 100644 index 0000000..5c12e7b --- /dev/null +++ b/src/Managers/Visual/TribePreviewManager.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 TribePreviewManager +{ + /// + /// 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/Registry.cs b/src/Registry.cs index 5bfd5f1..a3d075f 100644 --- a/src/Registry.cs +++ b/src/Registry.cs @@ -34,7 +34,7 @@ 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. From b3546ef122e51fc7c35d8fc761efb1cb6cd3e62f Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sat, 17 Oct 2026 13:16:41 +0200 Subject: [PATCH 4/9] Finished new unit prefabs --- src/Loader.cs | 5 +- src/Managers/Visual.cs | 36 ++----- src/Managers/Visual/PrefabCreator.cs | 133 +++++++++++++++++++------- src/Managers/Visual/PrefabRegistry.cs | 48 +++++++++- 4 files changed, 154 insertions(+), 68 deletions(-) diff --git a/src/Loader.cs b/src/Loader.cs index 9e91445..d373cf3 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; @@ -606,8 +605,8 @@ public static void LoadSpriteFile(Mod mod, Mod.File file) } 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); + GameManager.GetSpriteAtlasManager().cachedSprites["Heads"][name] = sprite; + Registry.sprites[name] = sprite; } /// diff --git a/src/Managers/Visual.cs b/src/Managers/Visual.cs index 82999a9..64cb294 100644 --- a/src/Managers/Visual.cs +++ b/src/Managers/Visual.cs @@ -10,6 +10,7 @@ using PolytopiaBackendBase.Common; using Il2CppInterop.Runtime; using TMPro; +using SevenZip.Compression.LZMA; namespace PolyMod.Managers; @@ -32,7 +33,6 @@ public record SkinInfo(int idx, string id, SkinData? skinData); /// 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 enableOutlines = false; private static bool seenWarningWCPopup = false; private static Dictionary? cachedSpriteDataReverse = null; @@ -149,38 +149,18 @@ private static void SpriteAtlasManager_DoSpriteLookup(ref SpriteAtlasManager.Spr if (sprite != null) __result.sprite = sprite; + Sprite? outlineSprite = Registry.GetSprite(baseName + "_outline", tribe, skin, level); + if (outlineSprite != null) + __result.outlineSprite = outlineSprite; + string formattedName = FormatSpriteName(baseName); sprite = Registry.GetSprite(formattedName, tribe, skin, level); if (sprite != null) __result.sprite = sprite; - } - - #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) - { - enableOutlines = true; // bro - return true; - } - /// 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) - { - return !(spriteRenderer.sortingOrder == -1 && !enableOutlines); // uhhh tf is this hack - } - - /// 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) - { - enableOutlines = false; + outlineSprite = Registry.GetSprite(formattedName + "_outline", tribe, skin, level); + if (outlineSprite != null) + __result.outlineSprite = outlineSprite; } #endregion diff --git a/src/Managers/Visual/PrefabCreator.cs b/src/Managers/Visual/PrefabCreator.cs index 46a186e..6ff3bec 100644 --- a/src/Managers/Visual/PrefabCreator.cs +++ b/src/Managers/Visual/PrefabCreator.cs @@ -5,12 +5,13 @@ using PolytopiaBackendBase.Common; using UnityEngine; using static PolyMod.Managers.PrefabRegistry; +using static SkinVisualsReference; namespace PolyMod.Managers; public static class PrefabCreator { - + internal static Material? cachedUnitMaterial; internal static Dictionary> prefabsFilesToProcess = new(); public static List unitInfos = new(); @@ -166,58 +167,100 @@ internal static void ProcessPrefabs() { ProcessResourcePrefabInfo(resourcePrefabInfo); } - } - /// The material of the original sprite parts. - private static Material? ClearExistingPartsAndExtractMaterial(Transform spriteContainer) - { - Material? material = null; - for (int i = 0; i < spriteContainer.childCount; i++) + foreach (var item in unitsNew) { - var child = spriteContainer.GetChild(i); - if (child.gameObject.name == "Head") + var skinnedKey = item.Key; + var unit = item.Value; + SkinVisualsReference skinVisualsReference = unit.GetComponent(); + if (skinVisualsReference == null) + { + skinVisualsReference = unit.GetComponentInChildren(true); + } + if (skinVisualsReference == null || skinVisualsReference.visualParts == null) + { + Plugin.logger.LogWarning($"{skinnedKey.unitType} {skinnedKey.tribeType} {skinnedKey.skinType}: no SkinVisualsReference"); + continue; + } + + for (int partIndex = 0; partIndex < skinVisualsReference.visualParts.Length; partIndex++) { - var renderer = child.GetComponent(); - if (renderer != null) - material = renderer.material; + VisualPart currentPart = skinVisualsReference.visualParts[partIndex]; + Plugin.logger.LogInfo($"{skinnedKey.unitType} {skinnedKey.tribeType} {skinnedKey.skinType} part {partIndex}"); + Plugin.logger.LogInfo($" visualPart: {DescribeVisualObject(currentPart.visualPart)}"); + Plugin.logger.LogInfo($" outline: {DescribeVisualObject(currentPart.outline)}"); } - GameObject.Destroy(child.gameObject); } - return material; } + private static string DescribeVisualObject(GameObject targetObject) + { + if (targetObject == null) + { + return "null"; + } + + string layerName = LayerMask.LayerToName(targetObject.layer); + string description = $"name={targetObject.name}, layer={targetObject.layer} ({layerName}), activeSelf={targetObject.activeSelf}"; + + SpriteRenderer spriteRenderer = targetObject.GetComponent(); + if (spriteRenderer != null) + { + description += $", SpriteRenderer(enabled={spriteRenderer.enabled}, sortingLayer={SortingLayer.IDToName(spriteRenderer.sortingLayerID)}, sortingOrder={spriteRenderer.sortingOrder}, hasSprite={spriteRenderer.sprite != null})"; + } + + PolytopiaSpriteRenderer polytopiaSpriteRenderer = targetObject.GetComponent(); + if (polytopiaSpriteRenderer != null) + { + description += $", PolytopiaSpriteRenderer(enabled={polytopiaSpriteRenderer.enabled}, sortingLayer={SortingLayer.IDToName(polytopiaSpriteRenderer.SortingLayer)}, sortingOrder={polytopiaSpriteRenderer.SortingOrder}, hasSprite={polytopiaSpriteRenderer.HasSprite()})"; + } + + return description; + } + private static List ApplyVisualParts( UnitPrefabInfo prefab, - Transform spriteContainer, - Material? material) + Transform spriteContainer) { - if (prefab.visualParts.Count == 0) - return new List(); List parts = new(); + if (prefab.visualParts.Count == 0) + return parts; + + int nextPartSortingOrder = 0; + int nextOutlineSortingOrder = -prefab.visualParts.Count; foreach (var info in prefab.visualParts) { - parts.Add(CreateVisualPart(info, spriteContainer, material)); + parts.Add(CreateUnitVisualPart(info, spriteContainer, nextPartSortingOrder, nextOutlineSortingOrder)); + if(nextOutlineSortingOrder < -1) + { + nextOutlineSortingOrder++; + } + nextPartSortingOrder++; + Console.Write($"{prefab.visualParts.Count} New orders {nextPartSortingOrder} {nextOutlineSortingOrder}"); } return parts; } - private static SkinVisualsReference.VisualPart CreateVisualPart( + private static SkinVisualsReference.VisualPart CreateUnitVisualPart( VisualPartInfo info, Transform parent, - Material? material) + int visualPartSortingOrder, + int outlineSortingOrder) { var visualPartObj = new GameObject(info.defaultSpriteName); visualPartObj.transform.SetParent(parent); - visualPartObj.transform.position = info.coordinates; - visualPartObj.transform.rotation = Quaternion.Euler(0f, 0f, info.rotation); - var outlineObj = new GameObject("Outline"); + var outlineObj = new GameObject($"{info.defaultSpriteName}_Outline"); outlineObj.transform.SetParent(visualPartObj.transform); - outlineObj.transform.position = info.coordinates; - outlineObj.transform.rotation = Quaternion.Euler(0f, 0f, info.rotation); + + visualPartObj.transform.position = 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 = new SkinVisualsReference.VisualPart { @@ -228,22 +271,38 @@ private static SkinVisualsReference.VisualPart CreateVisualPart( }; var renderer = visualPartObj.AddComponent(); - renderer.material = material; + renderer.material = cachedUnitMaterial; + renderer.sortingOrder = visualPartSortingOrder; renderer.sortingLayerName = "Units"; - renderer.sortingOrder = info.tintable ? 0 : 1; - visualPart.renderer = new SkinVisualsReference.RendererUnion { spriteRenderer = renderer }; var outlineRenderer = outlineObj.AddComponent(); - outlineRenderer.material = material; + outlineRenderer.material = cachedUnitMaterial; + outlineRenderer.sortingOrder = outlineSortingOrder; outlineRenderer.sortingLayerName = "Units"; - outlineRenderer.sortingOrder = -1; - visualPart.outlineRenderer = new SkinVisualsReference.RendererUnion { spriteRenderer = outlineRenderer }; return visualPart; } + private static SkinVisualsReference.VisualPart CreateImprovementVisualPart( + VisualPartInfo info, + Transform parent, + int visualPartSortingOrder, + int outlineSortingOrder) + { + return null; + } + + private static SkinVisualsReference.VisualPart CreateResourceVisualPart( + VisualPartInfo info, + Transform parent, + int visualPartSortingOrder, + int outlineSortingOrder) + { + return null; + } + internal static void ProcessUnitPrefabInfo(UnitPrefabInfo unitPrefabInfo) { var baseUnit = PrefabManager.GetPrefab(UnitData.Type.Warrior, TribeType.Imperius, SkinType.Default); @@ -254,10 +313,14 @@ internal static void ProcessUnitPrefabInfo(UnitPrefabInfo unitPrefabInfo) if (unitInstance == null) return; - var spriteContainer = unitInstance.transform.GetChild(0); - var material = ClearExistingPartsAndExtractMaterial(spriteContainer); + 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 = ApplyVisualParts(unitPrefabInfo, spriteContainer, material); + var visualParts = ApplyVisualParts(unitPrefabInfo, spriteContainer); Transform? headPositionMarker = null; foreach (var vp in visualParts) diff --git a/src/Managers/Visual/PrefabRegistry.cs b/src/Managers/Visual/PrefabRegistry.cs index 6b3f50e..4c14ec6 100644 --- a/src/Managers/Visual/PrefabRegistry.cs +++ b/src/Managers/Visual/PrefabRegistry.cs @@ -1,9 +1,9 @@ -using System.Text.Json.Serialization; using HarmonyLib; -using PolyMod.Json; using Polytopia.Data; using PolytopiaBackendBase.Common; +using UnityEngine; using static PrefabManager; +using static SkinVisualsReference; namespace PolyMod.Managers; @@ -23,6 +23,27 @@ private static void PrefabManager_Awake_Postfix(PrefabManager __instance) foreach (UnitPrefabData unitPrefabData in __instance.unitPrefabs) { + if(PrefabCreator.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) + { + PrefabCreator.cachedUnitMaterial = headSpriteRenderer.sharedMaterial; + break; + } + } + } + } unitsNew[new SkinnedKey(unitPrefabData.type, unitPrefabData.tribe, unitPrefabData.skin)] = unitPrefabData.prefab; } @@ -75,6 +96,29 @@ public static bool PrefabManager_GetPrefab(ref Unit __result, UnitData.Type type 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() From caf61e3cad052641d337ff345c201ef184bfd1bc Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sat, 17 Oct 2026 21:09:04 +0200 Subject: [PATCH 5/9] Implemented resources prefabs. Added unit prefabs dumping. Patched the last sprite getting method. Bunch of other stuff like splitting Visual manager into multiple small ones --- src/Loader.cs | 48 +- src/Managers/Hub.cs | 56 +- src/Managers/Prefabs.cs | 518 ++++++++++++++++++ ...ribePreviewManager.cs => TribePreviews.cs} | 2 +- src/Managers/Visual.cs | 68 +-- src/Managers/Visual/PrefabCreator.cs | 352 ------------ src/Managers/Visual/PrefabRegistry.cs | 157 ------ src/Prefabs/OtherPrefabInfo.cs | 34 ++ src/Prefabs/PrefabsInfo.cs | 16 + .../Visual => Prefabs}/SkinnedKey.cs | 2 +- src/Prefabs/VisualPartInfo.cs | 37 ++ src/Registry.cs | 7 +- 12 files changed, 704 insertions(+), 593 deletions(-) create mode 100644 src/Managers/Prefabs.cs rename src/Managers/{Visual/TribePreviewManager.cs => TribePreviews.cs} (99%) delete mode 100644 src/Managers/Visual/PrefabCreator.cs delete mode 100644 src/Managers/Visual/PrefabRegistry.cs create mode 100644 src/Prefabs/OtherPrefabInfo.cs create mode 100644 src/Prefabs/PrefabsInfo.cs rename src/{Managers/Visual => Prefabs}/SkinnedKey.cs (97%) create mode 100644 src/Prefabs/VisualPartInfo.cs diff --git a/src/Loader.cs b/src/Loader.cs index d373cf3..c431c06 100644 --- a/src/Loader.cs +++ b/src/Loader.cs @@ -125,7 +125,7 @@ out TribeAbility.Type tribeAbilityType } if (token["preview"] != null) { - TribePreviewManager.PreviewTile[] preview = JsonSerializer.Deserialize(token["preview"].ToString())!; + TribePreviews.PreviewTile[] preview = JsonSerializer.Deserialize(token["preview"].ToString())!; Registry.tribePreviews[Util.GetJTokenName(token)] = preview; } } @@ -682,12 +682,12 @@ public static void LoadPrefabsFile(Mod mod, Mod.File file) { try { - if(!PrefabCreator.prefabsFilesToProcess.ContainsKey("all") || PrefabCreator.prefabsFilesToProcess["all"] == null) - PrefabCreator.prefabsFilesToProcess["all"] = new(); + if(!Prefabs.prefabsFilesToProcess.ContainsKey("all") || Prefabs.prefabsFilesToProcess["all"] == null) + Prefabs.prefabsFilesToProcess["all"] = new(); - PrefabCreator.prefabsFilesToProcess["all"].Add(file.bytes); + Prefabs.prefabsFilesToProcess["all"].Add(file.bytes); - // var prefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + // var prefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions // { // Converters = { new Vector2Json() }, // PropertyNameCaseInsensitive = true, @@ -695,9 +695,9 @@ public static void LoadPrefabsFile(Mod mod, Mod.File file) // if (prefab == null) // return; - // PrefabCreator.unitInfos.AddRange(prefab.units); - // PrefabCreator.improvementInfos.AddRange(prefab.improvements); - // PrefabCreator.resourceInfos.AddRange(prefab.resources); + // Prefabs.unitInfos.AddRange(prefab.units); + // Prefabs.improvementInfos.AddRange(prefab.improvements); + // Prefabs.resourceInfos.AddRange(prefab.resources); Plugin.logger.LogInfo($"Registered prefabs {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod."); } @@ -711,12 +711,12 @@ public static void LoadUnitPrefabFile(Mod mod, Mod.File file) { try { - if(!PrefabCreator.prefabsFilesToProcess.ContainsKey("units") || PrefabCreator.prefabsFilesToProcess["units"] == null) - PrefabCreator.prefabsFilesToProcess["units"] = new(); + if(!Prefabs.prefabsFilesToProcess.ContainsKey("units") || Prefabs.prefabsFilesToProcess["units"] == null) + Prefabs.prefabsFilesToProcess["units"] = new(); - PrefabCreator.prefabsFilesToProcess["units"].Add(file.bytes); + Prefabs.prefabsFilesToProcess["units"].Add(file.bytes); - // var unitPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + // var unitPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions // { // Converters = { new Vector2Json() }, // PropertyNameCaseInsensitive = true, @@ -724,7 +724,7 @@ public static void LoadUnitPrefabFile(Mod mod, Mod.File file) // if (unitPrefab == null) // return; - // PrefabCreator.unitInfos.Add(unitPrefab); + // Prefabs.unitInfos.Add(unitPrefab); Plugin.logger.LogInfo($"Registered unit prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod."); } @@ -738,12 +738,12 @@ public static void LoadImprovementPrefabFile(Mod mod, Mod.File file) { try { - if(!PrefabCreator.prefabsFilesToProcess.ContainsKey("improvements") || PrefabCreator.prefabsFilesToProcess["improvements"] == null) - PrefabCreator.prefabsFilesToProcess["improvements"] = new(); + if(!Prefabs.prefabsFilesToProcess.ContainsKey("improvements") || Prefabs.prefabsFilesToProcess["improvements"] == null) + Prefabs.prefabsFilesToProcess["improvements"] = new(); - PrefabCreator.prefabsFilesToProcess["improvements"].Add(file.bytes); + Prefabs.prefabsFilesToProcess["improvements"].Add(file.bytes); - // var improvementPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + // var improvementPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions // { // Converters = { new Vector2Json() }, // PropertyNameCaseInsensitive = true, @@ -751,7 +751,7 @@ public static void LoadImprovementPrefabFile(Mod mod, Mod.File file) // if (improvementPrefab == null) // return; - // PrefabCreator.improvementInfos.Add(improvementPrefab); + // Prefabs.improvementInfos.Add(improvementPrefab); Plugin.logger.LogInfo($"Registered improvement prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod."); } @@ -765,12 +765,12 @@ public static void LoadResourcePrefabFile(Mod mod, Mod.File file) { try { - if(!PrefabCreator.prefabsFilesToProcess.ContainsKey("resources") || PrefabCreator.prefabsFilesToProcess["resources"] == null) - PrefabCreator.prefabsFilesToProcess["resources"] = new(); + if(!Prefabs.prefabsFilesToProcess.ContainsKey("resources") || Prefabs.prefabsFilesToProcess["resources"] == null) + Prefabs.prefabsFilesToProcess["resources"] = new(); - PrefabCreator.prefabsFilesToProcess["resources"].Add(file.bytes); + Prefabs.prefabsFilesToProcess["resources"].Add(file.bytes); - // var resourcePrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + // var resourcePrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions // { // Converters = { new Vector2Json() }, // PropertyNameCaseInsensitive = true, @@ -778,7 +778,7 @@ public static void LoadResourcePrefabFile(Mod mod, Mod.File file) // if (resourcePrefab == null) // return; - // PrefabCreator.resourceInfos.Add(resourcePrefab); + // Prefabs.resourceInfos.Add(resourcePrefab); Plugin.logger.LogInfo($"Registered resource prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod."); } @@ -948,7 +948,7 @@ internal static void CreateMappings(JObject rootObject) /// internal static void ProcessPrefabs() { - PrefabCreator.ProcessPrefabs(); + Prefabs.ProcessPrefabs(); } /// diff --git a/src/Managers/Hub.cs b/src/Managers/Hub.cs index 553652c..9400b51 100644 --- a/src/Managers/Hub.cs +++ b/src/Managers/Hub.cs @@ -3,17 +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 Il2CppInterop.Runtime.InteropTypes.Arrays; -using UnityEngine.Experimental.U2D; +using PolyMod.Json; namespace PolyMod.Managers; @@ -343,7 +338,7 @@ 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++) { @@ -352,7 +347,7 @@ internal static void DumpTribePreviewLegacy() Vector2Int pos = new Vector2Int(x, y); if (popup.UIWorldPreview.worldPreviewData.TryGetData(pos, type, out UITileData tileData)) { - TribePreviewManager.PreviewTile previewTile = new TribePreviewManager.PreviewTile + TribePreviews.PreviewTile previewTile = new TribePreviews.PreviewTile { x = tileData.Position.x, y = tileData.Position.y, @@ -373,7 +368,50 @@ internal static void DumpTribePreviewLegacy() } 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 + { + 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 + }; + unitPrefabInfo.visualParts.Add(visualPartInfo); + + } + prefabsInfo.units.Add(unitPrefabInfo); + } + File.WriteAllTextAsync( + Path.Combine(Plugin.PREFABS_DUMP_PATH, $"prefabs.json"), + JsonSerializer.Serialize(prefabsInfo, new JsonSerializerOptions { WriteIndented = true, Converters = { new Vector2Json() },}) + ); + } /// /// Shows the configuration popup. diff --git a/src/Managers/Prefabs.cs b/src/Managers/Prefabs.cs new file mode 100644 index 0000000..addacf2 --- /dev/null +++ b/src/Managers/Prefabs.cs @@ -0,0 +1,518 @@ +using HarmonyLib; +using Polytopia.Data; +using PolytopiaBackendBase.Common; +using UnityEngine; +using static PrefabManager; +using static SkinVisualsReference; +using System.Text.Json; +using PolyMod.Json; + +namespace PolyMod.Managers; + +public static class Prefabs +{ + internal static Dictionary unitsNew = new(); + internal static Dictionary> unitOverridesNew = new(); + internal static Dictionary unitUIOverridesNew = new(); + + internal static Material? cachedUnitMaterial; + internal static Material? cachedResourceMaterial; + internal static int cachedResourceSortingLayerId; + + internal static Dictionary> prefabsFilesToProcess = new(); + + public static List unitInfos = new(); + + public static List improvementInfos = new(); + + public static List resourceInfos = 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 ProcessPrefabs() + { + // get materials + Material[] allLoadedMaterials = Resources.FindObjectsOfTypeAll(); + foreach (Material loadedMaterial in allLoadedMaterials) + { + if(loadedMaterial.name == "SpriteMaterial") + { + cachedResourceMaterial = loadedMaterial; + Console.Write("MATERIAL FUCKING FOUND GOODBYE!"); + GameObject.DontDestroyOnLoad(cachedResourceMaterial); + break; + } + Plugin.logger.LogInfo($"Material: {loadedMaterial.name}, shader: {loadedMaterial.shader.name}"); + } + + cachedResourceSortingLayerId = MeshCache.TERRAIN_LAYER_ID; + // i will remove this bs + foreach (var keyValuePair in prefabsFilesToProcess) + { + if(keyValuePair.Key == "all") + { + foreach (var bytes in keyValuePair.Value) + { + var prefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (prefab == null) + return; + + unitInfos.AddRange(prefab.units); + improvementInfos.AddRange(prefab.improvements); + resourceInfos.AddRange(prefab.resources); + } + } + else if(keyValuePair.Key == "units") + { + foreach (var bytes in keyValuePair.Value) + { + var unitPrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (unitPrefab == null) + return; + + unitInfos.Add(unitPrefab); + } + } + else if(keyValuePair.Key == "improvements") + { + foreach (var bytes in keyValuePair.Value) + { + var improvementPrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (improvementPrefab == null) + return; + + improvementInfos.Add(improvementPrefab); + } + } + else if(keyValuePair.Key == "resources") + { + foreach (var bytes in keyValuePair.Value) + { + var resourcePrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (resourcePrefab == null) + return; + + resourceInfos.Add(resourcePrefab); + } + } + } + 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); + + 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, nextPartSortingOrder, nextOutlineSortingOrder)); + if(nextOutlineSortingOrder < -1) + { + nextOutlineSortingOrder++; + } + nextPartSortingOrder++; + Console.Write($"{visualPartInfos.Count} New orders {nextPartSortingOrder} {nextOutlineSortingOrder}"); + } + + 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 = new SkinVisualsReference.VisualPart + { + DefaultSpriteName = info.defaultSpriteName, + visualPart = visualPartObj, + outline = outlineObj, + tintable = info.tintable, + skinLogic = info.skinningLogic, + improvementSkinLogic = info.improvementSkinningLogic, + levels = info.levels + }; + + 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) + { + var baseImprovement = PrefabManager.GetPrefab(ImprovementData.Type.LumberHut); + } + + private static SkinVisualsReference.VisualPart CreateImprovementVisualPart( + VisualPartInfo info, + Transform parent, + int visualPartSortingOrder, + int outlineSortingOrder) + { + return null; + } + + #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, resourceInstance.transform); + + 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++; + Console.Write($"{visualPartInfos.Count} New orders {nextPartSortingOrder} {nextOutlineSortingOrder}"); + } + + 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 = new SkinVisualsReference.VisualPart + { + DefaultSpriteName = info.defaultSpriteName, + visualPart = visualPartObj, + outline = outlineObj, + tintable = info.tintable, + skinLogic = info.skinningLogic, + improvementSkinLogic = info.improvementSkinningLogic, + levels = info.levels + }; + 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; + } + + private static PolytopiaSpriteRenderer AddPolytopiaSpriteRenderer(GameObject targetGameObject, int sortingOrder) + { + 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("Terrain"); + polytopiaSpriteRenderer.SortingOrder = sortingOrder; + if (cachedResourceMaterial != null) + { + polytopiaSpriteRenderer.SharedMaterial = cachedResourceMaterial; + } + return polytopiaSpriteRenderer; + } + + #endregion +} \ No newline at end of file diff --git a/src/Managers/Visual/TribePreviewManager.cs b/src/Managers/TribePreviews.cs similarity index 99% rename from src/Managers/Visual/TribePreviewManager.cs rename to src/Managers/TribePreviews.cs index 5c12e7b..f3ab13d 100644 --- a/src/Managers/Visual/TribePreviewManager.cs +++ b/src/Managers/TribePreviews.cs @@ -9,7 +9,7 @@ namespace PolyMod.Managers; -public static class TribePreviewManager +public static class TribePreviews { /// /// Represents a tile in a tribe preview. diff --git a/src/Managers/Visual.cs b/src/Managers/Visual.cs index 64cb294..f67a3f8 100644 --- a/src/Managers/Visual.cs +++ b/src/Managers/Visual.cs @@ -2,15 +2,9 @@ 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 SevenZip.Compression.LZMA; namespace PolyMod.Managers; @@ -47,45 +41,6 @@ private static void TechItem_SetupComplete() { } - /// Patches the sprite atlas manager to load custom sprites. TODOOOO - // [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); - - return !found; - - void GetAtlas(SpriteAtlas spriteAtlas) - { - 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 = null; // Registry.GetSprite(name, style); - if (newSprite != null) - { - completion?.Invoke(atlas, sprite, newSprite); - found = true; - } - } - } - } - /// Patches the sprite atlas manager to look up custom sprites. internal static void CacheSpriteNames() { @@ -163,6 +118,25 @@ private static void SpriteAtlasManager_DoSpriteLookup(ref SpriteAtlasManager.Spr __result.outlineSprite = outlineSprite; } + [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) + { + Sprite? spriteResult = Registry.GetSprite(sprite); + + string formattedName = FormatSpriteName(sprite); + Sprite? formattedSprite = Registry.GetSprite(formattedName); + if (formattedSprite != null) + spriteResult = formattedSprite; + + if (spriteResult != null) + { + completion.Invoke(atlas, sprite, spriteResult); + return false; + } + return true; + } + #endregion #region Level @@ -425,7 +399,7 @@ public static Sprite BuildSpriteWithTexture(Texture2D texture, Vector2? pivot = internal static void Init() { Harmony.CreateAndPatchAll(typeof(Visual)); - Harmony.CreateAndPatchAll(typeof(PrefabRegistry)); - Harmony.CreateAndPatchAll(typeof(TribePreviewManager)); + Harmony.CreateAndPatchAll(typeof(Prefabs)); + Harmony.CreateAndPatchAll(typeof(TribePreviews)); } } diff --git a/src/Managers/Visual/PrefabCreator.cs b/src/Managers/Visual/PrefabCreator.cs deleted file mode 100644 index 6ff3bec..0000000 --- a/src/Managers/Visual/PrefabCreator.cs +++ /dev/null @@ -1,352 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using PolyMod.Json; -using Polytopia.Data; -using PolytopiaBackendBase.Common; -using UnityEngine; -using static PolyMod.Managers.PrefabRegistry; -using static SkinVisualsReference; - -namespace PolyMod.Managers; - -public static class PrefabCreator -{ - internal static Material? cachedUnitMaterial; - internal static Dictionary> prefabsFilesToProcess = new(); - - public static List unitInfos = new(); - - public static List improvementInfos = new(); - - public static List resourceInfos = new(); - - 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(); - - [JsonInclude] - public float rotation = 0f; - - [JsonInclude] - public bool tintable = false; - - [JsonInclude] - public bool levels = false; // improvement specific - } - - public class BasePrefabInfo - { - [JsonInclude] - public List visualParts = new(); - } - public class UnitPrefabInfo : BasePrefabInfo - { - [JsonInclude] - public SkinnedKey skinnedKey; - - [JsonInclude] - public string headPositionMarker = ""; - } - - public class ImprovementPrefabInfo : BasePrefabInfo - { - [JsonInclude] - [JsonConverter(typeof(EnumCacheJson))] - public ImprovementData.Type? improvementType = null; - } - - public class ResourcePrefabInfo : BasePrefabInfo - { - [JsonInclude] - [JsonConverter(typeof(EnumCacheJson))] - public ResourceData.Type? resourceType = null; - } - - public class PrefabsInfo - { - [JsonInclude] - public List units = new(); - - [JsonInclude] - public List improvements = new(); - - [JsonInclude] - public List resources = new(); - } - - internal static void ProcessPrefabs() - { - foreach (var keyValuePair in prefabsFilesToProcess) - { - if(keyValuePair.Key == "all") - { - foreach (var bytes in keyValuePair.Value) - { - var prefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions - { - Converters = { new Vector2Json() }, - PropertyNameCaseInsensitive = true, - }); - if (prefab == null) - return; - - unitInfos.AddRange(prefab.units); - improvementInfos.AddRange(prefab.improvements); - resourceInfos.AddRange(prefab.resources); - } - } - else if(keyValuePair.Key == "units") - { - foreach (var bytes in keyValuePair.Value) - { - var unitPrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions - { - Converters = { new Vector2Json() }, - PropertyNameCaseInsensitive = true, - }); - if (unitPrefab == null) - return; - - unitInfos.Add(unitPrefab); - } - } - else if(keyValuePair.Key == "improvements") - { - foreach (var bytes in keyValuePair.Value) - { - var improvementPrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions - { - Converters = { new Vector2Json() }, - PropertyNameCaseInsensitive = true, - }); - if (improvementPrefab == null) - return; - - improvementInfos.Add(improvementPrefab); - } - } - else if(keyValuePair.Key == "resources") - { - foreach (var bytes in keyValuePair.Value) - { - var resourcePrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions - { - Converters = { new Vector2Json() }, - PropertyNameCaseInsensitive = true, - }); - if (resourcePrefab == null) - return; - - PrefabCreator.resourceInfos.Add(resourcePrefab); - } - } - } - foreach (var unitPrefabInfo in unitInfos) - { - ProcessUnitPrefabInfo(unitPrefabInfo); - } - - foreach (var improvementPrefabInfo in improvementInfos) - { - ProcessImprovementPrefabInfo(improvementPrefabInfo); - } - - foreach (var resourcePrefabInfo in resourceInfos) - { - ProcessResourcePrefabInfo(resourcePrefabInfo); - } - - foreach (var item in unitsNew) - { - var skinnedKey = item.Key; - var unit = item.Value; - SkinVisualsReference skinVisualsReference = unit.GetComponent(); - if (skinVisualsReference == null) - { - skinVisualsReference = unit.GetComponentInChildren(true); - } - if (skinVisualsReference == null || skinVisualsReference.visualParts == null) - { - Plugin.logger.LogWarning($"{skinnedKey.unitType} {skinnedKey.tribeType} {skinnedKey.skinType}: no SkinVisualsReference"); - continue; - } - - for (int partIndex = 0; partIndex < skinVisualsReference.visualParts.Length; partIndex++) - { - VisualPart currentPart = skinVisualsReference.visualParts[partIndex]; - Plugin.logger.LogInfo($"{skinnedKey.unitType} {skinnedKey.tribeType} {skinnedKey.skinType} part {partIndex}"); - Plugin.logger.LogInfo($" visualPart: {DescribeVisualObject(currentPart.visualPart)}"); - Plugin.logger.LogInfo($" outline: {DescribeVisualObject(currentPart.outline)}"); - } - } - } - - private static string DescribeVisualObject(GameObject targetObject) - { - if (targetObject == null) - { - return "null"; - } - - string layerName = LayerMask.LayerToName(targetObject.layer); - string description = $"name={targetObject.name}, layer={targetObject.layer} ({layerName}), activeSelf={targetObject.activeSelf}"; - - SpriteRenderer spriteRenderer = targetObject.GetComponent(); - if (spriteRenderer != null) - { - description += $", SpriteRenderer(enabled={spriteRenderer.enabled}, sortingLayer={SortingLayer.IDToName(spriteRenderer.sortingLayerID)}, sortingOrder={spriteRenderer.sortingOrder}, hasSprite={spriteRenderer.sprite != null})"; - } - - PolytopiaSpriteRenderer polytopiaSpriteRenderer = targetObject.GetComponent(); - if (polytopiaSpriteRenderer != null) - { - description += $", PolytopiaSpriteRenderer(enabled={polytopiaSpriteRenderer.enabled}, sortingLayer={SortingLayer.IDToName(polytopiaSpriteRenderer.SortingLayer)}, sortingOrder={polytopiaSpriteRenderer.SortingOrder}, hasSprite={polytopiaSpriteRenderer.HasSprite()})"; - } - - return description; - } - - private static List ApplyVisualParts( - UnitPrefabInfo prefab, - Transform spriteContainer) - { - - List parts = new(); - - if (prefab.visualParts.Count == 0) - return parts; - - int nextPartSortingOrder = 0; - int nextOutlineSortingOrder = -prefab.visualParts.Count; - foreach (var info in prefab.visualParts) - { - parts.Add(CreateUnitVisualPart(info, spriteContainer, nextPartSortingOrder, nextOutlineSortingOrder)); - if(nextOutlineSortingOrder < -1) - { - nextOutlineSortingOrder++; - } - nextPartSortingOrder++; - Console.Write($"{prefab.visualParts.Count} New orders {nextPartSortingOrder} {nextOutlineSortingOrder}"); - } - - 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 = 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 = new SkinVisualsReference.VisualPart - { - DefaultSpriteName = info.defaultSpriteName, - visualPart = visualPartObj, - outline = outlineObj, - tintable = info.tintable - }; - - 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; - } - - private static SkinVisualsReference.VisualPart CreateImprovementVisualPart( - VisualPartInfo info, - Transform parent, - int visualPartSortingOrder, - int outlineSortingOrder) - { - return null; - } - - private static SkinVisualsReference.VisualPart CreateResourceVisualPart( - VisualPartInfo info, - Transform parent, - int visualPartSortingOrder, - int outlineSortingOrder) - { - return null; - } - - internal static void ProcessUnitPrefabInfo(UnitPrefabInfo unitPrefabInfo) - { - 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 = ApplyVisualParts(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); - - PrefabRegistry.unitsNew[unitPrefabInfo.skinnedKey] = unitInstance; - } - - internal static void ProcessImprovementPrefabInfo(ImprovementPrefabInfo unitPrefabInfo) - { - } - - internal static void ProcessResourcePrefabInfo(ResourcePrefabInfo unitPrefabInfo) - { - } -} \ No newline at end of file diff --git a/src/Managers/Visual/PrefabRegistry.cs b/src/Managers/Visual/PrefabRegistry.cs deleted file mode 100644 index 4c14ec6..0000000 --- a/src/Managers/Visual/PrefabRegistry.cs +++ /dev/null @@ -1,157 +0,0 @@ -using HarmonyLib; -using Polytopia.Data; -using PolytopiaBackendBase.Common; -using UnityEngine; -using static PrefabManager; -using static SkinVisualsReference; - -namespace PolyMod.Managers; - -public static class PrefabRegistry -{ - internal static Dictionary unitsNew = new(); - internal static Dictionary> unitOverridesNew = new(); - internal static Dictionary unitUIOverridesNew = 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(PrefabCreator.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) - { - PrefabCreator.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!; - } -} \ No newline at end of file diff --git a/src/Prefabs/OtherPrefabInfo.cs b/src/Prefabs/OtherPrefabInfo.cs new file mode 100644 index 0000000..ce5a44f --- /dev/null +++ b/src/Prefabs/OtherPrefabInfo.cs @@ -0,0 +1,34 @@ + +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 = ""; +} + +public class ImprovementPrefabInfo : BasePrefabInfo +{ + [JsonInclude] + [JsonConverter(typeof(EnumCacheJson))] + public ImprovementData.Type? improvementType = null; +} + +public class ResourcePrefabInfo : BasePrefabInfo +{ + [JsonInclude] + [JsonConverter(typeof(EnumCacheJson))] + public ResourceData.Type? resourceType = 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/Managers/Visual/SkinnedKey.cs b/src/Prefabs/SkinnedKey.cs similarity index 97% rename from src/Managers/Visual/SkinnedKey.cs rename to src/Prefabs/SkinnedKey.cs index 0f42c9c..080b6df 100644 --- a/src/Managers/Visual/SkinnedKey.cs +++ b/src/Prefabs/SkinnedKey.cs @@ -3,7 +3,7 @@ using Polytopia.Data; using PolytopiaBackendBase.Common; -namespace PolyMod.Managers; +namespace PolyMod; public readonly struct SkinnedKey : IEquatable { diff --git a/src/Prefabs/VisualPartInfo.cs b/src/Prefabs/VisualPartInfo.cs new file mode 100644 index 0000000..4b6fd47 --- /dev/null +++ b/src/Prefabs/VisualPartInfo.cs @@ -0,0 +1,37 @@ +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(); + + [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; +} \ No newline at end of file diff --git a/src/Registry.cs b/src/Registry.cs index a3d075f..718dfdc 100644 --- a/src/Registry.cs +++ b/src/Registry.cs @@ -34,7 +34,7 @@ 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. @@ -77,6 +77,8 @@ public static class Registry /// public static Sprite? GetSpriteByText(string baseName, string tribeText = "", string skinText = "", int level = -1) { + if(baseName.Contains("oil")) + Console.Write("YESSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS"); Sprite? sprite = null; baseName = baseName.ToLower(); tribeText = tribeText.ToLower(); @@ -104,7 +106,8 @@ public static class Registry { sprite = sprites.GetOrDefault(spriteName, sprite); } - + if(baseName.Contains("oil")) + Console.Write(sprite == null); return sprite; } From c08b968809f7831e4b7ace9226b51ce0296750bb Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sat, 17 Oct 2026 21:34:57 +0200 Subject: [PATCH 6/9] Added a queue to process files after whole gld was merged (for prefabs). Cleanup --- src/Loader.cs | 133 ++++++++++++++++++++++------------------ src/Managers/Main.cs | 22 ++----- src/Managers/Prefabs.cs | 75 +--------------------- src/Registry.cs | 7 +-- 4 files changed, 86 insertions(+), 151 deletions(-) diff --git a/src/Loader.cs b/src/Loader.cs index c431c06..11fc6ad 100644 --- a/src/Loader.cs +++ b/src/Loader.cs @@ -682,22 +682,17 @@ public static void LoadPrefabsFile(Mod mod, Mod.File file) { try { - if(!Prefabs.prefabsFilesToProcess.ContainsKey("all") || Prefabs.prefabsFilesToProcess["all"] == null) - Prefabs.prefabsFilesToProcess["all"] = new(); - - Prefabs.prefabsFilesToProcess["all"].Add(file.bytes); - - // var prefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions - // { - // Converters = { new Vector2Json() }, - // PropertyNameCaseInsensitive = true, - // }); - // if (prefab == null) - // return; + var prefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (prefab == null) + return; - // Prefabs.unitInfos.AddRange(prefab.units); - // Prefabs.improvementInfos.AddRange(prefab.improvements); - // Prefabs.resourceInfos.AddRange(prefab.resources); + Prefabs.unitInfos.AddRange(prefab.units); + Prefabs.improvementInfos.AddRange(prefab.improvements); + Prefabs.resourceInfos.AddRange(prefab.resources); Plugin.logger.LogInfo($"Registered prefabs {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod."); } @@ -711,20 +706,15 @@ public static void LoadUnitPrefabFile(Mod mod, Mod.File file) { try { - if(!Prefabs.prefabsFilesToProcess.ContainsKey("units") || Prefabs.prefabsFilesToProcess["units"] == null) - Prefabs.prefabsFilesToProcess["units"] = new(); - - Prefabs.prefabsFilesToProcess["units"].Add(file.bytes); - - // var unitPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions - // { - // Converters = { new Vector2Json() }, - // PropertyNameCaseInsensitive = true, - // }); - // if (unitPrefab == null) - // return; + var unitPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (unitPrefab == null) + return; - // Prefabs.unitInfos.Add(unitPrefab); + Prefabs.unitInfos.Add(unitPrefab); Plugin.logger.LogInfo($"Registered unit prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod."); } @@ -738,20 +728,15 @@ public static void LoadImprovementPrefabFile(Mod mod, Mod.File file) { try { - if(!Prefabs.prefabsFilesToProcess.ContainsKey("improvements") || Prefabs.prefabsFilesToProcess["improvements"] == null) - Prefabs.prefabsFilesToProcess["improvements"] = new(); - - Prefabs.prefabsFilesToProcess["improvements"].Add(file.bytes); - - // var improvementPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions - // { - // Converters = { new Vector2Json() }, - // PropertyNameCaseInsensitive = true, - // }); - // if (improvementPrefab == null) - // return; + var improvementPrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (improvementPrefab == null) + return; - // Prefabs.improvementInfos.Add(improvementPrefab); + Prefabs.improvementInfos.Add(improvementPrefab); Plugin.logger.LogInfo($"Registered improvement prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod."); } @@ -765,20 +750,15 @@ public static void LoadResourcePrefabFile(Mod mod, Mod.File file) { try { - if(!Prefabs.prefabsFilesToProcess.ContainsKey("resources") || Prefabs.prefabsFilesToProcess["resources"] == null) - Prefabs.prefabsFilesToProcess["resources"] = new(); - - Prefabs.prefabsFilesToProcess["resources"].Add(file.bytes); - - // var resourcePrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions - // { - // Converters = { new Vector2Json() }, - // PropertyNameCaseInsensitive = true, - // }); - // if (resourcePrefab == null) - // return; + var resourcePrefab = JsonSerializer.Deserialize(file.bytes, new JsonSerializerOptions + { + Converters = { new Vector2Json() }, + PropertyNameCaseInsensitive = true, + }); + if (resourcePrefab == null) + return; - // Prefabs.resourceInfos.Add(resourcePrefab); + Prefabs.resourceInfos.Add(resourcePrefab); Plugin.logger.LogInfo($"Registered resource prefab {Path.GetFileNameWithoutExtension(file.name)} file from {mod.id} mod."); } @@ -833,8 +813,6 @@ internal static void ProcessGameLogicData(GameLogicData gameLogicData, JObject r { CreateMappings(rootObject); ProcessCustomTribes(); - // OKAY BASICALLY I need to find a way to load certain files at this stage (?) - ProcessPrefabs(); ProcessEmbarkOverrides(); ProcessAttractOverrides(); } @@ -946,9 +924,48 @@ 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() { - Prefabs.ProcessPrefabs(); + try + { + foreach (KeyValuePair keyValuePair in Registry.filesQueueToBeProcessedAfterGLDMerge) + { + Mod mod = keyValuePair.Key; + Mod.File file = keyValuePair.Value; + Match prefabsMatch = Regex.Match(Path.GetFileName(file.name), @"^prefabs(?:_(.*))?\.json$"); + if (prefabsMatch.Success) + { + 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; + } + + } + } + catch (Exception e) + { + Plugin.logger.LogError($"Error on processing post gld merge files queue : {e.StackTrace}"); + } } /// diff --git a/src/Managers/Main.cs b/src/Managers/Main.cs index 0e36936..2ce6f4c 100644 --- a/src/Managers/Main.cs +++ b/src/Managers/Main.cs @@ -436,40 +436,28 @@ internal static void Load(GameLogicData gameLogicData, JObject json) Match prefabsMatch = Regex.Match(Path.GetFileName(file.name), @"^prefabs(?:_(.*))?\.json$"); if (prefabsMatch.Success) { - Loader.LoadPrefabsFile( - mod, - file - ); + Registry.filesQueueToBeProcessedAfterGLDMerge.Add(mod, file); continue; } Match unitPrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^unitPrefab(?:_(.*))?\.json$"); if (unitPrefabMatch.Success) { - Loader.LoadUnitPrefabFile( - mod, - file - ); + Registry.filesQueueToBeProcessedAfterGLDMerge.Add(mod, file); continue; } Match improvementPrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^improvementPrefab(?:_(.*))?\.json$"); if (improvementPrefabMatch.Success) { - Loader.LoadImprovementPrefabFile( - mod, - file - ); + Registry.filesQueueToBeProcessedAfterGLDMerge.Add(mod, file); continue; } Match resourcePrefabMatch = Regex.Match(Path.GetFileName(file.name), @"^resourcePrefab(?:_(.*))?\.json$"); if (resourcePrefabMatch.Success) { - Loader.LoadResourcePrefabFile( - mod, - file - ); + Registry.filesQueueToBeProcessedAfterGLDMerge.Add(mod, file); continue; } @@ -493,6 +481,8 @@ internal static void Load(GameLogicData gameLogicData, JObject json) TechItem.techTierFirebaseId.Add($"tech_research_{i}"); } Loader.ProcessGameLogicData(gameLogicData, json); + Loader.ProcessFilesQueue(); + 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 index addacf2..8d48c13 100644 --- a/src/Managers/Prefabs.cs +++ b/src/Managers/Prefabs.cs @@ -4,8 +4,6 @@ using UnityEngine; using static PrefabManager; using static SkinVisualsReference; -using System.Text.Json; -using PolyMod.Json; namespace PolyMod.Managers; @@ -17,7 +15,6 @@ public static class Prefabs internal static Material? cachedUnitMaterial; internal static Material? cachedResourceMaterial; - internal static int cachedResourceSortingLayerId; internal static Dictionary> prefabsFilesToProcess = new(); @@ -169,7 +166,7 @@ private static Unit ResolveUnitPrefab(UnitData.Type unitType, TribeType tribeTyp return lastResortPrefab!; } - internal static void ProcessPrefabs() + internal static void CreatePrefabs() { // get materials Material[] allLoadedMaterials = Resources.FindObjectsOfTypeAll(); @@ -178,80 +175,12 @@ internal static void ProcessPrefabs() if(loadedMaterial.name == "SpriteMaterial") { cachedResourceMaterial = loadedMaterial; - Console.Write("MATERIAL FUCKING FOUND GOODBYE!"); GameObject.DontDestroyOnLoad(cachedResourceMaterial); break; } Plugin.logger.LogInfo($"Material: {loadedMaterial.name}, shader: {loadedMaterial.shader.name}"); } - cachedResourceSortingLayerId = MeshCache.TERRAIN_LAYER_ID; - // i will remove this bs - foreach (var keyValuePair in prefabsFilesToProcess) - { - if(keyValuePair.Key == "all") - { - foreach (var bytes in keyValuePair.Value) - { - var prefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions - { - Converters = { new Vector2Json() }, - PropertyNameCaseInsensitive = true, - }); - if (prefab == null) - return; - - unitInfos.AddRange(prefab.units); - improvementInfos.AddRange(prefab.improvements); - resourceInfos.AddRange(prefab.resources); - } - } - else if(keyValuePair.Key == "units") - { - foreach (var bytes in keyValuePair.Value) - { - var unitPrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions - { - Converters = { new Vector2Json() }, - PropertyNameCaseInsensitive = true, - }); - if (unitPrefab == null) - return; - - unitInfos.Add(unitPrefab); - } - } - else if(keyValuePair.Key == "improvements") - { - foreach (var bytes in keyValuePair.Value) - { - var improvementPrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions - { - Converters = { new Vector2Json() }, - PropertyNameCaseInsensitive = true, - }); - if (improvementPrefab == null) - return; - - improvementInfos.Add(improvementPrefab); - } - } - else if(keyValuePair.Key == "resources") - { - foreach (var bytes in keyValuePair.Value) - { - var resourcePrefab = JsonSerializer.Deserialize(bytes, new JsonSerializerOptions - { - Converters = { new Vector2Json() }, - PropertyNameCaseInsensitive = true, - }); - if (resourcePrefab == null) - return; - - resourceInfos.Add(resourcePrefab); - } - } - } foreach (var unitPrefabInfo in unitInfos) { ProcessUnitPrefabInfo(unitPrefabInfo); @@ -395,7 +324,7 @@ private static SkinVisualsReference.VisualPart CreateImprovementVisualPart( int visualPartSortingOrder, int outlineSortingOrder) { - return null; + return new VisualPart(); } #endregion diff --git a/src/Registry.cs b/src/Registry.cs index 718dfdc..9ac5c49 100644 --- a/src/Registry.cs +++ b/src/Registry.cs @@ -66,6 +66,8 @@ 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); @@ -77,8 +79,6 @@ public static class Registry /// public static Sprite? GetSpriteByText(string baseName, string tribeText = "", string skinText = "", int level = -1) { - if(baseName.Contains("oil")) - Console.Write("YESSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS"); Sprite? sprite = null; baseName = baseName.ToLower(); tribeText = tribeText.ToLower(); @@ -106,8 +106,7 @@ public static class Registry { sprite = sprites.GetOrDefault(spriteName, sprite); } - if(baseName.Contains("oil")) - Console.Write(sprite == null); + return sprite; } From 8fa7f3ea5dcea88c9af36a1d2f3cd2ebcdce3e37 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sat, 17 Oct 2026 22:50:33 +0200 Subject: [PATCH 7/9] Finished improvements --- src/Managers/Hub.cs | 23 +++++- src/Managers/Prefabs.cs | 143 ++++++++++++++++++++++++++------- src/Managers/Visual.cs | 36 +++------ src/Prefabs/OtherPrefabInfo.cs | 3 + 4 files changed, 149 insertions(+), 56 deletions(-) diff --git a/src/Managers/Hub.cs b/src/Managers/Hub.cs index 9400b51..69e399d 100644 --- a/src/Managers/Hub.cs +++ b/src/Managers/Hub.cs @@ -392,6 +392,25 @@ internal static void DumpPrefabs() // TODO { continue; } + + 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; + } + } + var visualPartInfo = new VisualPartInfo { coordinates = visualPart.visualPart.transform.position, @@ -400,7 +419,9 @@ internal static void DumpPrefabs() // TODO tintable = visualPart.tintable, skinningLogic = visualPart.skinLogic, improvementSkinningLogic = visualPart.improvementSkinLogic, - levels = visualPart.levels + levels = visualPart.levels, + sortingOrderOverride = sortingOrderOverride, + outlineSortingOrderOverride = outlineSortingOrderOverride }; unitPrefabInfo.visualParts.Add(visualPartInfo); diff --git a/src/Managers/Prefabs.cs b/src/Managers/Prefabs.cs index 8d48c13..33b2d74 100644 --- a/src/Managers/Prefabs.cs +++ b/src/Managers/Prefabs.cs @@ -14,7 +14,7 @@ public static class Prefabs internal static Dictionary unitUIOverridesNew = new(); internal static Material? cachedUnitMaterial; - internal static Material? cachedResourceMaterial; + internal static Material? cachedSharedMaterial; internal static Dictionary> prefabsFilesToProcess = new(); @@ -174,8 +174,8 @@ internal static void CreatePrefabs() { if(loadedMaterial.name == "SpriteMaterial") { - cachedResourceMaterial = loadedMaterial; - GameObject.DontDestroyOnLoad(cachedResourceMaterial); + cachedSharedMaterial = loadedMaterial; + GameObject.DontDestroyOnLoad(cachedSharedMaterial); break; } Plugin.logger.LogInfo($"Material: {loadedMaterial.name}, shader: {loadedMaterial.shader.name}"); @@ -254,7 +254,7 @@ internal static void ProcessUnitPrefabInfo(UnitPrefabInfo unitPrefabInfo) newVisualInfo.Reverse(); foreach (var info in newVisualInfo) { - parts.Add(CreateUnitVisualPart(info, spriteContainer, nextPartSortingOrder, nextOutlineSortingOrder)); + parts.Add(CreateUnitVisualPart(info, spriteContainer, info.sortingOrderOverride ?? nextPartSortingOrder, info.outlineSortingOrderOverride ?? nextOutlineSortingOrder)); if(nextOutlineSortingOrder < -1) { nextOutlineSortingOrder++; @@ -315,16 +315,119 @@ private static SkinVisualsReference.VisualPart CreateUnitVisualPart( #region Improvements internal static void ProcessImprovementPrefabInfo(ImprovementPrefabInfo improvementPrefabInfo) { - var baseImprovement = PrefabManager.GetPrefab(ImprovementData.Type.LumberHut); + 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 SkinVisualsReference.VisualPart CreateImprovementVisualPart( + 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) + 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 = new SkinVisualsReference.VisualPart + { + DefaultSpriteName = info.defaultSpriteName, + visualPart = visualPartObj, + outline = outlineObj, + tintable = info.tintable, + skinLogic = info.skinningLogic, + improvementSkinLogic = info.improvementSkinningLogic, + levels = info.levels, + isImprovement = true, + }; + 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") { - return new VisualPart(); + 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 @@ -349,7 +452,7 @@ internal static void ProcessResourcePrefabInfo(ResourcePrefabInfo resourcePrefab GameObject.Destroy(resourceTransform.GetChild(childIndex).gameObject); } - var visualParts = ApplyResourceVisualParts(resourcePrefabInfo, resourceInstance.transform); + var visualParts = ApplyResourceVisualParts(resourcePrefabInfo, resourceTransform); var svr = resourceInstance.GetComponent(); svr.visualParts = visualParts.ToArray(); @@ -421,27 +524,5 @@ private static SkinVisualsReference.VisualPart CreateResourceVisualPart( return visualPart; } - private static PolytopiaSpriteRenderer AddPolytopiaSpriteRenderer(GameObject targetGameObject, int sortingOrder) - { - 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("Terrain"); - polytopiaSpriteRenderer.SortingOrder = sortingOrder; - if (cachedResourceMaterial != null) - { - polytopiaSpriteRenderer.SharedMaterial = cachedResourceMaterial; - } - return polytopiaSpriteRenderer; - } - #endregion } \ No newline at end of file diff --git a/src/Managers/Visual.cs b/src/Managers/Visual.cs index f67a3f8..6f3d2cf 100644 --- a/src/Managers/Visual.cs +++ b/src/Managers/Visual.cs @@ -243,41 +243,29 @@ private static void PolytopiaSpriteRenderer_ForceUpdateMesh(PolytopiaSpriteRende #endregion #region UI - // check if still needed - /// 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) + [HarmonyPatch(typeof(SpriteData), nameof(SpriteData.TerrainToString))] + private static void SpriteData_TerrainToString(ref string __result, Polytopia.Data.TerrainData.Type terrain) { - Sprite? sprite = Registry.GetSprite(EnumCache.GetName(improvement), tribe, skin); - Console.Write(EnumCache.GetName(improvement)); - Console.Write(sprite == null); - if (sprite != null) - { - __result = sprite; - } + if((int)terrain >= Plugin.AUTOIDX_STARTS_FROM) + __result = EnumCache.GetName(terrain); } - /// 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) + [HarmonyPatch(typeof(SpriteData), nameof(SpriteData.ResourceToString))] + private static void SpriteData_ResourceToString(ref string __result, ResourceData.Type resource) { - UIUtils_GetImprovementSprite(ref __result, improvement, data.foundingTribeSettings.tribe, data.foundingTribeSettings.skin, atlasManager); + if((int)resource >= Plugin.AUTOIDX_STARTS_FROM) + __result = EnumCache.GetName(resource); } - /// 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) + [HarmonyPatch(typeof(SpriteData), nameof(SpriteData.ImprovementToString))] + private static void SpriteData_ImprovementToString(ref string __result, ImprovementData.Type improvement) { - TribeAndSkin tileClimateSettings = data.tileClimateSettings; - Sprite? sprite = Registry.GetSprite(EnumCache.GetName(resource), tileClimateSettings.tribe, tileClimateSettings.skin); - if (sprite != null) - { - __result = sprite; - } + if((int)improvement >= Plugin.AUTOIDX_STARTS_FROM) + __result = EnumCache.GetName(improvement); } #endregion diff --git a/src/Prefabs/OtherPrefabInfo.cs b/src/Prefabs/OtherPrefabInfo.cs index ce5a44f..46f26e2 100644 --- a/src/Prefabs/OtherPrefabInfo.cs +++ b/src/Prefabs/OtherPrefabInfo.cs @@ -24,6 +24,9 @@ public class ImprovementPrefabInfo : BasePrefabInfo [JsonInclude] [JsonConverter(typeof(EnumCacheJson))] public ImprovementData.Type? improvementType = null; + + [JsonInclude] + public bool needsOutlines = false; } public class ResourcePrefabInfo : BasePrefabInfo From 8fef382a82516b17001f0f253f54a30c4402f304 Mon Sep 17 00:00:00 2001 From: Maksym Muraviov$ Date: Sat, 17 Oct 2026 23:48:33 +0200 Subject: [PATCH 8/9] added improvement and resource dumping, added helpers methods to visual parts --- src/Managers/Hub.cs | 81 ++++++++++++++++++++++++----------- src/Managers/Prefabs.cs | 42 +++++------------- src/Prefabs/VisualPartInfo.cs | 44 +++++++++++++++++++ 3 files changed, 111 insertions(+), 56 deletions(-) diff --git a/src/Managers/Hub.cs b/src/Managers/Hub.cs index 69e399d..551c527 100644 --- a/src/Managers/Hub.cs +++ b/src/Managers/Hub.cs @@ -393,41 +393,72 @@ internal static void DumpPrefabs() // TODO continue; } - int? sortingOrderOverride = null; - SpriteRenderer spriteRenderer = visualPart.visualPart.GetComponent(); - if (spriteRenderer != null) + 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) { - sortingOrderOverride = spriteRenderer.sortingOrder; + continue; } - int? outlineSortingOrderOverride = null; + var visualPartInfo = new VisualPartInfo { }; + visualPartInfo.CopyFromVisualPart(visualPart); + improvementPrefabInfo.visualParts.Add(visualPartInfo); - if (visualPart.outline != null) + } + 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) { - spriteRenderer = visualPart.outline.GetComponent(); - if (spriteRenderer != null) - { - outlineSortingOrderOverride = spriteRenderer.sortingOrder; - } + continue; } - var visualPartInfo = new VisualPartInfo - { - 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, - sortingOrderOverride = sortingOrderOverride, - outlineSortingOrderOverride = outlineSortingOrderOverride - }; - unitPrefabInfo.visualParts.Add(visualPartInfo); + var visualPartInfo = new VisualPartInfo { }; + visualPartInfo.CopyFromVisualPart(visualPart); + resourcePrefabInfo.visualParts.Add(visualPartInfo); } - prefabsInfo.units.Add(unitPrefabInfo); + prefabsInfo.resources.Add(resourcePrefabInfo); } + File.WriteAllTextAsync( Path.Combine(Plugin.PREFABS_DUMP_PATH, $"prefabs.json"), JsonSerializer.Serialize(prefabsInfo, new JsonSerializerOptions { WriteIndented = true, Converters = { new Vector2Json() },}) diff --git a/src/Managers/Prefabs.cs b/src/Managers/Prefabs.cs index 33b2d74..bca4d32 100644 --- a/src/Managers/Prefabs.cs +++ b/src/Managers/Prefabs.cs @@ -285,16 +285,9 @@ private static SkinVisualsReference.VisualPart CreateUnitVisualPart( // this bs was hunting me for so long. that is the thing which makes outline hidden in ui outlineObj.SetActive(false); - var visualPart = new SkinVisualsReference.VisualPart - { - DefaultSpriteName = info.defaultSpriteName, - visualPart = visualPartObj, - outline = outlineObj, - tintable = info.tintable, - skinLogic = info.skinningLogic, - improvementSkinLogic = info.improvementSkinningLogic, - levels = info.levels - }; + var visualPart = info.ToVisualPart(false); + visualPart.visualPart = visualPartObj; + visualPart.outline = outlineObj; var renderer = visualPartObj.AddComponent(); renderer.material = cachedUnitMaterial; @@ -385,17 +378,10 @@ private static SkinVisualsReference.VisualPart CreateBuildingVisualPart( outlineObj.SetActive(false); - var visualPart = new SkinVisualsReference.VisualPart - { - DefaultSpriteName = info.defaultSpriteName, - visualPart = visualPartObj, - outline = outlineObj, - tintable = info.tintable, - skinLogic = info.skinningLogic, - improvementSkinLogic = info.improvementSkinningLogic, - levels = info.levels, - isImprovement = true, - }; + var visualPart = info.ToVisualPart(true); + visualPart.visualPart = visualPartObj; + visualPart.outline = outlineObj; + var renderer = AddPolytopiaSpriteRenderer(visualPartObj, visualPartSortingOrder); visualPart.renderer = new SkinVisualsReference.RendererUnion { polytopiaSpriteRenderer = renderer }; @@ -505,16 +491,10 @@ private static SkinVisualsReference.VisualPart CreateResourceVisualPart( outlineObj.SetActive(false); - var visualPart = new SkinVisualsReference.VisualPart - { - DefaultSpriteName = info.defaultSpriteName, - visualPart = visualPartObj, - outline = outlineObj, - tintable = info.tintable, - skinLogic = info.skinningLogic, - improvementSkinLogic = info.improvementSkinningLogic, - levels = info.levels - }; + var visualPart = info.ToVisualPart(false); + visualPart.visualPart = visualPartObj; + visualPart.outline = outlineObj; + var renderer = AddPolytopiaSpriteRenderer(visualPartObj, visualPartSortingOrder); visualPart.renderer = new SkinVisualsReference.RendererUnion { polytopiaSpriteRenderer = renderer }; diff --git a/src/Prefabs/VisualPartInfo.cs b/src/Prefabs/VisualPartInfo.cs index 4b6fd47..1296396 100644 --- a/src/Prefabs/VisualPartInfo.cs +++ b/src/Prefabs/VisualPartInfo.cs @@ -34,4 +34,48 @@ public class VisualPartInfo [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 From 593d6438d278039499bc01d6d3a44c4ced131aff Mon Sep 17 00:00:00 2001 From: Maksym Muraviov <109823841+johnklipi@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:09:19 +0200 Subject: [PATCH 9/9] minor thinghy --- src/Managers/Prefabs.cs | 9 ++++++--- src/Prefabs/OtherPrefabInfo.cs | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Managers/Prefabs.cs b/src/Managers/Prefabs.cs index bca4d32..cae6fb9 100644 --- a/src/Managers/Prefabs.cs +++ b/src/Managers/Prefabs.cs @@ -10,14 +10,12 @@ namespace PolyMod.Managers; public static class Prefabs { internal static Dictionary unitsNew = new(); - internal static Dictionary> unitOverridesNew = 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 static Dictionary> prefabsFilesToProcess = new(); - public static List unitInfos = new(); public static List improvementInfos = new(); @@ -235,6 +233,11 @@ internal static void ProcessUnitPrefabInfo(UnitPrefabInfo unitPrefabInfo) GameObject.DontDestroyOnLoad(unitInstance.gameObject); + if(unitPrefabInfo.uiOverride) + { + unitUIOverridesNew[unitPrefabInfo.skinnedKey] = unitInstance; + return; + } unitsNew[unitPrefabInfo.skinnedKey] = unitInstance; } diff --git a/src/Prefabs/OtherPrefabInfo.cs b/src/Prefabs/OtherPrefabInfo.cs index 46f26e2..fd2eecf 100644 --- a/src/Prefabs/OtherPrefabInfo.cs +++ b/src/Prefabs/OtherPrefabInfo.cs @@ -17,6 +17,9 @@ public class UnitPrefabInfo : BasePrefabInfo [JsonInclude] public string headPositionMarker = ""; + + [JsonInclude] + public bool uiOverride = false; } public class ImprovementPrefabInfo : BasePrefabInfo @@ -34,4 +37,11 @@ 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