diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index db51c1a..8a4b34d 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -15,8 +15,8 @@ jobs:
with:
path: ~/.nuget/packages
key: nuget-${{ hashFiles('**/*.csproj') }}
- - name: Build
- run: dotnet build -warnaserror
+ - name: Build and test
+ run: dotnet test -warnaserror
- name: Build nuget
if: github.event_name == 'workflow_dispatch'
run: dotnet pack PolyMod.csproj -o nuget
diff --git a/PolyMod.sln b/PolyMod.sln
index 2e325f4..e01aa6c 100644
--- a/PolyMod.sln
+++ b/PolyMod.sln
@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33205.214
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PolyMod", "PolyMod.csproj", "{58B09361-FD7A-48F1-82E1-E2359ADA512F}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PolyMod", "src\PolyMod.csproj", "{58B09361-FD7A-48F1-82E1-E2359ADA512F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "tests\Tests.csproj", "{85432E7B-2023-477C-861E-0141E8974DDC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -12,6 +14,8 @@ Global
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{58B09361-FD7A-48F1-82E1-E2359ADA512F}.IL2CPP|Any CPU.ActiveCfg = IL2CPP|Any CPU
{58B09361-FD7A-48F1-82E1-E2359ADA512F}.IL2CPP|Any CPU.Build.0 = IL2CPP|Any CPU
+ {85432E7B-2023-477C-861E-0141E8974DDC}.IL2CPP|Any CPU.ActiveCfg = Debug|Any CPU
+ {85432E7B-2023-477C-861E-0141E8974DDC}.IL2CPP|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..6ebe1ff
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Api/Config.cs b/src/Api/Config.cs
index 881ccb9..f253bf8 100644
--- a/src/Api/Config.cs
+++ b/src/Api/Config.cs
@@ -27,25 +27,25 @@ public Config(string modName, ConfigTypes configType)
switch (configType)
{
case ConfigTypes.PerMod:
- {
- if (!File.Exists(perModConfigPath))
{
- return;
+ if (!File.Exists(perModConfigPath))
+ {
+ return;
+ }
+ var jsonText = File.ReadAllText(perModConfigPath);
+ currentConfig = JsonSerializer.Deserialize(jsonText);
+ break;
}
- var jsonText = File.ReadAllText(perModConfigPath);
- currentConfig = JsonSerializer.Deserialize(jsonText);
- break;
- }
case ConfigTypes.Exposed:
- {
- if (!File.Exists(ExposedConfigPath))
{
- return;
+ if (!File.Exists(ExposedConfigPath))
+ {
+ return;
+ }
+ var jsonText = File.ReadAllText(ExposedConfigPath);
+ currentConfig = JsonNode.Parse(jsonText)![modName]?.Deserialize();
+ break;
}
- var jsonText = File.ReadAllText(ExposedConfigPath);
- currentConfig = JsonNode.Parse(jsonText)![modName]?.Deserialize();
- break;
- }
default:
throw new ArgumentOutOfRangeException();
}
@@ -60,7 +60,7 @@ public void SetDefaultConfig(T defaultValue)
Write(defaultConfig);
SaveChanges();
}
-
+
///
/// Writes the **entire** config. Usage not recommended, use Edit() instead
///
diff --git a/src/Api/GLDConfig.cs b/src/Api/GLDConfig.cs
index 6384782..ecda511 100644
--- a/src/Api/GLDConfig.cs
+++ b/src/Api/GLDConfig.cs
@@ -8,7 +8,7 @@ namespace PolyMod.Api;
public class GldConfigTemplate
{
private static readonly string ConfigPath = Path.Combine(Plugin.BASE_PATH, "mods.json");
-
+
private readonly string templateText;
private JsonObject currentConfig = new();
private string modName;
@@ -24,8 +24,8 @@ private void Load()
if (File.Exists(ConfigPath))
{
var json = File.ReadAllText(ConfigPath);
- if (JsonNode.Parse(json) is JsonObject modsConfig
- && modsConfig.TryGetPropertyValue(modName, out var modConfigNode)
+ if (JsonNode.Parse(json) is JsonObject modsConfig
+ && modsConfig.TryGetPropertyValue(modName, out var modConfigNode)
&& modConfigNode is JsonObject modConfig)
{
currentConfig = modConfig;
@@ -41,9 +41,9 @@ private void Load()
var template = Template.Parse(templateText);
var context = new TemplateContext();
var scriptObject = new ScriptObject();
-
+
bool changedConfig = false;
- scriptObject.Import("config",
+ scriptObject.Import("config",
new Func((key, defaultValue) =>
{
if (currentConfig.TryGetPropertyValue(key, out var token) && token != null)
@@ -53,7 +53,7 @@ private void Load()
changedConfig = true;
currentConfig[key] = defaultValue;
-
+
return defaultValue;
})
);
diff --git a/src/Api/PolyScript.cs b/src/Api/PolyScript.cs
index 8bf8ab2..09fd36e 100644
--- a/src/Api/PolyScript.cs
+++ b/src/Api/PolyScript.cs
@@ -11,7 +11,7 @@ public abstract class PolyScriptBase
/// The mod instance.
/// The logger instance.
internal abstract void Initialize(Mod mod, ManualLogSource logger);
-
+
/// >
/// Called when the mod is loaded.
///
@@ -41,7 +41,7 @@ internal override void Initialize(Mod mod, ManualLogSource logger)
return Registry.mods[Mod.id].files.FirstOrDefault(f => f.name == fileName)?.bytes;
}
- public override void Unload() {}
+ public override void Unload() { }
public Mod Mod { get; private set; } = null!;
protected Config Config { get; private set; } = null!;
diff --git a/src/Json/JsonMerger.cs b/src/Json/JsonMerger.cs
index 3c11a7b..44eb212 100644
--- a/src/Json/JsonMerger.cs
+++ b/src/Json/JsonMerger.cs
@@ -99,13 +99,13 @@ private static JArray MergeArrays(JArray original, JArray patch, bool isSkins)
result.Remove(rem);
hasCustomValues = true;
}
- else if(isSkins)
+ else if (isSkins)
{
result.Add(str);
hasCustomValues = true;
}
}
- if(!hasCustomValues)
+ if (!hasCustomValues)
{
result = new JArray(patch);
}
diff --git a/src/Loader.cs b/src/Loader.cs
index 69fe92b..87d5a76 100644
--- a/src/Loader.cs
+++ b/src/Loader.cs
@@ -24,1096 +24,1096 @@ namespace PolyMod;
///
public static class Loader
{
- internal record TypeMapping(Type type, bool shouldCreateCache = true);
-
- ///
- /// Mappings from JSON data types to their corresponding C# types.
- ///
- internal static Dictionary typeMappings = new()
- {
- { "tribeData", new TypeMapping(typeof(TribeType)) },
- { "techData", new TypeMapping(typeof(TechData.Type)) },
- { "unitData", new TypeMapping(typeof(UnitData.Type)) },
- { "improvementData", new TypeMapping(typeof(ImprovementData.Type)) },
- { "terrainData", new TypeMapping(typeof(Polytopia.Data.TerrainData.Type)) },
- { "resourceData", new TypeMapping(typeof(ResourceData.Type)) },
- { "taskData", new TypeMapping(typeof(TaskData.Type)) },
- { "tribeAbility", new TypeMapping(typeof(TribeAbility.Type)) },
- { "unitAbility", new TypeMapping(typeof(UnitAbility.Type)) },
- { "improvementAbility", new TypeMapping(typeof(ImprovementAbility.Type)) },
- { "playerAbility", new TypeMapping(typeof(PlayerAbility.Type)) },
- { "weaponData", new TypeMapping(typeof(UnitData.WeaponEnum)) },
- { "skinData", new TypeMapping(typeof(SkinData), false) }
- };
-
- ///
- /// List of custom game modes to be added.
- ///
- internal static List gamemodes = new();
-
- ///
- /// Handlers for processing specific data types during mod loading.
- ///
- internal static readonly Dictionary>> typeHandlers = new()
- {
- [typeof(TribeType)] = new List>() {
- new((token, duringEnumCacheCreation) =>
- {
- if (duringEnumCacheCreation)
- {
- Registry.customTribes.Add((TribeType)(int)token["idx"]);
- token["style"] = Registry.climateAutoidx;
- token["climate"] = Registry.climateAutoidx;
- Registry.climateAutoidx++;
- }
- else
- {
- if (token["skins"] != null)
- {
- JArray skins = token["skins"].Cast();
- List skinValues = skins._values.ToArray().ToList();
- foreach (var skin in skinValues)
- {
- string skinId = skin.ToString();
- if (!Enum.TryParse(skinId, ignoreCase: true, out _))
- {
- SkinType skinValue = (SkinType)Registry.autoidx;
- EnumCache.AddMapping(skinId.ToLowerInvariant(), skinValue);
- EnumCache.AddMapping(skinId.ToLowerInvariant(), skinValue);
- Registry.skinInfo.Add(new Visual.SkinInfo(Registry.autoidx, skinId, null));
- Plugin.logger.LogInfo("Created mapping for skinType with id " + skinId + " and index " + Registry.autoidx);
- Registry.autoidx++;
- if(token["tribeAbilities"] != null)
- {
- JArray tribeAbilities = token["tribeAbilities"].Cast();
- List tribeAbilitiesValues = tribeAbilities._values.ToArray().ToList();
-
- foreach(JToken ability in tribeAbilitiesValues)
- {
- string abilityValue = ability.ToString();
-
- if (Enum.TryParse(
- abilityValue, ignoreCase: true,
- out TribeAbility.Type tribeAbilityType
- ) && tribeAbilityType == TribeAbility.Type.Flooded)
- {
- string tileEffectId = skinId.ToLowerInvariant() + "_flood";
- EnumCache.AddMapping(tileEffectId, (TileData.EffectType)Registry.autoidx);
- EnumCache.AddMapping(tileEffectId, (TileData.EffectType)Registry.autoidx);
+ internal record TypeMapping(Type type, bool shouldCreateCache = true);
+
+ ///
+ /// Mappings from JSON data types to their corresponding C# types.
+ ///
+ internal static Dictionary typeMappings = new()
+ {
+ { "tribeData", new TypeMapping(typeof(TribeType)) },
+ { "techData", new TypeMapping(typeof(TechData.Type)) },
+ { "unitData", new TypeMapping(typeof(UnitData.Type)) },
+ { "improvementData", new TypeMapping(typeof(ImprovementData.Type)) },
+ { "terrainData", new TypeMapping(typeof(Polytopia.Data.TerrainData.Type)) },
+ { "resourceData", new TypeMapping(typeof(ResourceData.Type)) },
+ { "taskData", new TypeMapping(typeof(TaskData.Type)) },
+ { "tribeAbility", new TypeMapping(typeof(TribeAbility.Type)) },
+ { "unitAbility", new TypeMapping(typeof(UnitAbility.Type)) },
+ { "improvementAbility", new TypeMapping(typeof(ImprovementAbility.Type)) },
+ { "playerAbility", new TypeMapping(typeof(PlayerAbility.Type)) },
+ { "weaponData", new TypeMapping(typeof(UnitData.WeaponEnum)) },
+ { "skinData", new TypeMapping(typeof(SkinData), false) }
+ };
+
+ ///
+ /// List of custom game modes to be added.
+ ///
+ internal static List gamemodes = new();
+
+ ///
+ /// Handlers for processing specific data types during mod loading.
+ ///
+ internal static readonly Dictionary>> typeHandlers = new()
+ {
+ [typeof(TribeType)] = new List>() {
+ new((token, duringEnumCacheCreation) =>
+ {
+ if (duringEnumCacheCreation)
+ {
+ Registry.customTribes.Add((TribeType)(int)token["idx"]);
+ token["style"] = Registry.climateAutoidx;
+ token["climate"] = Registry.climateAutoidx;
+ Registry.climateAutoidx++;
+ }
+ else
+ {
+ if (token["skins"] != null)
+ {
+ JArray skins = token["skins"].Cast();
+ List skinValues = skins._values.ToArray().ToList();
+ foreach (var skin in skinValues)
+ {
+ string skinId = skin.ToString();
+ if (!Enum.TryParse(skinId, ignoreCase: true, out _))
+ {
+ SkinType skinValue = (SkinType)Registry.autoidx;
+ EnumCache.AddMapping(skinId.ToLowerInvariant(), skinValue);
+ EnumCache.AddMapping(skinId.ToLowerInvariant(), skinValue);
+ Registry.skinInfo.Add(new Visual.SkinInfo(Registry.autoidx, skinId, null));
+ Plugin.logger.LogInfo("Created mapping for skinType with id " + skinId + " and index " + Registry.autoidx);
+ Registry.autoidx++;
+ if(token["tribeAbilities"] != null)
+ {
+ JArray tribeAbilities = token["tribeAbilities"].Cast();
+ List tribeAbilitiesValues = tribeAbilities._values.ToArray().ToList();
+
+ foreach(JToken ability in tribeAbilitiesValues)
+ {
+ string abilityValue = ability.ToString();
+
+ if (Enum.TryParse(
+ abilityValue, ignoreCase: true,
+ out TribeAbility.Type tribeAbilityType
+ ) && tribeAbilityType == TribeAbility.Type.Flooded)
+ {
+ string tileEffectId = skinId.ToLowerInvariant() + "_flood";
+ EnumCache.AddMapping(tileEffectId, (TileData.EffectType)Registry.autoidx);
+ EnumCache.AddMapping(tileEffectId, (TileData.EffectType)Registry.autoidx);
Visual.customFloodingSkins.Add((TileData.EffectType)Registry.autoidx, skinValue);
- Plugin.logger.LogInfo("Created mapping for tileEffect with id " + tileEffectId + " and index " + Registry.autoidx);
- Registry.autoidx++;
- }
- }
- }
- }
- }
- Il2CppSystem.Collections.Generic.List modifiedSkins = skins._values;
- foreach (var skin in Registry.skinInfo)
- {
- if (modifiedSkins.Contains(skin.id))
- {
- modifiedSkins.Remove(skin.id);
- modifiedSkins.Add(skin.idx.ToString());
- }
- }
- JArray newSkins = new JArray();
- foreach (var item in modifiedSkins)
- {
- newSkins.Add(item);
- }
- token["skins"] = newSkins;
- }
- if (token["preview"] != null)
- {
- Visual.PreviewTile[] preview = JsonSerializer.Deserialize(token["preview"].ToString())!;
- Registry.tribePreviews[Util.GetJTokenName(token)] = preview;
- }
- }
- })
- },
- [typeof(UnitData.Type)] = new List>() {
- new((token, 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);
- string embarkUnitId = token["embarksTo"].ToString();
- Main.embarkNames[unitId] = embarkUnitId;
- }
- if (token["weapon"] != null)
- {
- string weaponString = token["weapon"].ToString();
- if (EnumCache.TryGetType(weaponString, out UnitData.WeaponEnum type))
- {
- token["weapon"] = (int)type;
- }
- }
- }
- })
- },
-
- [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
- {
- string improvementId = Util.GetJTokenName(token);
- if (token["attractsResource"] != null)
- {
- string attractsId = token["attractsResource"].ToString();
- Main.attractsResourceNames[improvementId] = attractsId;
- }
- if (token["attractsToTerrain"] != null)
- {
- string attractsId = token["attractsToTerrain"].ToString();
- Main.attractsTerrainNames[improvementId] = attractsId;
- }
- if(token["infoOverride"] != null)
- {
- Loc.buildingsInfoOverrides[improvementId] = Loc.ReplaceDashesWithDots(token["infoOverride"].ToString());
- }
- }
- })
- },
-
- [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>() {
- new((token, duringEnumCacheCreation) =>
- {
- var prop = token.Parent.Cast();
- prop.Replace(new JProperty(prop.Name.ToLower(), prop.Value));
- })
- },
- };
-
- ///
- /// Represents information for a custom game mode button.
- ///
- /// The index of the game mode.
- /// The action to perform when the button is clicked.
- /// The index of the button in the UI.
- /// The sprite for the button.
- /// The name of the sprite for the button.
- public record GameModeButtonsInformation(int gameModeIndex, UIButtonBase.ButtonAction action, int? buttonIndex, Sprite? sprite, string? spriteName);
-
- ///
- /// Adds a new game mode button.
- ///
- /// The unique identifier for the game mode.
- /// The action to perform when the button is clicked.
- /// The boolean which decides if GameMode should appear in the menu.
- /// The sprite for the button.
- /// The name of the sprite for the button.
- public static void AddGameMode(string id, UIButtonBase.ButtonAction action, bool shouldShowInMenu = true, Sprite? sprite = null, string? spriteName = null)
- {
- EnumCache.AddMapping(id, (GameMode)Registry.gameModesAutoidx);
- EnumCache.AddMapping(id, (GameMode)Registry.gameModesAutoidx);
- if(shouldShowInMenu)
- gamemodes.Add(new GameModeButtonsInformation(Registry.gameModesAutoidx, action, null, sprite, spriteName));
- Registry.gameModesAutoidx++;
- }
-
- ///
- /// Adds a new data type for patching.
- ///
- /// The identifier for the data type in JSON.
- /// "The C# type corresponding to the identifier.
- public static void AddPatchDataType(string typeId, Type type)
- {
- if (!typeMappings.ContainsKey(typeId))
- typeMappings.Add(typeId, new TypeMapping(type));
- }
-
- public static void AddPatchDataType(string typeId, Type type, bool shouldCreateCache)
- {
- if (!typeMappings.ContainsKey(typeId))
- typeMappings.Add(typeId, new TypeMapping(type, shouldCreateCache));
- }
-
- public static void AddTypeHandler(Type type, Action handler)
- {
- string typeString = type.ToString();
- if(!typeMappings.ContainsValue(new TypeMapping(type, true)) && !typeMappings.ContainsValue(new TypeMapping(type, false)))
- {
- Plugin.logger.LogWarning($"Tried adding TypeHandler for type: {typeString} with missing TypeMapping. Please, add TypeMapping first.");
- return;
- }
- if(!typeHandlers.ContainsKey(type))
- typeHandlers[type] = new();
-
- typeHandlers[type].Add(handler);
-
- Plugin.logger.LogInfo($"Added TypeHandler for type: {typeString}.");
- }
-
- ///
- /// Loads all mods from the mods directory.
- ///
- /// A dictionary to populate with the loaded mods.
- internal static void RegisterMods(Dictionary mods)
- {
- Directory.CreateDirectory(Plugin.MODS_PATH);
- string[] modContainers = Directory.GetDirectories(Plugin.MODS_PATH)
- .Union(Directory.GetFiles(Plugin.MODS_PATH, "*.polymod"))
- .Union(Directory.GetFiles(Plugin.MODS_PATH, "*.zip"))
- .ToArray();
- foreach (var modContainer in modContainers)
- {
- Mod.Manifest? manifest = null;
- List files = new();
-
- // Load mod from directory or zip archive
- if (Directory.Exists(modContainer))
- {
- foreach (var file in Directory.GetFiles(modContainer, "*", SearchOption.AllDirectories))
- {
- ProcessModFile(
- Path.GetRelativePath(modContainer, file),
- File.ReadAllBytes(file),
- files,
- ref manifest
- );
- }
- }
- else
- {
- foreach (var entry in new ZipArchive(File.OpenRead(modContainer)).Entries)
- {
- ProcessModFile(
- entry.FullName,
- entry.ReadBytes(),
- files,
- ref manifest
- );
- }
- }
- #region ValidateManifest()
- if (manifest == null)
- {
- Plugin.logger.LogError($"Mod manifest not found in {modContainer}");
- continue;
- }
- if (manifest.id == null)
- {
- Plugin.logger.LogError($"Mod id not found in {modContainer}");
- continue;
- }
- if (!Regex.IsMatch(manifest.id, @"^(?!polytopia$)[a-z_]+$"))
- {
- Plugin.logger.LogError($"Mod id {manifest.id} is invalid in {modContainer}");
- continue;
- }
- if (manifest.version == null)
- {
- Plugin.logger.LogError($"Mod version not found in {modContainer}");
- continue;
- }
- if (manifest.authors == null || manifest.authors.Length == 0)
- {
- Plugin.logger.LogError($"Mod authors not found in {modContainer}");
- continue;
- }
- if (mods.ContainsKey(manifest.id))
- {
- Plugin.logger.LogError($"Mod {manifest.id} already exists");
- continue;
- }
- #endregion
- mods.Add(manifest.id, new(
- manifest,
- Mod.Status.Success,
- files
- ));
- Plugin.logger.LogInfo($"Registered mod {manifest.id}");
- }
-
- CheckDependencies(mods);
- }
-
- private static void ProcessModFile(string fullName, byte[] bytes, List files, ref Mod.Manifest? manifest)
- {
- if (fullName == "manifest.json")
- {
- manifest = JsonSerializer.Deserialize(
- bytes,
- new JsonSerializerOptions()
- {
- Converters = { new VersionJson() },
- }
- );
- return;
- }
- files.Add(new(fullName, bytes));
- }
-
- internal static void LoadMods(Dictionary mods, out bool dependencyCycle)
- {
- dependencyCycle = !SortMods(Registry.mods);
- if (dependencyCycle) return;
-
- StringBuilder checksumString = new();
- foreach (var (id, mod) in Registry.mods)
- {
- if (mod.status != Mod.Status.Success) continue;
- foreach (var file in mod.files)
- {
- checksumString.Append(JsonSerializer.Serialize(file));
- if (Path.GetExtension(file.name) == ".dll")
- {
- LoadAssemblyFile(mod, file);
- }
- Match spritesMatch = Regex.Match(Path.GetFileName(file.name), @"^sprites(?:_(.*))?\.json$");
- if (spritesMatch.Success)
- {
- LoadSpriteInfoFile(mod, file);
- }
- Match languageMatch = Regex.Match(Path.GetFileName(file.name), @"^language(?:_(.*))?\.json$");
- if (languageMatch.Success)
- {
- string languageName = languageMatch.Groups[1].Value;
- LoadLanguageFile(
- mod,
- file,
- languageName
- );
- }
- }
- if (!mod.client && id != "polytopia")
- {
- checksumString.Append(id);
- checksumString.Append(mod.version.ToString());
- }
- }
- Compatibility.HashSignatures(checksumString);
-
- }
- private static void CheckDependencies(Dictionary mods)
- {
- foreach (var (id, mod) in mods)
- {
- foreach (var dependency in mod.dependencies ?? Array.Empty())
- {
- string? message = null;
- if (!mods.ContainsKey(dependency.id))
- {
- message = $"Dependency {dependency.id} not found";
- }
- else
- {
- Version version = mods[dependency.id].version;
- if (
- (dependency.min != null && version < dependency.min)
- ||
- (dependency.max != null && version > dependency.max)
- )
- {
- message = $"Need dependency {dependency.id} version {dependency.min} - {dependency.max} found {version}";
- }
- }
- if (message != null)
- {
- if (dependency.required)
- {
- Plugin.logger.LogError(message);
- mod.status = Mod.Status.DependenciesUnsatisfied;
- }
- else
- {
- Plugin.logger.LogWarning(message);
- }
- }
- }
- }
- }
-
- ///
- /// Sorts mods based on their dependencies using a topological sort.
- ///
- /// The dictionary of mods to sort.
- /// True if the mods could be sorted (no circular dependencies), false otherwise.
- private static bool SortMods(Dictionary mods)
- {
- Stopwatch s = new();
- Dictionary> graph = new();
- Dictionary inDegree = new();
- Dictionary successfulMods = new();
- Dictionary unsuccessfulMods = new();
- foreach (var (id, mod) in mods)
- {
- if (mod.status == Mod.Status.Success) successfulMods.Add(id, mod);
- else unsuccessfulMods.Add(id, mod);
- }
- foreach (var (id, _) in successfulMods)
- {
- graph[id] = new();
- inDegree[id] = 0;
- }
- foreach (var (id, mod) in successfulMods)
- {
- foreach (var dependency in mod.dependencies ?? Array.Empty())
- {
- if (!graph.ContainsKey(dependency.id)) continue;
- graph[dependency.id].Add(id);
- inDegree[id]++;
- }
- }
- Queue queue = new();
- foreach (var (id, _) in successfulMods)
- {
- if (inDegree[id] == 0)
- {
- queue.Enqueue(id);
- }
- }
- Dictionary sorted = new();
- while (queue.Count > 0)
- {
- var id = queue.Dequeue();
- var mod = successfulMods[id];
- sorted.Add(id, mod);
- foreach (var neighbor in graph[id])
- {
- inDegree[neighbor]--;
- if (inDegree[neighbor] == 0)
- {
- queue.Enqueue(neighbor);
- }
- }
- }
- if (sorted.Count != successfulMods.Count)
- {
- return false;
- }
- mods.Clear();
- mods.AddRange(sorted);
- mods.AddRange(unsuccessfulMods);
-
- return true;
- }
-
- ///
- /// Loads an assembly file from a mod.
- ///
- /// The mod the assembly belongs to.
- /// The assembly file to load.
- public static void LoadAssemblyFile(Mod mod, Mod.File file)
- {
- try
- {
- Assembly assembly = Assembly.Load(file.bytes);
- if (assembly
- .GetTypes()
- .FirstOrDefault(t => t.IsSubclassOf(typeof(Api.PolyScriptBase)))
- is { } modType)
- {
- var modInstance = (Api.PolyScriptBase) Activator.CreateInstance(modType)!;
- modInstance.Initialize(mod, BepInEx.Logging.Logger.CreateLogSource($"PolyMod] [{mod.id}"));
- modInstance.Load();
- return;
- }
- foreach (Type type in assembly.GetTypes())
- {
- MethodInfo? loadWithLogger = type.GetMethod("Load", new Type[] { typeof(ManualLogSource) });
- if (loadWithLogger != null)
- {
- loadWithLogger.Invoke(null, new object[]
- {
- BepInEx.Logging.Logger.CreateLogSource($"PolyMod] [{mod.id}")
- });
- Plugin.logger.LogInfo($"Invoked Load method with logger from {type.FullName} from {mod.id} mod");
- }
- MethodInfo? load = type.GetMethod("Load", Array.Empty());
- if (load != null)
- {
- load.Invoke(null, null);
- Plugin.logger.LogInfo($"Invoked Load method from {type.FullName} from {mod.id} mod");
- }
- }
- }
- catch (TargetInvocationException exception)
- {
- if (exception.InnerException != null)
- {
- Plugin.logger.LogError($"Error on loading assembly from {mod.id} mod: {exception.InnerException.Message}");
- mod.status = Mod.Status.Error;
- }
- }
- }
-
- ///
- /// Loads a localization file from a mod.
- ///
- /// The mod the localization file belongs to.
- /// The localization file to load.
- public static void LoadLocalizationFile(Mod mod, Mod.File file)
- {
- try
- {
- Loc.BuildAndLoadLocalization(JsonSerializer
- .Deserialize>>(file.bytes)!);
- Plugin.logger.LogInfo($"Registered localization from {mod.id} mod");
- }
- catch (Exception e)
- {
- Plugin.logger.LogError($"Error on loading locatization from {mod.id} mod: {e.StackTrace}");
- }
- }
-
- public static void LoadLanguageFile(Mod mod, Mod.File file, string languageName)
- {
- try
- {
- var dict = JsonSerializer.Deserialize>(file.bytes);
- if(dict != null)
- {
- Loc.languagesToAdd.Add(languageName, dict);
- Plugin.logger.LogInfo($"Registered language from {mod.id} mod");
- }
- }
- catch (Exception e)
- {
- Plugin.logger.LogError($"Error on loading language file from {mod.id} mod: {e.StackTrace}");
- }
- }
-
- ///
- /// Loads a sprite file from a mod.
- ///
- /// The mod the sprite file belongs to.
- /// The sprite file to load.
- public static void LoadSpriteFile(Mod mod, Mod.File file)
- {
- string name = Path.GetFileNameWithoutExtension(file.name);
- Vector2 pivot = name.Split("_")[0] switch
- {
- "field" => new(0.5f, 0.0f),
- "mountain" => new(0.5f, -0.375f),
- _ => new(0.5f, 0.5f),
- };
- float pixelsPerUnit = 2112f;
- if (Registry.spriteInfos.ContainsKey(name))
- {
- Visual.SpriteInfo spriteData = Registry.spriteInfos[name];
- pivot = spriteData.pivot ?? pivot;
- pixelsPerUnit = spriteData.pixelsPerUnit ?? pixelsPerUnit;
- }
- Sprite sprite = Visual.BuildSprite(file.bytes, pivot, pixelsPerUnit);
- GameManager.GetSpriteAtlasManager().cachedSprites.TryAdd("Heads", new());
- GameManager.GetSpriteAtlasManager().cachedSprites["Heads"].Add(name, sprite);
- Registry.sprites.Add(name, sprite);
- }
-
- ///
- /// Updates a sprite with new information.
- ///
- /// The name of the sprite to update.
- public static void UpdateSprite(string name)
- {
- if (Registry.spriteInfos.ContainsKey(name) && Registry.sprites.ContainsKey(name))
- {
- Visual.SpriteInfo spriteData = Registry.spriteInfos[name];
- Sprite sprite = Visual.BuildSpriteWithTexture(
- Registry.sprites[name].texture,
- spriteData.pivot,
- spriteData.pixelsPerUnit
- );
- GameManager.GetSpriteAtlasManager().cachedSprites["Heads"][name] = sprite;
- Registry.sprites[name] = sprite;
- }
- }
-
- ///
- /// Loads a sprite info file from a mod.
- ///
- /// The mod the sprite info file belongs to.
- /// The sprite info file to load.
- /// A dictionary of sprite information, or null if an error occurred.
- public static Dictionary? LoadSpriteInfoFile(Mod mod, Mod.File file)
- {
- try
- {
- var deserialized = JsonSerializer.Deserialize>(
- file.bytes,
- new JsonSerializerOptions()
- {
- Converters = { new Vector2Json() },
- }
- );
-
- if (deserialized != null)
- {
- foreach (var kvp in deserialized)
- {
- Registry.spriteInfos[kvp.Key] = kvp.Value;
- }
- }
-
- Plugin.logger.LogInfo($"Registered sprite data from {mod.id} mod");
- return deserialized;
- }
- catch (Exception e)
- {
- Plugin.logger.LogError($"Error on loading sprite data from {mod.id} mod: {e.StackTrace}");
- return null;
- }
- }
-
- ///
- /// Loads an audio file from a mod.
- ///
- /// The mod the audio file belongs to.
- /// The audio file to load.
- public static void LoadAudioFile(Mod mod, Mod.File file)
- {
- // AudioSource audioSource = new GameObject().AddComponent();
- // GameObject.DontDestroyOnLoad(audioSource);
- // audioSource.clip = Managers.Audio.BuildAudioClip(file.bytes);
- // Registry.audioClips.Add(Path.GetFileNameWithoutExtension(file.name), audioSource);
- // 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);
-
- var visualParts = ApplyVisualParts(prefab, spriteContainer, material);
-
- Transform? headPositionMarker = null;
- foreach (var vp in visualParts)
- {
- if (vp.visualPart.gameObject.name == prefab.headPositionMarker)
- {
- headPositionMarker = vp.visualPart.gameObject.transform;
- break;
- }
- }
-
- unitInstance.headPositionMarker = headPositionMarker ?? visualParts[0].visualPart.transform;
-
- var svr = unitInstance.GetComponent();
- svr.visualParts = visualParts.ToArray();
-
- 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;
- }
-
- ///
- /// Loads and applies a game logic data patch from a mod.
- ///
- /// The mod the patch belongs to.
- /// The original game logic data.
- /// The patch to apply.
- public static void LoadGameLogicDataPatch(Mod mod, JObject gld, JObject patch)
- {
- try
- {
- // Merge the patch into the game logic data
- gld = JsonMerger.Merge(gld, patch);
- Plugin.logger.LogInfo($"Registered patch from {mod.id} mod");
- }
- catch (Exception e)
- {
- Plugin.logger.LogError($"Error on loading patch from {mod.id} mod: {e.StackTrace}");
- mod.status = Mod.Status.Error;
- }
- }
-
- ///
- /// Loads an asset bundle from a mod.
- ///
- /// The mod the asset bundle belongs to.
- /// The asset bundle file to load.
- public static void LoadAssetBundle(Mod mod, Mod.File file)
- {
- Registry.assetBundles.Add(
- Path.GetFileNameWithoutExtension(file.name),
- AssetBundle.LoadFromMemory(file.bytes)
- );
- }
-
- ///
- /// Processes the merged game logic data after all mods have been loaded and patched.
- ///
- /// The game logic data object to populate.
- /// The root JObject of the merged game logic data.
- internal static void ProcessGameLogicData(GameLogicData gameLogicData, JObject rootObject)
- {
- try
- {
- CreateMappings(rootObject);
- ProcessCustomTribes();
- ProcessPrefabs();
- ProcessEmbarkOverrides();
- ProcessAttractOverrides();
- }
- catch (Exception e)
- {
- Plugin.logger.LogError($"Error on processing modified game logic data : {e.StackTrace}");
- }
- }
-
- internal static void ProcessCustomTribes()
- {
- foreach (var tribe in Registry.customTribes)
- {
- if(!GameLogicData.legacyTribeTypesOrder.Contains(tribe))
- {
- GameLogicData.legacyTribeTypesOrder.Add(tribe);
- }
- }
- }
-
- ///
- /// Creates EnumCache mappings for custom enum values and invokes type handlers.
- ///
- ///
- internal static void CreateMappings(JObject rootObject)
- {
- foreach (JToken jtoken in rootObject.SelectTokens("$.*.*").ToArray())
- {
- JObject? token = jtoken.TryCast();
- if (token == null)
- continue;
-
- string dataType = Util.GetJTokenName(token, 2);
- if (!typeMappings.TryGetValue(dataType, out TypeMapping? typeMapping))
- continue;
-
- if (token["idx"] == null || !typeMapping.shouldCreateCache)
- continue;
-
- Type targetType = typeMapping.type;
- if (!targetType.IsEnum)
- {
- Plugin.logger.LogWarning($"Type {targetType.FullName} is not an enum, skipping!");
- continue;
- }
-
- string id = Util.GetJTokenName(token);
-
- if((int)token["idx"] == -1)
- {
- token["idx"] = Registry.autoidx;
- Registry.autoidx++;
- }
- else if(Plugin.config.allowUnsafeIndexes)
- {
- Array values = Enum.GetValues(targetType);
-
- var maxValue = values.Cast().Max();
-
- if(maxValue >= (int)token["idx"])
- {
- continue;
- }
- }
- else
- {
- continue;
- }
-
- MethodInfo? methodInfo = typeof(EnumCache<>).MakeGenericType(targetType).GetMethod("AddMapping");
- if (methodInfo == null)
- {
- Plugin.logger.LogWarning($"Missing AddMapping method for {targetType.FullName}");
- continue;
- }
-
- methodInfo.Invoke(null, new object[] { id, (int)token["idx"] });
- methodInfo.Invoke(null, new object[] { id, (int)token["idx"] });
-
- if (typeHandlers.TryGetValue(targetType, out var handlers))
- {
- foreach(var handler in handlers)
- {
- handler(token, true);
- }
- }
- Plugin.logger.LogInfo("Created mapping for " + targetType.ToString() + " with id " + id + " and index " + (int)token["idx"]);
- }
- foreach (JToken jtoken in rootObject.SelectTokens("$.*.*").ToArray())
- {
- JObject? token = jtoken.TryCast();
- if (token != null)
- {
- string dataType = Util.GetJTokenName(token, 2);
- if (typeMappings.TryGetValue(dataType, out TypeMapping? typeMapping))
- {
- if (typeHandlers.TryGetValue(typeMapping.type, out var handlers))
- {
- foreach(var handler in handlers)
- {
- handler(token, false);
- }
- }
- }
- }
- }
- }
-
- ///
- /// Processes the prefab registry and populates the PrefabManager with custom prefabs.
- ///
- 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)]
- );
- }
- }
-
- ///
- /// Processes embark overrides by mapping original embark unit types to configured overrides.
- ///
- internal static void ProcessEmbarkOverrides()
- {
- foreach (KeyValuePair entry in Main.embarkNames)
- {
- try
- {
- UnitData.Type unit = EnumCache.GetType(entry.Key);
- UnitData.Type newUnit = EnumCache.GetType(entry.Value);
- Main.embarkOverrides[unit] = newUnit;
- Plugin.logger.LogInfo($"Embark unit type for {entry.Key} is now {entry.Value}");
- }
- catch
- {
- Plugin.logger.LogError($"Embark unit type for {entry.Key} is not valid: {entry.Value}");
- }
- }
- }
-
- ///
- /// Processes attract overrides by mapping improvements to resources and terrain types based on configured overrides.
- ///
- internal static void ProcessAttractOverrides()
- {
- foreach (KeyValuePair entry in Main.attractsResourceNames)
- {
- try
- {
- ImprovementData.Type improvement = EnumCache.GetType(entry.Key);
- ResourceData.Type resource = EnumCache.GetType(entry.Value);
- Main.attractsResourceOverrides[improvement] = resource;
- Plugin.logger.LogInfo($"Improvement {entry.Key} now attracts {entry.Value}");
- }
- catch
- {
- Plugin.logger.LogError($"Improvement {entry.Key} resource type is not valid: {entry.Value}");
- }
- }
- foreach (KeyValuePair entry in Main.attractsTerrainNames)
- {
- try
- {
- ImprovementData.Type improvement = EnumCache.GetType(entry.Key);
- Polytopia.Data.TerrainData.Type terrain = EnumCache.GetType(entry.Value);
- Main.attractsTerrainOverrides[improvement] = terrain;
- Plugin.logger.LogInfo($"Improvement {entry.Key} now attracts on {entry.Value}");
- }
- catch
- {
- Plugin.logger.LogError($"Improvement {entry.Key} terrain type is not valid: {entry.Value}");
- }
- }
- }
+ Plugin.logger.LogInfo("Created mapping for tileEffect with id " + tileEffectId + " and index " + Registry.autoidx);
+ Registry.autoidx++;
+ }
+ }
+ }
+ }
+ }
+ Il2CppSystem.Collections.Generic.List modifiedSkins = skins._values;
+ foreach (var skin in Registry.skinInfo)
+ {
+ if (modifiedSkins.Contains(skin.id))
+ {
+ modifiedSkins.Remove(skin.id);
+ modifiedSkins.Add(skin.idx.ToString());
+ }
+ }
+ JArray newSkins = new JArray();
+ foreach (var item in modifiedSkins)
+ {
+ newSkins.Add(item);
+ }
+ token["skins"] = newSkins;
+ }
+ if (token["preview"] != null)
+ {
+ Visual.PreviewTile[] preview = JsonSerializer.Deserialize(token["preview"].ToString())!;
+ Registry.tribePreviews[Util.GetJTokenName(token)] = preview;
+ }
+ }
+ })
+ },
+ [typeof(UnitData.Type)] = new List>() {
+ new((token, 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);
+ string embarkUnitId = token["embarksTo"].ToString();
+ Main.embarkNames[unitId] = embarkUnitId;
+ }
+ if (token["weapon"] != null)
+ {
+ string weaponString = token["weapon"].ToString();
+ if (EnumCache.TryGetType(weaponString, out UnitData.WeaponEnum type))
+ {
+ token["weapon"] = (int)type;
+ }
+ }
+ }
+ })
+ },
+
+ [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
+ {
+ string improvementId = Util.GetJTokenName(token);
+ if (token["attractsResource"] != null)
+ {
+ string attractsId = token["attractsResource"].ToString();
+ Main.attractsResourceNames[improvementId] = attractsId;
+ }
+ if (token["attractsToTerrain"] != null)
+ {
+ string attractsId = token["attractsToTerrain"].ToString();
+ Main.attractsTerrainNames[improvementId] = attractsId;
+ }
+ if(token["infoOverride"] != null)
+ {
+ Loc.buildingsInfoOverrides[improvementId] = Loc.ReplaceDashesWithDots(token["infoOverride"].ToString());
+ }
+ }
+ })
+ },
+
+ [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>() {
+ new((token, duringEnumCacheCreation) =>
+ {
+ var prop = token.Parent.Cast();
+ prop.Replace(new JProperty(prop.Name.ToLower(), prop.Value));
+ })
+ },
+ };
+
+ ///
+ /// Represents information for a custom game mode button.
+ ///
+ /// The index of the game mode.
+ /// The action to perform when the button is clicked.
+ /// The index of the button in the UI.
+ /// The sprite for the button.
+ /// The name of the sprite for the button.
+ public record GameModeButtonsInformation(int gameModeIndex, UIButtonBase.ButtonAction action, int? buttonIndex, Sprite? sprite, string? spriteName);
+
+ ///
+ /// Adds a new game mode button.
+ ///
+ /// The unique identifier for the game mode.
+ /// The action to perform when the button is clicked.
+ /// The boolean which decides if GameMode should appear in the menu.
+ /// The sprite for the button.
+ /// The name of the sprite for the button.
+ public static void AddGameMode(string id, UIButtonBase.ButtonAction action, bool shouldShowInMenu = true, Sprite? sprite = null, string? spriteName = null)
+ {
+ EnumCache.AddMapping(id, (GameMode)Registry.gameModesAutoidx);
+ EnumCache.AddMapping(id, (GameMode)Registry.gameModesAutoidx);
+ if (shouldShowInMenu)
+ gamemodes.Add(new GameModeButtonsInformation(Registry.gameModesAutoidx, action, null, sprite, spriteName));
+ Registry.gameModesAutoidx++;
+ }
+
+ ///
+ /// Adds a new data type for patching.
+ ///
+ /// The identifier for the data type in JSON.
+ /// "The C# type corresponding to the identifier.
+ public static void AddPatchDataType(string typeId, Type type)
+ {
+ if (!typeMappings.ContainsKey(typeId))
+ typeMappings.Add(typeId, new TypeMapping(type));
+ }
+
+ public static void AddPatchDataType(string typeId, Type type, bool shouldCreateCache)
+ {
+ if (!typeMappings.ContainsKey(typeId))
+ typeMappings.Add(typeId, new TypeMapping(type, shouldCreateCache));
+ }
+
+ public static void AddTypeHandler(Type type, Action handler)
+ {
+ string typeString = type.ToString();
+ if (!typeMappings.ContainsValue(new TypeMapping(type, true)) && !typeMappings.ContainsValue(new TypeMapping(type, false)))
+ {
+ Plugin.logger.LogWarning($"Tried adding TypeHandler for type: {typeString} with missing TypeMapping. Please, add TypeMapping first.");
+ return;
+ }
+ if (!typeHandlers.ContainsKey(type))
+ typeHandlers[type] = new();
+
+ typeHandlers[type].Add(handler);
+
+ Plugin.logger.LogInfo($"Added TypeHandler for type: {typeString}.");
+ }
+
+ ///
+ /// Loads all mods from the mods directory.
+ ///
+ /// A dictionary to populate with the loaded mods.
+ internal static void RegisterMods(Dictionary mods)
+ {
+ Directory.CreateDirectory(Plugin.MODS_PATH);
+ string[] modContainers = Directory.GetDirectories(Plugin.MODS_PATH)
+ .Union(Directory.GetFiles(Plugin.MODS_PATH, "*.polymod"))
+ .Union(Directory.GetFiles(Plugin.MODS_PATH, "*.zip"))
+ .ToArray();
+ foreach (var modContainer in modContainers)
+ {
+ Mod.Manifest? manifest = null;
+ List files = new();
+
+ // Load mod from directory or zip archive
+ if (Directory.Exists(modContainer))
+ {
+ foreach (var file in Directory.GetFiles(modContainer, "*", SearchOption.AllDirectories))
+ {
+ ProcessModFile(
+ Path.GetRelativePath(modContainer, file),
+ File.ReadAllBytes(file),
+ files,
+ ref manifest
+ );
+ }
+ }
+ else
+ {
+ foreach (var entry in new ZipArchive(File.OpenRead(modContainer)).Entries)
+ {
+ ProcessModFile(
+ entry.FullName,
+ entry.ReadBytes(),
+ files,
+ ref manifest
+ );
+ }
+ }
+ #region ValidateManifest()
+ if (manifest == null)
+ {
+ Plugin.logger.LogError($"Mod manifest not found in {modContainer}");
+ continue;
+ }
+ if (manifest.id == null)
+ {
+ Plugin.logger.LogError($"Mod id not found in {modContainer}");
+ continue;
+ }
+ if (!Regex.IsMatch(manifest.id, @"^(?!polytopia$)[a-z_]+$"))
+ {
+ Plugin.logger.LogError($"Mod id {manifest.id} is invalid in {modContainer}");
+ continue;
+ }
+ if (manifest.version == null)
+ {
+ Plugin.logger.LogError($"Mod version not found in {modContainer}");
+ continue;
+ }
+ if (manifest.authors == null || manifest.authors.Length == 0)
+ {
+ Plugin.logger.LogError($"Mod authors not found in {modContainer}");
+ continue;
+ }
+ if (mods.ContainsKey(manifest.id))
+ {
+ Plugin.logger.LogError($"Mod {manifest.id} already exists");
+ continue;
+ }
+ #endregion
+ mods.Add(manifest.id, new(
+ manifest,
+ Mod.Status.Success,
+ files
+ ));
+ Plugin.logger.LogInfo($"Registered mod {manifest.id}");
+ }
+
+ CheckDependencies(mods);
+ }
+
+ private static void ProcessModFile(string fullName, byte[] bytes, List files, ref Mod.Manifest? manifest)
+ {
+ if (fullName == "manifest.json")
+ {
+ manifest = JsonSerializer.Deserialize(
+ bytes,
+ new JsonSerializerOptions()
+ {
+ Converters = { new VersionJson() },
+ }
+ );
+ return;
+ }
+ files.Add(new(fullName, bytes));
+ }
+
+ internal static void LoadMods(Dictionary mods, out bool dependencyCycle)
+ {
+ dependencyCycle = !SortMods(Registry.mods);
+ if (dependencyCycle) return;
+
+ StringBuilder checksumString = new();
+ foreach (var (id, mod) in Registry.mods)
+ {
+ if (mod.status != Mod.Status.Success) continue;
+ foreach (var file in mod.files)
+ {
+ checksumString.Append(JsonSerializer.Serialize(file));
+ if (Path.GetExtension(file.name) == ".dll")
+ {
+ LoadAssemblyFile(mod, file);
+ }
+ Match spritesMatch = Regex.Match(Path.GetFileName(file.name), @"^sprites(?:_(.*))?\.json$");
+ if (spritesMatch.Success)
+ {
+ LoadSpriteInfoFile(mod, file);
+ }
+ Match languageMatch = Regex.Match(Path.GetFileName(file.name), @"^language(?:_(.*))?\.json$");
+ if (languageMatch.Success)
+ {
+ string languageName = languageMatch.Groups[1].Value;
+ LoadLanguageFile(
+ mod,
+ file,
+ languageName
+ );
+ }
+ }
+ if (!mod.client && id != "polytopia")
+ {
+ checksumString.Append(id);
+ checksumString.Append(mod.version.ToString());
+ }
+ }
+ Compatibility.HashSignatures(checksumString);
+
+ }
+ internal static void CheckDependencies(Dictionary mods)
+ {
+ foreach (var (id, mod) in mods)
+ {
+ foreach (var dependency in mod.dependencies ?? Array.Empty())
+ {
+ string? message = null;
+ if (!mods.ContainsKey(dependency.id))
+ {
+ message = $"Dependency {dependency.id} not found";
+ }
+ else
+ {
+ Version version = mods[dependency.id].version;
+ if (
+ (dependency.min != null && version < dependency.min)
+ ||
+ (dependency.max != null && version > dependency.max)
+ )
+ {
+ message = $"Need dependency {dependency.id} version {dependency.min} - {dependency.max} found {version}";
+ }
+ }
+ if (message != null)
+ {
+ if (dependency.required)
+ {
+ Plugin.logger.LogError(message);
+ mod.status = Mod.Status.DependenciesUnsatisfied;
+ }
+ else
+ {
+ Plugin.logger.LogWarning(message);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Sorts mods based on their dependencies using a topological sort.
+ ///
+ /// The dictionary of mods to sort.
+ /// True if the mods could be sorted (no circular dependencies), false otherwise.
+ internal static bool SortMods(Dictionary mods)
+ {
+ Stopwatch s = new();
+ Dictionary> graph = new();
+ Dictionary inDegree = new();
+ Dictionary successfulMods = new();
+ Dictionary unsuccessfulMods = new();
+ foreach (var (id, mod) in mods)
+ {
+ if (mod.status == Mod.Status.Success) successfulMods.Add(id, mod);
+ else unsuccessfulMods.Add(id, mod);
+ }
+ foreach (var (id, _) in successfulMods)
+ {
+ graph[id] = new();
+ inDegree[id] = 0;
+ }
+ foreach (var (id, mod) in successfulMods)
+ {
+ foreach (var dependency in mod.dependencies ?? Array.Empty())
+ {
+ if (!graph.ContainsKey(dependency.id)) continue;
+ graph[dependency.id].Add(id);
+ inDegree[id]++;
+ }
+ }
+ Queue queue = new();
+ foreach (var (id, _) in successfulMods)
+ {
+ if (inDegree[id] == 0)
+ {
+ queue.Enqueue(id);
+ }
+ }
+ Dictionary sorted = new();
+ while (queue.Count > 0)
+ {
+ var id = queue.Dequeue();
+ var mod = successfulMods[id];
+ sorted.Add(id, mod);
+ foreach (var neighbor in graph[id])
+ {
+ inDegree[neighbor]--;
+ if (inDegree[neighbor] == 0)
+ {
+ queue.Enqueue(neighbor);
+ }
+ }
+ }
+ if (sorted.Count != successfulMods.Count)
+ {
+ return false;
+ }
+ mods.Clear();
+ mods.AddRange(sorted);
+ mods.AddRange(unsuccessfulMods);
+
+ return true;
+ }
+
+ ///
+ /// Loads an assembly file from a mod.
+ ///
+ /// The mod the assembly belongs to.
+ /// The assembly file to load.
+ public static void LoadAssemblyFile(Mod mod, Mod.File file)
+ {
+ try
+ {
+ Assembly assembly = Assembly.Load(file.bytes);
+ if (assembly
+ .GetTypes()
+ .FirstOrDefault(t => t.IsSubclassOf(typeof(Api.PolyScriptBase)))
+ is { } modType)
+ {
+ var modInstance = (Api.PolyScriptBase)Activator.CreateInstance(modType)!;
+ modInstance.Initialize(mod, BepInEx.Logging.Logger.CreateLogSource($"PolyMod] [{mod.id}"));
+ modInstance.Load();
+ return;
+ }
+ foreach (Type type in assembly.GetTypes())
+ {
+ MethodInfo? loadWithLogger = type.GetMethod("Load", new Type[] { typeof(ManualLogSource) });
+ if (loadWithLogger != null)
+ {
+ loadWithLogger.Invoke(null, new object[]
+ {
+ BepInEx.Logging.Logger.CreateLogSource($"PolyMod] [{mod.id}")
+ });
+ Plugin.logger.LogInfo($"Invoked Load method with logger from {type.FullName} from {mod.id} mod");
+ }
+ MethodInfo? load = type.GetMethod("Load", Array.Empty());
+ if (load != null)
+ {
+ load.Invoke(null, null);
+ Plugin.logger.LogInfo($"Invoked Load method from {type.FullName} from {mod.id} mod");
+ }
+ }
+ }
+ catch (TargetInvocationException exception)
+ {
+ if (exception.InnerException != null)
+ {
+ Plugin.logger.LogError($"Error on loading assembly from {mod.id} mod: {exception.InnerException.Message}");
+ mod.status = Mod.Status.Error;
+ }
+ }
+ }
+
+ ///
+ /// Loads a localization file from a mod.
+ ///
+ /// The mod the localization file belongs to.
+ /// The localization file to load.
+ public static void LoadLocalizationFile(Mod mod, Mod.File file)
+ {
+ try
+ {
+ Loc.BuildAndLoadLocalization(JsonSerializer
+ .Deserialize>>(file.bytes)!);
+ Plugin.logger.LogInfo($"Registered localization from {mod.id} mod");
+ }
+ catch (Exception e)
+ {
+ Plugin.logger.LogError($"Error on loading locatization from {mod.id} mod: {e.StackTrace}");
+ }
+ }
+
+ public static void LoadLanguageFile(Mod mod, Mod.File file, string languageName)
+ {
+ try
+ {
+ var dict = JsonSerializer.Deserialize>(file.bytes);
+ if (dict != null)
+ {
+ Loc.languagesToAdd.Add(languageName, dict);
+ Plugin.logger.LogInfo($"Registered language from {mod.id} mod");
+ }
+ }
+ catch (Exception e)
+ {
+ Plugin.logger.LogError($"Error on loading language file from {mod.id} mod: {e.StackTrace}");
+ }
+ }
+
+ ///
+ /// Loads a sprite file from a mod.
+ ///
+ /// The mod the sprite file belongs to.
+ /// The sprite file to load.
+ public static void LoadSpriteFile(Mod mod, Mod.File file)
+ {
+ string name = Path.GetFileNameWithoutExtension(file.name);
+ Vector2 pivot = name.Split("_")[0] switch
+ {
+ "field" => new(0.5f, 0.0f),
+ "mountain" => new(0.5f, -0.375f),
+ _ => new(0.5f, 0.5f),
+ };
+ float pixelsPerUnit = 2112f;
+ if (Registry.spriteInfos.ContainsKey(name))
+ {
+ Visual.SpriteInfo spriteData = Registry.spriteInfos[name];
+ pivot = spriteData.pivot ?? pivot;
+ pixelsPerUnit = spriteData.pixelsPerUnit ?? pixelsPerUnit;
+ }
+ Sprite sprite = Visual.BuildSprite(file.bytes, pivot, pixelsPerUnit);
+ GameManager.GetSpriteAtlasManager().cachedSprites.TryAdd("Heads", new());
+ GameManager.GetSpriteAtlasManager().cachedSprites["Heads"].Add(name, sprite);
+ Registry.sprites.Add(name, sprite);
+ }
+
+ ///
+ /// Updates a sprite with new information.
+ ///
+ /// The name of the sprite to update.
+ public static void UpdateSprite(string name)
+ {
+ if (Registry.spriteInfos.ContainsKey(name) && Registry.sprites.ContainsKey(name))
+ {
+ Visual.SpriteInfo spriteData = Registry.spriteInfos[name];
+ Sprite sprite = Visual.BuildSpriteWithTexture(
+ Registry.sprites[name].texture,
+ spriteData.pivot,
+ spriteData.pixelsPerUnit
+ );
+ GameManager.GetSpriteAtlasManager().cachedSprites["Heads"][name] = sprite;
+ Registry.sprites[name] = sprite;
+ }
+ }
+
+ ///
+ /// Loads a sprite info file from a mod.
+ ///
+ /// The mod the sprite info file belongs to.
+ /// The sprite info file to load.
+ /// A dictionary of sprite information, or null if an error occurred.
+ public static Dictionary? LoadSpriteInfoFile(Mod mod, Mod.File file)
+ {
+ try
+ {
+ var deserialized = JsonSerializer.Deserialize>(
+ file.bytes,
+ new JsonSerializerOptions()
+ {
+ Converters = { new Vector2Json() },
+ }
+ );
+
+ if (deserialized != null)
+ {
+ foreach (var kvp in deserialized)
+ {
+ Registry.spriteInfos[kvp.Key] = kvp.Value;
+ }
+ }
+
+ Plugin.logger.LogInfo($"Registered sprite data from {mod.id} mod");
+ return deserialized;
+ }
+ catch (Exception e)
+ {
+ Plugin.logger.LogError($"Error on loading sprite data from {mod.id} mod: {e.StackTrace}");
+ return null;
+ }
+ }
+
+ ///
+ /// Loads an audio file from a mod.
+ ///
+ /// The mod the audio file belongs to.
+ /// The audio file to load.
+ public static void LoadAudioFile(Mod mod, Mod.File file)
+ {
+ // AudioSource audioSource = new GameObject().AddComponent();
+ // GameObject.DontDestroyOnLoad(audioSource);
+ // audioSource.clip = Managers.Audio.BuildAudioClip(file.bytes);
+ // Registry.audioClips.Add(Path.GetFileNameWithoutExtension(file.name), audioSource);
+ // 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);
+
+ var visualParts = ApplyVisualParts(prefab, spriteContainer, material);
+
+ Transform? headPositionMarker = null;
+ foreach (var vp in visualParts)
+ {
+ if (vp.visualPart.gameObject.name == prefab.headPositionMarker)
+ {
+ headPositionMarker = vp.visualPart.gameObject.transform;
+ break;
+ }
+ }
+
+ unitInstance.headPositionMarker = headPositionMarker ?? visualParts[0].visualPart.transform;
+
+ var svr = unitInstance.GetComponent();
+ svr.visualParts = visualParts.ToArray();
+
+ 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;
+ }
+
+ ///
+ /// Loads and applies a game logic data patch from a mod.
+ ///
+ /// The mod the patch belongs to.
+ /// The original game logic data.
+ /// The patch to apply.
+ public static void LoadGameLogicDataPatch(Mod mod, JObject gld, JObject patch)
+ {
+ try
+ {
+ // Merge the patch into the game logic data
+ gld = JsonMerger.Merge(gld, patch);
+ Plugin.logger.LogInfo($"Registered patch from {mod.id} mod");
+ }
+ catch (Exception e)
+ {
+ Plugin.logger.LogError($"Error on loading patch from {mod.id} mod: {e.StackTrace}");
+ mod.status = Mod.Status.Error;
+ }
+ }
+
+ ///
+ /// Loads an asset bundle from a mod.
+ ///
+ /// The mod the asset bundle belongs to.
+ /// The asset bundle file to load.
+ public static void LoadAssetBundle(Mod mod, Mod.File file)
+ {
+ Registry.assetBundles.Add(
+ Path.GetFileNameWithoutExtension(file.name),
+ AssetBundle.LoadFromMemory(file.bytes)
+ );
+ }
+
+ ///
+ /// Processes the merged game logic data after all mods have been loaded and patched.
+ ///
+ /// The game logic data object to populate.
+ /// The root JObject of the merged game logic data.
+ internal static void ProcessGameLogicData(GameLogicData gameLogicData, JObject rootObject)
+ {
+ try
+ {
+ CreateMappings(rootObject);
+ ProcessCustomTribes();
+ ProcessPrefabs();
+ ProcessEmbarkOverrides();
+ ProcessAttractOverrides();
+ }
+ catch (Exception e)
+ {
+ Plugin.logger.LogError($"Error on processing modified game logic data : {e.StackTrace}");
+ }
+ }
+
+ internal static void ProcessCustomTribes()
+ {
+ foreach (var tribe in Registry.customTribes)
+ {
+ if (!GameLogicData.legacyTribeTypesOrder.Contains(tribe))
+ {
+ GameLogicData.legacyTribeTypesOrder.Add(tribe);
+ }
+ }
+ }
+
+ ///
+ /// Creates EnumCache mappings for custom enum values and invokes type handlers.
+ ///
+ ///
+ internal static void CreateMappings(JObject rootObject)
+ {
+ foreach (JToken jtoken in rootObject.SelectTokens("$.*.*").ToArray())
+ {
+ JObject? token = jtoken.TryCast();
+ if (token == null)
+ continue;
+
+ string dataType = Util.GetJTokenName(token, 2);
+ if (!typeMappings.TryGetValue(dataType, out TypeMapping? typeMapping))
+ continue;
+
+ if (token["idx"] == null || !typeMapping.shouldCreateCache)
+ continue;
+
+ Type targetType = typeMapping.type;
+ if (!targetType.IsEnum)
+ {
+ Plugin.logger.LogWarning($"Type {targetType.FullName} is not an enum, skipping!");
+ continue;
+ }
+
+ string id = Util.GetJTokenName(token);
+
+ if ((int)token["idx"] == -1)
+ {
+ token["idx"] = Registry.autoidx;
+ Registry.autoidx++;
+ }
+ else if (Plugin.config.allowUnsafeIndexes)
+ {
+ Array values = Enum.GetValues(targetType);
+
+ var maxValue = values.Cast().Max();
+
+ if (maxValue >= (int)token["idx"])
+ {
+ continue;
+ }
+ }
+ else
+ {
+ continue;
+ }
+
+ MethodInfo? methodInfo = typeof(EnumCache<>).MakeGenericType(targetType).GetMethod("AddMapping");
+ if (methodInfo == null)
+ {
+ Plugin.logger.LogWarning($"Missing AddMapping method for {targetType.FullName}");
+ continue;
+ }
+
+ methodInfo.Invoke(null, new object[] { id, (int)token["idx"] });
+ methodInfo.Invoke(null, new object[] { id, (int)token["idx"] });
+
+ if (typeHandlers.TryGetValue(targetType, out var handlers))
+ {
+ foreach (var handler in handlers)
+ {
+ handler(token, true);
+ }
+ }
+ Plugin.logger.LogInfo("Created mapping for " + targetType.ToString() + " with id " + id + " and index " + (int)token["idx"]);
+ }
+ foreach (JToken jtoken in rootObject.SelectTokens("$.*.*").ToArray())
+ {
+ JObject? token = jtoken.TryCast();
+ if (token != null)
+ {
+ string dataType = Util.GetJTokenName(token, 2);
+ if (typeMappings.TryGetValue(dataType, out TypeMapping? typeMapping))
+ {
+ if (typeHandlers.TryGetValue(typeMapping.type, out var handlers))
+ {
+ foreach (var handler in handlers)
+ {
+ handler(token, false);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Processes the prefab registry and populates the PrefabManager with custom prefabs.
+ ///
+ 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)]
+ );
+ }
+ }
+
+ ///
+ /// Processes embark overrides by mapping original embark unit types to configured overrides.
+ ///
+ internal static void ProcessEmbarkOverrides()
+ {
+ foreach (KeyValuePair entry in Main.embarkNames)
+ {
+ try
+ {
+ UnitData.Type unit = EnumCache.GetType(entry.Key);
+ UnitData.Type newUnit = EnumCache.GetType(entry.Value);
+ Main.embarkOverrides[unit] = newUnit;
+ Plugin.logger.LogInfo($"Embark unit type for {entry.Key} is now {entry.Value}");
+ }
+ catch
+ {
+ Plugin.logger.LogError($"Embark unit type for {entry.Key} is not valid: {entry.Value}");
+ }
+ }
+ }
+
+ ///
+ /// Processes attract overrides by mapping improvements to resources and terrain types based on configured overrides.
+ ///
+ internal static void ProcessAttractOverrides()
+ {
+ foreach (KeyValuePair entry in Main.attractsResourceNames)
+ {
+ try
+ {
+ ImprovementData.Type improvement = EnumCache.GetType(entry.Key);
+ ResourceData.Type resource = EnumCache.GetType(entry.Value);
+ Main.attractsResourceOverrides[improvement] = resource;
+ Plugin.logger.LogInfo($"Improvement {entry.Key} now attracts {entry.Value}");
+ }
+ catch
+ {
+ Plugin.logger.LogError($"Improvement {entry.Key} resource type is not valid: {entry.Value}");
+ }
+ }
+ foreach (KeyValuePair entry in Main.attractsTerrainNames)
+ {
+ try
+ {
+ ImprovementData.Type improvement = EnumCache.GetType(entry.Key);
+ Polytopia.Data.TerrainData.Type terrain = EnumCache.GetType(entry.Value);
+ Main.attractsTerrainOverrides[improvement] = terrain;
+ Plugin.logger.LogInfo($"Improvement {entry.Key} now attracts on {entry.Value}");
+ }
+ catch
+ {
+ Plugin.logger.LogError($"Improvement {entry.Key} terrain type is not valid: {entry.Value}");
+ }
+ }
+ }
}
diff --git a/src/Managers/AutoUpdate.cs b/src/Managers/AutoUpdate.cs
index 6384616..58ea639 100644
--- a/src/Managers/AutoUpdate.cs
+++ b/src/Managers/AutoUpdate.cs
@@ -53,7 +53,7 @@ private static void StartScreen_UI2_OnShow()
_ => "unknown",
};
if (os == "unknown")
- {
+ {
Plugin.logger.LogError("Unsupported platform for autoupdate!");
return;
}
diff --git a/src/Managers/Hub.cs b/src/Managers/Hub.cs
index 57da0b6..5afdaf9 100644
--- a/src/Managers/Hub.cs
+++ b/src/Managers/Hub.cs
@@ -109,7 +109,7 @@ private static void StartScreen_UI2_Init(StartScreen_UI2 __instance, RectTransfo
[HarmonyPatch(typeof(StartScreen_UI2), nameof(StartScreen_UI2.RunLayout))]
private static void StartScreen_UI2_RunLayout(StartScreen_UI2 __instance, ScreenBase_UI2.ScreenSize screenSize)
{
- if(polyModButton == null)
+ if (polyModButton == null)
{
Plugin.logger.LogWarning("PolyMod Hub button is null when running layout!");
return;
@@ -118,10 +118,10 @@ private static void StartScreen_UI2_RunLayout(StartScreen_UI2 __instance, Screen
polyModButton.outline.gameObject.SetActive(false);
polyModButton.bg.color = Color.white;
polyModButton.Text = Localization.Get("polymod.hub");
- float num = 50f;
- polyModButton.SetPosition(screenSize.safeRect.Right - (num * 2.5f), screenSize.safeRect.Top - num);
+ float num = 50f;
+ polyModButton.SetPosition(screenSize.safeRect.Right - (num * 2.5f), screenSize.safeRect.Top - num);
- if(polyModVersion == null)
+ if (polyModVersion == null)
{
Plugin.logger.LogWarning("PolyMod Version is null when running layout!");
return;
@@ -336,7 +336,7 @@ void OpenDiscord()
));
popupButtons.Add(new(
"polymod.hub.spriteinfo.update",
- callback: DelegateSupport.ConvertDelegate(UpdateSpriteInfos),
+ callback: DelegateSupport.ConvertDelegate(UpdateSpriteInfos),
closesPopup: false
));
}
diff --git a/src/Managers/Loc.cs b/src/Managers/Loc.cs
index 6a8711f..b3b7d17 100644
--- a/src/Managers/Loc.cs
+++ b/src/Managers/Loc.cs
@@ -13,163 +13,163 @@ namespace PolyMod.Managers;
///
public static class Loc
{
- internal static Dictionary> languagesToAdd = new();
- internal static Dictionary buildingsInfoOverrides = new();
-
- ///
- /// Patches the localization getter to handle custom enum values.
- ///
- [HarmonyPrefix]
- [HarmonyPatch(typeof(Localization), nameof(Localization.Get), typeof(string), typeof(Il2CppReferenceArray))]
- private static bool Localization_Get(ref string key, Il2CppReferenceArray args)
- {
- List keys = key.Split('.').ToList();
- int? idx = null;
- string? name = null;
-
- // Find any custom enum indices in the localization key
- foreach (string item in keys)
- {
- if (int.TryParse(item, out int parsedIdx))
- {
- if (parsedIdx >= Plugin.AUTOIDX_STARTS_FROM)
- {
- idx = parsedIdx;
- }
- }
- }
-
- // If a custom enum index is found, try to resolve its name
- if (idx != null)
- {
- foreach (var typeMapping in Loader.typeMappings.Values)
- {
- if(!typeMapping.shouldCreateCache)
- continue;
-
- MethodInfo? methodInfo = typeof(EnumCache<>).MakeGenericType(typeMapping.type).GetMethod("TryGetName");
- if (methodInfo != null)
- {
- object?[] parameters = { idx, null };
- object? methodInvokeResult = methodInfo.Invoke(null, parameters);
- if (methodInvokeResult != null)
- {
- if ((bool)methodInvokeResult)
- {
- name = (string?)parameters[1];
- }
- }
- }
- }
-
- // If the name was resolved, replace the index with the name in the key
- if (name != null && idx != null)
- {
- int index = keys.IndexOf(idx.ToString()!);
- keys[index] = name;
- key = string.Join(".", keys);
- }
- }
- return true;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(Localization), nameof(Localization.Init))]
- private static bool Localization_Init()
- {
- if (Localization.initialized)
- return true;
-
- if (LocalizationManager.Sources.Count == 0)
- LocalizationManager.UpdateSources();
-
- LanguageSourceData source = LocalizationManager.Sources[0];
-
- // Elyrion has no language code. I am almost completely sure it is a bug, by looking at the code.
- LanguageData elyrionLanguage = source.GetLanguageData("Elyrion");
- elyrionLanguage.Code = Localization.LANG_CODE_ELYRION;
-
- foreach(var languageCode in languagesToAdd.Keys)
- {
- Dictionary terms = languagesToAdd[languageCode];
-
- int languageIndex = source.GetLanguageIndexFromCode(languageCode);
- string languageName = terms["language"];
- if (languageIndex == -1)
- {
- source.AddLanguage(languageName, languageCode);
- languageIndex = source.GetLanguageIndex(languageName);
- }
-
- foreach (var kvp in terms)
- {
- TermData term = source.GetTermData(kvp.Key);
- if (term == null)
- {
- source.AddTerm(kvp.Key);
- term = source.GetTermData(kvp.Key);
- }
-
- term.Languages[languageIndex] = kvp.Value;
- }
-
- LocalizationManager.UpdateSources();
-
- Plugin.logger.LogInfo($"{languageCode} language terms added.");
- }
- return true;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(BuildingUtils), nameof(BuildingUtils.GetInfo))]
- private static bool BuildingUtils_GetInfo(ref string __result, SkinType skinOfCurrentLocalPlayer,
- ImprovementData improvementData, ImprovementState improvementState, PlayerState owner, TileData tileData)
- {
- TribeType tribe = TribeType.None;
- if (owner != null)
- tribe = owner.tribe;
-
- string key = EnumCache.GetName(improvementData.type);
- if(buildingsInfoOverrides.ContainsKey(key))
- {
- __result = Localization.GetSkinned(skinOfCurrentLocalPlayer, tribe, buildingsInfoOverrides[key]);
- return false;
- }
-
- return true;
- }
-
- ///
- /// Builds and loads localization data from a dictionary.
- ///
- /// A dictionary containing the localization data.
- internal static void BuildAndLoadLocalization(Dictionary> localization)
- {
- foreach (var (key, data) in localization)
- {
- string name = ReplaceDashesWithDots(key);
- if (name.StartsWith("tribeskins")) name = "TribeSkins/" + name;
- TermData term = LocalizationManager.Sources[0].AddTerm(name);
- List strings = new();
- foreach (string language in LocalizationManager.GetAllLanguages(false))
- {
- strings.Add(data.GetOrDefault(language, data.GetOrDefault("English", term.Term))!);
- }
- term.Languages = new Il2CppStringArray(strings.ToArray());
- }
- }
-
- internal static string ReplaceDashesWithDots(string key)
- {
- string name = key.Replace("_", ".");
- return name.Replace("..", "_");
- }
-
- ///
- /// Initializes the Loc manager by patching the necessary methods.
- ///
- internal static void Init()
- {
- Harmony.CreateAndPatchAll(typeof(Loc));
- }
+ internal static Dictionary> languagesToAdd = new();
+ internal static Dictionary buildingsInfoOverrides = new();
+
+ ///
+ /// Patches the localization getter to handle custom enum values.
+ ///
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(Localization), nameof(Localization.Get), typeof(string), typeof(Il2CppReferenceArray))]
+ private static bool Localization_Get(ref string key, Il2CppReferenceArray args)
+ {
+ List keys = key.Split('.').ToList();
+ int? idx = null;
+ string? name = null;
+
+ // Find any custom enum indices in the localization key
+ foreach (string item in keys)
+ {
+ if (int.TryParse(item, out int parsedIdx))
+ {
+ if (parsedIdx >= Plugin.AUTOIDX_STARTS_FROM)
+ {
+ idx = parsedIdx;
+ }
+ }
+ }
+
+ // If a custom enum index is found, try to resolve its name
+ if (idx != null)
+ {
+ foreach (var typeMapping in Loader.typeMappings.Values)
+ {
+ if (!typeMapping.shouldCreateCache)
+ continue;
+
+ MethodInfo? methodInfo = typeof(EnumCache<>).MakeGenericType(typeMapping.type).GetMethod("TryGetName");
+ if (methodInfo != null)
+ {
+ object?[] parameters = { idx, null };
+ object? methodInvokeResult = methodInfo.Invoke(null, parameters);
+ if (methodInvokeResult != null)
+ {
+ if ((bool)methodInvokeResult)
+ {
+ name = (string?)parameters[1];
+ }
+ }
+ }
+ }
+
+ // If the name was resolved, replace the index with the name in the key
+ if (name != null && idx != null)
+ {
+ int index = keys.IndexOf(idx.ToString()!);
+ keys[index] = name;
+ key = string.Join(".", keys);
+ }
+ }
+ return true;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(Localization), nameof(Localization.Init))]
+ private static bool Localization_Init()
+ {
+ if (Localization.initialized)
+ return true;
+
+ if (LocalizationManager.Sources.Count == 0)
+ LocalizationManager.UpdateSources();
+
+ LanguageSourceData source = LocalizationManager.Sources[0];
+
+ // Elyrion has no language code. I am almost completely sure it is a bug, by looking at the code.
+ LanguageData elyrionLanguage = source.GetLanguageData("Elyrion");
+ elyrionLanguage.Code = Localization.LANG_CODE_ELYRION;
+
+ foreach (var languageCode in languagesToAdd.Keys)
+ {
+ Dictionary terms = languagesToAdd[languageCode];
+
+ int languageIndex = source.GetLanguageIndexFromCode(languageCode);
+ string languageName = terms["language"];
+ if (languageIndex == -1)
+ {
+ source.AddLanguage(languageName, languageCode);
+ languageIndex = source.GetLanguageIndex(languageName);
+ }
+
+ foreach (var kvp in terms)
+ {
+ TermData term = source.GetTermData(kvp.Key);
+ if (term == null)
+ {
+ source.AddTerm(kvp.Key);
+ term = source.GetTermData(kvp.Key);
+ }
+
+ term.Languages[languageIndex] = kvp.Value;
+ }
+
+ LocalizationManager.UpdateSources();
+
+ Plugin.logger.LogInfo($"{languageCode} language terms added.");
+ }
+ return true;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(BuildingUtils), nameof(BuildingUtils.GetInfo))]
+ private static bool BuildingUtils_GetInfo(ref string __result, SkinType skinOfCurrentLocalPlayer,
+ ImprovementData improvementData, ImprovementState improvementState, PlayerState owner, TileData tileData)
+ {
+ TribeType tribe = TribeType.None;
+ if (owner != null)
+ tribe = owner.tribe;
+
+ string key = EnumCache.GetName(improvementData.type);
+ if (buildingsInfoOverrides.ContainsKey(key))
+ {
+ __result = Localization.GetSkinned(skinOfCurrentLocalPlayer, tribe, buildingsInfoOverrides[key]);
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Builds and loads localization data from a dictionary.
+ ///
+ /// A dictionary containing the localization data.
+ internal static void BuildAndLoadLocalization(Dictionary> localization)
+ {
+ foreach (var (key, data) in localization)
+ {
+ string name = ReplaceDashesWithDots(key);
+ if (name.StartsWith("tribeskins")) name = "TribeSkins/" + name;
+ TermData term = LocalizationManager.Sources[0].AddTerm(name);
+ List strings = new();
+ foreach (string language in LocalizationManager.GetAllLanguages(false))
+ {
+ strings.Add(data.GetOrDefault(language, data.GetOrDefault("English", term.Term))!);
+ }
+ term.Languages = new Il2CppStringArray(strings.ToArray());
+ }
+ }
+
+ internal static string ReplaceDashesWithDots(string key)
+ {
+ string name = key.Replace("_", ".");
+ return name.Replace("..", "_");
+ }
+
+ ///
+ /// Initializes the Loc manager by patching the necessary methods.
+ ///
+ internal static void Init()
+ {
+ Harmony.CreateAndPatchAll(typeof(Loc));
+ }
}
diff --git a/src/Managers/Main.cs b/src/Managers/Main.cs
index 6bead52..f271e77 100644
--- a/src/Managers/Main.cs
+++ b/src/Managers/Main.cs
@@ -17,432 +17,432 @@ namespace PolyMod.Managers;
///
public static class Main
{
- internal static bool dependencyCycle;
-
- ///
- /// The maximum tier for technology, used to extend the tech tree.
- ///
- internal const int MAX_TECH_TIER = 100;
-
- ///
- /// A stopwatch to measure the time taken to load mods.
- ///
- internal static readonly Stopwatch stopwatch = new();
-
- ///
- /// Whether the mod has been fully initialized.
- ///
- internal static bool fullyInitialized;
-
- ///
- /// A dictionary mapping unit IDs to the IDs of the units they embark into.
- ///
- internal static Dictionary embarkNames = new();
-
- ///
- /// A dictionary mapping unit types to the types of the units they embark into.
- ///
- internal static Dictionary embarkOverrides = new();
-
- ///
- /// Whether an embark action is currently being executed.
- ///
- internal static bool currentlyEmbarking = false;
-
- ///
- /// A dictionary mapping improvement IDs to the IDs of the resources they attract.
- ///
- internal static Dictionary attractsResourceNames = new();
-
- ///
- /// A dictionary mapping improvement IDs to the IDs of the terrain types they attract resources on.
- ///
- internal static Dictionary attractsTerrainNames = new();
-
- ///
- /// A dictionary mapping improvement types to the types of the resources they attract.
- ///
- internal static Dictionary attractsResourceOverrides = new();
-
- ///
- /// A dictionary mapping improvement types to the types of the terrain they attract resources on.
- ///
- internal static Dictionary attractsTerrainOverrides = new();
-
- ///
- /// Patches the game logic data parsing to load PolyMod content.
- ///
- [HarmonyPrefix]
- [HarmonyPatch(typeof(GameLogicData), nameof(GameLogicData.AddGameLogicPlaceholders))]
- private static void GameLogicData_AddGameLogicPlaceholders(GameLogicData __instance, ref JObject rootObject)
- {
- if (!fullyInitialized)
- {
- Load(__instance, rootObject);
- fullyInitialized = true;
- }
- }
-
- ///
- /// Patches the purchase manager to unlock custom skins.
- ///
- [HarmonyPrefix]
- [HarmonyPatch(typeof(PurchaseManager), nameof(PurchaseManager.IsSkinUnlocked))]
- private static bool PurchaseManager_IsSkinUnlockedInternal(ref bool __result, SkinType skinType)
- {
- __result = (int)skinType >= Plugin.AUTOIDX_STARTS_FROM && skinType != SkinType.Test;
- return !__result;
- }
-
- ///
- /// Patches the purchase manager to unlock custom tribes.
- ///
- [HarmonyPostfix]
- [HarmonyPatch(typeof(PurchaseManager), nameof(PurchaseManager.IsTribeUnlocked))]
- private static void PurchaseManager_IsTribeUnlocked(ref bool __result, TribeType type)
- {
- __result = (int)type >= Plugin.AUTOIDX_STARTS_FROM || __result;
- }
-
- ///
- /// Patches the steam purchase manager to unlock custom content.
- ///
- [HarmonyPrefix]
- [HarmonyPatch(typeof(SteamPlatformPurchaseManager), nameof(SteamPlatformPurchaseManager.IsProductUnlocked))]
- private static bool SteamPlatformPurchaseManager_IsProductUnlocked(ref bool __result, IAPProduct iapProduct)
- {
- __result = iapProduct == null;
- return iapProduct != null;
- }
-
- ///
- /// Patches the purchase manager to add custom tribes to the list of unlocked tribes.
- ///
- [HarmonyPostfix]
- [HarmonyPatch(typeof(PurchaseManager), nameof(PurchaseManager.GetUnlockedTribes))]
- private static void PurchaseManager_GetUnlockedTribes(
- ref Il2CppSystem.Collections.Generic.List __result,
- bool forceUpdate = false
- )
- {
- foreach (var tribe in Registry.customTribes) __result.Add(tribe);
- }
-
- ///
- /// Patches the Unity log callback to filter out spammy messages.
- ///
- [HarmonyPrefix]
- [HarmonyPatch(typeof(IL2CPPUnityLogSource), nameof(IL2CPPUnityLogSource.UnityLogCallback))]
- private static bool IL2CPPUnityLogSource_UnityLogCallback(string logLine, string exception, LogType type)
- {
- foreach (string stringToIgnore in Plugin.LOG_MESSAGES_IGNORE)
- {
- if (logLine.Contains(stringToIgnore))
- return false;
- }
- return true;
- }
-
- ///
- /// Patches the game mode screen to add custom game modes.
- ///
- [HarmonyPrefix]
- [HarmonyPatch(typeof(GameModeScreen), nameof(GameModeScreen.Init))]
- private static void GameModeScreen_Init(GameModeScreen __instance)
- {
- List list = __instance.buttons.ToList();
- for (int i = 0; i < Loader.gamemodes.Count; i++)
- {
- var item = Loader.gamemodes[i];
- var button = GameObject.Instantiate(__instance.buttons[2]);
- list.Add(button);
- Sprite? sprite = item.sprite;
- if (item.sprite == null && item.spriteName != null)
- {
- sprite = Registry.GetSprite(item.spriteName);
- }
- Loader.gamemodes[i] = new Loader.GameModeButtonsInformation(item.gameModeIndex, item.action, __instance.buttons.Length, sprite, item.spriteName);
- }
-
- var newArray = list.ToArray();
- for (int i = 0; i < __instance.buttons.Length; i++)
- {
- if (newArray[i] != null) newArray[i].OnClicked = __instance.buttons[i].OnClicked;
- }
-
- for (int i = 0; i < Loader.gamemodes.Count; i++)
- {
- if (Loader.gamemodes[i].buttonIndex != null)
- newArray[Loader.gamemodes[i].buttonIndex!.Value].OnClicked = Loader.gamemodes[i].action;
- }
-
- __instance.buttons = newArray;
-
- for (int i = 0; i < __instance.buttons.Length; i++)
- {
- GamemodeButton button = __instance.buttons[i];
- var newData = button.gamemodeData.ToList();
- foreach (var info in Loader.gamemodes)
- {
- string id = EnumCache.GetName((GameMode)info.gameModeIndex).ToLower();
- newData.Add(new GamemodeButton.GamemodeButtonData()
- {
- gameMode = (GameMode)info.gameModeIndex,
- id = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(id),
- descriptionKey = "gamemode." + id + ".description.button",
- headerKey = "gamemode." + id + ".caps",
- icon = info.sprite
- });
- }
- button.gamemodeData = newData.ToArray();
-
- for (int j = 0; j < Loader.gamemodes.Count; j++)
- {
- Loader.GameModeButtonsInformation info = Loader.gamemodes[j];
-
- if (info.buttonIndex == i)
- {
- button.SetGamemode(info.buttonIndex.Value);
- }
- }
- }
- }
-
- ///
- /// Patches the tech view to correctly display the tech tree with custom technologies.
- ///
- [HarmonyPrefix]
- [HarmonyPatch(typeof(TechView), nameof(TechView.CreateNode))]
- public static bool TechView_CreateNode(TechView __instance, TechData data, TechItem parentItem, float angle)
- {
- // This patch is a reimplementation of the original CreateNode method to fix layout issues with custom techs.
- // It recalculates the angles for each node to ensure they are displayed correctly.
- GameLogicData gameLogicData = GameManager.GameState.GameLogicData;
- TribeData tribeData = gameLogicData.GetTribeData(GameManager.LocalPlayer.tribe);
- float baseAngle = 360 / gameLogicData.GetOverride(gameLogicData.GetTechData(TechData.Type.Basic), tribeData).techUnlocks.Count;
- float childAngle = 0f;
- if (parentItem != null)
- childAngle = angle + baseAngle * (data.techUnlocks.Count - 1) / 2f;
- foreach (var techData in data.techUnlocks)
- {
- if (gameLogicData.TryGetData(techData.type, out TechData techData2))
- {
- TechData @override = gameLogicData.GetOverride(techData, tribeData);
- TechItem techItem = __instance.CreateTechItem(@override, parentItem, childAngle);
- __instance.currTechIdx++;
- if (@override.techUnlocks != null && @override.techUnlocks.Count > 0)
- __instance.CreateNode(@override, techItem, childAngle);
- childAngle -= baseAngle;
- }
- }
- Il2CppSystem.Action onItemsRefreshed = __instance.OnItemsRefreshed;
- if (onItemsRefreshed == null)
- {
- return false;
- }
- onItemsRefreshed.Invoke(__instance);
- return false;
- }
-
- ///
- /// Patches the embark action to set a flag indicating that an embark is in progress.
- ///
- [HarmonyPrefix]
- [HarmonyPatch(typeof(EmbarkAction), nameof(EmbarkAction.Execute))]
- private static bool EmbarkAction_Execute_Prefix(EmbarkAction __instance, GameState gameState)
- {
- currentlyEmbarking = true;
- return true;
- }
-
- ///
- /// Patches the unit training method to handle custom embark units.
- ///
- [HarmonyPrefix]
- [HarmonyPatch(typeof(ActionUtils), nameof(ActionUtils.TrainUnit))]
- private static bool ActionUtils_TrainUnit(ref UnitState __result, GameState gameState, PlayerState playerState, TileData tile, ref UnitData unitData)
- {
- if (tile == null || tile.unit == null)
- {
- return true;
- }
-
- if (currentlyEmbarking)
- {
- if (embarkOverrides.TryGetValue(tile.unit.type, out UnitData.Type newType))
- {
- gameState.GameLogicData.TryGetData(newType, out unitData);
- }
- currentlyEmbarking = false;
- }
- return true;
- }
-
- ///
- /// Patches the start turn action to handle the 'Attract' ability for custom improvements.
- ///
- [HarmonyPostfix]
- [HarmonyPatch(typeof(StartTurnAction), nameof(StartTurnAction.Execute))]
- private static void StartTurnAction_Execute(StartTurnAction __instance, GameState state)
- {
- // Clear any existing CreateResource actions to prevent duplicates.
- for (int i = state.ActionStack.Count - 1; i >= 0; i--)
- {
- if (state.ActionStack[i].GetActionType() == ActionType.CreateResource)
- {
- state.ActionStack.RemoveAt(i);
- }
- }
-
- // Iterate through all tiles owned by the current player.
- for (int i = 0; i < state.Map.Tiles.Length; i++)
- {
- TileData tileData = state.Map.Tiles[i];
- if (tileData.owner == __instance.PlayerId && tileData.improvement != null && state.CurrentTurn > 0U)
- {
- ImprovementData improvementData;
- state.GameLogicData.TryGetData(tileData.improvement.type, out improvementData);
- if (improvementData != null)
- {
- // If the improvement has the 'Attract' ability and is ready to spawn a resource.
- if (improvementData.HasAbility(ImprovementAbility.Type.Attract) && tileData.improvement.GetAge(state) % improvementData.growthRate == 0)
- {
- ResourceData.Type resourceType = ResourceData.Type.Game;
- if (attractsResourceOverrides.TryGetValue(tileData.improvement.type, out ResourceData.Type newType))
- {
- resourceType = newType;
- }
- Polytopia.Data.TerrainData.Type targetTerrain = Polytopia.Data.TerrainData.Type.Forest;
- if (attractsTerrainOverrides.TryGetValue(tileData.improvement.type, out Polytopia.Data.TerrainData.Type newTerrain))
- {
- targetTerrain = newTerrain;
- }
-
- // Find a valid tile to spawn the resource on.
- foreach (TileData tileData2 in state.Map.GetArea(tileData.coordinates, 1, true, false))
- {
- if (tileData2.owner == __instance.PlayerId && tileData2.improvement == null && tileData2.resource == null && tileData2.terrain == targetTerrain)
- {
- state.ActionStack.Add(new CreateResourceAction(__instance.PlayerId, resourceType, tileData2.coordinates, CreateResourceAction.CreateReason.Attract));
- break;
- }
- }
- }
- }
- }
- }
- }
-
- ///
- /// 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);
- if (unit == null) Console.Write("THIS FUCKING SHIT IS NULL WHAT THE FUCK");
- return true;
- }
-
- ///
- /// Initializes the Main manager.
- ///
- internal static void Init()
- {
- stopwatch.Start();
- Harmony.CreateAndPatchAll(typeof(Main));
- Mod.Manifest polytopia = new(
- "polytopia",
- "The Battle of Polytopia",
- null,
- new(Application.version.ToString()),
- new string[] { "Midjiwan AB" },
- Array.Empty()
- );
- Registry.mods.Add(polytopia.id, new(polytopia, Mod.Status.Success, new()));
- Loader.RegisterMods(Registry.mods);
- Loader.LoadMods(Registry.mods, out var cycle);
- dependencyCycle = cycle;
- stopwatch.Stop();
- }
-
- ///
- /// Loads all mod content.
- ///
- /// The game logic data to patch.
- /// The JSON object representing the game logic data.
- internal static void Load(GameLogicData gameLogicData, JObject json)
- {
- stopwatch.Start();
- Loc.BuildAndLoadLocalization(
- JsonSerializer.Deserialize>>(
- Plugin.GetResource("localization.json")
- )!
- );
- if (dependencyCycle) return;
-
- foreach (var (id, mod) in Registry.mods)
- {
- if (mod.status != Mod.Status.Success) continue;
- foreach (var file in mod.files)
- {
- if (mod.status != Mod.Status.Success) break;
- Match localizationMatch = Regex.Match(Path.GetFileName(file.name), @"^localization(_.*)?\.json$");
- if (localizationMatch.Success)
- {
- Loader.LoadLocalizationFile(mod, file);
- continue;
- }
- Match patchMatch = Regex.Match(Path.GetFileName(file.name), @"^patch(_.*)?\.json$");
- if (patchMatch.Success)
- {
- var patchText = new StreamReader(new MemoryStream(file.bytes)).ReadToEnd();
- var template = new Api.GldConfigTemplate(patchText, mod.id);
- var text = template.Render();
- if (text is null)
- {
- mod.status = Mod.Status.Error;
- continue;
- }
- Loader.LoadGameLogicDataPatch(
- mod,
- json,
- JObject.Parse(text)
- );
- continue;
- }
- Match prefabMatch = Regex.Match(Path.GetFileName(file.name), @"^prefab(?:_(.*))?\.json$");
- if (prefabMatch.Success)
- {
- Loader.LoadPrefabInfoFile(
- mod,
- file
- );
- continue;
- }
-
- switch (Path.GetExtension(file.name))
- {
- case ".png":
- Loader.LoadSpriteFile(mod, file);
- break;
- case ".wav":
- Loader.LoadAudioFile(mod, file);
- break;
- case ".bundle":
- Loader.LoadAssetBundle(mod, file);
- break;
- }
- }
- }
- TechItem.techTierFirebaseId.Clear();
- for (int i = 0; i <= MAX_TECH_TIER; i++)
- {
- TechItem.techTierFirebaseId.Add($"tech_research_{i}");
- }
- Loader.ProcessGameLogicData(gameLogicData, json);
- stopwatch.Stop();
- Plugin.logger.LogInfo($"Loaded all mods in {stopwatch.ElapsedMilliseconds}ms");
- }
+ internal static bool dependencyCycle;
+
+ ///
+ /// The maximum tier for technology, used to extend the tech tree.
+ ///
+ internal const int MAX_TECH_TIER = 100;
+
+ ///
+ /// A stopwatch to measure the time taken to load mods.
+ ///
+ internal static readonly Stopwatch stopwatch = new();
+
+ ///
+ /// Whether the mod has been fully initialized.
+ ///
+ internal static bool fullyInitialized;
+
+ ///
+ /// A dictionary mapping unit IDs to the IDs of the units they embark into.
+ ///
+ internal static Dictionary embarkNames = new();
+
+ ///
+ /// A dictionary mapping unit types to the types of the units they embark into.
+ ///
+ internal static Dictionary embarkOverrides = new();
+
+ ///
+ /// Whether an embark action is currently being executed.
+ ///
+ internal static bool currentlyEmbarking = false;
+
+ ///
+ /// A dictionary mapping improvement IDs to the IDs of the resources they attract.
+ ///
+ internal static Dictionary attractsResourceNames = new();
+
+ ///
+ /// A dictionary mapping improvement IDs to the IDs of the terrain types they attract resources on.
+ ///
+ internal static Dictionary attractsTerrainNames = new();
+
+ ///
+ /// A dictionary mapping improvement types to the types of the resources they attract.
+ ///
+ internal static Dictionary attractsResourceOverrides = new();
+
+ ///
+ /// A dictionary mapping improvement types to the types of the terrain they attract resources on.
+ ///
+ internal static Dictionary attractsTerrainOverrides = new();
+
+ ///
+ /// Patches the game logic data parsing to load PolyMod content.
+ ///
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(GameLogicData), nameof(GameLogicData.AddGameLogicPlaceholders))]
+ private static void GameLogicData_AddGameLogicPlaceholders(GameLogicData __instance, ref JObject rootObject)
+ {
+ if (!fullyInitialized)
+ {
+ Load(__instance, rootObject);
+ fullyInitialized = true;
+ }
+ }
+
+ ///
+ /// Patches the purchase manager to unlock custom skins.
+ ///
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(PurchaseManager), nameof(PurchaseManager.IsSkinUnlocked))]
+ private static bool PurchaseManager_IsSkinUnlockedInternal(ref bool __result, SkinType skinType)
+ {
+ __result = (int)skinType >= Plugin.AUTOIDX_STARTS_FROM && skinType != SkinType.Test;
+ return !__result;
+ }
+
+ ///
+ /// Patches the purchase manager to unlock custom tribes.
+ ///
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(PurchaseManager), nameof(PurchaseManager.IsTribeUnlocked))]
+ private static void PurchaseManager_IsTribeUnlocked(ref bool __result, TribeType type)
+ {
+ __result = (int)type >= Plugin.AUTOIDX_STARTS_FROM || __result;
+ }
+
+ ///
+ /// Patches the steam purchase manager to unlock custom content.
+ ///
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(SteamPlatformPurchaseManager), nameof(SteamPlatformPurchaseManager.IsProductUnlocked))]
+ private static bool SteamPlatformPurchaseManager_IsProductUnlocked(ref bool __result, IAPProduct iapProduct)
+ {
+ __result = iapProduct == null;
+ return iapProduct != null;
+ }
+
+ ///
+ /// Patches the purchase manager to add custom tribes to the list of unlocked tribes.
+ ///
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(PurchaseManager), nameof(PurchaseManager.GetUnlockedTribes))]
+ private static void PurchaseManager_GetUnlockedTribes(
+ ref Il2CppSystem.Collections.Generic.List __result,
+ bool forceUpdate = false
+ )
+ {
+ foreach (var tribe in Registry.customTribes) __result.Add(tribe);
+ }
+
+ ///
+ /// Patches the Unity log callback to filter out spammy messages.
+ ///
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(IL2CPPUnityLogSource), nameof(IL2CPPUnityLogSource.UnityLogCallback))]
+ private static bool IL2CPPUnityLogSource_UnityLogCallback(string logLine, string exception, LogType type)
+ {
+ foreach (string stringToIgnore in Plugin.LOG_MESSAGES_IGNORE)
+ {
+ if (logLine.Contains(stringToIgnore))
+ return false;
+ }
+ return true;
+ }
+
+ ///
+ /// Patches the game mode screen to add custom game modes.
+ ///
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(GameModeScreen), nameof(GameModeScreen.Init))]
+ private static void GameModeScreen_Init(GameModeScreen __instance)
+ {
+ List list = __instance.buttons.ToList();
+ for (int i = 0; i < Loader.gamemodes.Count; i++)
+ {
+ var item = Loader.gamemodes[i];
+ var button = GameObject.Instantiate(__instance.buttons[2]);
+ list.Add(button);
+ Sprite? sprite = item.sprite;
+ if (item.sprite == null && item.spriteName != null)
+ {
+ sprite = Registry.GetSprite(item.spriteName);
+ }
+ Loader.gamemodes[i] = new Loader.GameModeButtonsInformation(item.gameModeIndex, item.action, __instance.buttons.Length, sprite, item.spriteName);
+ }
+
+ var newArray = list.ToArray();
+ for (int i = 0; i < __instance.buttons.Length; i++)
+ {
+ if (newArray[i] != null) newArray[i].OnClicked = __instance.buttons[i].OnClicked;
+ }
+
+ for (int i = 0; i < Loader.gamemodes.Count; i++)
+ {
+ if (Loader.gamemodes[i].buttonIndex != null)
+ newArray[Loader.gamemodes[i].buttonIndex!.Value].OnClicked = Loader.gamemodes[i].action;
+ }
+
+ __instance.buttons = newArray;
+
+ for (int i = 0; i < __instance.buttons.Length; i++)
+ {
+ GamemodeButton button = __instance.buttons[i];
+ var newData = button.gamemodeData.ToList();
+ foreach (var info in Loader.gamemodes)
+ {
+ string id = EnumCache.GetName((GameMode)info.gameModeIndex).ToLower();
+ newData.Add(new GamemodeButton.GamemodeButtonData()
+ {
+ gameMode = (GameMode)info.gameModeIndex,
+ id = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(id),
+ descriptionKey = "gamemode." + id + ".description.button",
+ headerKey = "gamemode." + id + ".caps",
+ icon = info.sprite
+ });
+ }
+ button.gamemodeData = newData.ToArray();
+
+ for (int j = 0; j < Loader.gamemodes.Count; j++)
+ {
+ Loader.GameModeButtonsInformation info = Loader.gamemodes[j];
+
+ if (info.buttonIndex == i)
+ {
+ button.SetGamemode(info.buttonIndex.Value);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Patches the tech view to correctly display the tech tree with custom technologies.
+ ///
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(TechView), nameof(TechView.CreateNode))]
+ public static bool TechView_CreateNode(TechView __instance, TechData data, TechItem parentItem, float angle)
+ {
+ // This patch is a reimplementation of the original CreateNode method to fix layout issues with custom techs.
+ // It recalculates the angles for each node to ensure they are displayed correctly.
+ GameLogicData gameLogicData = GameManager.GameState.GameLogicData;
+ TribeData tribeData = gameLogicData.GetTribeData(GameManager.LocalPlayer.tribe);
+ float baseAngle = 360 / gameLogicData.GetOverride(gameLogicData.GetTechData(TechData.Type.Basic), tribeData).techUnlocks.Count;
+ float childAngle = 0f;
+ if (parentItem != null)
+ childAngle = angle + baseAngle * (data.techUnlocks.Count - 1) / 2f;
+ foreach (var techData in data.techUnlocks)
+ {
+ if (gameLogicData.TryGetData(techData.type, out TechData techData2))
+ {
+ TechData @override = gameLogicData.GetOverride(techData, tribeData);
+ TechItem techItem = __instance.CreateTechItem(@override, parentItem, childAngle);
+ __instance.currTechIdx++;
+ if (@override.techUnlocks != null && @override.techUnlocks.Count > 0)
+ __instance.CreateNode(@override, techItem, childAngle);
+ childAngle -= baseAngle;
+ }
+ }
+ Il2CppSystem.Action onItemsRefreshed = __instance.OnItemsRefreshed;
+ if (onItemsRefreshed == null)
+ {
+ return false;
+ }
+ onItemsRefreshed.Invoke(__instance);
+ return false;
+ }
+
+ ///
+ /// Patches the embark action to set a flag indicating that an embark is in progress.
+ ///
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(EmbarkAction), nameof(EmbarkAction.Execute))]
+ private static bool EmbarkAction_Execute_Prefix(EmbarkAction __instance, GameState gameState)
+ {
+ currentlyEmbarking = true;
+ return true;
+ }
+
+ ///
+ /// Patches the unit training method to handle custom embark units.
+ ///
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(ActionUtils), nameof(ActionUtils.TrainUnit))]
+ private static bool ActionUtils_TrainUnit(ref UnitState __result, GameState gameState, PlayerState playerState, TileData tile, ref UnitData unitData)
+ {
+ if (tile == null || tile.unit == null)
+ {
+ return true;
+ }
+
+ if (currentlyEmbarking)
+ {
+ if (embarkOverrides.TryGetValue(tile.unit.type, out UnitData.Type newType))
+ {
+ gameState.GameLogicData.TryGetData(newType, out unitData);
+ }
+ currentlyEmbarking = false;
+ }
+ return true;
+ }
+
+ ///
+ /// Patches the start turn action to handle the 'Attract' ability for custom improvements.
+ ///
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(StartTurnAction), nameof(StartTurnAction.Execute))]
+ private static void StartTurnAction_Execute(StartTurnAction __instance, GameState state)
+ {
+ // Clear any existing CreateResource actions to prevent duplicates.
+ for (int i = state.ActionStack.Count - 1; i >= 0; i--)
+ {
+ if (state.ActionStack[i].GetActionType() == ActionType.CreateResource)
+ {
+ state.ActionStack.RemoveAt(i);
+ }
+ }
+
+ // Iterate through all tiles owned by the current player.
+ for (int i = 0; i < state.Map.Tiles.Length; i++)
+ {
+ TileData tileData = state.Map.Tiles[i];
+ if (tileData.owner == __instance.PlayerId && tileData.improvement != null && state.CurrentTurn > 0U)
+ {
+ ImprovementData improvementData;
+ state.GameLogicData.TryGetData(tileData.improvement.type, out improvementData);
+ if (improvementData != null)
+ {
+ // If the improvement has the 'Attract' ability and is ready to spawn a resource.
+ if (improvementData.HasAbility(ImprovementAbility.Type.Attract) && tileData.improvement.GetAge(state) % improvementData.growthRate == 0)
+ {
+ ResourceData.Type resourceType = ResourceData.Type.Game;
+ if (attractsResourceOverrides.TryGetValue(tileData.improvement.type, out ResourceData.Type newType))
+ {
+ resourceType = newType;
+ }
+ Polytopia.Data.TerrainData.Type targetTerrain = Polytopia.Data.TerrainData.Type.Forest;
+ if (attractsTerrainOverrides.TryGetValue(tileData.improvement.type, out Polytopia.Data.TerrainData.Type newTerrain))
+ {
+ targetTerrain = newTerrain;
+ }
+
+ // Find a valid tile to spawn the resource on.
+ foreach (TileData tileData2 in state.Map.GetArea(tileData.coordinates, 1, true, false))
+ {
+ if (tileData2.owner == __instance.PlayerId && tileData2.improvement == null && tileData2.resource == null && tileData2.terrain == targetTerrain)
+ {
+ state.ActionStack.Add(new CreateResourceAction(__instance.PlayerId, resourceType, tileData2.coordinates, CreateResourceAction.CreateReason.Attract));
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// 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);
+ if (unit == null) Console.Write("THIS FUCKING SHIT IS NULL WHAT THE FUCK");
+ return true;
+ }
+
+ ///
+ /// Initializes the Main manager.
+ ///
+ internal static void Init()
+ {
+ stopwatch.Start();
+ Harmony.CreateAndPatchAll(typeof(Main));
+ Mod.Manifest polytopia = new(
+ "polytopia",
+ "The Battle of Polytopia",
+ null,
+ new(Application.version.ToString()),
+ new string[] { "Midjiwan AB" },
+ Array.Empty()
+ );
+ Registry.mods.Add(polytopia.id, new(polytopia, Mod.Status.Success, new()));
+ Loader.RegisterMods(Registry.mods);
+ Loader.LoadMods(Registry.mods, out var cycle);
+ dependencyCycle = cycle;
+ stopwatch.Stop();
+ }
+
+ ///
+ /// Loads all mod content.
+ ///
+ /// The game logic data to patch.
+ /// The JSON object representing the game logic data.
+ internal static void Load(GameLogicData gameLogicData, JObject json)
+ {
+ stopwatch.Start();
+ Loc.BuildAndLoadLocalization(
+ JsonSerializer.Deserialize>>(
+ Plugin.GetResource("localization.json")
+ )!
+ );
+ if (dependencyCycle) return;
+
+ foreach (var (id, mod) in Registry.mods)
+ {
+ if (mod.status != Mod.Status.Success) continue;
+ foreach (var file in mod.files)
+ {
+ if (mod.status != Mod.Status.Success) break;
+ Match localizationMatch = Regex.Match(Path.GetFileName(file.name), @"^localization(_.*)?\.json$");
+ if (localizationMatch.Success)
+ {
+ Loader.LoadLocalizationFile(mod, file);
+ continue;
+ }
+ Match patchMatch = Regex.Match(Path.GetFileName(file.name), @"^patch(_.*)?\.json$");
+ if (patchMatch.Success)
+ {
+ var patchText = new StreamReader(new MemoryStream(file.bytes)).ReadToEnd();
+ var template = new Api.GldConfigTemplate(patchText, mod.id);
+ var text = template.Render();
+ if (text is null)
+ {
+ mod.status = Mod.Status.Error;
+ continue;
+ }
+ Loader.LoadGameLogicDataPatch(
+ mod,
+ json,
+ JObject.Parse(text)
+ );
+ continue;
+ }
+ Match prefabMatch = Regex.Match(Path.GetFileName(file.name), @"^prefab(?:_(.*))?\.json$");
+ if (prefabMatch.Success)
+ {
+ Loader.LoadPrefabInfoFile(
+ mod,
+ file
+ );
+ continue;
+ }
+
+ switch (Path.GetExtension(file.name))
+ {
+ case ".png":
+ Loader.LoadSpriteFile(mod, file);
+ break;
+ case ".wav":
+ Loader.LoadAudioFile(mod, file);
+ break;
+ case ".bundle":
+ Loader.LoadAssetBundle(mod, file);
+ break;
+ }
+ }
+ }
+ TechItem.techTierFirebaseId.Clear();
+ for (int i = 0; i <= MAX_TECH_TIER; i++)
+ {
+ TechItem.techTierFirebaseId.Add($"tech_research_{i}");
+ }
+ Loader.ProcessGameLogicData(gameLogicData, json);
+ stopwatch.Stop();
+ Plugin.logger.LogInfo($"Loaded all mods in {stopwatch.ElapsedMilliseconds}ms");
+ }
}
diff --git a/src/Managers/Visual.cs b/src/Managers/Visual.cs
index bccc5fc..628b7b2 100644
--- a/src/Managers/Visual.cs
+++ b/src/Managers/Visual.cs
@@ -18,942 +18,942 @@ 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);
-
- /// Represents information about a custom skin.
- public record SkinInfo(int idx, string id, SkinData? skinData);
-
- /// A dictionary of custom widths for basic popups.
- public static Dictionary basicPopupWidths = new();
- /// Represents information about a unit prefab.
- public struct UnitPrefabInfo
- {
- public UnitPrefabInfo(string type, string tribe, string skin)
- {
- this.type = type;
- this.tribe = tribe;
- this.skin = skin;
- }
- public string type;
- public string tribe;
- public string skin;
- }
- private const int BASIC_POPUP_LEGACY_ID = 29;
- public static Dictionary customPrefabs = new();
- private static UnitData.Type currentUnitTypeUI = UnitData.Type.None;
- private static TribeType attackerTribe = TribeType.None;
- ///
- /// A dictionary of skin types of tribes, which can flood tiles, keyed by custom flood tile effect.
- ///
- internal static Dictionary customFloodingSkins = new();
- private static bool isTakingSnapshot = false;
- private static float? baseOrthographicCameraSize = null;
-
- /// The type of a custom prefab.
- public enum PrefabType
- {
- Unit,
- Improvement,
- Resource
- }
-
- /// Represents information about a custom prefab.
- public record PrefabInfo
- {
- public PrefabType type { get; init; }
- public string name { get; init; }
- public List visualParts { get; init; }
- public string headPositionMarker { get; init; }
-
- [JsonConstructor]
- public PrefabInfo(
- PrefabType type,
- List visualParts,
- string name = "",
- string headPositionMarker = "")
- {
- this.type = type;
- this.name = name.ToLowerInvariant();
- this.visualParts = visualParts;
- this.headPositionMarker = headPositionMarker;
- }
- }
-
- /// Represents information about a visual part of a prefab.
- public record VisualPartInfo(
- string gameObjectName,
- string baseName,
- float rotation = 0f,
- Vector2 coordinates = new Vector2(),
- Vector2 scale = new Vector2(),
- bool tintable = false
- );
- private static bool enableOutlines = false;
- private static bool seenWarningWCPopup = false;
- public record PreviewInfo(int arrayIdx, SaveStateData saveStateData, PreviewTile[] customPreview);
-
- #region General
-
- /// A placeholder patch for the TechItem.SetupComplete method.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(TechItem), nameof(TechItem.SetupComplete))]
- private static void TechItem_SetupComplete()
- {
- }
-
- /// Patches the sprite atlas manager to load custom sprites.
- [HarmonyPrefix]
- [HarmonyPatch(typeof(SpriteAtlasManager), nameof(SpriteAtlasManager.LoadSprite), typeof(string), typeof(string), typeof(SpriteCallback))]
- private static bool SpriteAtlasManager_LoadSprite(SpriteAtlasManager __instance, string atlas, string sprite, SpriteCallback completion)
- {
- bool found = false;
- __instance.LoadSpriteAtlas(atlas, (Il2CppSystem.Action)GetAtlas);
-
- 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 = Registry.GetSprite(name, style);
- if (newSprite != null)
- {
- completion?.Invoke(atlas, sprite, newSprite);
- found = true;
- }
- }
- }
- }
-
- /// Patches the sprite atlas manager to look up custom sprites.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(SpriteAtlasManager), nameof(SpriteAtlasManager.DoSpriteLookup))]
- private static void SpriteAtlasManager_DoSpriteLookup(ref SpriteAtlasManager.SpriteLookupResult __result, SpriteAtlasManager __instance, string baseName, TribeType tribe, SkinType skin, int level)
- {
- baseName = Util.FormatSpriteName(baseName);
-
- Sprite? sprite = Registry.GetSprite(baseName, Util.GetStyle(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;
- 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);
- }
-
- /// 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;
- }
-
- /// 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))]
- private static void TerrainRenderer_UpdateGraphics(TerrainRenderer __instance, Tile tile)
- {
- string terrain = EnumCache.GetName(tile.data.terrain) ?? string.Empty;
-
- TribeType tribe = tile.data.climate;
- SkinType skinType = tile.data.Skin;
-
- string flood = "";
- if (
- tile.data.effects.Contains(TileData.EffectType.Flooded) ||
- (tribe == TribeType.Aquarion && tile.data.terrain == Polytopia.Data.TerrainData.Type.Mountain)
- ) // TODO: Check all tribes with flooded abil instead of Aquarion only.
- {
- foreach (var effect in tile.data.effects)
- {
- if(customFloodingSkins.ContainsKey(effect))
- {
- skinType = customFloodingSkins[effect];
- break;
- }
- }
- flood = "_flooded";
- }
- if (tile.data.terrain is Polytopia.Data.TerrainData.Type.Forest or Polytopia.Data.TerrainData.Type.Mountain)
- {
- string propertyName = terrain.ToLower();
- terrain = "field";
-
- PropertyInfo? rendererProperty = tile.GetType().GetProperty(propertyName + "Renderer",
- BindingFlags.Public | BindingFlags.Instance);
-
- if (rendererProperty != null)
- {
- PolytopiaSpriteRenderer? renderer = (PolytopiaSpriteRenderer?)rendererProperty.GetValue(tile);
- if (renderer != null)
- {
- Sprite? additionalSprite = Registry.GetSprite(propertyName + flood, Util.GetStyle(tribe, skinType));
- if (additionalSprite != null)
- {
- renderer.Sprite = additionalSprite;
- rendererProperty.SetValue(tile, renderer);
- }
- }
- }
- }
-
- Sprite? sprite = Registry.GetSprite(terrain + flood, Util.GetStyle(tribe, skinType));
- if (sprite != null)
- {
- __instance.spriteRenderer.Sprite = sprite;
- }
- }
-
- /// Adds custom flood tile effect when tile is flooded for custom aqua skins.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(TileData), nameof(TileData.Flood))]
- private static void TileData_Flood(TileData __instance, PlayerState playerState)
- {
- if(customFloodingSkins.ContainsValue(playerState.skinType))
- {
- TileData.EffectType effectType = customFloodingSkins.FirstOrDefault(x => x.Value == playerState.skinType).Key;
- __instance.AddEffect(effectType);
- }
- }
-
- /// Removes custom flood effect loc from tile tip header.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(TileTipGenerator), nameof(TileTipGenerator.GetTileTipHeader))]
- public static void GetTileTipHeader(ref string __result, TileData tileData, SkinType skin,
- PlayerState playerState, GameState gameState, GameLogicData gameLogicData)
- {
- foreach(var effect in tileData.effects)
- {
- if(customFloodingSkins.ContainsKey(effect))
- {
- string localizedEffect = Localization.GetSkinned(skin, playerState.tribe, $"tile.effect.{effect.GetName()}");
- __result = __result.Replace($", {localizedEffect}", string.Empty);
- }
- }
- }
-
- /// Forces an update of the mesh for a PolytopiaSpriteRenderer with a custom sprite.
- [HarmonyPostfix]
- [HarmonyPatch(typeof(PolytopiaSpriteRenderer), nameof(PolytopiaSpriteRenderer.ForceUpdateMesh))]
- private static void PolytopiaSpriteRenderer_ForceUpdateMesh(PolytopiaSpriteRenderer __instance)
- {
- if (__instance.sprite != null && string.IsNullOrEmpty(__instance.atlasName))
- {
- MaterialPropertyBlock materialPropertyBlock = new();
- materialPropertyBlock.SetVector("_Flip", new Vector4(1f, 1f, 0f, 0f));
- materialPropertyBlock.SetTexture("_MainTex", __instance.sprite.texture);
- __instance.meshRenderer.SetPropertyBlock(materialPropertyBlock);
- }
- }
+ ///
+ /// 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);
+
+ /// Represents information about a custom skin.
+ public record SkinInfo(int idx, string id, SkinData? skinData);
+
+ /// A dictionary of custom widths for basic popups.
+ public static Dictionary basicPopupWidths = new();
+ /// Represents information about a unit prefab.
+ public struct UnitPrefabInfo
+ {
+ public UnitPrefabInfo(string type, string tribe, string skin)
+ {
+ this.type = type;
+ this.tribe = tribe;
+ this.skin = skin;
+ }
+ public string type;
+ public string tribe;
+ public string skin;
+ }
+ private const int BASIC_POPUP_LEGACY_ID = 29;
+ public static Dictionary customPrefabs = new();
+ private static UnitData.Type currentUnitTypeUI = UnitData.Type.None;
+ private static TribeType attackerTribe = TribeType.None;
+ ///
+ /// A dictionary of skin types of tribes, which can flood tiles, keyed by custom flood tile effect.
+ ///
+ internal static Dictionary customFloodingSkins = new();
+ private static bool isTakingSnapshot = false;
+ private static float? baseOrthographicCameraSize = null;
+
+ /// The type of a custom prefab.
+ public enum PrefabType
+ {
+ Unit,
+ Improvement,
+ Resource
+ }
+
+ /// Represents information about a custom prefab.
+ public record PrefabInfo
+ {
+ public PrefabType type { get; init; }
+ public string name { get; init; }
+ public List visualParts { get; init; }
+ public string headPositionMarker { get; init; }
+
+ [JsonConstructor]
+ public PrefabInfo(
+ PrefabType type,
+ List visualParts,
+ string name = "",
+ string headPositionMarker = "")
+ {
+ this.type = type;
+ this.name = name.ToLowerInvariant();
+ this.visualParts = visualParts;
+ this.headPositionMarker = headPositionMarker;
+ }
+ }
+
+ /// Represents information about a visual part of a prefab.
+ public record VisualPartInfo(
+ string gameObjectName,
+ string baseName,
+ float rotation = 0f,
+ Vector2 coordinates = new Vector2(),
+ Vector2 scale = new Vector2(),
+ bool tintable = false
+ );
+ private static bool enableOutlines = false;
+ private static bool seenWarningWCPopup = false;
+ public record PreviewInfo(int arrayIdx, SaveStateData saveStateData, PreviewTile[] customPreview);
+
+ #region General
+
+ /// A placeholder patch for the TechItem.SetupComplete method.
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(TechItem), nameof(TechItem.SetupComplete))]
+ private static void TechItem_SetupComplete()
+ {
+ }
+
+ /// Patches the sprite atlas manager to load custom sprites.
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(SpriteAtlasManager), nameof(SpriteAtlasManager.LoadSprite), typeof(string), typeof(string), typeof(SpriteCallback))]
+ private static bool SpriteAtlasManager_LoadSprite(SpriteAtlasManager __instance, string atlas, string sprite, SpriteCallback completion)
+ {
+ bool found = false;
+ __instance.LoadSpriteAtlas(atlas, (Il2CppSystem.Action)GetAtlas);
+
+ 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 = Registry.GetSprite(name, style);
+ if (newSprite != null)
+ {
+ completion?.Invoke(atlas, sprite, newSprite);
+ found = true;
+ }
+ }
+ }
+ }
+
+ /// Patches the sprite atlas manager to look up custom sprites.
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(SpriteAtlasManager), nameof(SpriteAtlasManager.DoSpriteLookup))]
+ private static void SpriteAtlasManager_DoSpriteLookup(ref SpriteAtlasManager.SpriteLookupResult __result, SpriteAtlasManager __instance, string baseName, TribeType tribe, SkinType skin, int level)
+ {
+ baseName = Util.FormatSpriteName(baseName);
+
+ Sprite? sprite = Registry.GetSprite(baseName, Util.GetStyle(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;
+ 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);
+ }
+
+ /// 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;
+ }
+
+ /// 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))]
+ private static void TerrainRenderer_UpdateGraphics(TerrainRenderer __instance, Tile tile)
+ {
+ string terrain = EnumCache.GetName(tile.data.terrain) ?? string.Empty;
+
+ TribeType tribe = tile.data.climate;
+ SkinType skinType = tile.data.Skin;
+
+ string flood = "";
+ if (
+ tile.data.effects.Contains(TileData.EffectType.Flooded) ||
+ (tribe == TribeType.Aquarion && tile.data.terrain == Polytopia.Data.TerrainData.Type.Mountain)
+ ) // TODO: Check all tribes with flooded abil instead of Aquarion only.
+ {
+ foreach (var effect in tile.data.effects)
+ {
+ if (customFloodingSkins.ContainsKey(effect))
+ {
+ skinType = customFloodingSkins[effect];
+ break;
+ }
+ }
+ flood = "_flooded";
+ }
+ if (tile.data.terrain is Polytopia.Data.TerrainData.Type.Forest or Polytopia.Data.TerrainData.Type.Mountain)
+ {
+ string propertyName = terrain.ToLower();
+ terrain = "field";
+
+ PropertyInfo? rendererProperty = tile.GetType().GetProperty(propertyName + "Renderer",
+ BindingFlags.Public | BindingFlags.Instance);
+
+ if (rendererProperty != null)
+ {
+ PolytopiaSpriteRenderer? renderer = (PolytopiaSpriteRenderer?)rendererProperty.GetValue(tile);
+ if (renderer != null)
+ {
+ Sprite? additionalSprite = Registry.GetSprite(propertyName + flood, Util.GetStyle(tribe, skinType));
+ if (additionalSprite != null)
+ {
+ renderer.Sprite = additionalSprite;
+ rendererProperty.SetValue(tile, renderer);
+ }
+ }
+ }
+ }
+
+ Sprite? sprite = Registry.GetSprite(terrain + flood, Util.GetStyle(tribe, skinType));
+ if (sprite != null)
+ {
+ __instance.spriteRenderer.Sprite = sprite;
+ }
+ }
+
+ ///