From 3b7eb91c3cdb17f1e7e80bd7cf8e8b340cb256ce Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Tue, 28 Nov 2023 19:30:14 +0800 Subject: [PATCH 01/14] Misc fixs. --- src/GameContext.cs | 36 +++++++++++++++++++++++++++++------- src/Hooks.cs | 2 +- src/InputMonitor.cs | 2 +- src/ModLoader.cs | 21 ++++++++++----------- src/SFHRZModLoaderPlugin.cs | 3 +-- 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/GameContext.cs b/src/GameContext.cs index 3a5d02c..a59b16b 100644 --- a/src/GameContext.cs +++ b/src/GameContext.cs @@ -10,17 +10,20 @@ namespace SFHR_ZModLoader { public class GameContext { - public GlobalData GlobalData { get; } + public GlobalData? GlobalData { get => GI.GlobalData; } public ManualLogSource Logger { get; } - public GameContext(GlobalData gd, ManualLogSource logger) + public GameContext(ManualLogSource logger) { - GlobalData = gd; Logger = logger; } public void PatchCamoData(string name, Action patcher) { + if(GlobalData == null) { + Logger.LogWarning($"GameContext: Patch CamoData failed: GlobalData not loaded."); + return; + } Logger.LogInfo($"GameContext: Patching CamoData '{name}'..."); var obj = GlobalData.GetItem(name, GI.EItemType.Camo); if(obj == null) @@ -34,15 +37,18 @@ public void PatchCamoData(string name, Action patcher) patcher(camoData); Logger.LogInfo($"GameContext: Patch CamoData '{name}' completed."); } - catch + catch(Exception e) { - Logger.LogError($"The type is {obj.GetType().Name}"); - Logger.LogError($"GameContext: Patch CamoData '{name}' failed."); + Logger.LogWarning($"GameContext: Patch CamoData '{name}' failed: '{e}'."); } } public void PatchWeaponData(string name, Action patcher) { + if(GlobalData == null) { + Logger.LogWarning($"GameContext: Patch WeaponData failed: GlobalData not loaded."); + return; + } Logger.LogInfo($"GameContext: Patching WeaponData '{name}'..."); var obj = GlobalData.GetItem(name, GI.EItemType.All); if(obj == null) @@ -58,12 +64,16 @@ public void PatchWeaponData(string name, Action patcher) } catch(Exception e) { - Logger.LogError($"GameContext: Patch WeaponData '{name}' error: '{e}'."); + Logger.LogWarning($"GameContext: Patch WeaponData '{name}' error: '{e}'."); } } public void InsertTexture(string name, Texture2D newTexture) { + if(GlobalData == null) { + Logger.LogWarning($"GameContext: Insert Texture failed: GlobalData not loaded."); + return; + } Logger.LogInfo($"GameContext: Inserting Texture '{name}'..."); GlobalData.Textures.Add(name, newTexture); Logger.LogInfo($"GameContext: Insert Texture '{name}' completed."); @@ -71,6 +81,10 @@ public void InsertTexture(string name, Texture2D newTexture) public void PatchTexture(string name, Action patcher, bool fallbackInsert = false) { + if(GlobalData == null) { + Logger.LogWarning($"GameContext: Patch Texture failed: GlobalData not loaded."); + return; + } Logger.LogInfo($"GameContext: Patching texture '{name}'..."); if (GlobalData.Textures.ContainsKey(name)) { @@ -102,6 +116,10 @@ public void PatchTexture(string name, Action patcher, bool fallbackIn public void InsertSound(string name, AudioClip newSound) { + if(GlobalData == null) { + Logger.LogWarning($"GameContext: Insert sound failed: GlobalData not loaded."); + return; + } Logger.LogInfo($"GameContext: Inserting sound '{name}'..."); GlobalData.Sounds.Add(name, newSound); Logger.LogInfo($"GameContext: Insert sound '{name}' completed."); @@ -109,6 +127,10 @@ public void InsertSound(string name, AudioClip newSound) public void InsertSong(string name, AudioClip newSong) { + if(GlobalData == null) { + Logger.LogWarning($"GameContext: Insert song failed: GlobalData not loaded."); + return; + } Logger.LogInfo($"GameContext: Inserting sound '{name}'..."); GlobalData.Songs.Add(name, newSong); Logger.LogInfo($"GameContext: Insert sound '{name}' completed."); diff --git a/src/Hooks.cs b/src/Hooks.cs index f62366a..216f781 100644 --- a/src/Hooks.cs +++ b/src/Hooks.cs @@ -28,7 +28,7 @@ public static void Postfix_GlobalData_Load() if (Logger != null && !isGameContextLoaded) { isGameContextLoaded = true; - SFHRZModLoaderPlugin.GameContext = new(globalData, Logger); + SFHRZModLoaderPlugin.GameContext = new(Logger); EventManager?.EmitEvent(new Event { type = "GAMECONTEXT_LOADED", diff --git a/src/InputMonitor.cs b/src/InputMonitor.cs index b8ea9ed..67a2ef6 100644 --- a/src/InputMonitor.cs +++ b/src/InputMonitor.cs @@ -9,7 +9,7 @@ namespace SFHR_ZModLoader { public class InputMonitor: MonoBehaviour { - private ManualLogSource? Logger { get; set; } = SFHRZModLoaderPlugin.Logger; + private static ManualLogSource? Logger { get; set; } = SFHRZModLoaderPlugin.Logger; private Dictionary KeyboradListeners { get; set; } public InputMonitor() diff --git a/src/ModLoader.cs b/src/ModLoader.cs index 22dd8d6..7527bab 100644 --- a/src/ModLoader.cs +++ b/src/ModLoader.cs @@ -184,14 +184,13 @@ public class ModLoader { private readonly string dir; private Dictionary mods; - private readonly ManualLogSource logger; + private ManualLogSource? logger { get => SFHRZModLoaderPlugin.Logger; } - public ModLoader(string dir, ManualLogSource logger, EventManager eventManager) + public ModLoader(string dir) { this.dir = dir; this.mods = new(); - this.logger = logger; - this.logger.LogInfo("ModLoader created."); + logger?.LogInfo("ModLoader created."); } public void RegisterEvents(EventManager eventManager) @@ -205,14 +204,14 @@ public void RegisterEvents(EventManager eventManager) eventManager.RegisterEventHandler("GAMECONTEXT_PATCH", ev => { if(ev.data == null || ev.data.GetType() != typeof(GameContext)) { - logger.LogError("GAMECONTEXT_PATCH data incorrect!"); + logger?.LogError("GAMECONTEXT_PATCH data incorrect!"); return; } LoadMods(); var gctx = (GameContext)ev.data; - logger.LogInfo("Game patching..."); + logger?.LogInfo("Game patching..."); PatchToGameContext(gctx); - logger.LogInfo("Game patch completed."); + logger?.LogInfo("Game patch completed."); }); eventManager.RegisterEventHandler("GAMECONTEXT_LOADED", ev => { var gctx = (GameContext)ev.data; @@ -225,7 +224,7 @@ public void RegisterEvents(EventManager eventManager) public void LoadMods() { - logger.LogInfo($"Loading Mods from directory: {dir}..."); + logger?.LogInfo($"Loading Mods from directory: {dir}..."); if(!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } @@ -236,7 +235,7 @@ public void LoadMods() var metadata = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(item, "mod.json"))); try { - logger.LogInfo($"Loading Mod from directory: {item}..."); + logger?.LogInfo($"Loading Mod from directory: {item}..."); if(mods.TryGetValue(metadata.id, out var mod)) { this.mods[mod.metadata.id] = Mod.LoadFromDirectory(item, mod); @@ -245,11 +244,11 @@ public void LoadMods() { this.mods.Add(metadata.id, Mod.LoadFromDirectory(item)); } - logger.LogInfo($"Loading Mod '{metadata.id}' completed."); + logger?.LogInfo($"Loading Mod '{metadata.id}' completed."); } catch(Exception e) { - logger.LogError($"Load Mod in '{item}' failed: {e}."); + logger?.LogError($"Load Mod in '{item}' failed: {e}."); } } } diff --git a/src/SFHRZModLoaderPlugin.cs b/src/SFHRZModLoaderPlugin.cs index 6ec0021..38a46ad 100644 --- a/src/SFHRZModLoaderPlugin.cs +++ b/src/SFHRZModLoaderPlugin.cs @@ -36,7 +36,6 @@ public override void Load() } } - EventManager = new(Logger); ClassInjector.RegisterTypeInIl2Cpp(); @@ -54,7 +53,7 @@ public override void Load() InputMonitor = ZeroComponents.AddComponent(); } - ModLoader = new ModLoader(Path.Combine(Paths.GameRootPath, "mods"), Logger, EventManager); + ModLoader = new ModLoader(Path.Combine(Paths.GameRootPath, "mods")); ModLoader.RegisterEvents(EventManager); EventManager.EmitEvent(new Event { type = "MODS_LOAD" From 1a8e06885ed0c050c5d6bb9ff6a8c9fa3bc11d97 Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Tue, 28 Nov 2023 19:30:42 +0800 Subject: [PATCH 02/14] Make Publish.ps1 simple. --- scripts/Publish.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/Publish.ps1 b/scripts/Publish.ps1 index ad2aca5..9e85062 100644 --- a/scripts/Publish.ps1 +++ b/scripts/Publish.ps1 @@ -1,2 +1 @@ -dotnet publish -c Release -o .\publish -Compress-Archive -Path .\publish\* -Destination publish.zip \ No newline at end of file +dotnet publish -c Release -o .\publish \ No newline at end of file From 2d279728ad01a2a9a547aad2b85e40c98a8514e8 Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Tue, 28 Nov 2023 19:35:33 +0800 Subject: [PATCH 03/14] Make FetchDependencies more beautiful. --- scripts/FetchDependencies.ps1 | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/scripts/FetchDependencies.ps1 b/scripts/FetchDependencies.ps1 index 55e49f8..e049c7b 100644 --- a/scripts/FetchDependencies.ps1 +++ b/scripts/FetchDependencies.ps1 @@ -1,12 +1,20 @@ $gamePath = Resolve-Path $(Get-Content '.gamepath') +$dependencies = @( + "Assembly-CSharp.dll", + "Assembly-CSharp-firstpass.dll", + "Il2Cppmscorlib.dll", + "UnityEngine.dll", + "UnityEngine.InputModule.dll", + "UnityEngine.InputLegacyModule.dll", + "UnityEngine.CoreModule.dll", + "UnityEngine.AudioModule.dll", + "UnityEngine.ImageConversionModule.dll", + "UnityEngine.ImageConversionModule.dll", + "deps/FishNet.Runtime.dll" +) + New-Item -ItemType Directory -Path 'deps' -ErrorAction SilentlyContinue -New-Item -ItemType SymbolicLink -Path 'deps/Assembly-CSharp.dll' -Value $(Join-Path -Path $gamePath -ChildPath 'BepInEx/Interop/Assembly-CSharp.dll') -ErrorAction SilentlyContinue -New-Item -ItemType SymbolicLink -Path 'deps/Assembly-CSharp-firstpass.dll' -Value $(Join-Path -Path $gamePath -ChildPath 'BepInEx/Interop/Assembly-CSharp-firstpass.dll') -ErrorAction SilentlyContinue -New-Item -ItemType SymbolicLink -Path 'deps/Il2Cppmscorlib.dll' -Value $(Join-Path -Path $gamePath -ChildPath 'BepInEx/Interop/Il2Cppmscorlib.dll') -ErrorAction SilentlyContinue -New-Item -ItemType SymbolicLink -Path 'deps/UnityEngine.dll' -Value $(Join-Path -Path $gamePath -ChildPath 'BepInEx/Interop/UnityEngine.dll') -ErrorAction SilentlyContinue -New-Item -ItemType SymbolicLink -Path 'deps/UnityEngine.InputModule.dll' -Value $(Join-Path -Path $gamePath -ChildPath 'BepInEx/Interop/UnityEngine.InputModule.dll') -ErrorAction SilentlyContinue -New-Item -ItemType SymbolicLink -Path 'deps/UnityEngine.InputLegacyModule.dll' -Value $(Join-Path -Path $gamePath -ChildPath 'BepInEx/Interop/UnityEngine.InputLegacyModule.dll') -ErrorAction SilentlyContinue -New-Item -ItemType SymbolicLink -Path 'deps/UnityEngine.CoreModule.dll' -Value $(Join-Path -Path $gamePath -ChildPath 'BepInEx/Interop/UnityEngine.CoreModule.dll') -ErrorAction SilentlyContinue -New-Item -ItemType SymbolicLink -Path 'deps/UnityEngine.AudioModule.dll' -Value $(Join-Path -Path $gamePath -ChildPath 'BepInEx/Interop/UnityEngine.AudioModule.dll') -ErrorAction SilentlyContinue -New-Item -ItemType SymbolicLink -Path 'deps/UnityEngine.ImageConversionModule.dll' -Value $(Join-Path -Path $gamePath -ChildPath 'BepInEx/Interop/UnityEngine.ImageConversionModule.dll') -ErrorAction SilentlyContinue -New-Item -ItemType SymbolicLink -Path 'deps/FishNet.Runtime.dll' -Value $(Join-Path -Path $gamePath -ChildPath 'BepInEx/Interop/FishNet.Runtime.dll') -ErrorAction SilentlyContinue \ No newline at end of file + +foreach ($dependency in $dependencies) { + New-Item -ItemType SymbolicLink -Path "deps/$dependency" -Value $(Join-Path -Path $gamePath -ChildPath "BepInEx/Interop/$dependency") -ErrorAction SilentlyContinue +} \ No newline at end of file From 8d2e1d54919e87864e6bf148a6bfdc515650097e Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Tue, 28 Nov 2023 19:38:13 +0800 Subject: [PATCH 04/14] Simple Fix. --- scripts/FetchDependencies.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/FetchDependencies.ps1 b/scripts/FetchDependencies.ps1 index e049c7b..ff949ed 100644 --- a/scripts/FetchDependencies.ps1 +++ b/scripts/FetchDependencies.ps1 @@ -16,5 +16,5 @@ $dependencies = @( New-Item -ItemType Directory -Path 'deps' -ErrorAction SilentlyContinue foreach ($dependency in $dependencies) { - New-Item -ItemType SymbolicLink -Path "deps/$dependency" -Value $(Join-Path -Path $gamePath -ChildPath "BepInEx/Interop/$dependency") -ErrorAction SilentlyContinue + New-Item -ItemType SymbolicLink -Path "deps/$dependency" -Value $(Join-Path -Path $gamePath -ChildPath "BepInEx/interop/$dependency") -ErrorAction SilentlyContinue } \ No newline at end of file From 9fb249885577751755091a2ad0e6fd6653f0f722 Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Tue, 28 Nov 2023 19:41:54 +0800 Subject: [PATCH 05/14] Fix a dependency. --- scripts/FetchDependencies.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/FetchDependencies.ps1 b/scripts/FetchDependencies.ps1 index ff949ed..d2db1b7 100644 --- a/scripts/FetchDependencies.ps1 +++ b/scripts/FetchDependencies.ps1 @@ -10,7 +10,9 @@ $dependencies = @( "UnityEngine.AudioModule.dll", "UnityEngine.ImageConversionModule.dll", "UnityEngine.ImageConversionModule.dll", - "deps/FishNet.Runtime.dll" + "FishNet.Runtime.dll", + "UnityEngine.UI.dll" + "UnityEngine.UIModule.dll" ) New-Item -ItemType Directory -Path 'deps' -ErrorAction SilentlyContinue From ea8b5df209b676fe41d084478a8c3166d4c8c2d8 Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Sun, 3 Dec 2023 20:05:21 +0800 Subject: [PATCH 06/14] Moved all ModDatas to the ModData directory. Enhanced the Mod loading logics. Tried to add support to scripting in JavaScript. --- SFHR_ZModLoader.csproj | 9 + src/ModData.cs | 343 -------------------------------- src/ModData/CamoData.cs | 179 +++++++++++++++++ src/ModData/ModData.cs | 156 +++++++++++++++ src/ModData/ScriptData.cs | 0 src/ModData/WeaponData.cs | 175 ++++++++++++++++ src/ModLoader.cs | 387 ++++++++++++++++++------------------ src/SFHRZModLoaderPlugin.cs | 8 +- 8 files changed, 710 insertions(+), 547 deletions(-) delete mode 100644 src/ModData.cs create mode 100644 src/ModData/CamoData.cs create mode 100644 src/ModData/ModData.cs create mode 100644 src/ModData/ScriptData.cs create mode 100644 src/ModData/WeaponData.cs diff --git a/SFHR_ZModLoader.csproj b/SFHR_ZModLoader.csproj index 33f78e0..49fc533 100644 --- a/SFHR_ZModLoader.csproj +++ b/SFHR_ZModLoader.csproj @@ -18,6 +18,7 @@ + @@ -67,5 +68,13 @@ ./deps/Il2Cppmscorlib.dll false + + ./deps/UnityEngine.UI.dll + false + + + ./deps/UnityEngine.UIModule.dll + false + diff --git a/src/ModData.cs b/src/ModData.cs deleted file mode 100644 index 9a102d2..0000000 --- a/src/ModData.cs +++ /dev/null @@ -1,343 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.IO; -using Newtonsoft.Json; -using UnityEngine; - -namespace SFHR_ZModLoader -{ - [Serializable] - public struct ModVector3 - { - public float x; - public float y; - public float z; - } - - [Serializable] - public struct ModColor - { - public float r; - public float g; - public float b; - public float a; - } - - [Serializable] - public struct ModNamespaceConf - { - public string? camos; - public string? weapons; - } - - [Serializable] - public struct ModWeaponsConf - { - public List? includes; - public List? excludes; - } - - [Serializable] - public struct ModWeaponDataConf - { - public string? equipTexture; - public string? equipTextureAlt; - public string? menuTexture; - public string? unequipTexture; - } - - public struct ModWeaponData - { - public string name; - public Texture2D? equipTexture; - public Texture2D? equipTextureAlt; - public Texture2D? menuTexture; - public Texture2D? unequipTexture; - - public static ModWeaponData LoadFromDirectory(string dir, ModWeaponData? weaponData = null) - { - if (!Directory.Exists(dir)) - { - throw new ModLoadingException($"CamoData Directory '{dir}' not exists."); - } - var name = Path.GetFileName(dir); - var equipTexture = weaponData?.equipTexture; - var equipTextureAlt = weaponData?.equipTextureAlt; - var menuTexture = weaponData?.menuTexture; - var unequipTexture = weaponData?.unequipTexture; - - var weaponDataConf = new ModWeaponDataConf {}; - if(File.Exists(Path.Combine(dir, "camo.json"))) - { - var newConf = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, "camo.json"))); - weaponDataConf.equipTexture = newConf.equipTexture; - weaponDataConf.equipTextureAlt = newConf.equipTextureAlt; - weaponDataConf.menuTexture = newConf.menuTexture; - weaponDataConf.unequipTexture = newConf.unequipTexture; - } - else - { - if(File.Exists(Path.Combine(dir, "equipTexture.png"))) - { - weaponDataConf.equipTexture = "equipTexture.png"; - } - if(File.Exists(Path.Combine(dir, "equipTextureAlt.png"))) - { - weaponDataConf.equipTextureAlt = "equipTextureAlt.png"; - } - if(File.Exists(Path.Combine(dir, "menuTexture.png"))) - { - weaponDataConf.menuTexture = "menuTexture.png"; - } - if(File.Exists(Path.Combine(dir, "unequipTexture.png"))) - { - weaponDataConf.unequipTexture = "unequipTexture.png"; - } - } - if(weaponDataConf.equipTexture != null && File.Exists(Path.Combine(dir, weaponDataConf.equipTexture))) - { - if(equipTexture == null) - { - equipTexture = new Texture2D(1, 1); - } - SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, weaponDataConf.equipTexture)}"); - ImageConversion.LoadImage(equipTexture, File.ReadAllBytes(Path.Combine(dir, weaponDataConf.equipTexture))); - equipTexture.name = $"zmod_weapon_{name}_equipTexture"; - } - if(weaponDataConf.equipTextureAlt != null && File.Exists(Path.Combine(dir, weaponDataConf.equipTextureAlt))) - { - if(equipTextureAlt == null) - { - equipTextureAlt = new Texture2D(1, 1); - } - SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, weaponDataConf.equipTextureAlt)}"); - ImageConversion.LoadImage(equipTextureAlt, File.ReadAllBytes(Path.Combine(dir, weaponDataConf.equipTextureAlt))); - equipTextureAlt.name = $"zmod_weapon_{name}_equipTextureAlt"; - } - if(weaponDataConf.menuTexture != null && File.Exists(Path.Combine(dir, weaponDataConf.menuTexture))) - { - if(menuTexture == null) - { - menuTexture = new Texture2D(1, 1); - } - SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, weaponDataConf.menuTexture)}"); - ImageConversion.LoadImage(menuTexture, File.ReadAllBytes(Path.Combine(dir, weaponDataConf.menuTexture))); - menuTexture.name = $"zmod_weapon_{name}_menuTexture"; - } - if(weaponDataConf.unequipTexture != null && File.Exists(Path.Combine(dir, weaponDataConf.unequipTexture))) - { - if(unequipTexture == null) - { - unequipTexture = new Texture2D(1, 1); - } - SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, weaponDataConf.unequipTexture)}"); - ImageConversion.LoadImage(unequipTexture, File.ReadAllBytes(Path.Combine(dir, weaponDataConf.unequipTexture))); - unequipTexture.name = $"zmod_weapon_{name}_unequipTexture"; - } - return new ModWeaponData { - name = name, - equipTexture = equipTexture, - equipTextureAlt = equipTextureAlt, - menuTexture = menuTexture, - unequipTexture = unequipTexture, - }; - } - - public readonly void PatchToGameContext(GameContext gctx, string? namespaceName) - { - var self = this; - gctx.PatchWeaponData(namespaceName != null ? $"{namespaceName}:{name}" : name, weaponData => { - if(self.equipTexture != null) - { - if(weaponData.EquipTexture != null && weaponData.EquipTexture.isReadable) - { - ImageConversion.LoadImage(weaponData.EquipTexture, self.equipTexture.EncodeToPNG()); - } - else - { - weaponData.EquipTexture = self.equipTexture; - } - } - if(self.equipTextureAlt != null) - { - if(weaponData.EquipTextureAlt!= null && weaponData.EquipTextureAlt.isReadable) - { - ImageConversion.LoadImage(weaponData.EquipTextureAlt, self.equipTextureAlt.EncodeToPNG()); - } - else - { - weaponData.EquipTextureAlt = self.equipTextureAlt; - } - } - if(self.menuTexture != null) - { - if(weaponData.MenuTexture != null && weaponData.MenuTexture.isReadable) - { - ImageConversion.LoadImage(weaponData.MenuTexture, self.menuTexture.EncodeToPNG()); - } - else - { - weaponData.MenuTexture = self.menuTexture; - } - } - if(self.unequipTexture != null) - { - if(weaponData.UnequipTexture != null && weaponData.UnequipTexture.isReadable) - { - ImageConversion.LoadImage(weaponData.UnequipTexture, self.unequipTexture.EncodeToPNG()); - } - else - { - weaponData.UnequipTexture = self.unequipTexture; - } - } - }); - } - } - - [Serializable] - public struct ModCamosConf - { - public List? includes; - public List? excludes; - } - - [Serializable] - public struct ModCamoDataConf - { - public string? texture; - public string? redCamo; - public string? icon; - public int? classTextureNum; - } - - public struct ModCamoData - { - public string name; - public Texture2D? texture; - public Texture2D? redCamo; - public Texture2D? icon; - public int classTextureNum; - - public static ModCamoData LoadFromDirectory(string dir, ModCamoData? camoData = null) - { - if (!Directory.Exists(dir)) - { - throw new ModLoadingException($"CamoData Directory '{dir}' not exists."); - } - var name = Path.GetFileName(dir); - var texture = camoData?.texture; - var redCamo = camoData?.redCamo; - var icon = camoData?.icon; - - var camoDataConf = new ModCamoDataConf { - classTextureNum = -1, - }; - if(File.Exists(Path.Combine(dir, "camo.json"))) - { - var newConf = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, "camo.json"))); - camoDataConf.texture = newConf.texture; - camoDataConf.redCamo = newConf.redCamo; - camoDataConf.icon = newConf.icon; - } - else - { - if(File.Exists(Path.Combine(dir, "texture.png"))) - { - camoDataConf.texture = "texture.png"; - } - if(File.Exists(Path.Combine(dir, "redCamo.png"))) - { - camoDataConf.redCamo = "redCamo.png"; - } - if(File.Exists(Path.Combine(dir, "icon.png"))) - { - camoDataConf.icon = "icon.png"; - } - } - var classTextureNum = camoDataConf.classTextureNum ?? -1; - if(camoDataConf.texture != null && File.Exists(Path.Combine(dir, camoDataConf.texture))) - { - if(texture == null) - { - texture = new Texture2D(1, 1); - } - SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, camoDataConf.texture)}"); - ImageConversion.LoadImage(texture, File.ReadAllBytes(Path.Combine(dir, camoDataConf.texture))); - texture.name = $"zmod_camo_{name}_texture"; - } - if(camoDataConf.redCamo != null && File.Exists(Path.Combine(dir, camoDataConf.redCamo))) - { - if(redCamo == null) - { - redCamo = new Texture2D(1, 1); - } - SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, camoDataConf.redCamo)}"); - ImageConversion.LoadImage(redCamo, File.ReadAllBytes(Path.Combine(dir, camoDataConf.redCamo))); - redCamo.name = $"zmod_camo_{name}_redCamo"; - } - if(camoDataConf.icon != null && File.Exists(Path.Combine(dir, camoDataConf.icon))) - { - if(icon == null) - { - icon = new Texture2D(1, 1); - } - SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, camoDataConf.icon)}"); - ImageConversion.LoadImage(icon, File.ReadAllBytes(Path.Combine(dir, camoDataConf.icon))); - icon.name = $"zmod_camo_{name}_icon"; - } - return new ModCamoData { - name = name, - texture = texture, - redCamo = redCamo, - icon = icon, - classTextureNum = classTextureNum, - }; - } - - public readonly void PatchToGameContext(GameContext gctx, string? namespaceName) - { - var self = this; - gctx.PatchCamoData(namespaceName != null ? $"{namespaceName}:{name}" : name, camoData => { - camoData.ClassTextureNum = self.classTextureNum; - if(self.texture != null) - { - if(camoData.Texture != null && camoData.Texture.isReadable) - { - SFHRZModLoaderPlugin.Logger?.LogInfo($"Hot reloading camo texture: {camoData.Texture.name}"); - ImageConversion.LoadImage(camoData.Texture, self.texture.EncodeToPNG()); - } - else - { - camoData.Texture = self.texture; - } - } - if(self.redCamo != null) - { - if(camoData.RedCamo != null && camoData.RedCamo.isReadable) - { - SFHRZModLoaderPlugin.Logger?.LogInfo($"Hot reloading camo redCamo: {camoData.Texture.name}"); - ImageConversion.LoadImage(camoData.RedCamo, self.redCamo.EncodeToPNG()); - } - else - { - camoData.RedCamo = self.redCamo; - } - } - if(self.icon != null) - { - if(camoData.Icon != null && camoData.Icon.isReadable) - { - SFHRZModLoaderPlugin.Logger?.LogInfo($"Hot reloading camo icon: {camoData.Texture.name}"); - ImageConversion.LoadImage(camoData.Icon, self.icon.EncodeToPNG()); - } - else - { - camoData.Icon = self.icon; - } - } - }); - } - } -} \ No newline at end of file diff --git a/src/ModData/CamoData.cs b/src/ModData/CamoData.cs new file mode 100644 index 0000000..3de7073 --- /dev/null +++ b/src/ModData/CamoData.cs @@ -0,0 +1,179 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using UnityEngine; + +namespace SFHR_ZModLoader; + + + +[Serializable] +public struct ModCamosConf +{ + public List? includes; + public List? excludes; +} + +[Serializable] +public struct ModCamoDataConf +{ + public string? texture; + public string? redCamo; + public string? icon; + public int? classTextureNum; +} + +public struct ModCamoData +{ + public string name; + public Texture2D? texture; + public Texture2D? redCamo; + public Texture2D? icon; + public int classTextureNum; + + public static ModCamoData LoadFromDirectory(string dir, ModCamoData? camoData = null) + { + if (!Directory.Exists(dir)) + { + throw new ModLoadingException($"CamoData Directory '{dir}' not exists."); + } + var name = Path.GetFileName(dir); + var texture = camoData?.texture; + var redCamo = camoData?.redCamo; + var icon = camoData?.icon; + + var camoDataConf = new ModCamoDataConf + { + classTextureNum = -1, + }; + if (File.Exists(Path.Combine(dir, "camo.json"))) + { + var newConf = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, "camo.json"))); + camoDataConf.texture = newConf.texture; + camoDataConf.redCamo = newConf.redCamo; + camoDataConf.icon = newConf.icon; + } + + if (camoDataConf.texture == null && File.Exists(Path.Combine(dir, "texture.png"))) + { + camoDataConf.texture = "texture.png"; + } + if (camoDataConf.redCamo == null && File.Exists(Path.Combine(dir, "redCamo.png"))) + { + camoDataConf.redCamo = "redCamo.png"; + } + if (camoDataConf.icon == null && File.Exists(Path.Combine(dir, "icon.png"))) + { + camoDataConf.icon = "icon.png"; + } + + var classTextureNum = camoDataConf.classTextureNum ?? -1; + if (camoDataConf.texture != null) + { + if (texture == null) + { + texture = new Texture2D(1, 1); + } + SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, camoDataConf.texture)}"); + try + { + ImageConversion.LoadImage(texture, File.ReadAllBytes(Path.Combine(dir, camoDataConf.texture))); + texture.name = $"zmod_camo_{name}_texture"; + } + catch (Exception e) + { + SFHRZModLoaderPlugin.Logger?.LogWarning($"Load texture from '{Path.Combine(dir, camoDataConf.texture)}' failed: '{e}'."); + } + } + if (camoDataConf.redCamo != null) + { + if (redCamo == null) + { + redCamo = new Texture2D(1, 1); + } + SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, camoDataConf.redCamo)}"); + try + { + ImageConversion.LoadImage(redCamo, File.ReadAllBytes(Path.Combine(dir, camoDataConf.redCamo))); + redCamo.name = $"zmod_camo_{name}_redCamo"; + } + catch (Exception e) + { + SFHRZModLoaderPlugin.Logger?.LogWarning($"Load texture from '{Path.Combine(dir, camoDataConf.redCamo)}' failed: '{e}'."); + } + } + if (camoDataConf.icon != null) + { + if (icon == null) + { + icon = new Texture2D(1, 1); + } + SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, camoDataConf.icon)}"); + try + { + ImageConversion.LoadImage(icon, File.ReadAllBytes(Path.Combine(dir, camoDataConf.icon))); + icon.name = $"zmod_camo_{name}_icon"; + } + catch (Exception e) + { + SFHRZModLoaderPlugin.Logger?.LogWarning($"Load texture from '{Path.Combine(dir, camoDataConf.icon)}' failed: '{e}'."); + } + } + return new ModCamoData + { + name = name, + texture = texture, + redCamo = redCamo, + icon = icon, + classTextureNum = classTextureNum, + }; + } + + public readonly void PatchToGameContext(GameContext gctx, string? namespaceName) + { + var self = this; + gctx.PatchCamoData(namespaceName != null ? $"{namespaceName}:{name}" : name, camoData => + { + camoData.ClassTextureNum = self.classTextureNum; + if (self.texture != null) + { + if (camoData.Texture != null && camoData.Texture.isReadable) + { + SFHRZModLoaderPlugin.Logger?.LogInfo($"Hot reloading camo texture: {camoData.Texture.name}"); + ImageConversion.LoadImage(camoData.Texture, self.texture.EncodeToPNG()); + } + else + { + camoData.Texture = self.texture; + } + } + if (self.redCamo != null) + { + if (camoData.RedCamo != null && camoData.RedCamo.isReadable) + { + SFHRZModLoaderPlugin.Logger?.LogInfo($"Hot reloading camo redCamo: {camoData.Texture.name}"); + ImageConversion.LoadImage(camoData.RedCamo, self.redCamo.EncodeToPNG()); + } + else + { + camoData.RedCamo = self.redCamo; + } + } + if (self.icon != null) + { + if (camoData.Icon != null && camoData.Icon.isReadable) + { + SFHRZModLoaderPlugin.Logger?.LogInfo($"Hot reloading camo icon: {camoData.Texture.name}"); + ImageConversion.LoadImage(camoData.Icon, self.icon.EncodeToPNG()); + } + else + { + camoData.Icon = self.icon; + } + } + }); + } +} \ No newline at end of file diff --git a/src/ModData/ModData.cs b/src/ModData/ModData.cs new file mode 100644 index 0000000..a3b7652 --- /dev/null +++ b/src/ModData/ModData.cs @@ -0,0 +1,156 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using UnityEngine; + +namespace SFHR_ZModLoader; + +[Serializable] +public struct ModVector3 +{ + public float x; + public float y; + public float z; +} + +[Serializable] +public struct ModColor +{ + public float r; + public float g; + public float b; + public float a; +} + +[Serializable] +public struct ModNamespaceConf +{ + public string? camos; + public string? weapons; + public string? scripts; +} + +[Serializable] +public struct ModMetadata +{ + public string id; + public string displayName; + public ulong versionCode; + public string version; +} + +public struct ModNamespace +{ + public string name; + public Dictionary camoDatas; + + public Dictionary weaponDatas; + public Dictionary scripts; + + public static ModNamespace LoadFromDirectory(string dir, ModNamespace? ns = null) + { + if (!Directory.Exists(dir)) + { + throw new ModLoadingException($"Namespace directory '{dir}' not exists."); + } + var nsname = Path.GetFileName(dir); + var camoDatas = new Dictionary(); + var weaponDatas = new Dictionary(); + var nsConf = new ModNamespaceConf + { + camos = "camos", + weapons = "weapons", + scripts = "scripts", + }; + if (File.Exists(Path.Combine(dir, "namespace.json"))) + { + var newConf = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, "namespace.json"))); + nsConf = new ModNamespaceConf + { + camos = newConf.camos ?? nsConf.camos, + weapons = newConf.weapons ?? nsConf.weapons, + }; + } + + var camosConf = new ModCamosConf { }; + if (File.Exists(Path.Combine(dir, nsConf.camos, "camos.json"))) + { + var newConf = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, nsConf.camos, "camos.json"))); + camosConf = newConf; + } + if (Directory.Exists(Path.Combine(dir, nsConf.camos))) + { + foreach (var item in Directory.EnumerateDirectories(Path.Combine(dir, nsConf.camos))) + { + // TODO: includes 和 excludes处理 + var camoName = Path.GetFileName(item); + if (ns?.camoDatas.TryGetValue(item, out var camoData) ?? false) + { + camoDatas[camoName] = ModCamoData.LoadFromDirectory(item, camoData); + } + else + { + camoDatas.Add(camoName, ModCamoData.LoadFromDirectory(item)); + } + } + } + else + { + SFHRZModLoaderPlugin.Logger?.LogInfo($"Skips camos at '{Path.Combine(dir, nsConf.camos)}'."); + } + + var weaponsConf = new ModWeaponsConf { }; + if (File.Exists(Path.Combine(dir, nsConf.weapons, "weapons.json"))) + { + var newConf = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, nsConf.weapons, "camos.json"))); + weaponsConf = newConf; + } + if (Directory.Exists(Path.Combine(dir, nsConf.weapons))) + { + foreach (var item in Directory.EnumerateDirectories(Path.Combine(dir, nsConf.weapons))) + { + // TODO: includes 和 excludes处理 + var weaponName = Path.GetFileName(item); + if (ns?.weaponDatas.TryGetValue(item, out var weaponData) ?? false) + { + weaponDatas[weaponName] = ModWeaponData.LoadFromDirectory(item, weaponData); + } + else + { + weaponDatas.Add(weaponName, ModWeaponData.LoadFromDirectory(item)); + } + } + } + else + { + SFHRZModLoaderPlugin.Logger?.LogInfo($"Skips weapons at '{Path.Combine(dir, nsConf.weapons)}'."); + } + + + return new ModNamespace + { + name = nsname, + camoDatas = camoDatas, + weaponDatas = weaponDatas, + }; + } + + public readonly void PatchToGameContext(GameContext gctx) + { + foreach (var item in camoDatas) + { + item.Value.PatchToGameContext(gctx, name == "sfh" ? null : name); + } + foreach (var item in weaponDatas) + { + item.Value.PatchToGameContext(gctx, name == "sfh" ? null : name); + } + } + + public readonly void UnpatchToGameContext(GameContext gctx) + { + // TODO: 实现反补丁游戏 + } +} \ No newline at end of file diff --git a/src/ModData/ScriptData.cs b/src/ModData/ScriptData.cs new file mode 100644 index 0000000..e69de29 diff --git a/src/ModData/WeaponData.cs b/src/ModData/WeaponData.cs new file mode 100644 index 0000000..7286a97 --- /dev/null +++ b/src/ModData/WeaponData.cs @@ -0,0 +1,175 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using UnityEngine; + +namespace SFHR_ZModLoader; + +[Serializable] +public struct ModWeaponsConf +{ + public List? includes; + public List? excludes; +} + +[Serializable] +public struct ModWeaponDataConf +{ + public string? equipTexture; + public string? equipTextureAlt; + public string? menuTexture; + public string? unequipTexture; +} + +public struct ModWeaponData +{ + public string name; + public Texture2D? equipTexture; + public Texture2D? equipTextureAlt; + public Texture2D? menuTexture; + public Texture2D? unequipTexture; + + public static ModWeaponData LoadFromDirectory(string dir, ModWeaponData? weaponData = null) + { + if (!Directory.Exists(dir)) + { + throw new ModLoadingException($"CamoData Directory '{dir}' not exists."); + } + var name = Path.GetFileName(dir); + var equipTexture = weaponData?.equipTexture; + var equipTextureAlt = weaponData?.equipTextureAlt; + var menuTexture = weaponData?.menuTexture; + var unequipTexture = weaponData?.unequipTexture; + + var weaponDataConf = new ModWeaponDataConf { }; + if (File.Exists(Path.Combine(dir, "camo.json"))) + { + var newConf = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, "camo.json"))); + weaponDataConf.equipTexture = newConf.equipTexture; + weaponDataConf.equipTextureAlt = newConf.equipTextureAlt; + weaponDataConf.menuTexture = newConf.menuTexture; + weaponDataConf.unequipTexture = newConf.unequipTexture; + } + else + { + if (File.Exists(Path.Combine(dir, "equipTexture.png"))) + { + weaponDataConf.equipTexture = "equipTexture.png"; + } + if (File.Exists(Path.Combine(dir, "equipTextureAlt.png"))) + { + weaponDataConf.equipTextureAlt = "equipTextureAlt.png"; + } + if (File.Exists(Path.Combine(dir, "menuTexture.png"))) + { + weaponDataConf.menuTexture = "menuTexture.png"; + } + if (File.Exists(Path.Combine(dir, "unequipTexture.png"))) + { + weaponDataConf.unequipTexture = "unequipTexture.png"; + } + } + if (weaponDataConf.equipTexture != null && File.Exists(Path.Combine(dir, weaponDataConf.equipTexture))) + { + if (equipTexture == null) + { + equipTexture = new Texture2D(1, 1); + } + SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, weaponDataConf.equipTexture)}"); + ImageConversion.LoadImage(equipTexture, File.ReadAllBytes(Path.Combine(dir, weaponDataConf.equipTexture))); + equipTexture.name = $"zmod_weapon_{name}_equipTexture"; + } + if (weaponDataConf.equipTextureAlt != null && File.Exists(Path.Combine(dir, weaponDataConf.equipTextureAlt))) + { + if (equipTextureAlt == null) + { + equipTextureAlt = new Texture2D(1, 1); + } + SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, weaponDataConf.equipTextureAlt)}"); + ImageConversion.LoadImage(equipTextureAlt, File.ReadAllBytes(Path.Combine(dir, weaponDataConf.equipTextureAlt))); + equipTextureAlt.name = $"zmod_weapon_{name}_equipTextureAlt"; + } + if (weaponDataConf.menuTexture != null && File.Exists(Path.Combine(dir, weaponDataConf.menuTexture))) + { + if (menuTexture == null) + { + menuTexture = new Texture2D(1, 1); + } + SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, weaponDataConf.menuTexture)}"); + ImageConversion.LoadImage(menuTexture, File.ReadAllBytes(Path.Combine(dir, weaponDataConf.menuTexture))); + menuTexture.name = $"zmod_weapon_{name}_menuTexture"; + } + if (weaponDataConf.unequipTexture != null && File.Exists(Path.Combine(dir, weaponDataConf.unequipTexture))) + { + if (unequipTexture == null) + { + unequipTexture = new Texture2D(1, 1); + } + SFHRZModLoaderPlugin.Logger?.LogInfo($"Loading texture: {Path.Combine(dir, weaponDataConf.unequipTexture)}"); + ImageConversion.LoadImage(unequipTexture, File.ReadAllBytes(Path.Combine(dir, weaponDataConf.unequipTexture))); + unequipTexture.name = $"zmod_weapon_{name}_unequipTexture"; + } + return new ModWeaponData + { + name = name, + equipTexture = equipTexture, + equipTextureAlt = equipTextureAlt, + menuTexture = menuTexture, + unequipTexture = unequipTexture, + }; + } + + public readonly void PatchToGameContext(GameContext gctx, string? namespaceName) + { + var self = this; + gctx.PatchWeaponData(namespaceName != null ? $"{namespaceName}:{name}" : name, weaponData => + { + if (self.equipTexture != null) + { + if (weaponData.EquipTexture != null && weaponData.EquipTexture.isReadable) + { + ImageConversion.LoadImage(weaponData.EquipTexture, self.equipTexture.EncodeToPNG()); + } + else + { + weaponData.EquipTexture = self.equipTexture; + } + } + if (self.equipTextureAlt != null) + { + if (weaponData.EquipTextureAlt != null && weaponData.EquipTextureAlt.isReadable) + { + ImageConversion.LoadImage(weaponData.EquipTextureAlt, self.equipTextureAlt.EncodeToPNG()); + } + else + { + weaponData.EquipTextureAlt = self.equipTextureAlt; + } + } + if (self.menuTexture != null) + { + if (weaponData.MenuTexture != null && weaponData.MenuTexture.isReadable) + { + ImageConversion.LoadImage(weaponData.MenuTexture, self.menuTexture.EncodeToPNG()); + } + else + { + weaponData.MenuTexture = self.menuTexture; + } + } + if (self.unequipTexture != null) + { + if (weaponData.UnequipTexture != null && weaponData.UnequipTexture.isReadable) + { + ImageConversion.LoadImage(weaponData.UnequipTexture, self.unequipTexture.EncodeToPNG()); + } + else + { + weaponData.UnequipTexture = self.unequipTexture; + } + } + }); + } +} \ No newline at end of file diff --git a/src/ModLoader.cs b/src/ModLoader.cs index 7527bab..1e14f8b 100644 --- a/src/ModLoader.cs +++ b/src/ModLoader.cs @@ -1,233 +1,178 @@ #nullable enable using System; using System.Collections.Generic; -using System.Drawing; using System.IO; -using BepInEx; using BepInEx.Logging; using Newtonsoft.Json; -namespace SFHR_ZModLoader +namespace SFHR_ZModLoader; + +public struct Mod { - [Serializable] - public struct ModMetadata + public ModMetadata metadata; + public Dictionary namespaces; + public Mod(ModMetadata metadata) { - public string id; - public string displayName; - public ulong versionCode; - public string version; + this.metadata = metadata; + this.namespaces = new(); } - public struct ModNamespace + public static Mod LoadFromDirectory(string dir, Mod? mod = null) { - public string name; - public Dictionary camoDatas; - - public Dictionary weaponDatas; - - public static ModNamespace LoadFromDirectory(string dir, ModNamespace? ns = null) + if (!Directory.Exists(dir)) { - if(!Directory.Exists(dir)) - { - throw new ModLoadingException($"Namespace directory '{dir}' not exists."); - } - var nsname = Path.GetFileName(dir); - var camoDatas = new Dictionary(); - var weaponDatas = new Dictionary(); - var nsConf = new ModNamespaceConf { - camos = "camos", - weapons = "weapons", - }; - if(File.Exists(Path.Combine(dir, "namespace.json"))) - { - var newConf = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, "namespace.json"))); - nsConf = new ModNamespaceConf { - camos = newConf.camos ?? nsConf.camos, - weapons = newConf.weapons ?? nsConf.weapons, - }; - } + throw new ModLoadingException($"Mod directory '{dir}' not found."); + } + ModMetadata metadata; + try + { + metadata = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, "mod.json"))); + } + catch + { + throw new ModLoadingException($"Errors in the metadata file 'mod.json'."); + } + var namespaces = mod?.namespaces ?? new Dictionary(); - var camosConf = new ModCamosConf {}; - if(File.Exists(Path.Combine(dir, nsConf.camos, "camos.json"))) - { - var newConf = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, nsConf.camos, "camos.json"))); - camosConf = newConf; - } - if(Directory.Exists(Path.Combine(dir, nsConf.camos))) + foreach (var nsdir in Directory.EnumerateDirectories(dir)) + { + if (namespaces.TryGetValue(Path.GetFileName(nsdir), out var ns)) { - foreach (var item in Directory.EnumerateDirectories(Path.Combine(dir, nsConf.camos))) - { - // TODO: includes 和 excludes处理 - var camoName = Path.GetFileName(item); - if (ns?.camoDatas.TryGetValue(item, out var camoData) ?? false) - { - camoDatas[camoName] = ModCamoData.LoadFromDirectory(item, camoData); - } - else - { - camoDatas.Add(camoName, ModCamoData.LoadFromDirectory(item)); - } - } + namespaces[Path.GetFileName(nsdir)] = ModNamespace.LoadFromDirectory(nsdir, ns); } else { - SFHRZModLoaderPlugin.Logger?.LogInfo($"Skips camos at '{Path.Combine(dir, nsConf.camos)}'."); + namespaces.Add(Path.GetFileName(nsdir), ModNamespace.LoadFromDirectory(nsdir)); } - - var weaponsConf = new ModWeaponsConf {}; - if(File.Exists(Path.Combine(dir, nsConf.weapons, "weapons.json"))) - { - var newConf = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, nsConf.weapons, "camos.json"))); - weaponsConf = newConf; - } - if(Directory.Exists(Path.Combine(dir, nsConf.weapons))) - { - foreach (var item in Directory.EnumerateDirectories(Path.Combine(dir, nsConf.weapons))) - { - // TODO: includes 和 excludes处理 - var weaponName = Path.GetFileName(item); - if (ns?.weaponDatas.TryGetValue(item, out var weaponData) ?? false) - { - weaponDatas[weaponName] = ModWeaponData.LoadFromDirectory(item, weaponData); - } - else - { - weaponDatas.Add(weaponName, ModWeaponData.LoadFromDirectory(item)); - } - } - } - else - { - SFHRZModLoaderPlugin.Logger?.LogInfo($"Skips weapons at '{Path.Combine(dir, nsConf.weapons)}'."); - } - - - return new ModNamespace { - name = nsname, - camoDatas = camoDatas, - weaponDatas = weaponDatas, - }; } + return new Mod + { + metadata = metadata, + namespaces = namespaces, + }; + } - public readonly void PatchToGameContext(GameContext gctx) + public readonly void PatchToGameContext(GameContext gctx) + { + foreach (var item in namespaces) { - foreach (var item in camoDatas) - { - item.Value.PatchToGameContext(gctx, name == "sfh" ? null : name); - } - foreach (var item in weaponDatas) - { - item.Value.PatchToGameContext(gctx, name == "sfh" ? null : name); - } + item.Value.PatchToGameContext(gctx); } } - public struct Mod { - public ModMetadata metadata; - public Dictionary namespaces; - public Mod(ModMetadata metadata) + public readonly void UnpatchToGameContext(GameContext gctx) + { + foreach (var item in namespaces) { - this.metadata = metadata; - this.namespaces = new(); + item.Value.UnpatchToGameContext(gctx); } + } +} - public static Mod LoadFromDirectory(string dir, Mod? mod = null) - { - if(!Directory.Exists(dir)) - { - throw new ModLoadingException($"Mod directory '{dir}' not found."); - } - ModMetadata metadata; - try - { - metadata = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, "mod.json"))); - } - catch - { - throw new ModLoadingException($"Errors in the metadata file 'mod.json'."); - } - var namespaces = mod?.namespaces ?? new Dictionary(); +public class ModLoadingException : Exception +{ + public ModLoadingException(string messages) : base(messages) + { } +} + +public class ModLoader +{ + private readonly string dir; + private Dictionary mods; + private List? modLoadOrder; + private ManualLogSource? logger { get => SFHRZModLoaderPlugin.Logger; } - foreach (var nsdir in Directory.EnumerateDirectories(dir)) + public ModLoader(string dir) + { + this.dir = dir; + mods = new(); + LoadModLoadOrder(); + logger?.LogInfo("ModLoader created."); + } + + public void LoadModLoadOrder() + { + if (File.Exists(Path.Combine(dir, "mod_load_order.txt"))) + { + modLoadOrder = new(); + foreach (var line in File.ReadLines(Path.Combine(dir, "mod_load_order.txt"))) { - if(namespaces.TryGetValue(Path.GetFileName(nsdir), out var ns)) - { - namespaces[Path.GetFileName(nsdir)] = ModNamespace.LoadFromDirectory(nsdir, ns); - } - else + if (!line.TrimStart().StartsWith("#")) { - namespaces.Add(Path.GetFileName(nsdir), ModNamespace.LoadFromDirectory(nsdir)); + modLoadOrder.Add(line.Trim()); } } - return new Mod { - metadata = metadata, - namespaces = namespaces, - }; + logger?.LogInfo("'mod_load_order.txt' loaded."); } - - public readonly void PatchToGameContext(GameContext gctx) + else { - foreach (var item in namespaces) - { - item.Value.PatchToGameContext(gctx); - } + modLoadOrder = null; + logger?.LogInfo("Skipped 'mod_load_order.txt' because it is missing, defaults to load all the mods."); } } - public class ModLoadingException: Exception + public void RegisterEvents(EventManager eventManager) { - public ModLoadingException(string messages): base(messages) - {} - } - - public class ModLoader - { - private readonly string dir; - private Dictionary mods; - private ManualLogSource? logger { get => SFHRZModLoaderPlugin.Logger; } - - public ModLoader(string dir) + eventManager.RegisterEventHandler("MODS_LOAD", ev => { - this.dir = dir; - this.mods = new(); - logger?.LogInfo("ModLoader created."); - } - - public void RegisterEvents(EventManager eventManager) + LoadMods(); + eventManager.EmitEvent(new Event + { + type = "MODS_LOADED", + }); + }); + eventManager.RegisterEventHandler("GAMECONTEXT_PATCH", ev => { - eventManager.RegisterEventHandler("MODS_LOAD", ev => { - LoadMods(); - eventManager.EmitEvent(new Event { - type = "MODS_LOADED", - }); + if (ev.data == null || ev.data.GetType() != typeof(GameContext)) + { + logger?.LogError("GAMECONTEXT_PATCH data incorrect!"); + return; + } + LoadMods(); + var gctx = (GameContext)ev.data; + logger?.LogInfo("Game patching..."); + PatchToGameContext(gctx); + logger?.LogInfo("Game patch completed."); + }); + eventManager.RegisterEventHandler("GAMECONTEXT_LOADED", ev => + { + var gctx = (GameContext)ev.data; + eventManager.EmitEvent(new Event + { + type = "GAMECONTEXT_PATCH", + data = gctx, }); - eventManager.RegisterEventHandler("GAMECONTEXT_PATCH", ev => { - if(ev.data == null || ev.data.GetType() != typeof(GameContext)) + }); + eventManager.RegisterEventHandler("MODS_RELOAD", ev => + { + if (SFHRZModLoaderPlugin.GameContext != null) + { + UnpatchToGameContext(SFHRZModLoaderPlugin.GameContext); + } + UnloadMods(); + LoadModLoadOrder(); + LoadMods(); + if (SFHRZModLoaderPlugin.GameContext != null) + { + eventManager.EmitEvent(new Event { - logger?.LogError("GAMECONTEXT_PATCH data incorrect!"); - return; - } - LoadMods(); - var gctx = (GameContext)ev.data; - logger?.LogInfo("Game patching..."); - PatchToGameContext(gctx); - logger?.LogInfo("Game patch completed."); - }); - eventManager.RegisterEventHandler("GAMECONTEXT_LOADED", ev => { - var gctx = (GameContext)ev.data; - eventManager.EmitEvent(new Event { - type = "GAMECONTEXT_PATCH", - data = gctx, + type = "GAMECONTEXT_PATCH", + data = SFHRZModLoaderPlugin.GameContext, }); - }); - } + } + }); + logger?.LogInfo("All ModLoader events registered."); + } - public void LoadMods() + public void LoadMods() + { + logger?.LogInfo($"Loading Mods from directory: {dir}..."); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + if (modLoadOrder == null) { - logger?.LogInfo($"Loading Mods from directory: {dir}..."); - if(!Directory.Exists(dir)) { - Directory.CreateDirectory(dir); - } foreach (var item in Directory.EnumerateDirectories(dir)) { if (File.Exists(Path.Combine(item, "mod.json"))) @@ -236,7 +181,7 @@ public void LoadMods() try { logger?.LogInfo($"Loading Mod from directory: {item}..."); - if(mods.TryGetValue(metadata.id, out var mod)) + if (mods.TryGetValue(metadata.id, out var mod)) { this.mods[mod.metadata.id] = Mod.LoadFromDirectory(item, mod); } @@ -246,32 +191,76 @@ public void LoadMods() } logger?.LogInfo($"Loading Mod '{metadata.id}' completed."); } - catch(Exception e) + catch (Exception e) { logger?.LogError($"Load Mod in '{item}' failed: {e}."); } } } } - - public Mod? GetMod(string name) + else { - if (mods.ContainsKey(name)) - { - return mods[name]; - } - else + foreach (var modName in modLoadOrder) { - return null; + if (File.Exists(Path.Combine(dir, modName, "mod.json"))) + { + var metadata = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, modName, "mod.json"))); + try + { + logger?.LogInfo($"Loading Mod from directory: {Path.Combine(dir, modName)}..."); + if (mods.TryGetValue(metadata.id, out var mod)) + { + this.mods[mod.metadata.id] = Mod.LoadFromDirectory(Path.Combine(dir, modName), mod); + } + else + { + this.mods.Add(metadata.id, Mod.LoadFromDirectory(Path.Combine(dir, modName))); + } + logger?.LogInfo($"Loading Mod '{metadata.id}' completed."); + } + catch (Exception e) + { + logger?.LogError($"Load Mod in '{Path.Combine(dir, modName)}' failed: {e}."); + } + } + else + { + logger?.LogWarning($"Skipped load Mod '{Path.Combine(dir, modName)}': Not exist."); + } } } + } + + public void UnloadMods() + { - public void PatchToGameContext(GameContext gctx) + } + + public Mod? GetMod(string name) + { + if (mods.ContainsKey(name)) { - foreach (var item in mods) - { - item.Value.PatchToGameContext(gctx); - } + return mods[name]; + } + else + { + return null; + } + } + + public void PatchToGameContext(GameContext gctx) + { + foreach (var item in mods) + { + item.Value.PatchToGameContext(gctx); + } + } + + public void UnpatchToGameContext(GameContext gctx) + { + foreach (var item in mods) + { + item.Value.UnpatchToGameContext(gctx); } } } \ No newline at end of file diff --git a/src/SFHRZModLoaderPlugin.cs b/src/SFHRZModLoaderPlugin.cs index 38a46ad..9271b98 100644 --- a/src/SFHRZModLoaderPlugin.cs +++ b/src/SFHRZModLoaderPlugin.cs @@ -5,8 +5,6 @@ using BepInEx.Logging; using HarmonyLib; using Il2CppInterop.Runtime.Injection; -using UnityEngine; -using Il2CppInterop.Runtime; namespace SFHR_ZModLoader; @@ -53,6 +51,8 @@ public override void Load() InputMonitor = ZeroComponents.AddComponent(); } + // 测试下UI + ModLoader = new ModLoader(Path.Combine(Paths.GameRootPath, "mods")); ModLoader.RegisterEvents(EventManager); EventManager.EmitEvent(new Event { @@ -64,11 +64,9 @@ public override void Load() return; } EventManager.EmitEvent(new Event { - type = "GAMECONTEXT_PATCH", - data = GameContext, + type = "MODS_RELOAD" }); }); - Harmony.CreateAndPatchAll(typeof(Hooks)); } } From 71fdd56bf6e75224333389ce299da8b92b6223e5 Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Sun, 3 Dec 2023 20:12:33 +0800 Subject: [PATCH 07/14] Moved Mod sources again. --- src/{ModData => Mod}/CamoData.cs | 0 src/Mod/Mod.cs | 68 ++++++++++++++++++++++++++++++ src/{ModData => Mod}/ModData.cs | 0 src/Mod/ScriptData.cs | 12 ++++++ src/{ModData => Mod}/WeaponData.cs | 0 src/ModData/ScriptData.cs | 0 6 files changed, 80 insertions(+) rename src/{ModData => Mod}/CamoData.cs (100%) create mode 100644 src/Mod/Mod.cs rename src/{ModData => Mod}/ModData.cs (100%) create mode 100644 src/Mod/ScriptData.cs rename src/{ModData => Mod}/WeaponData.cs (100%) delete mode 100644 src/ModData/ScriptData.cs diff --git a/src/ModData/CamoData.cs b/src/Mod/CamoData.cs similarity index 100% rename from src/ModData/CamoData.cs rename to src/Mod/CamoData.cs diff --git a/src/Mod/Mod.cs b/src/Mod/Mod.cs new file mode 100644 index 0000000..cf5a231 --- /dev/null +++ b/src/Mod/Mod.cs @@ -0,0 +1,68 @@ +#nullable enable +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; + +namespace SFHR_ZModLoader; + +public struct Mod +{ + public ModMetadata metadata; + public Dictionary namespaces; + public Mod(ModMetadata metadata) + { + this.metadata = metadata; + this.namespaces = new(); + } + + public static Mod LoadFromDirectory(string dir, Mod? mod = null) + { + if (!Directory.Exists(dir)) + { + throw new ModLoadingException($"Mod directory '{dir}' not found."); + } + ModMetadata metadata; + try + { + metadata = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, "mod.json"))); + } + catch + { + throw new ModLoadingException($"Errors in the metadata file 'mod.json'."); + } + var namespaces = mod?.namespaces ?? new Dictionary(); + + foreach (var nsdir in Directory.EnumerateDirectories(dir)) + { + if (namespaces.TryGetValue(Path.GetFileName(nsdir), out var ns)) + { + namespaces[Path.GetFileName(nsdir)] = ModNamespace.LoadFromDirectory(nsdir, ns); + } + else + { + namespaces.Add(Path.GetFileName(nsdir), ModNamespace.LoadFromDirectory(nsdir)); + } + } + return new Mod + { + metadata = metadata, + namespaces = namespaces, + }; + } + + public readonly void PatchToGameContext(GameContext gctx) + { + foreach (var item in namespaces) + { + item.Value.PatchToGameContext(gctx); + } + } + + public readonly void UnpatchToGameContext(GameContext gctx) + { + foreach (var item in namespaces) + { + item.Value.UnpatchToGameContext(gctx); + } + } +} \ No newline at end of file diff --git a/src/ModData/ModData.cs b/src/Mod/ModData.cs similarity index 100% rename from src/ModData/ModData.cs rename to src/Mod/ModData.cs diff --git a/src/Mod/ScriptData.cs b/src/Mod/ScriptData.cs new file mode 100644 index 0000000..e51ff3a --- /dev/null +++ b/src/Mod/ScriptData.cs @@ -0,0 +1,12 @@ +#nullable enable + +using System; +using System.Collections.Generic; + +[Serializable] +public struct ModWeaponsConf +{ + public List? includes; + public List? excludes; +} + diff --git a/src/ModData/WeaponData.cs b/src/Mod/WeaponData.cs similarity index 100% rename from src/ModData/WeaponData.cs rename to src/Mod/WeaponData.cs diff --git a/src/ModData/ScriptData.cs b/src/ModData/ScriptData.cs deleted file mode 100644 index e69de29..0000000 From e3188bbac821cab76691155ea7d643b152290cb8 Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Tue, 5 Dec 2023 20:06:05 +0800 Subject: [PATCH 08/14] Half done the scripting feature. --- src/Mod/CamoData.cs | 6 +- src/Mod/Mod.cs | 16 +- src/Mod/ModData.cs | 43 +++++- src/Mod/ScriptData.cs | 15 +- src/Mod/WeaponData.cs | 4 +- src/ModLoader.cs | 206 +++++++++++-------------- src/SFHRZModLoaderPlugin.cs | 8 +- src/Scripting/ModScriptModuleLoader.cs | 166 ++++++++++++++++++++ src/Scripting/ScriptConsole.cs | 17 ++ src/Scripting/ScriptEngine.cs | 21 +++ 10 files changed, 357 insertions(+), 145 deletions(-) create mode 100644 src/Scripting/ModScriptModuleLoader.cs create mode 100644 src/Scripting/ScriptConsole.cs create mode 100644 src/Scripting/ScriptEngine.cs diff --git a/src/Mod/CamoData.cs b/src/Mod/CamoData.cs index 3de7073..f6f9ec9 100644 --- a/src/Mod/CamoData.cs +++ b/src/Mod/CamoData.cs @@ -8,13 +8,11 @@ namespace SFHR_ZModLoader; - - [Serializable] public struct ModCamosConf { - public List? includes; - public List? excludes; + public string[]? includes; + public string[]? excludes; } [Serializable] diff --git a/src/Mod/Mod.cs b/src/Mod/Mod.cs index cf5a231..dd287d7 100644 --- a/src/Mod/Mod.cs +++ b/src/Mod/Mod.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.IO; +using System.Linq; using Newtonsoft.Json; namespace SFHR_ZModLoader; @@ -9,11 +10,6 @@ public struct Mod { public ModMetadata metadata; public Dictionary namespaces; - public Mod(ModMetadata metadata) - { - this.metadata = metadata; - this.namespaces = new(); - } public static Mod LoadFromDirectory(string dir, Mod? mod = null) { @@ -36,11 +32,11 @@ public static Mod LoadFromDirectory(string dir, Mod? mod = null) { if (namespaces.TryGetValue(Path.GetFileName(nsdir), out var ns)) { - namespaces[Path.GetFileName(nsdir)] = ModNamespace.LoadFromDirectory(nsdir, ns); + namespaces[Path.GetFileName(nsdir)] = ModNamespace.LoadFromDirectory(nsdir, metadata.id, ns); } else { - namespaces.Add(Path.GetFileName(nsdir), ModNamespace.LoadFromDirectory(nsdir)); + namespaces.Add(Path.GetFileName(nsdir), ModNamespace.LoadFromDirectory(nsdir, metadata.id)); } } return new Mod @@ -50,6 +46,12 @@ public static Mod LoadFromDirectory(string dir, Mod? mod = null) }; } + public readonly string[] LoadScripts(ModScriptModules modScriptModules) + { + var modId = metadata.id; + return namespaces.ToList().Select(item => item.Value.LoadScripts(modId, modScriptModules)).Select(item => item ?? "").Where(item => item != "").ToArray(); + } + public readonly void PatchToGameContext(GameContext gctx) { foreach (var item in namespaces) diff --git a/src/Mod/ModData.cs b/src/Mod/ModData.cs index a3b7652..224e3b2 100644 --- a/src/Mod/ModData.cs +++ b/src/Mod/ModData.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Newtonsoft.Json; using UnityEngine; @@ -43,14 +44,17 @@ public struct ModMetadata public struct ModNamespace { + public string modId; public string name; public Dictionary camoDatas; public Dictionary weaponDatas; public Dictionary scripts; + public string? scriptsEntry; - public static ModNamespace LoadFromDirectory(string dir, ModNamespace? ns = null) + public static ModNamespace LoadFromDirectory(string dir, string modId, ModNamespace? ns = null) { + // Namespace if (!Directory.Exists(dir)) { throw new ModLoadingException($"Namespace directory '{dir}' not exists."); @@ -71,9 +75,11 @@ public static ModNamespace LoadFromDirectory(string dir, ModNamespace? ns = null { camos = newConf.camos ?? nsConf.camos, weapons = newConf.weapons ?? nsConf.weapons, + scripts = newConf.scripts ?? nsConf.scripts, }; } + // CamoData var camosConf = new ModCamosConf { }; if (File.Exists(Path.Combine(dir, nsConf.camos, "camos.json"))) { @@ -98,9 +104,10 @@ public static ModNamespace LoadFromDirectory(string dir, ModNamespace? ns = null } else { - SFHRZModLoaderPlugin.Logger?.LogInfo($"Skips camos at '{Path.Combine(dir, nsConf.camos)}'."); + SFHRZModLoaderPlugin.Logger?.LogInfo($"Skips camos at '{Path.Combine(dir, nsConf.camos)}' beause it is missing."); } + // WeaponData var weaponsConf = new ModWeaponsConf { }; if (File.Exists(Path.Combine(dir, nsConf.weapons, "weapons.json"))) { @@ -125,15 +132,33 @@ public static ModNamespace LoadFromDirectory(string dir, ModNamespace? ns = null } else { - SFHRZModLoaderPlugin.Logger?.LogInfo($"Skips weapons at '{Path.Combine(dir, nsConf.weapons)}'."); + SFHRZModLoaderPlugin.Logger?.LogInfo($"Skipped weapons at '{Path.Combine(dir, nsConf.weapons)}' beause it is missing."); } + var scripts = new Dictionary(); + var scriptsEntry = (string?)null; + // Script + if (Directory.Exists(Path.Combine(dir, nsConf.scripts))) + { + var scriptsDirectory = Path.Combine(dir, nsConf.scripts); + scriptsEntry = "index.js"; + Directory.GetFiles(scriptsDirectory).ToList().ForEach(script => { + scripts.Add($"{Path.GetRelativePath(scriptsDirectory, script).Replace('\\', '/')}", File.ReadAllText(script)); + }); + } + else + { + SFHRZModLoaderPlugin.Logger?.LogInfo($"Skipped scripts at '{Path.Combine(dir, nsConf.scripts)}' beause it is missing."); + } return new ModNamespace { name = nsname, camoDatas = camoDatas, weaponDatas = weaponDatas, + scripts = scripts, + scriptsEntry = scriptsEntry, + modId = modId, }; } @@ -149,6 +174,18 @@ public readonly void PatchToGameContext(GameContext gctx) } } + public readonly string? LoadScripts(string modId, ModScriptModules modScriptModules) + { + var scriptsEntry = this.scriptsEntry; + var nsname = this.name; + scripts.ToList().ForEach(item => { + var moduleName = $"mod://{Path.Join(modId, nsname, item.Key).Replace('\\', '/')}"; + modScriptModules.AddModule(modId, nsname, item.Key.Replace('\\', '/'), item.Value); + SFHRZModLoaderPlugin.Logger?.LogInfo($"Loaded script: '{moduleName}'."); + }); + return this.scriptsEntry != null ? $"mod://{Path.Join(modId, nsname, scriptsEntry).Replace('\\', '/')}" : null; + } + public readonly void UnpatchToGameContext(GameContext gctx) { // TODO: 实现反补丁游戏 diff --git a/src/Mod/ScriptData.cs b/src/Mod/ScriptData.cs index e51ff3a..25507b6 100644 --- a/src/Mod/ScriptData.cs +++ b/src/Mod/ScriptData.cs @@ -1,12 +1,13 @@ #nullable enable - using System; -using System.Collections.Generic; + +namespace SFHR_ZModLoader; + [Serializable] -public struct ModWeaponsConf +public struct ModScriptsDataConf { - public List? includes; - public List? excludes; -} - + public string? entry; + public string[]? includes; + public string[]? excludes; +} \ No newline at end of file diff --git a/src/Mod/WeaponData.cs b/src/Mod/WeaponData.cs index 7286a97..7a54aa6 100644 --- a/src/Mod/WeaponData.cs +++ b/src/Mod/WeaponData.cs @@ -10,8 +10,8 @@ namespace SFHR_ZModLoader; [Serializable] public struct ModWeaponsConf { - public List? includes; - public List? excludes; + public string[]? includes; + public string[]? excludes; } [Serializable] diff --git a/src/ModLoader.cs b/src/ModLoader.cs index 1e14f8b..1626bb1 100644 --- a/src/ModLoader.cs +++ b/src/ModLoader.cs @@ -2,73 +2,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using BepInEx.Logging; using Newtonsoft.Json; namespace SFHR_ZModLoader; -public struct Mod -{ - public ModMetadata metadata; - public Dictionary namespaces; - public Mod(ModMetadata metadata) - { - this.metadata = metadata; - this.namespaces = new(); - } - - public static Mod LoadFromDirectory(string dir, Mod? mod = null) - { - if (!Directory.Exists(dir)) - { - throw new ModLoadingException($"Mod directory '{dir}' not found."); - } - ModMetadata metadata; - try - { - metadata = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, "mod.json"))); - } - catch - { - throw new ModLoadingException($"Errors in the metadata file 'mod.json'."); - } - var namespaces = mod?.namespaces ?? new Dictionary(); - - foreach (var nsdir in Directory.EnumerateDirectories(dir)) - { - if (namespaces.TryGetValue(Path.GetFileName(nsdir), out var ns)) - { - namespaces[Path.GetFileName(nsdir)] = ModNamespace.LoadFromDirectory(nsdir, ns); - } - else - { - namespaces.Add(Path.GetFileName(nsdir), ModNamespace.LoadFromDirectory(nsdir)); - } - } - return new Mod - { - metadata = metadata, - namespaces = namespaces, - }; - } - - public readonly void PatchToGameContext(GameContext gctx) - { - foreach (var item in namespaces) - { - item.Value.PatchToGameContext(gctx); - } - } - - public readonly void UnpatchToGameContext(GameContext gctx) - { - foreach (var item in namespaces) - { - item.Value.UnpatchToGameContext(gctx); - } - } -} - public class ModLoadingException : Exception { public ModLoadingException(string messages) : base(messages) @@ -80,14 +19,14 @@ public class ModLoader private readonly string dir; private Dictionary mods; private List? modLoadOrder; - private ManualLogSource? logger { get => SFHRZModLoaderPlugin.Logger; } + private ManualLogSource? Logger { get => SFHRZModLoaderPlugin.Logger; } public ModLoader(string dir) { this.dir = dir; mods = new(); LoadModLoadOrder(); - logger?.LogInfo("ModLoader created."); + Logger?.LogInfo("ModLoader created."); } public void LoadModLoadOrder() @@ -102,12 +41,12 @@ public void LoadModLoadOrder() modLoadOrder.Add(line.Trim()); } } - logger?.LogInfo("'mod_load_order.txt' loaded."); + Logger?.LogInfo("'mod_load_order.txt' loaded."); } else { modLoadOrder = null; - logger?.LogInfo("Skipped 'mod_load_order.txt' because it is missing, defaults to load all the mods."); + Logger?.LogInfo("Skipped 'mod_load_order.txt' because it is missing, defaults to load all the mods."); } } @@ -125,14 +64,14 @@ public void RegisterEvents(EventManager eventManager) { if (ev.data == null || ev.data.GetType() != typeof(GameContext)) { - logger?.LogError("GAMECONTEXT_PATCH data incorrect!"); + Logger?.LogError("GAMECONTEXT_PATCH data incorrect!"); return; } LoadMods(); var gctx = (GameContext)ev.data; - logger?.LogInfo("Game patching..."); + Logger?.LogInfo("Game patching..."); PatchToGameContext(gctx); - logger?.LogInfo("Game patch completed."); + Logger?.LogInfo("Game patch completed."); }); eventManager.RegisterEventHandler("GAMECONTEXT_LOADED", ev => { @@ -161,76 +100,103 @@ public void RegisterEvents(EventManager eventManager) }); } }); - logger?.LogInfo("All ModLoader events registered."); + eventManager.RegisterEventHandler("SCRIPT_ENGINE_READY", ev => { + LoadModsScripts(SFHRZModLoaderPlugin.ScriptEngine); + }); + Logger?.LogInfo("All ModLoader events registered."); + } + + public void LoadMod(string dir, string modId) + { + try + { + Logger?.LogInfo($"Loading Mod from directory: {dir}..."); + if (mods.TryGetValue(modId, out var mod)) + { + this.mods[modId] = Mod.LoadFromDirectory(dir, mod); + } + else + { + this.mods.Add(modId, Mod.LoadFromDirectory(dir)); + } + Logger?.LogInfo($"Loading Mod '{modId}' completed."); + } + catch (Exception e) + { + Logger?.LogError($"Load Mod in '{dir}' failed: {e}."); + } } public void LoadMods() { - logger?.LogInfo($"Loading Mods from directory: {dir}..."); + Logger?.LogInfo($"Loading Mods from directory: {dir}..."); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } - if (modLoadOrder == null) - { - foreach (var item in Directory.EnumerateDirectories(dir)) + var modDirToMetaData = Directory.EnumerateDirectories(dir).Select(modDir => { + if(File.Exists(Path.Combine(modDir, "mod.json"))) { - if (File.Exists(Path.Combine(item, "mod.json"))) + var metadata = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(modDir, "mod.json"))); + return (modDir, metadata); + } + else + { + return (modDir, (ModMetadata?)null); + } + }).ToList(); + var modIdToDir = modLoadOrder?.Select(modId => { + return (modId, modDirToMetaData.Find(item => item.Item2?.id == modId).modDir); + }).ToList(); + + if(modIdToDir != null) + { + modIdToDir.ForEach(item => { + if(item.modDir != null) { - var metadata = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(item, "mod.json"))); - try - { - logger?.LogInfo($"Loading Mod from directory: {item}..."); - if (mods.TryGetValue(metadata.id, out var mod)) - { - this.mods[mod.metadata.id] = Mod.LoadFromDirectory(item, mod); - } - else - { - this.mods.Add(metadata.id, Mod.LoadFromDirectory(item)); - } - logger?.LogInfo($"Loading Mod '{metadata.id}' completed."); - } - catch (Exception e) - { - logger?.LogError($"Load Mod in '{item}' failed: {e}."); - } + LoadMod(item.modDir, item.modId); } - } + }); } else { - foreach (var modName in modLoadOrder) - { - if (File.Exists(Path.Combine(dir, modName, "mod.json"))) - { - var metadata = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(dir, modName, "mod.json"))); - try - { - logger?.LogInfo($"Loading Mod from directory: {Path.Combine(dir, modName)}..."); - if (mods.TryGetValue(metadata.id, out var mod)) - { - this.mods[mod.metadata.id] = Mod.LoadFromDirectory(Path.Combine(dir, modName), mod); - } - else - { - this.mods.Add(metadata.id, Mod.LoadFromDirectory(Path.Combine(dir, modName))); - } - logger?.LogInfo($"Loading Mod '{metadata.id}' completed."); - } - catch (Exception e) - { - logger?.LogError($"Load Mod in '{Path.Combine(dir, modName)}' failed: {e}."); - } - } - else + modDirToMetaData.ForEach(item => { + if(item.Item2 != null) { - logger?.LogWarning($"Skipped load Mod '{Path.Combine(dir, modName)}': Not exist."); + LoadMod(item.modDir, item.Item2.Value.id); } - } + }); } } + public void LoadModsScripts(ModScriptEngineWrapper engine) + { + List scriptEntries = new(); + mods.ToList().ForEach(item => { + try + { + item.Value.LoadScripts(engine.ModScriptModules).ToList().ForEach(script => { + scriptEntries.Add(script); + }); + } + catch(Exception e) + { + SFHRZModLoaderPlugin.Logger?.LogError($"Load Mod scripts failed in Mod '{item.Key}': '{e}'."); + } + }); + scriptEntries.ForEach(script => { + Logger?.LogInfo($"Try import script module: '{script}'."); + try + { + engine.Engine.ImportModule(script); + } + catch(Exception e) + { + SFHRZModLoaderPlugin.Logger?.LogError($"Import Mod script module failed in script '{script}': '{e}'."); + } + }); + } + public void UnloadMods() { diff --git a/src/SFHRZModLoaderPlugin.cs b/src/SFHRZModLoaderPlugin.cs index 9271b98..4e337d1 100644 --- a/src/SFHRZModLoaderPlugin.cs +++ b/src/SFHRZModLoaderPlugin.cs @@ -5,6 +5,7 @@ using BepInEx.Logging; using HarmonyLib; using Il2CppInterop.Runtime.Injection; +using Jint; namespace SFHR_ZModLoader; @@ -19,6 +20,7 @@ public class SFHRZModLoaderPlugin : BasePlugin public static ModLoader? ModLoader { get; set; } public static GameContext? GameContext { get; set; } public static EventManager? EventManager { get; set; } + public static ModScriptEngineWrapper ScriptEngine { get; set; } = new(); public static bool DebugEmit { get; set; } = true; public override void Load() @@ -51,8 +53,6 @@ public override void Load() InputMonitor = ZeroComponents.AddComponent(); } - // 测试下UI - ModLoader = new ModLoader(Path.Combine(Paths.GameRootPath, "mods")); ModLoader.RegisterEvents(EventManager); EventManager.EmitEvent(new Event { @@ -67,6 +67,10 @@ public override void Load() type = "MODS_RELOAD" }); }); + ScriptEngine.Engine.SetValue("console", new ScriptObjectConsole(Logger)); + EventManager.EmitEvent(new Event { + type = "SCRIPT_ENGINE_READY" + }); Harmony.CreateAndPatchAll(typeof(Hooks)); } } diff --git a/src/Scripting/ModScriptModuleLoader.cs b/src/Scripting/ModScriptModuleLoader.cs new file mode 100644 index 0000000..7cc4240 --- /dev/null +++ b/src/Scripting/ModScriptModuleLoader.cs @@ -0,0 +1,166 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using Esprima; +using Esprima.Ast; +using Jint; +using Jint.Native; +using Jint.Runtime.Modules; + +namespace SFHR_ZModLoader; + +public class ModScriptModules +{ + private Dictionary>> moduleSourceDict = new(); + + public void AddModule(string modId, string nsname, string path, string source) + { + if(!moduleSourceDict.ContainsKey(modId)) + { + moduleSourceDict.Add(modId, new()); + } + var modDict = moduleSourceDict[modId]; + if(!modDict.ContainsKey(nsname)) + { + modDict.Add(nsname, new()); + } + var nsDict = modDict[nsname]; + nsDict.Add(path, source); + } + + public string GetModuleSource(Uri uri) + { + switch(uri.Scheme) + { + case "mod": + { + if(moduleSourceDict.TryGetValue(uri.Host, out var modDict)) + { + if(uri.Segments.Length >= 3) + { + var nsname = uri.Segments[1].TrimEnd('/'); + var path = string.Join("", uri.Segments, 2, uri.Segments.Length - 2).TrimEnd('/'); + if(modDict.TryGetValue(nsname, out var nsDict)) + { + if(nsDict.TryGetValue(path, out var source)) + { + return source; + } + else + { + throw new Exception($"Unknown path in namespace '{nsname}': {path}"); + } + } + else + { + throw new Exception($"Unknown namespace in Mod '{uri.Host}': '{nsname}'."); + } + } + else + { + throw new Exception($"Path segments missing: '{uri.AbsolutePath}'."); + } + } + else + { + throw new Exception($"Unknown Mod ID: '{uri.Host}'."); + } + } + default: + throw new Exception($"Unknown schema: '{uri.Scheme}'."); + } + } +} + +public class ModScriptModuleLoader : IModuleLoader +{ + public ModScriptModules ModScriptModules { get; } = new(); + + public Module LoadModule(Engine engine, ResolvedSpecifier resolved) + { + if (resolved.Type != SpecifierType.RelativeOrAbsolute) + { + throw new Exception($"The default module loader can only resolve files. You can define modules directly to allow imports using {"Engine"}.{"AddModule"}(). Attempted to resolve: '{resolved.Specifier}'."); + } + + if (resolved.Uri == null) + { + throw new Exception($"Module '{resolved.Specifier}' of type '{resolved.Type}' has no resolved URI."); + } + if (resolved.Uri.Segments.Length < 3) + { + throw new Exception($"Invalid module: {resolved.Uri}"); + } + + var realUri = new Uri($"{resolved.Uri.Segments[1].TrimEnd('/')}://{resolved.Uri.Segments[2].TrimEnd('/')}/{string.Join("", resolved.Uri.Segments, 3, resolved.Uri.Segments.Length - 3).TrimEnd('/')}"); + string code = ModScriptModules.GetModuleSource(realUri); + string path = resolved.Uri.ToString(); + try + { + var module = new JavaScriptParser(new ParserOptions()).ParseModule(code, path); + return module; + } + catch (ParserException ex) + { + throw new Exception($"Error while loading module: error in module '{path}': {ex.Error}"); + } + catch (Exception) + { + throw new Exception($"Could not load module: '{path}'."); + } + } + + public ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier) + { + SFHRZModLoaderPlugin.Logger?.LogWarning($"referencingModuleLocation: {referencingModuleLocation}"); + if (string.IsNullOrEmpty(specifier)) + { + throw new Exception($"Invalid Module Specifier: '{specifier}' in '{referencingModuleLocation}'"); + } + + // Specifications from ESM_RESOLVE Algorithm: https://nodejs.org/api/esm.html#resolution-algorithm + + Uri resolved; + if (Uri.TryCreate(specifier, UriKind.Absolute, out var uri)) + { + resolved = new Uri($"vfs:///{uri.Scheme}/{uri.Host}{uri.LocalPath}"); + } + else if (IsRelative(specifier)) + { + if(referencingModuleLocation == null) + { + throw new Exception($"No base module location for '{specifier}'"); + } + resolved = new Uri(new Uri($"vfs://{referencingModuleLocation}"), specifier); + } + else + { + return new ResolvedSpecifier( + specifier, + specifier, + Uri: null, + SpecifierType.Bare + ); + } + + if (resolved.IsFile) + { + throw new Exception("Real module file access is not allowed."); + } + + + SFHRZModLoaderPlugin.Logger?.LogWarning($"resolved: {resolved}"); + return new ResolvedSpecifier( + specifier, + resolved.ToString(), + resolved, + SpecifierType.RelativeOrAbsolute + ); + } + + private static bool IsRelative(string specifier) + { + return specifier.StartsWith('.'); + } +} \ No newline at end of file diff --git a/src/Scripting/ScriptConsole.cs b/src/Scripting/ScriptConsole.cs new file mode 100644 index 0000000..03d57a0 --- /dev/null +++ b/src/Scripting/ScriptConsole.cs @@ -0,0 +1,17 @@ +#nullable enable + +using BepInEx.Logging; + +namespace SFHR_ZModLoader; + +class ScriptObjectConsole { + private ManualLogSource Logger { get; set; } + public ScriptObjectConsole(ManualLogSource logger) + { + Logger = logger; + } + public void log(string message) + { + Logger.LogInfo(message); + } +} \ No newline at end of file diff --git a/src/Scripting/ScriptEngine.cs b/src/Scripting/ScriptEngine.cs new file mode 100644 index 0000000..a3bce35 --- /dev/null +++ b/src/Scripting/ScriptEngine.cs @@ -0,0 +1,21 @@ +#nullable enable + +using Jint; + +namespace SFHR_ZModLoader; + +public class ModScriptEngineWrapper +{ + public ModScriptModuleLoader ModScriptModuleLoader { get; } = new(); + public ModScriptModules ModScriptModules { get => ModScriptModuleLoader.ModScriptModules; } + + public Engine Engine { get; private set; } + + public ModScriptEngineWrapper() + { + Engine = new(options => { + options.AllowClr(typeof(GlobalData).Assembly) + .EnableModules(ModScriptModuleLoader); + }); + } +} \ No newline at end of file From aeaabaeb7aa51b1127415025bd74bd50bf4b9c2b Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Tue, 5 Dec 2023 20:44:16 +0800 Subject: [PATCH 09/14] Reconstucted namespaces. Added EventManager for scripting. --- src/EventManager.cs | 2 +- src/ModLoader.cs | 2 ++ src/{Mod => Modding}/CamoData.cs | 2 +- src/{Mod => Modding}/Mod.cs | 3 ++- src/{Mod => Modding}/ModData.cs | 4 ++-- src/{Mod => Modding}/ScriptData.cs | 2 +- src/{Mod => Modding}/WeaponData.cs | 3 +-- src/SFHRZModLoaderPlugin.cs | 2 ++ src/Scripting/ModScriptModuleLoader.cs | 4 +--- src/Scripting/ScriptConsole.cs | 2 +- src/Scripting/ScriptEngine.cs | 2 +- 11 files changed, 15 insertions(+), 13 deletions(-) rename src/{Mod => Modding}/CamoData.cs (99%) rename src/{Mod => Modding}/Mod.cs (96%) rename src/{Mod => Modding}/ModData.cs (98%) rename src/{Mod => Modding}/ScriptData.cs (83%) rename src/{Mod => Modding}/WeaponData.cs (99%) diff --git a/src/EventManager.cs b/src/EventManager.cs index 66857a0..bd3600b 100644 --- a/src/EventManager.cs +++ b/src/EventManager.cs @@ -34,7 +34,7 @@ public void EmitEvent(Event ev) } } - public void RegisterEventHandler(string type, Action handler) + public void RegisterEventHandler(string type, Action handler, string? handlerId = null) { if(eventHandlers.TryGetValue(type, out var curHandler)) { diff --git a/src/ModLoader.cs b/src/ModLoader.cs index 1626bb1..7da64b7 100644 --- a/src/ModLoader.cs +++ b/src/ModLoader.cs @@ -5,6 +5,8 @@ using System.Linq; using BepInEx.Logging; using Newtonsoft.Json; +using SFHR_ZModLoader.Scripting; +using SFHR_ZModLoader.Modding; namespace SFHR_ZModLoader; diff --git a/src/Mod/CamoData.cs b/src/Modding/CamoData.cs similarity index 99% rename from src/Mod/CamoData.cs rename to src/Modding/CamoData.cs index f6f9ec9..bcacabc 100644 --- a/src/Mod/CamoData.cs +++ b/src/Modding/CamoData.cs @@ -6,7 +6,7 @@ using Newtonsoft.Json; using UnityEngine; -namespace SFHR_ZModLoader; +namespace SFHR_ZModLoader.Modding; [Serializable] public struct ModCamosConf diff --git a/src/Mod/Mod.cs b/src/Modding/Mod.cs similarity index 96% rename from src/Mod/Mod.cs rename to src/Modding/Mod.cs index dd287d7..09563eb 100644 --- a/src/Mod/Mod.cs +++ b/src/Modding/Mod.cs @@ -3,8 +3,9 @@ using System.IO; using System.Linq; using Newtonsoft.Json; +using SFHR_ZModLoader.Scripting; -namespace SFHR_ZModLoader; +namespace SFHR_ZModLoader.Modding; public struct Mod { diff --git a/src/Mod/ModData.cs b/src/Modding/ModData.cs similarity index 98% rename from src/Mod/ModData.cs rename to src/Modding/ModData.cs index 224e3b2..da07f1f 100644 --- a/src/Mod/ModData.cs +++ b/src/Modding/ModData.cs @@ -4,9 +4,9 @@ using System.IO; using System.Linq; using Newtonsoft.Json; -using UnityEngine; +using SFHR_ZModLoader.Scripting; -namespace SFHR_ZModLoader; +namespace SFHR_ZModLoader.Modding; [Serializable] public struct ModVector3 diff --git a/src/Mod/ScriptData.cs b/src/Modding/ScriptData.cs similarity index 83% rename from src/Mod/ScriptData.cs rename to src/Modding/ScriptData.cs index 25507b6..e9c55b3 100644 --- a/src/Mod/ScriptData.cs +++ b/src/Modding/ScriptData.cs @@ -1,7 +1,7 @@ #nullable enable using System; -namespace SFHR_ZModLoader; +namespace SFHR_ZModLoader.Modding; [Serializable] diff --git a/src/Mod/WeaponData.cs b/src/Modding/WeaponData.cs similarity index 99% rename from src/Mod/WeaponData.cs rename to src/Modding/WeaponData.cs index 7a54aa6..308dbd3 100644 --- a/src/Mod/WeaponData.cs +++ b/src/Modding/WeaponData.cs @@ -1,11 +1,10 @@ #nullable enable using System; -using System.Collections.Generic; using System.IO; using Newtonsoft.Json; using UnityEngine; -namespace SFHR_ZModLoader; +namespace SFHR_ZModLoader.Modding; [Serializable] public struct ModWeaponsConf diff --git a/src/SFHRZModLoaderPlugin.cs b/src/SFHRZModLoaderPlugin.cs index 4e337d1..ddf48ed 100644 --- a/src/SFHRZModLoaderPlugin.cs +++ b/src/SFHRZModLoaderPlugin.cs @@ -6,6 +6,7 @@ using HarmonyLib; using Il2CppInterop.Runtime.Injection; using Jint; +using SFHR_ZModLoader.Scripting; namespace SFHR_ZModLoader; @@ -68,6 +69,7 @@ public override void Load() }); }); ScriptEngine.Engine.SetValue("console", new ScriptObjectConsole(Logger)); + ScriptEngine.Engine.SetValue("EventManager", EventManager); EventManager.EmitEvent(new Event { type = "SCRIPT_ENGINE_READY" }); diff --git a/src/Scripting/ModScriptModuleLoader.cs b/src/Scripting/ModScriptModuleLoader.cs index 7cc4240..dce6c5e 100644 --- a/src/Scripting/ModScriptModuleLoader.cs +++ b/src/Scripting/ModScriptModuleLoader.cs @@ -1,14 +1,12 @@ #nullable enable using System; using System.Collections.Generic; -using System.IO; using Esprima; using Esprima.Ast; using Jint; -using Jint.Native; using Jint.Runtime.Modules; -namespace SFHR_ZModLoader; +namespace SFHR_ZModLoader.Scripting; public class ModScriptModules { diff --git a/src/Scripting/ScriptConsole.cs b/src/Scripting/ScriptConsole.cs index 03d57a0..d7a78cc 100644 --- a/src/Scripting/ScriptConsole.cs +++ b/src/Scripting/ScriptConsole.cs @@ -2,7 +2,7 @@ using BepInEx.Logging; -namespace SFHR_ZModLoader; +namespace SFHR_ZModLoader.Scripting; class ScriptObjectConsole { private ManualLogSource Logger { get; set; } diff --git a/src/Scripting/ScriptEngine.cs b/src/Scripting/ScriptEngine.cs index a3bce35..9739ddf 100644 --- a/src/Scripting/ScriptEngine.cs +++ b/src/Scripting/ScriptEngine.cs @@ -2,7 +2,7 @@ using Jint; -namespace SFHR_ZModLoader; +namespace SFHR_ZModLoader.Scripting; public class ModScriptEngineWrapper { From 64419fbbc895bca9f9f632960e3f6b027553fc5e Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Fri, 8 Dec 2023 18:38:50 +0800 Subject: [PATCH 10/14] Update README.md. --- README.md | 47 +++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 156bbf3..1d4edbe 100644 --- a/README.md +++ b/README.md @@ -49,39 +49,34 @@ 在每一个Mod文件夹之下,都有一定的文件结构来表示Mod,比如: Mod文件夹 - -|-sfh - -|--camos - -|---Red1 - -|----texture.png - -|----icon.png - -|----redCamo.png - -|--weapons - -|----equipTexture.png - -|----equipTextureAlt.png - -|----menuTexture.png - -|----unequipTexture.png - -|mod.json +- (Mod根目录) + - sfh + - camos + - Red1 + - texture.png + - icon.png + - redCamo.pn + - weapons + - equipTexture.png + - equipTextureAlt.png + - menuTexture.png + - unequipTexture.png + - mymod + - scripts + - index.js + - mod.json - `sfh`:所有原版数据和资产都在这个文件夹(你也可以定制你自己的文件夹名,比如说xxddc之类的,但sfh被指定为原版的文件夹,又称**命名空间**)下,更改这个命名空间下的文件说明你将覆盖原版的数据和资产。 - `camos`:迷彩文件夹,其下每一个文件夹代表一个与之名称相同的迷彩 -- `Red1`:这是原版的“邪恶”迷彩,内部名称为`Red1` +- `Red1`:这是原版的“邪恶”迷彩,内部名称为`Red1`。你可以将其更换为 - `texture.png`:这是人物贴图文件 - `icon.png`:这是人物图标文件 - `redCamo.png`:这是迷彩层文件 - `weapons`:武器文件夹,其下每一个文件夹代表一个与之名称相同的武器 -- `weapons`文件夹下每一个贴图代表了武器的某种状态的贴图,目前只能确定`equipTexture.png`是装备在人物身上时显示的贴图。 +- `weapons`文件夹下每一个贴图代表了武器的某种状态的贴图,目前只能确定`equipTexture.png`是装备在人物身上时显示的贴图,其他贴图请自行尝试。 +- `mymod`文件夹是和`sfh`平行的命名空间,其中`mymod`替换为你的mod名称。一般来说,在原版之外新增加的东西应该增加到你专有的mod命名空间当中,比如脚本。 +- `scripts`文件夹是存放脚本的文件夹,所有同命名空间的脚本都放置在此文件夹下。 +- `index.js`是脚本文件的入口,每个命名空间下的`scripts/index.js`都是各自的脚本执行的入口。其余脚本文件只能被引用,不会被执行。 (更多内容正在开发中……) From 2cf9ef5b8b94f036a1d217eeed84dfef79ddccf8 Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Fri, 8 Dec 2023 18:39:19 +0800 Subject: [PATCH 11/14] Update README.md. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d4edbe..f7c435f 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Mod文件夹 - index.js - mod.json -- `sfh`:所有原版数据和资产都在这个文件夹(你也可以定制你自己的文件夹名,比如说xxddc之类的,但sfh被指定为原版的文件夹,又称**命名空间**)下,更改这个命名空间下的文件说明你将覆盖原版的数据和资产。 +- `sfh`:所有原版数据和资产都在这个文件夹(你也可以定制你自己的文件夹名,比如说`xxddc`之类的,但`sfh`被指定为原版的文件夹,又称**命名空间**)下,更改这个命名空间下的文件说明你将覆盖原版的数据和资产。 - `camos`:迷彩文件夹,其下每一个文件夹代表一个与之名称相同的迷彩 - `Red1`:这是原版的“邪恶”迷彩,内部名称为`Red1`。你可以将其更换为 - `texture.png`:这是人物贴图文件 From 5fb487b486091729bd5c4057785fdb5c72b1daf2 Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Fri, 8 Dec 2023 18:49:48 +0800 Subject: [PATCH 12/14] Update README..md. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f7c435f..898be5d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ - 在游戏原版资源之外加载迷彩。 - 在游戏原版资源之外加载武器。 +- 运行自定义脚本呢来扩展游戏功能 - 在游戏根目录下生成`DebugEmit`文件夹提供Mod开发所需的一些资料。 - 更多特性正在开发中…… From af1910dcbd412fe326f7cf9219aa3737acbbc11a Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Fri, 8 Dec 2023 21:01:57 +0800 Subject: [PATCH 13/14] Update README.md. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 898be5d..bf90aea 100644 --- a/README.md +++ b/README.md @@ -94,4 +94,4 @@ Mod文件夹 ## 许可证 -GPL-v3 \ No newline at end of file +[**GPL-v3**](./LICENSE) \ No newline at end of file From b3e3c1a111cac1bcf26fdf9141fdf0449d99c467 Mon Sep 17 00:00:00 2001 From: ZeroDegress Date: Sat, 16 Dec 2023 10:14:10 +0800 Subject: [PATCH 14/14] Completed Mod2 loading and reloading feature. Added scripting support to Mod2. Removed scripting support from Mod. Added partial event feature to scripting, needed to be completed. Rewrite EventManager handles manage methods to support scripting. --- src/EventManager.cs | 32 ++++--- src/Modding/Mod.cs | 6 -- src/Modding/Mod2.cs | 58 ++++++++++++ src/Modding/ModData.cs | 32 ------- src/{ => Modding/Modloader}/ModLoader.cs | 92 +++++++++--------- src/Modding/Modloader/ModLoader2.cs | 50 ++++++++++ src/Modding/ScriptData.cs | 13 --- src/SFHRZModLoaderPlugin.cs | 26 +++--- src/Scripting/ModScriptModuleLoader.cs | 114 ++++++++++++++--------- src/Scripting/ScriptConsole.cs | 23 ++++- src/Scripting/ScriptEngine.cs | 3 +- src/Scripting/ScriptEventManager.cs | 96 +++++++++++++++++++ src/Scripting/ScriptModFs.cs | 33 +++++++ 13 files changed, 412 insertions(+), 166 deletions(-) create mode 100644 src/Modding/Mod2.cs rename src/{ => Modding/Modloader}/ModLoader.cs (75%) create mode 100644 src/Modding/Modloader/ModLoader2.cs delete mode 100644 src/Modding/ScriptData.cs create mode 100644 src/Scripting/ScriptEventManager.cs create mode 100644 src/Scripting/ScriptModFs.cs diff --git a/src/EventManager.cs b/src/EventManager.cs index bd3600b..9aca8fb 100644 --- a/src/EventManager.cs +++ b/src/EventManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using BepInEx.Logging; +using Cpp2IL.Core.Extensions; namespace SFHR_ZModLoader { @@ -13,7 +14,7 @@ public struct Event public class EventManager { - private readonly Dictionary> eventHandlers; + private readonly Dictionary>> eventHandlers; private readonly ManualLogSource logger; public EventManager(ManualLogSource logger) @@ -25,33 +26,38 @@ public EventManager(ManualLogSource logger) public void EmitEvent(Event ev) { logger.LogInfo($"Event: {ev.type}"); - foreach(var handler in eventHandlers) + if (eventHandlers.TryGetValue(ev.type, out var handlers)) { - if(handler.Key == ev.type) + foreach (var handler in handlers.Clone()) { - handler.Value(ev); + handler.Value.Invoke(ev); } } } - public void RegisterEventHandler(string type, Action handler, string? handlerId = null) + public string RegisterEventHandler(string type, Action handler, string? handlerId = null) { - if(eventHandlers.TryGetValue(type, out var curHandler)) + Dictionary> handlers; + if (eventHandlers.TryGetValue(type, out var _handlers)) { - eventHandlers[type] = ev => { - curHandler(ev); - handler(ev); - }; + handlers = _handlers; } else { - eventHandlers[type] = handler; + eventHandlers.Add(type, new()); + handlers = eventHandlers[type]; } + var id = handlerId ?? Guid.NewGuid().ToString(); + handlers.Add(id, handler); + return id; } - public void ClearEventHandler(string type) + public void UnregisterEventHandler(string type, string handlerId) { - eventHandlers.Remove(type); + if (eventHandlers.TryGetValue(type, out var handlers)) + { + handlers.Remove(handlerId); + } } } } \ No newline at end of file diff --git a/src/Modding/Mod.cs b/src/Modding/Mod.cs index 09563eb..840a9ed 100644 --- a/src/Modding/Mod.cs +++ b/src/Modding/Mod.cs @@ -47,12 +47,6 @@ public static Mod LoadFromDirectory(string dir, Mod? mod = null) }; } - public readonly string[] LoadScripts(ModScriptModules modScriptModules) - { - var modId = metadata.id; - return namespaces.ToList().Select(item => item.Value.LoadScripts(modId, modScriptModules)).Select(item => item ?? "").Where(item => item != "").ToArray(); - } - public readonly void PatchToGameContext(GameContext gctx) { foreach (var item in namespaces) diff --git a/src/Modding/Mod2.cs b/src/Modding/Mod2.cs new file mode 100644 index 0000000..dab1bdb --- /dev/null +++ b/src/Modding/Mod2.cs @@ -0,0 +1,58 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using Newtonsoft.Json; +using SFHR_ZModLoader.Scripting; + +namespace SFHR_ZModLoader.Modding; + +[Serializable] +public struct Mod2Metadata +{ + public string id; + public string version; + public string? displayName; + public string? description; + [DefaultValue("index.js")] + public string entry; +} + +public struct Mod2 +{ + public Mod2Metadata metadata; + public string directory; + + public static Mod2 LoadFromDirectory(string directoryPath) + { + if (Directory.Exists(directoryPath)) + { + if (File.Exists(Path.Combine(directoryPath, "mod2.json"))) + { + try + { + var metadata = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(directoryPath, "mod2.json"))); + return new Mod2 + { + metadata = metadata, + directory = directoryPath + }; + } + catch (Exception e) + { + throw new ModLoadingException($"Loading mod from {directoryPath} failed: Read mod2.json failed: {e}."); + } + } + else + { + throw new ModLoadingException($"Loading mod from '{directoryPath}' failed: mod2.json not found."); + } + } + else + { + throw new ModLoadingException($"Loading mod from '{directoryPath}' failed: Not existed."); + } + } +} \ No newline at end of file diff --git a/src/Modding/ModData.cs b/src/Modding/ModData.cs index da07f1f..ae04c79 100644 --- a/src/Modding/ModData.cs +++ b/src/Modding/ModData.cs @@ -49,8 +49,6 @@ public struct ModNamespace public Dictionary camoDatas; public Dictionary weaponDatas; - public Dictionary scripts; - public string? scriptsEntry; public static ModNamespace LoadFromDirectory(string dir, string modId, ModNamespace? ns = null) { @@ -135,29 +133,11 @@ public static ModNamespace LoadFromDirectory(string dir, string modId, ModNamesp SFHRZModLoaderPlugin.Logger?.LogInfo($"Skipped weapons at '{Path.Combine(dir, nsConf.weapons)}' beause it is missing."); } - var scripts = new Dictionary(); - var scriptsEntry = (string?)null; - // Script - if (Directory.Exists(Path.Combine(dir, nsConf.scripts))) - { - var scriptsDirectory = Path.Combine(dir, nsConf.scripts); - scriptsEntry = "index.js"; - Directory.GetFiles(scriptsDirectory).ToList().ForEach(script => { - scripts.Add($"{Path.GetRelativePath(scriptsDirectory, script).Replace('\\', '/')}", File.ReadAllText(script)); - }); - } - else - { - SFHRZModLoaderPlugin.Logger?.LogInfo($"Skipped scripts at '{Path.Combine(dir, nsConf.scripts)}' beause it is missing."); - } - return new ModNamespace { name = nsname, camoDatas = camoDatas, weaponDatas = weaponDatas, - scripts = scripts, - scriptsEntry = scriptsEntry, modId = modId, }; } @@ -174,18 +154,6 @@ public readonly void PatchToGameContext(GameContext gctx) } } - public readonly string? LoadScripts(string modId, ModScriptModules modScriptModules) - { - var scriptsEntry = this.scriptsEntry; - var nsname = this.name; - scripts.ToList().ForEach(item => { - var moduleName = $"mod://{Path.Join(modId, nsname, item.Key).Replace('\\', '/')}"; - modScriptModules.AddModule(modId, nsname, item.Key.Replace('\\', '/'), item.Value); - SFHRZModLoaderPlugin.Logger?.LogInfo($"Loaded script: '{moduleName}'."); - }); - return this.scriptsEntry != null ? $"mod://{Path.Join(modId, nsname, scriptsEntry).Replace('\\', '/')}" : null; - } - public readonly void UnpatchToGameContext(GameContext gctx) { // TODO: 实现反补丁游戏 diff --git a/src/ModLoader.cs b/src/Modding/Modloader/ModLoader.cs similarity index 75% rename from src/ModLoader.cs rename to src/Modding/Modloader/ModLoader.cs index 7da64b7..68ca994 100644 --- a/src/ModLoader.cs +++ b/src/Modding/Modloader/ModLoader.cs @@ -6,9 +6,8 @@ using BepInEx.Logging; using Newtonsoft.Json; using SFHR_ZModLoader.Scripting; -using SFHR_ZModLoader.Modding; -namespace SFHR_ZModLoader; +namespace SFHR_ZModLoader.Modding; public class ModLoadingException : Exception { @@ -16,17 +15,16 @@ public ModLoadingException(string messages) : base(messages) { } } -public class ModLoader +public partial class ModLoader { private readonly string dir; - private Dictionary mods; + private Dictionary mods = new(); private List? modLoadOrder; private ManualLogSource? Logger { get => SFHRZModLoaderPlugin.Logger; } public ModLoader(string dir) { this.dir = dir; - mods = new(); LoadModLoadOrder(); Logger?.LogInfo("ModLoader created."); } @@ -57,6 +55,7 @@ public void RegisterEvents(EventManager eventManager) eventManager.RegisterEventHandler("MODS_LOAD", ev => { LoadMods(); + LoadMod2s(); eventManager.EmitEvent(new Event { type = "MODS_LOADED", @@ -93,6 +92,7 @@ public void RegisterEvents(EventManager eventManager) UnloadMods(); LoadModLoadOrder(); LoadMods(); + LoadMod2s(); if (SFHRZModLoaderPlugin.GameContext != null) { eventManager.EmitEvent(new Event @@ -102,8 +102,9 @@ public void RegisterEvents(EventManager eventManager) }); } }); - eventManager.RegisterEventHandler("SCRIPT_ENGINE_READY", ev => { - LoadModsScripts(SFHRZModLoaderPlugin.ScriptEngine); + eventManager.RegisterEventHandler("SCRIPT_ENGINE_READY", ev => + { + // LoadModsScripts(SFHRZModLoaderPlugin.ScriptEngine); }); Logger?.LogInfo("All ModLoader events registered."); } @@ -136,8 +137,9 @@ public void LoadMods() { Directory.CreateDirectory(dir); } - var modDirToMetaData = Directory.EnumerateDirectories(dir).Select(modDir => { - if(File.Exists(Path.Combine(modDir, "mod.json"))) + var modDirToMetaData = Directory.EnumerateDirectories(dir).Select(modDir => + { + if (File.Exists(Path.Combine(modDir, "mod.json"))) { var metadata = JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(modDir, "mod.json"))); return (modDir, metadata); @@ -147,14 +149,16 @@ public void LoadMods() return (modDir, (ModMetadata?)null); } }).ToList(); - var modIdToDir = modLoadOrder?.Select(modId => { + var modIdToDir = modLoadOrder?.Select(modId => + { return (modId, modDirToMetaData.Find(item => item.Item2?.id == modId).modDir); }).ToList(); - if(modIdToDir != null) + if (modIdToDir != null) { - modIdToDir.ForEach(item => { - if(item.modDir != null) + modIdToDir.ForEach(item => + { + if (item.modDir != null) { LoadMod(item.modDir, item.modId); } @@ -162,8 +166,9 @@ public void LoadMods() } else { - modDirToMetaData.ForEach(item => { - if(item.Item2 != null) + modDirToMetaData.ForEach(item => + { + if (item.Item2 != null) { LoadMod(item.modDir, item.Item2.Value.id); } @@ -171,33 +176,36 @@ public void LoadMods() } } - public void LoadModsScripts(ModScriptEngineWrapper engine) - { - List scriptEntries = new(); - mods.ToList().ForEach(item => { - try - { - item.Value.LoadScripts(engine.ModScriptModules).ToList().ForEach(script => { - scriptEntries.Add(script); - }); - } - catch(Exception e) - { - SFHRZModLoaderPlugin.Logger?.LogError($"Load Mod scripts failed in Mod '{item.Key}': '{e}'."); - } - }); - scriptEntries.ForEach(script => { - Logger?.LogInfo($"Try import script module: '{script}'."); - try - { - engine.Engine.ImportModule(script); - } - catch(Exception e) - { - SFHRZModLoaderPlugin.Logger?.LogError($"Import Mod script module failed in script '{script}': '{e}'."); - } - }); - } + // public void LoadModsScripts(ModScriptEngineWrapper engine) + // { + // List scriptEntries = new(); + // mods.ToList().ForEach(item => + // { + // try + // { + // item.Value.LoadScripts(engine.ModScriptModules).ToList().ForEach(script => + // { + // scriptEntries.Add(script); + // }); + // } + // catch (Exception e) + // { + // SFHRZModLoaderPlugin.Logger?.LogError($"Load Mod scripts failed in Mod '{item.Key}': '{e}'."); + // } + // }); + // scriptEntries.ForEach(script => + // { + // Logger?.LogInfo($"Try import script module: '{script}'."); + // try + // { + // engine.Engine.ImportModule(script); + // } + // catch (Exception e) + // { + // SFHRZModLoaderPlugin.Logger?.LogError($"Import Mod script module failed in script '{script}': '{e}'."); + // } + // }); + // } public void UnloadMods() { diff --git a/src/Modding/Modloader/ModLoader2.cs b/src/Modding/Modloader/ModLoader2.cs new file mode 100644 index 0000000..0587d42 --- /dev/null +++ b/src/Modding/Modloader/ModLoader2.cs @@ -0,0 +1,50 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using Jint; +using SFHR_ZModLoader.Scripting; + +namespace SFHR_ZModLoader.Modding; + +public partial class ModLoader +{ + private Dictionary mod2s = new(); + private ScriptObjectEventManager? scriptEventManager; + public void LoadMod2s() + { + var engineWrapper = new ModScriptEngineWrapper(); + engineWrapper.Engine.SetValue("ModFS", new ScriptObjectFs(dir)); + + scriptEventManager?.clearEventListeners(); + scriptEventManager = new ScriptObjectEventManager(engineWrapper.Engine, SFHRZModLoaderPlugin.EventManager!); + engineWrapper.Engine.SetValue("EventManager", scriptEventManager); + + engineWrapper.Engine.SetValue("console", new ScriptObjectConsole(SFHRZModLoaderPlugin.Logger!)); + foreach (var directory in Directory.EnumerateDirectories(dir)) + { + if (File.Exists(Path.Combine(directory, "mod2.json"))) + { + Mod2 mod; + try + { + mod = Mod2.LoadFromDirectory(directory); + } + catch (Exception e) + { + Logger?.LogError($"Load Mod failed in '{directory}': {e}."); + continue; + } + engineWrapper.ModScriptModuleLoader.ModScriptModules.AddModDirectory(mod.metadata.id, directory); + mod2s.Remove(mod.metadata.id); + mod2s.Add(mod.metadata.id, mod); + } + } + foreach (var modPair in mod2s) + { + engineWrapper.Engine.AddModule($"$load-{modPair.Value.metadata.id}", $"import 'mod://{modPair.Value.metadata.id}/{modPair.Value.metadata.entry}'\n"); + engineWrapper.Engine.ImportModule($"$load-{modPair.Value.metadata.id}"); + } + } +} \ No newline at end of file diff --git a/src/Modding/ScriptData.cs b/src/Modding/ScriptData.cs deleted file mode 100644 index e9c55b3..0000000 --- a/src/Modding/ScriptData.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable enable -using System; - -namespace SFHR_ZModLoader.Modding; - - -[Serializable] -public struct ModScriptsDataConf -{ - public string? entry; - public string[]? includes; - public string[]? excludes; -} \ No newline at end of file diff --git a/src/SFHRZModLoaderPlugin.cs b/src/SFHRZModLoaderPlugin.cs index ddf48ed..c3eb6c3 100644 --- a/src/SFHRZModLoaderPlugin.cs +++ b/src/SFHRZModLoaderPlugin.cs @@ -5,8 +5,8 @@ using BepInEx.Logging; using HarmonyLib; using Il2CppInterop.Runtime.Injection; -using Jint; using SFHR_ZModLoader.Scripting; +using SFHR_ZModLoader.Modding; namespace SFHR_ZModLoader; @@ -21,7 +21,6 @@ public class SFHRZModLoaderPlugin : BasePlugin public static ModLoader? ModLoader { get; set; } public static GameContext? GameContext { get; set; } public static EventManager? EventManager { get; set; } - public static ModScriptEngineWrapper ScriptEngine { get; set; } = new(); public static bool DebugEmit { get; set; } = true; public override void Load() @@ -32,7 +31,8 @@ public override void Load() if (DebugEmit) { - if(!Directory.Exists(Path.Combine(Paths.GameRootPath, "DebugEmit"))) { + if (!Directory.Exists(Path.Combine(Paths.GameRootPath, "DebugEmit"))) + { Directory.CreateDirectory(Path.Combine(Paths.GameRootPath, "DebugEmit")); } } @@ -42,35 +42,37 @@ public override void Load() ClassInjector.RegisterTypeInIl2Cpp(); ZeroComponents = UnityEngine.GameObject.Find(ZERO_COMPONENTS_NAME); - if(ZeroComponents == null) + if (ZeroComponents == null) { ZeroComponents = new UnityEngine.GameObject(ZERO_COMPONENTS_NAME); UnityEngine.GameObject.DontDestroyOnLoad(ZeroComponents); ZeroComponents.hideFlags = UnityEngine.HideFlags.HideAndDontSave; } InputMonitor = ZeroComponents.GetComponent(); - if(InputMonitor == null) + if (InputMonitor == null) { InputMonitor = ZeroComponents.AddComponent(); } ModLoader = new ModLoader(Path.Combine(Paths.GameRootPath, "mods")); ModLoader.RegisterEvents(EventManager); - EventManager.EmitEvent(new Event { + EventManager.EmitEvent(new Event + { type = "MODS_LOAD" }); - InputMonitor.SetAction(UnityEngine.KeyCode.P, () => { - if(GameContext == null) + InputMonitor.SetAction(UnityEngine.KeyCode.P, () => + { + if (GameContext == null) { return; } - EventManager.EmitEvent(new Event { + EventManager.EmitEvent(new Event + { type = "MODS_RELOAD" }); }); - ScriptEngine.Engine.SetValue("console", new ScriptObjectConsole(Logger)); - ScriptEngine.Engine.SetValue("EventManager", EventManager); - EventManager.EmitEvent(new Event { + EventManager.EmitEvent(new Event + { type = "SCRIPT_ENGINE_READY" }); Harmony.CreateAndPatchAll(typeof(Hooks)); diff --git a/src/Scripting/ModScriptModuleLoader.cs b/src/Scripting/ModScriptModuleLoader.cs index dce6c5e..eabb644 100644 --- a/src/Scripting/ModScriptModuleLoader.cs +++ b/src/Scripting/ModScriptModuleLoader.cs @@ -3,70 +3,84 @@ using System.Collections.Generic; using Esprima; using Esprima.Ast; +using Il2CppSystem.IO; using Jint; using Jint.Runtime.Modules; namespace SFHR_ZModLoader.Scripting; -public class ModScriptModules +public class ModScriptModules { - private Dictionary>> moduleSourceDict = new(); + private Dictionary modDirectoryMap = new(); - public void AddModule(string modId, string nsname, string path, string source) + public void AddModDirectory(string modId, string modDirectory) { - if(!moduleSourceDict.ContainsKey(modId)) + modDirectoryMap.Remove(modId); + modDirectoryMap.Add(modId, modDirectory); + } + + public string GetModuleSource(Uri uri) + { + if (uri.Scheme != "mod") { - moduleSourceDict.Add(modId, new()); + throw new Exception($"Not mod scheme: '{uri.Scheme}'."); } - var modDict = moduleSourceDict[modId]; - if(!modDict.ContainsKey(nsname)) + if (modDirectoryMap.TryGetValue(uri.Authority, out var directory)) { - modDict.Add(nsname, new()); + var scriptPath = Path.Combine(directory, uri.LocalPath.TrimStart('/')); + if (!Path.GetFullPath(scriptPath).StartsWith(Path.GetFullPath(directory))) + { + throw new Exception($"Mod '{uri.Authority}' script module should be in its mod directory: '{uri}'."); + } + if (File.Exists(scriptPath)) + { + try + { + return File.ReadAllText(scriptPath); + } + catch (Exception e) + { + throw new Exception($"Error while reading script file: '{Path.Combine(directory, uri.LocalPath)}': {e}."); + } + } + else + { + throw new Exception($"Script file not found: '{Path.Combine(directory, uri.LocalPath)}'."); + } + } + else + { + throw new Exception($"Mod not found: '{uri.Authority}'."); } - var nsDict = modDict[nsname]; - nsDict.Add(path, source); } - public string GetModuleSource(Uri uri) + public byte[] GetModFileBytes(Uri uri) { - switch(uri.Scheme) + if (uri.Scheme != "modfile") { - case "mod": + throw new Exception($"Not file scheme: '{uri.Scheme}'."); + } + if (modDirectoryMap.TryGetValue(uri.Authority, out var directory)) + { + if (File.Exists(Path.Combine(directory, uri.LocalPath))) { - if(moduleSourceDict.TryGetValue(uri.Host, out var modDict)) + try { - if(uri.Segments.Length >= 3) - { - var nsname = uri.Segments[1].TrimEnd('/'); - var path = string.Join("", uri.Segments, 2, uri.Segments.Length - 2).TrimEnd('/'); - if(modDict.TryGetValue(nsname, out var nsDict)) - { - if(nsDict.TryGetValue(path, out var source)) - { - return source; - } - else - { - throw new Exception($"Unknown path in namespace '{nsname}': {path}"); - } - } - else - { - throw new Exception($"Unknown namespace in Mod '{uri.Host}': '{nsname}'."); - } - } - else - { - throw new Exception($"Path segments missing: '{uri.AbsolutePath}'."); - } + return File.ReadAllBytes(Path.Combine(directory, uri.LocalPath)); } - else + catch (Exception e) { - throw new Exception($"Unknown Mod ID: '{uri.Host}'."); + throw new Exception($"Error while reading file: '{Path.Combine(directory, uri.LocalPath)}': {e}."); } } - default: - throw new Exception($"Unknown schema: '{uri.Scheme}'."); + else + { + throw new Exception($"File not found: '{Path.Combine(directory, uri.LocalPath)}'."); + } + } + else + { + throw new Exception($"Mod not found: '{uri.Authority}'."); } } } @@ -92,7 +106,19 @@ public Module LoadModule(Engine engine, ResolvedSpecifier resolved) } var realUri = new Uri($"{resolved.Uri.Segments[1].TrimEnd('/')}://{resolved.Uri.Segments[2].TrimEnd('/')}/{string.Join("", resolved.Uri.Segments, 3, resolved.Uri.Segments.Length - 3).TrimEnd('/')}"); - string code = ModScriptModules.GetModuleSource(realUri); + string code; + // SFHRZModLoaderPlugin.Logger?.LogWarning(realUri); + switch (realUri.Scheme) + { + case "mod": + code = ModScriptModules.GetModuleSource(realUri); + break; + case "modfile": + // TODO: 完成文件加载部分 + throw new Exception($"Unsupported scheme: {realUri.Scheme}"); + default: + throw new Exception($"Unknown scheme: {realUri.Scheme}"); + } string path = resolved.Uri.ToString(); try { @@ -126,7 +152,7 @@ public ResolvedSpecifier Resolve(string? referencingModuleLocation, string speci } else if (IsRelative(specifier)) { - if(referencingModuleLocation == null) + if (referencingModuleLocation == null) { throw new Exception($"No base module location for '{specifier}'"); } diff --git a/src/Scripting/ScriptConsole.cs b/src/Scripting/ScriptConsole.cs index d7a78cc..74efb52 100644 --- a/src/Scripting/ScriptConsole.cs +++ b/src/Scripting/ScriptConsole.cs @@ -1,17 +1,34 @@ #nullable enable using BepInEx.Logging; +using System.Linq; namespace SFHR_ZModLoader.Scripting; -class ScriptObjectConsole { +class ScriptObjectConsole +{ private ManualLogSource Logger { get; set; } public ScriptObjectConsole(ManualLogSource logger) { Logger = logger; } - public void log(string message) + public void log(params object[] objs) { - Logger.LogInfo(message); + Logger.LogInfo(string.Join(" ", objs.Select(obj => obj.ToString()))); + } + + public void info(params object[] objs) + { + log(objs); + } + + public void warn(params object[] objs) + { + Logger.LogWarning(string.Join(" ", objs.Select(obj => obj.ToString()))); + } + + public void error(params object[] objs) + { + Logger.LogError(string.Join(" ", objs.Select(obj => obj.ToString()))); } } \ No newline at end of file diff --git a/src/Scripting/ScriptEngine.cs b/src/Scripting/ScriptEngine.cs index 9739ddf..84f43f8 100644 --- a/src/Scripting/ScriptEngine.cs +++ b/src/Scripting/ScriptEngine.cs @@ -13,7 +13,8 @@ public class ModScriptEngineWrapper public ModScriptEngineWrapper() { - Engine = new(options => { + Engine = new(options => + { options.AllowClr(typeof(GlobalData).Assembly) .EnableModules(ModScriptModuleLoader); }); diff --git a/src/Scripting/ScriptEventManager.cs b/src/Scripting/ScriptEventManager.cs new file mode 100644 index 0000000..f4d0f80 --- /dev/null +++ b/src/Scripting/ScriptEventManager.cs @@ -0,0 +1,96 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using Jint; +using Jint.Native; + +namespace SFHR_ZModLoader.Scripting; + +public class ScriptObjectEventManager +{ + + public class AddEventListenerOptions + { + public bool? once; + } + + private readonly Dictionary> eventIds = new(); + private readonly EventManager eventManager; + private Engine engine; + public ScriptObjectEventManager(Engine engine, EventManager eventManager) + { + this.eventManager = eventManager; + this.engine = engine; + } + public string addEventListener(string type, Delegate listener, AddEventListenerOptions? options = null) + { + string id; + if (options != null) + { + if (options.once ?? false) + { + id = Guid.NewGuid().ToString(); + eventManager.RegisterEventHandler(type, ev => + { + listener.DynamicInvoke(null, new JsValue[] { JsValue.FromObject(engine, ev) }); + removeEventListener(type, id); + }, id); + } + else + { + id = eventManager.RegisterEventHandler(type, ev => + { + listener.DynamicInvoke(null, new JsValue[] { JsValue.FromObject(engine, ev) }); + }); + } + } + else + { + id = eventManager.RegisterEventHandler(type, ev => + { + listener.DynamicInvoke(null, new JsValue[] { JsValue.FromObject(engine, ev) }); + }); + } + List list; + if (eventIds.TryGetValue(type, out var _list)) + { + list = _list; + } + else + { + List __list = new(); + eventIds.Add(type, __list); + list = __list; + } + list.Add(id); + return id; + } + + public void removeEventListener(string type, string handlerId) + { + eventManager.UnregisterEventHandler(type, handlerId); + eventIds.Remove(handlerId); + } + + public void dispatchEvent(string type, object? data = null) + { + eventManager.EmitEvent(new Event + { + type = type, + data = data! + }); + } + + public void clearEventListeners() + { + foreach (var ids in eventIds) + { + foreach (var id in ids.Value) + { + removeEventListener(ids.Key, id); + } + } + eventIds.Clear(); + } +} \ No newline at end of file diff --git a/src/Scripting/ScriptModFs.cs b/src/Scripting/ScriptModFs.cs new file mode 100644 index 0000000..ebd6cab --- /dev/null +++ b/src/Scripting/ScriptModFs.cs @@ -0,0 +1,33 @@ +#nullable enable + +using System; +using System.IO; + +namespace SFHR_ZModLoader.Scripting; + +class ScriptObjectFs +{ + private string dir; + public ScriptObjectFs(string dir) + { + this.dir = dir; + } + + public string ReadAllText(string filePath) + { + if (!Path.GetFullPath(filePath).StartsWith(Path.GetFullPath(dir))) + { + throw new Exception($"__fs should only access the file inside its dir: {dir}"); + } + return File.ReadAllText(Path.Combine(dir, filePath)); + } + + public byte[] ReadAllBytes(string filePath) + { + if (!Path.GetFullPath(filePath).StartsWith(Path.GetFullPath(dir))) + { + throw new Exception($"__fs should only access the file inside its dir: {dir}"); + } + return File.ReadAllBytes(Path.Combine(dir, filePath)); + } +} \ No newline at end of file