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 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}"); } } } }