Files
2026-07-10 10:24:29 +08:00

150 lines
6.2 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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; }
}
}
}