using System.Text.Json; using Ananta.SDK.Logging; namespace Ananta.Server.Configuration; /// /// Process-wide id counters whose high-water mark survives a restart. /// /// 2026-09-24 自捐赠端 DremkaPS handoff §6.10 移植:「编号高水位必须落盘」。 /// /// 背景:本服此前所有自分配号段都是 private static long x = FLOOR; 加 /// Interlocked.Increment,序号只活在进程里。服务端一重启就回到 FLOOR 重新发号, /// 而对端(客户端 / 已落盘的存档)可能还记得上一轮的号 —— 同一个 UniqueId / unitId 发出去两遍, /// 表现就是「发了但看不见」「SyncLogicAgentLeave 变成 no-op」「换个号段才正常」这类 /// 查起来完全不像重号的怪现象。 /// /// 用法:把 Interlocked.Increment(ref s_x) 换成 /// IdCounterStore.Next("文件.字段名", FLOOR),语义完全一致(返回 +1 之后的值), /// 只是多了一步落盘。FLOOR 仍由调用方持有,存档里的值低于 FLOOR 时自动抬回 FLOOR, /// 所以调整号段不会把旧的高水位带进新号段。 /// /// 落盘是同步写:调用点全是低频的面板 / GM 控制台操作(生成 NPC、建车、发物品), /// 一个几百字节的 JSON 不值得为此引入延迟写入的复杂度。写失败只记日志、绝不影响发号。 /// internal static class IdCounterStore { private static readonly object Gate = new(); private static readonly JsonSerializerOptions Options = new() { WriteIndented = true }; // ServerLogger 没有全局实例(只读构造器,Write 是实例方法),所以这里自带一个。 private static readonly ServerLogger Log = new("IDHWM"); private static readonly Dictionary HighWater = Load(); internal static string FilePath { get; } = PrivateServerConfigStore.ResolveProjectPath(Path.Combine("config", "id-counters.json")); /// /// 取 这个号段的下一个号。 /// 是号段下界(也就是原来那个 static 字段的初值)。 /// internal static long Next(string name, long floor) { lock (Gate) { var current = HighWater.TryGetValue(name, out var stored) ? stored : floor; // 存档里的值可能来自更早/更小的号段。低于下界就抬回去,别把旧号带进新号段。 if (current < floor) current = floor; var next = current + 1; HighWater[name] = next; Persist(); return next; } } /// /// 把水位抬到至少 (单调,永不回退)。 /// 用于「外部已经拿到一个号,后续发号不能撞上它」——例如恢复存档里某个 item 的 UniqueId。 /// internal static void Reserve(string name, long value) { lock (Gate) { var current = HighWater.TryGetValue(name, out var stored) ? stored : value; if (value <= current) return; HighWater[name] = value; Persist(); } } /// 当前高水位(已发出的最大号),供日志/面板查看。没有记录则返回 floor。 internal static long Peek(string name, long floor) { lock (Gate) return HighWater.TryGetValue(name, out var stored) ? stored : floor; } private static Dictionary Load() { var path = FilePath; if (!File.Exists(path)) return []; try { return JsonSerializer.Deserialize>(File.ReadAllText(path)) ?? []; } catch (Exception ex) { Log.Warn( $"[IDHWM] id counters could not be read ({ex.GetType().Name}: {ex.Message}), starting from the in-code floors: {path}"); return []; } } private static void Persist() { var path = FilePath; var temp = path + ".tmp"; try { var directory = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) Directory.CreateDirectory(directory); File.WriteAllText(temp, JsonSerializer.Serialize(HighWater, Options)); File.Move(temp, path, overwrite: true); } catch (Exception ex) { // 落盘失败绝不阻断发号:最坏情况只是退回「重启即复用」的旧行为。 Log.Warn( $"[IDHWM] id counters could not be saved ({ex.GetType().Name}: {ex.Message}): {path}"); try { if (File.Exists(temp)) File.Delete(temp); } catch { // Nothing useful to do. } } } }