Files
AIC-Project/Server/Server.Host/GameModuleLoader.cs
T
2026-07-10 10:24:29 +08:00

81 lines
3.8 KiB
C#

using System;
using System.IO;
using System.Reflection;
using XWorld.Framework;
namespace XWorld.Server.Host
{
public sealed class GameModuleLoader
{
private readonly string _publicKeyPem;
public GameModuleLoader(string publicKeyPem = null)
{
_publicKeyPem = publicKeyPem;
}
public LoadedGameModule Load(string gamesRoot, string gameId, int version)
{
string moduleDir = Path.Combine(gamesRoot, gameId, version.ToString());
if (!Directory.Exists(moduleDir))
throw new DirectoryNotFoundException($"游戏模块目录不存在: {moduleDir}");
if (_publicKeyPem != null)
VerifySignature(moduleDir);
var manifest = GameManifest.Load(Path.Combine(moduleDir, "game.json"));
if (manifest.GameId != gameId || manifest.Version != version)
throw new InvalidDataException(
$"game.json 与目录不符: 期望 {gameId} v{version}, 实际 {manifest.GameId} v{manifest.Version}");
if (manifest.MinFrameworkVersion > XWorld.Framework.FrameworkInfo.Version)
throw new InvalidOperationException(
$"小游戏要求最小框架版本 {manifest.MinFrameworkVersion},当前框架 {XWorld.Framework.FrameworkInfo.Version},请更新框架后重试");
string serverDll = Path.Combine(moduleDir, manifest.ServerAssembly);
if (!File.Exists(serverDll))
throw new FileNotFoundException($"服务端入口 DLL 不存在: {serverDll}");
var alc = new GameModuleAlc($"{gameId}@{version}", moduleDir);
try
{
Assembly entryAsm = alc.LoadEntry(serverDll);
Type entryType = entryAsm.GetType(manifest.ServerEntryType);
if (entryType == null)
throw new InvalidOperationException(
$"在 {serverDll} 中找不到入口类型 {manifest.ServerEntryType}");
if (!typeof(IGameServerRoom).IsAssignableFrom(entryType))
throw new InvalidOperationException(
$"{manifest.ServerEntryType} 未实现 IGameServerRoom");
Func<IGameServerRoom> factory = () => (IGameServerRoom)Activator.CreateInstance(entryType);
return new LoadedGameModule(gameId, version, alc, factory);
}
catch
{
alc.Unload(); // 失败路径:立刻卸载,避免遗留锁住 DLL 的 collectible ALC
throw;
}
}
// 注意:签名清单仅覆盖 ServerAssembly/CoreDll/game.json(当前小游戏 = Core+Server 两 DLL,完整覆盖)。
// 若将来小游戏带私有第三方 DLL,发布工具(PublishLayout)与此处校验须同步把它们纳入清单。
private void VerifySignature(string moduleDir)
{
string filesTxt = Path.Combine(moduleDir, "files.txt");
string sigPath = Path.Combine(moduleDir, "files.txt.sig");
if (!File.Exists(filesTxt) || !File.Exists(sigPath))
throw new InvalidOperationException($"缺少签名清单,拒绝加载: {moduleDir}");
var fm = XWorld.PublishTool.FilesManifest.Parse(File.ReadAllText(filesTxt));
if (!XWorld.PublishTool.ManifestSigner.Verify(fm.Digest(), File.ReadAllBytes(sigPath), _publicKeyPem))
throw new InvalidOperationException($"签名验证失败,拒绝加载: {moduleDir}");
foreach (var kv in fm.Entries)
{
string p = Path.Combine(moduleDir, kv.Key);
if (!File.Exists(p) || XWorld.PublishTool.Md5Util.OfFile(p) != kv.Value.Md5)
throw new InvalidOperationException($"文件 md5 不符或缺失,拒绝加载: {kv.Key}");
}
}
}
}