diff --git a/README.md b/README.md index 156bbf3..bf90aea 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ - 在游戏原版资源之外加载迷彩。 - 在游戏原版资源之外加载武器。 +- 运行自定义脚本呢来扩展游戏功能 - 在游戏根目录下生成`DebugEmit`文件夹提供Mod开发所需的一些资料。 - 更多特性正在开发中…… @@ -49,39 +50,34 @@ 在每一个Mod文件夹之下,都有一定的文件结构来表示Mod,比如: Mod文件夹 - -|-sfh - -|--camos - -|---Red1 - -|----texture.png - -|----icon.png - -|----redCamo.png - -|--weapons - -|----equipTexture.png - -|----equipTextureAlt.png - -|----menuTexture.png - -|----unequipTexture.png - -|mod.json - -- `sfh`:所有原版数据和资产都在这个文件夹(你也可以定制你自己的文件夹名,比如说xxddc之类的,但sfh被指定为原版的文件夹,又称**命名空间**)下,更改这个命名空间下的文件说明你将覆盖原版的数据和资产。 +- (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`都是各自的脚本执行的入口。其余脚本文件只能被引用,不会被执行。 (更多内容正在开发中……) @@ -98,4 +94,4 @@ Mod文件夹 ## 许可证 -GPL-v3 \ No newline at end of file +[**GPL-v3**](./LICENSE) \ No newline at end of file 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/scripts/FetchDependencies.ps1 b/scripts/FetchDependencies.ps1 index 55e49f8..d2db1b7 100644 --- a/scripts/FetchDependencies.ps1 +++ b/scripts/FetchDependencies.ps1 @@ -1,12 +1,22 @@ $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", + "FishNet.Runtime.dll", + "UnityEngine.UI.dll" + "UnityEngine.UIModule.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 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 diff --git a/src/EventManager.cs b/src/EventManager.cs index 66857a0..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) + 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/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/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/ModLoader.cs b/src/ModLoader.cs deleted file mode 100644 index 22dd8d6..0000000 --- a/src/ModLoader.cs +++ /dev/null @@ -1,278 +0,0 @@ -#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 -{ - [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 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", - }; - 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 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 class ModLoadingException: Exception - { - public ModLoadingException(string messages): base(messages) - {} - } - - public class ModLoader - { - private readonly string dir; - private Dictionary mods; - private readonly ManualLogSource logger; - - public ModLoader(string dir, ManualLogSource logger, EventManager eventManager) - { - this.dir = dir; - this.mods = new(); - this.logger = logger; - this.logger.LogInfo("ModLoader created."); - } - - public void RegisterEvents(EventManager eventManager) - { - eventManager.RegisterEventHandler("MODS_LOAD", ev => { - LoadMods(); - eventManager.EmitEvent(new Event { - type = "MODS_LOADED", - }); - }); - eventManager.RegisterEventHandler("GAMECONTEXT_PATCH", ev => { - 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, - }); - }); - } - - public void LoadMods() - { - 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"))) - { - 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}."); - } - } - } - } - - public Mod? GetMod(string name) - { - if (mods.ContainsKey(name)) - { - return mods[name]; - } - else - { - return null; - } - } - - public void PatchToGameContext(GameContext gctx) - { - foreach (var item in mods) - { - item.Value.PatchToGameContext(gctx); - } - } - } -} \ No newline at end of file diff --git a/src/Modding/CamoData.cs b/src/Modding/CamoData.cs new file mode 100644 index 0000000..bcacabc --- /dev/null +++ b/src/Modding/CamoData.cs @@ -0,0 +1,177 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using UnityEngine; + +namespace SFHR_ZModLoader.Modding; + +[Serializable] +public struct ModCamosConf +{ + public string[]? includes; + public string[]? 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/Modding/Mod.cs b/src/Modding/Mod.cs new file mode 100644 index 0000000..840a9ed --- /dev/null +++ b/src/Modding/Mod.cs @@ -0,0 +1,65 @@ +#nullable enable +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using SFHR_ZModLoader.Scripting; + +namespace SFHR_ZModLoader.Modding; + +public struct Mod +{ + public ModMetadata metadata; + public Dictionary namespaces; + + 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, metadata.id, ns); + } + else + { + namespaces.Add(Path.GetFileName(nsdir), ModNamespace.LoadFromDirectory(nsdir, metadata.id)); + } + } + 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/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 new file mode 100644 index 0000000..ae04c79 --- /dev/null +++ b/src/Modding/ModData.cs @@ -0,0 +1,161 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using SFHR_ZModLoader.Scripting; + +namespace SFHR_ZModLoader.Modding; + +[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 modId; + public string name; + public Dictionary camoDatas; + + public Dictionary weaponDatas; + + public static ModNamespace LoadFromDirectory(string dir, string modId, ModNamespace? ns = null) + { + // Namespace + 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, + scripts = newConf.scripts ?? nsConf.scripts, + }; + } + + // CamoData + 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)}' beause it is missing."); + } + + // WeaponData + 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($"Skipped weapons at '{Path.Combine(dir, nsConf.weapons)}' beause it is missing."); + } + + return new ModNamespace + { + name = nsname, + camoDatas = camoDatas, + weaponDatas = weaponDatas, + modId = modId, + }; + } + + 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/Modding/Modloader/ModLoader.cs b/src/Modding/Modloader/ModLoader.cs new file mode 100644 index 0000000..68ca994 --- /dev/null +++ b/src/Modding/Modloader/ModLoader.cs @@ -0,0 +1,242 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using BepInEx.Logging; +using Newtonsoft.Json; +using SFHR_ZModLoader.Scripting; + +namespace SFHR_ZModLoader.Modding; + +public class ModLoadingException : Exception +{ + public ModLoadingException(string messages) : base(messages) + { } +} + +public partial class ModLoader +{ + private readonly string dir; + private Dictionary mods = new(); + private List? modLoadOrder; + private ManualLogSource? Logger { get => SFHRZModLoaderPlugin.Logger; } + + public ModLoader(string dir) + { + this.dir = dir; + 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 (!line.TrimStart().StartsWith("#")) + { + modLoadOrder.Add(line.Trim()); + } + } + 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."); + } + } + + public void RegisterEvents(EventManager eventManager) + { + eventManager.RegisterEventHandler("MODS_LOAD", ev => + { + LoadMods(); + LoadMod2s(); + eventManager.EmitEvent(new Event + { + type = "MODS_LOADED", + }); + }); + eventManager.RegisterEventHandler("GAMECONTEXT_PATCH", ev => + { + 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("MODS_RELOAD", ev => + { + if (SFHRZModLoaderPlugin.GameContext != null) + { + UnpatchToGameContext(SFHRZModLoaderPlugin.GameContext); + } + UnloadMods(); + LoadModLoadOrder(); + LoadMods(); + LoadMod2s(); + if (SFHRZModLoaderPlugin.GameContext != null) + { + eventManager.EmitEvent(new Event + { + type = "GAMECONTEXT_PATCH", + data = SFHRZModLoaderPlugin.GameContext, + }); + } + }); + 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}..."); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + 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); + } + 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) + { + LoadMod(item.modDir, item.modId); + } + }); + } + else + { + modDirToMetaData.ForEach(item => + { + if (item.Item2 != null) + { + 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() + { + + } + + public Mod? GetMod(string name) + { + if (mods.ContainsKey(name)) + { + 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/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/WeaponData.cs b/src/Modding/WeaponData.cs new file mode 100644 index 0000000..308dbd3 --- /dev/null +++ b/src/Modding/WeaponData.cs @@ -0,0 +1,174 @@ +#nullable enable +using System; +using System.IO; +using Newtonsoft.Json; +using UnityEngine; + +namespace SFHR_ZModLoader.Modding; + +[Serializable] +public struct ModWeaponsConf +{ + public string[]? includes; + public string[]? 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/SFHRZModLoaderPlugin.cs b/src/SFHRZModLoaderPlugin.cs index 6ec0021..c3eb6c3 100644 --- a/src/SFHRZModLoaderPlugin.cs +++ b/src/SFHRZModLoaderPlugin.cs @@ -5,8 +5,8 @@ using BepInEx.Logging; using HarmonyLib; using Il2CppInterop.Runtime.Injection; -using UnityEngine; -using Il2CppInterop.Runtime; +using SFHR_ZModLoader.Scripting; +using SFHR_ZModLoader.Modding; namespace SFHR_ZModLoader; @@ -31,45 +31,50 @@ 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")); } } - EventManager = new(Logger); 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"), Logger, EventManager); + 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 { - type = "GAMECONTEXT_PATCH", - data = GameContext, + EventManager.EmitEvent(new Event + { + type = "MODS_RELOAD" }); }); - + 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..eabb644 --- /dev/null +++ b/src/Scripting/ModScriptModuleLoader.cs @@ -0,0 +1,190 @@ +#nullable enable +using System; +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 +{ + private Dictionary modDirectoryMap = new(); + + public void AddModDirectory(string modId, string modDirectory) + { + modDirectoryMap.Remove(modId); + modDirectoryMap.Add(modId, modDirectory); + } + + public string GetModuleSource(Uri uri) + { + if (uri.Scheme != "mod") + { + throw new Exception($"Not mod scheme: '{uri.Scheme}'."); + } + if (modDirectoryMap.TryGetValue(uri.Authority, out var directory)) + { + 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}'."); + } + } + + public byte[] GetModFileBytes(Uri uri) + { + if (uri.Scheme != "modfile") + { + throw new Exception($"Not file scheme: '{uri.Scheme}'."); + } + if (modDirectoryMap.TryGetValue(uri.Authority, out var directory)) + { + if (File.Exists(Path.Combine(directory, uri.LocalPath))) + { + try + { + return File.ReadAllBytes(Path.Combine(directory, uri.LocalPath)); + } + catch (Exception e) + { + throw new Exception($"Error while reading file: '{Path.Combine(directory, uri.LocalPath)}': {e}."); + } + } + else + { + throw new Exception($"File not found: '{Path.Combine(directory, uri.LocalPath)}'."); + } + } + else + { + throw new Exception($"Mod not found: '{uri.Authority}'."); + } + } +} + +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; + // 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 + { + 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..74efb52 --- /dev/null +++ b/src/Scripting/ScriptConsole.cs @@ -0,0 +1,34 @@ +#nullable enable + +using BepInEx.Logging; +using System.Linq; + +namespace SFHR_ZModLoader.Scripting; + +class ScriptObjectConsole +{ + private ManualLogSource Logger { get; set; } + public ScriptObjectConsole(ManualLogSource logger) + { + Logger = logger; + } + public void log(params object[] objs) + { + 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 new file mode 100644 index 0000000..84f43f8 --- /dev/null +++ b/src/Scripting/ScriptEngine.cs @@ -0,0 +1,22 @@ +#nullable enable + +using Jint; + +namespace SFHR_ZModLoader.Scripting; + +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 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