150 lines
6.2 KiB
C#
150 lines
6.2 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text;
|
||
|
||
namespace XWorld.Server.Host
|
||
{
|
||
// (gameId,version) -> 已加载模块;按引用计数获取/释放。
|
||
// 淘汰有两个来源:①同 gameId 更高版本加载成功;②同 (gameId,version) 磁盘内容指纹变化(覆盖重发)。
|
||
// 被淘汰且引用归零即卸载;内容变化时新旧模块并存至旧房间结束(_retired,Release 按实例识别)。
|
||
// 生产环境版本不可变、指纹恒命中缓存,行为与旧实现一致。
|
||
public sealed class GameModuleRegistry
|
||
{
|
||
private sealed class Entry
|
||
{
|
||
public LoadedGameModule Module;
|
||
public int RefCount;
|
||
public bool Superseded;
|
||
public string Fingerprint;
|
||
}
|
||
|
||
private readonly GameModuleLoader _loader;
|
||
private readonly string _gamesRoot;
|
||
private readonly Dictionary<string, Entry> _current = new Dictionary<string, Entry>();
|
||
private readonly List<Entry> _retired = new List<Entry>();
|
||
|
||
public GameModuleRegistry(GameModuleLoader loader, string gamesRoot)
|
||
{
|
||
_loader = loader; _gamesRoot = gamesRoot;
|
||
}
|
||
|
||
private static string Key(string gameId, int version) => $"{gameId}@{version}";
|
||
private string ModuleDir(string gameId, int version) => Path.Combine(_gamesRoot, gameId, version.ToString());
|
||
|
||
public LoadedGameModule Acquire(string gameId, int version)
|
||
{
|
||
string key = Key(gameId, version);
|
||
if (_current.TryGetValue(key, out var e))
|
||
{
|
||
// 覆盖重发检测:磁盘内容指纹变化 → 旧条目退役、加载新内容。
|
||
// 指纹算不出(目录处于覆盖交换的瞬间窗口/被删)→ 视为未变化,用缓存兜底。
|
||
string fp = ComputeFingerprintOrNull(ModuleDir(gameId, version));
|
||
if (fp == null || fp == e.Fingerprint)
|
||
{
|
||
e.RefCount++;
|
||
return e.Module;
|
||
}
|
||
|
||
var reloaded = LoadEntry(gameId, version); // 失败直接抛出:旧条目原样保留,不污染状态
|
||
Retire(e);
|
||
_current[key] = reloaded;
|
||
SupersedeOlderVersions(gameId, version);
|
||
reloaded.RefCount++;
|
||
return reloaded.Module;
|
||
}
|
||
|
||
var loaded = LoadEntry(gameId, version);
|
||
_current[key] = loaded;
|
||
SupersedeOlderVersions(gameId, version);
|
||
loaded.RefCount++;
|
||
return loaded.Module;
|
||
}
|
||
|
||
public void Release(LoadedGameModule module)
|
||
{
|
||
string key = Key(module.GameId, module.Version);
|
||
if (_current.TryGetValue(key, out var e) && ReferenceEquals(e.Module, module))
|
||
{
|
||
if (e.RefCount <= 0)
|
||
throw new InvalidOperationException($"重复释放模块 {module.GameId}@{module.Version}");
|
||
e.RefCount--;
|
||
if (e.RefCount <= 0 && e.Superseded)
|
||
{
|
||
_current.Remove(key);
|
||
module.Unload();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 退役条目:同版本被覆盖重发淘汰后仍被旧房间持有,按实例识别
|
||
for (int i = 0; i < _retired.Count; i++)
|
||
{
|
||
if (!ReferenceEquals(_retired[i].Module, module)) continue;
|
||
var r = _retired[i];
|
||
if (r.RefCount <= 0)
|
||
throw new InvalidOperationException($"重复释放模块 {module.GameId}@{module.Version}");
|
||
r.RefCount--;
|
||
if (r.RefCount <= 0)
|
||
{
|
||
_retired.RemoveAt(i);
|
||
module.Unload();
|
||
}
|
||
return;
|
||
}
|
||
// 未知模块的释放忽略(与旧实现对不存在 key 直接 return 的行为一致)
|
||
}
|
||
|
||
public int RefCount(string gameId, int version)
|
||
=> _current.TryGetValue(Key(gameId, version), out var e) ? e.RefCount : 0;
|
||
|
||
public bool IsLoaded(string gameId, int version) => _current.ContainsKey(Key(gameId, version));
|
||
|
||
private Entry LoadEntry(string gameId, int version)
|
||
{
|
||
// 指纹在加载前采样:加载成功即与所载内容对应(单线程 tick actor 下无并发写窗口)
|
||
string fp = ComputeFingerprintOrNull(ModuleDir(gameId, version));
|
||
var loaded = _loader.Load(_gamesRoot, gameId, version);
|
||
return new Entry { Module = loaded, RefCount = 0, Superseded = false, Fingerprint = fp };
|
||
}
|
||
|
||
private void SupersedeOlderVersions(string gameId, int version)
|
||
{
|
||
foreach (var kv in _current)
|
||
if (kv.Value.Module.GameId == gameId && kv.Value.Module.Version < version)
|
||
kv.Value.Superseded = true;
|
||
}
|
||
|
||
private void Retire(Entry e)
|
||
{
|
||
e.Superseded = true;
|
||
if (e.RefCount <= 0) e.Module.Unload();
|
||
else _retired.Add(e);
|
||
}
|
||
|
||
// 目录内容指纹:全部 *.dll + game.json 按名 Ordinal 排序拼 name=md5; 再整体 md5;
|
||
// 读不到(目录不存在/IO 竞态)返回 null 由调用方兜底
|
||
private static string ComputeFingerprintOrNull(string dir)
|
||
{
|
||
try
|
||
{
|
||
if (!Directory.Exists(dir)) return null;
|
||
var names = Directory.GetFiles(dir, "*.dll")
|
||
.Select(Path.GetFileName)
|
||
.Append("game.json")
|
||
.Where(n => File.Exists(Path.Combine(dir, n)))
|
||
.Distinct()
|
||
.OrderBy(n => n, StringComparer.Ordinal);
|
||
var sb = new StringBuilder();
|
||
foreach (var n in names)
|
||
sb.Append(n).Append('=')
|
||
.Append(XWorld.PublishTool.Md5Util.OfFile(Path.Combine(dir, n))).Append(';');
|
||
return XWorld.PublishTool.Md5Util.OfBytes(Encoding.UTF8.GetBytes(sb.ToString()));
|
||
}
|
||
catch (IOException) { return null; }
|
||
catch (UnauthorizedAccessException) { return null; }
|
||
}
|
||
}
|
||
}
|