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