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