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.
}
}
}
}