diff --git a/FodyWeavers.xml b/FodyWeavers.xml new file mode 100644 index 0000000..d24cbce --- /dev/null +++ b/FodyWeavers.xml @@ -0,0 +1,8 @@ + + + + + Scriban + + + \ No newline at end of file diff --git a/PolyMod.csproj b/PolyMod.csproj index 072a080..5a2e73a 100644 --- a/PolyMod.csproj +++ b/PolyMod.csproj @@ -1,4 +1,4 @@ - + net6.0 enable @@ -20,6 +20,10 @@ + + all + + diff --git a/resources/localization.json b/resources/localization.json index e9afd56..c74653e 100644 --- a/resources/localization.json +++ b/resources/localization.json @@ -22,15 +22,15 @@ "German (Germany)": "UNSER DISCORD" }, "polymod_hub_footer": { - "English": "Join our discord! Feel free to discuss mods, create them and ask for help!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", - "Russian": "Присоединяйтесь к нашему дискорду! Не стесняйтесь обсуждать моды, создавать их и просить о помощи!\n\n{0}Особая благодарность{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", - "Turkish": "Discord sunucumuza katıl! Orada modlar oluşturabilir, tartışabilir ve yardım isteyebilirsin!\n\n{0}Hepinize çok teşekkür ederim:{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", - "Spanish (Mexico)": "Unete a nuestro discord! Aqui se puede discutir sobre la modificacion del juego, guias para crear su propio, preguntar a los creadores, y mas!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", - "French (France)": "Rejoignez notre discord! N'hésitez pas à discuter des mods, à en créer et à demander de l'aide!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", - "Polish": "Dołącz do naszego discorda! Zachęcamy do omawiania modów, tworzenia ich lub proszenia o pomoc!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", - "Portuguese (Brazil)": "Entre no nosso Discord! Sinta-se à vontade para discutir sobre os mods, criar novos mods e pedir ajuda!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", - "Elyrion": "§ii∫ Δi^#ȱrΔ! Δi^#₺^^ mȱΔ#, ȱrrȱ ỹ a^š ỹȱπ!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", - "German (Germany)": "Tritt unserem Discord bei, um Hilfe zu bekommen, Mods zu diskutieren oder sogar selbst zu erstellen!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon" + "English": "Join our discord! Feel free to discuss mods, create them and ask for help!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nincomplete_tree\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", + "Russian": "Присоединяйтесь к нашему дискорду! Не стесняйтесь обсуждать моды, создавать их и просить о помощи!\n\n{0}Особая благодарность{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nincomplete_tree\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", + "Turkish": "Discord sunucumuza katıl! Orada modlar oluşturabilir, tartışabilir ve yardım isteyebilirsin!\n\n{0}Hepinize çok teşekkür ederim:{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nincomplete_tree\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", + "Spanish (Mexico)": "Unete a nuestro discord! Aqui se puede discutir sobre la modificacion del juego, guias para crear su propio, preguntar a los creadores, y mas!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nincomplete_tree\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", + "French (France)": "Rejoignez notre discord! N'hésitez pas à discuter des mods, à en créer et à demander de l'aide!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nincomplete_tree\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", + "Polish": "Dołącz do naszego discorda! Zachęcamy do omawiania modów, tworzenia ich lub proszenia o pomoc!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nincomplete_tree\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", + "Portuguese (Brazil)": "Entre no nosso Discord! Sinta-se à vontade para discutir sobre os mods, criar novos mods e pedir ajuda!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nincomplete_tree\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", + "Elyrion": "§ii∫ Δi^#ȱrΔ! Δi^#₺^^ mȱΔ#, ȱrrȱ ỹ a^š ỹȱπ!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nincomplete_tree\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon", + "German (Germany)": "Tritt unserem Discord bei, um Hilfe zu bekommen, Mods zu diskutieren oder sogar selbst zu erstellen!\n\n{0}Special thanks{1}\n___exploit___\njohnklipi\nhighflyer\nMRB\nincomplete_tree\nArtemis\nParanoia\nNyrrv\nCitillan\nLukasAyas\nVaM\nWhail\nBrober\nMaradon" }, "polymod_hub_header": { "English": "{0}Welcome!{1}\nHere you can see the list of all currently loaded mods:", diff --git a/src/Loader.cs b/src/Loader.cs index f218f14..6bdc07e 100644 --- a/src/Loader.cs +++ b/src/Loader.cs @@ -12,6 +12,7 @@ using System.Globalization; using System.IO.Compression; using System.Reflection; +using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using UnityEngine; @@ -230,7 +231,7 @@ public static void AddPatchDataType(string typeId, Type type, bool shouldCreateC /// Loads all mods from the mods directory. /// /// A dictionary to populate with the loaded mods. - internal static void LoadMods(Dictionary mods) + internal static void RegisterMods(Dictionary mods) { Directory.CreateDirectory(Plugin.MODS_PATH); string[] modContainers = Directory.GetDirectories(Plugin.MODS_PATH) @@ -279,8 +280,7 @@ internal static void LoadMods(Dictionary mods) files.Add(new(entry.FullName, entry.ReadBytes())); } } - - // Validate manifest + #region ValidateManifest() if (manifest == null) { Plugin.logger.LogError($"Mod manifest not found in {modContainer}"); @@ -311,6 +311,7 @@ internal static void LoadMods(Dictionary mods) Plugin.logger.LogError($"Mod {manifest.id} already exists"); continue; } + #endregion mods.Add(manifest.id, new( manifest, Mod.Status.Success, @@ -319,7 +320,41 @@ internal static void LoadMods(Dictionary mods) Plugin.logger.LogInfo($"Registered mod {manifest.id}"); } - // Check dependencies + CheckDependencies(mods); + } + + 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); + } + if (Path.GetFileName(file.name) == "sprites.json") + { + LoadSpriteInfoFile(mod, file); + } + } + 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()) @@ -362,7 +397,7 @@ internal static void LoadMods(Dictionary mods) /// /// The dictionary of mods to sort. /// True if the mods could be sorted (no circular dependencies), false otherwise. - internal static bool SortMods(Dictionary mods) + private static bool SortMods(Dictionary mods) { Stopwatch s = new(); Dictionary> graph = new(); @@ -431,6 +466,16 @@ public static void LoadAssemblyFile(Mod mod, Mod.File file) try { Assembly assembly = Assembly.Load(file.bytes); + if (assembly + .GetTypes() + .FirstOrDefault(t => t.IsSubclassOf(typeof(PolyScriptBase))) + is { } modType) + { + var modInstance = (PolyScriptBase) Activator.CreateInstance(modType)!; + modInstance.Initialize(mod.id, 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) }); diff --git a/src/Managers/Config.cs b/src/Managers/Config.cs new file mode 100644 index 0000000..f320264 --- /dev/null +++ b/src/Managers/Config.cs @@ -0,0 +1,120 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace PolyMod.Managers; + +/// +/// Allows mods to save config. +/// +public class Config where T : class +{ + private T? currentConfig; + private readonly string modName; + private readonly ConfigTypes configType; + private static readonly string ExposedConfigPath = Path.Combine(Plugin.BASE_PATH, "mods.json"); + private readonly string perModConfigPath; + private T? defaultConfig; + public Config(string modName, ConfigTypes configType) + { + this.modName = modName; + this.configType = configType; + perModConfigPath = Path.Combine(Plugin.MODS_PATH, $"{modName}.json"); + Load(); + } + + internal void Load() // can be called internally if config changes; gui config not implemented yet + { + switch (configType) + { + case ConfigTypes.PerMod: + { + if (!File.Exists(perModConfigPath)) + { + return; + } + var jsonText = File.ReadAllText(perModConfigPath); + currentConfig = JsonSerializer.Deserialize(jsonText); + break; + } + case ConfigTypes.Exposed: + { + if (!File.Exists(ExposedConfigPath)) + { + return; + } + var jsonText = File.ReadAllText(ExposedConfigPath); + currentConfig = JsonNode.Parse(jsonText)![modName]?.Deserialize(); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + /// + /// Sets the default if the config does not exist yet. Always call this before reading from the config. + /// + public void SetDefaultConfig(T defaultValue) + { + defaultConfig = defaultValue; + if (currentConfig is not null) return; + Write(defaultConfig); + SaveChanges(); + } + + /// + /// Writes the **entire** config. Usage not recommended, use Edit() instead + /// + public void Write(T config) + { + currentConfig = config; + } + /// + /// Gets the config. Should only be called after setting a default. + /// + public T Get() + { + return currentConfig ?? throw new InvalidOperationException("Must set default before reading config."); + } + /// + /// Edits the config. Should only be called after setting a default. + /// + /// Call SaveChanges after editing + public void Edit(Action editor) + { + editor(currentConfig ?? throw new InvalidOperationException("Must set default before reading config.")); + } + /// + /// Gets part of the config. Should only be called after setting a default + /// + public TResult Get(Func getter) + { + return getter(currentConfig ?? throw new InvalidOperationException("Must set default before reading config.")); + } + /// + /// Writes the config to disk + /// + public void SaveChanges() + { + switch (configType) + { + case ConfigTypes.PerMod: + var perModJson = JsonSerializer.Serialize(currentConfig, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(perModConfigPath, perModJson); + break; + case ConfigTypes.Exposed: + var modsConfigText = File.ReadAllText(ExposedConfigPath); + var modsConfigJson = JsonNode.Parse(modsConfigText)!.AsObject(); + modsConfigJson[modName] = JsonSerializer.SerializeToNode(currentConfig!); + File.WriteAllText(ExposedConfigPath, modsConfigJson.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public enum ConfigTypes + { + PerMod, + Exposed + } +} \ No newline at end of file diff --git a/src/Managers/GLDConfig.cs b/src/Managers/GLDConfig.cs new file mode 100644 index 0000000..e341ab5 --- /dev/null +++ b/src/Managers/GLDConfig.cs @@ -0,0 +1,94 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Scriban; +using Scriban.Runtime; + +namespace PolyMod.Managers; + +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; + + public GldConfigTemplate(string templateText, string modName) + { + this.templateText = templateText; + this.modName = modName; + Load(); + } + 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) + && modConfigNode is JsonObject modConfig) + { + currentConfig = modConfig; + return; + } + } + currentConfig = new JsonObject(); + } + + public string? Render() + { + if (!templateText.Contains("{{")) return templateText; + var template = Template.Parse(templateText); + var context = new TemplateContext(); + var scriptObject = new ScriptObject(); + + bool changedConfig = false; + scriptObject.Import("config", + new Func((key, defaultValue) => + { + if (currentConfig.TryGetPropertyValue(key, out var token) && token != null) + { + return token.ToString(); + } + + changedConfig = true; + currentConfig[key] = defaultValue; + + return defaultValue; + }) + ); + context.PushGlobal(scriptObject); + string? result; + try + { + result = template.Render(context); + } + catch (Exception e) + { + Plugin.logger.LogError("error during parse of gld patch template: " + e.ToString()); + result = null; + } + if (changedConfig) + { + SaveChanges(); + } + return result; + } + + public void SaveChanges() + { + JsonObject modsConfigJson; + if (File.Exists(ConfigPath)) + { + var modsConfigText = File.ReadAllText(ConfigPath); + modsConfigJson = (JsonNode.Parse(modsConfigText) as JsonObject) ?? new JsonObject(); + } + else + { + modsConfigJson = new JsonObject(); + } + + modsConfigJson[modName] = currentConfig; + File.WriteAllText(ConfigPath, modsConfigJson.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); + } +} \ No newline at end of file diff --git a/src/Managers/Main.cs b/src/Managers/Main.cs index b9e19f4..37a06f8 100644 --- a/src/Managers/Main.cs +++ b/src/Managers/Main.cs @@ -18,6 +18,8 @@ namespace PolyMod.Managers; /// public static class Main { + internal static bool dependencyCycle; + /// /// The maximum tier for technology, used to extend the tech tree. /// @@ -32,12 +34,7 @@ public static class Main /// Whether the mod has been fully initialized. /// internal static bool fullyInitialized; - - /// - /// Whether a dependency cycle was detected among the loaded mods. - /// - internal static bool dependencyCycle; - + /// /// A dictionary mapping unit IDs to the IDs of the units they embark into. /// @@ -350,34 +347,9 @@ internal static void Init() Array.Empty() ); Registry.mods.Add(polytopia.id, new(polytopia, Mod.Status.Success, new())); - Loader.LoadMods(Registry.mods); - dependencyCycle = !Loader.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") - { - Loader.LoadAssemblyFile(mod, file); - } - if (Path.GetFileName(file.name) == "sprites.json") - { - Loader.LoadSpriteInfoFile(mod, file); - } - } - if (!mod.client && id != "polytopia") - { - checksumString.Append(id); - checksumString.Append(mod.version.ToString()); - } - } - Compatibility.HashSignatures(checksumString); - + Loader.RegisterMods(Registry.mods); + Loader.LoadMods(Registry.mods, out var cycle); + dependencyCycle = cycle; stopwatch.Stop(); } @@ -408,10 +380,18 @@ internal static void Load(GameLogicData gameLogicData, JObject json) } if (Regex.IsMatch(Path.GetFileName(file.name), @"^patch(_.*)?\.json$")) { + var patchText = new StreamReader(new MemoryStream(file.bytes)).ReadToEnd(); + var template = new GldConfigTemplate(patchText, mod.id); + var text = template.Render(); + if (text is null) + { + mod.status = Mod.Status.Error; + continue; + } Loader.LoadGameLogicDataPatch( mod, json, - JObject.Parse(new StreamReader(new MemoryStream(file.bytes)).ReadToEnd()) + JObject.Parse(text) ); continue; } diff --git a/src/PolyScriptMod.cs b/src/PolyScriptMod.cs new file mode 100644 index 0000000..d6975ce --- /dev/null +++ b/src/PolyScriptMod.cs @@ -0,0 +1,34 @@ +using BepInEx.Logging; +using Newtonsoft.Json.Linq; +using PolyMod.Managers; + +namespace PolyMod; + +public abstract class PolyScriptBase +{ + internal abstract void Initialize(string name, ManualLogSource logger); + public abstract void Load(); + public abstract void UnLoad(); + internal PolyScriptBase() + { + } +} +public abstract class PolyScript : PolyScriptBase where TConfig : class where TExposedConfig : class +{ + internal override void Initialize(string name, ManualLogSource logger) + { + ModName = name; + Config = new Config(name, Config.ConfigTypes.PerMod); + ExposedConfig = new Config(name, Config.ConfigTypes.Exposed); + Logger = logger; + } + + public string ModName { get; private set; } = null!; + protected Config Config { get; private set; } = null!; + protected Config ExposedConfig { get; private set; } = null!; + protected ManualLogSource Logger { get; private set; } = null!; +} + +public abstract class PolyScript : PolyScript +{ +} \ No newline at end of file