Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions FodyWeavers.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura>
<IncludeAssemblies>
Scriban
</IncludeAssemblies>
</Costura>
</Weavers>
6 changes: 5 additions & 1 deletion PolyMod.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
Expand All @@ -20,6 +20,10 @@

<ItemGroup>
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.738" />
<PackageReference Include="Costura.Fody" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Scriban" Version="6.2.1" />
<PackageReference Include="TheBattleOfPolytopia" Version="$(PolytopiaVersion)-738" />
<EmbeddedResource Include="resources\*.*" />
</ItemGroup>
Expand Down
18 changes: 9 additions & 9 deletions resources/localization.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
55 changes: 50 additions & 5 deletions src/Loader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -230,7 +231,7 @@ public static void AddPatchDataType(string typeId, Type type, bool shouldCreateC
/// Loads all mods from the mods directory.
/// </summary>
/// <param name="mods">A dictionary to populate with the loaded mods.</param>
internal static void LoadMods(Dictionary<string, Mod> mods)
internal static void RegisterMods(Dictionary<string, Mod> mods)
{
Directory.CreateDirectory(Plugin.MODS_PATH);
string[] modContainers = Directory.GetDirectories(Plugin.MODS_PATH)
Expand Down Expand Up @@ -279,8 +280,7 @@ internal static void LoadMods(Dictionary<string, Mod> mods)
files.Add(new(entry.FullName, entry.ReadBytes()));
}
}

// Validate manifest
#region ValidateManifest()
if (manifest == null)
{
Plugin.logger.LogError($"Mod manifest not found in {modContainer}");
Expand Down Expand Up @@ -311,6 +311,7 @@ internal static void LoadMods(Dictionary<string, Mod> mods)
Plugin.logger.LogError($"Mod {manifest.id} already exists");
continue;
}
#endregion
mods.Add(manifest.id, new(
manifest,
Mod.Status.Success,
Expand All @@ -319,7 +320,41 @@ internal static void LoadMods(Dictionary<string, Mod> mods)
Plugin.logger.LogInfo($"Registered mod {manifest.id}");
}

// Check dependencies
CheckDependencies(mods);
}

internal static void LoadMods(Dictionary<string, Mod> 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<string, Mod> mods)
{
foreach (var (id, mod) in mods)
{
foreach (var dependency in mod.dependencies ?? Array.Empty<Mod.Dependency>())
Expand Down Expand Up @@ -362,7 +397,7 @@ internal static void LoadMods(Dictionary<string, Mod> mods)
/// </summary>
/// <param name="mods">The dictionary of mods to sort.</param>
/// <returns>True if the mods could be sorted (no circular dependencies), false otherwise.</returns>
internal static bool SortMods(Dictionary<string, Mod> mods)
private static bool SortMods(Dictionary<string, Mod> mods)
{
Stopwatch s = new();
Dictionary<string, List<string>> graph = new();
Expand Down Expand Up @@ -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) });
Expand Down
120 changes: 120 additions & 0 deletions src/Managers/Config.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System.Text.Json;
using System.Text.Json.Nodes;

namespace PolyMod.Managers;

/// <summary>
/// Allows mods to save config.
/// </summary>
public class Config<T> 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<T>(jsonText);
break;
}
case ConfigTypes.Exposed:
{
if (!File.Exists(ExposedConfigPath))
{
return;
}
var jsonText = File.ReadAllText(ExposedConfigPath);
currentConfig = JsonNode.Parse(jsonText)![modName]?.Deserialize<T>();
break;
}
default:
throw new ArgumentOutOfRangeException();
}
}
/// <summary>
/// Sets the default if the config does not exist yet. Always call this before reading from the config.
/// </summary>
public void SetDefaultConfig(T defaultValue)
{
defaultConfig = defaultValue;
if (currentConfig is not null) return;
Write(defaultConfig);
SaveChanges();
}

/// <summary>
/// Writes the **entire** config. Usage not recommended, use Edit() instead
/// </summary>
public void Write(T config)
{
currentConfig = config;
}
/// <summary>
/// Gets the config. Should only be called after setting a default.
/// </summary>
public T Get()
{
return currentConfig ?? throw new InvalidOperationException("Must set default before reading config.");
}
/// <summary>
/// Edits the config. Should only be called after setting a default.
/// </summary>
/// <remarks>Call SaveChanges after editing</remarks>
public void Edit(Action<T> editor)
{
editor(currentConfig ?? throw new InvalidOperationException("Must set default before reading config."));
}
/// <summary>
/// Gets part of the config. Should only be called after setting a default
/// </summary>
public TResult Get<TResult>(Func<T, TResult> getter)
{
return getter(currentConfig ?? throw new InvalidOperationException("Must set default before reading config."));
}
/// <summary>
/// Writes the config to disk
/// </summary>
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
}
}
94 changes: 94 additions & 0 deletions src/Managers/GLDConfig.cs
Original file line number Diff line number Diff line change
@@ -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<string, string, string>((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 }));
}
}
Loading