Files

132 lines
4.9 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.Text.Json;
using Ananta.SDK.Logging;
namespace Ananta.Server.Configuration;
/// <summary>
/// Process-wide id counters whose high-water mark survives a restart.
///
/// 2026-09-24 自捐赠端 DremkaPS handoff §6.10 移植:「编号高水位必须落盘」。
///
/// 背景:本服此前所有自分配号段都是 <c>private static long x = FLOOR;</c> 加
/// <c>Interlocked.Increment</c>,序号只活在进程里。服务端一重启就回到 FLOOR 重新发号,
/// 而对端(客户端 / 已落盘的存档)可能还记得上一轮的号 —— 同一个 UniqueId / unitId 发出去两遍,
/// 表现就是「发了但看不见」「SyncLogicAgentLeave 变成 no-op」「换个号段才正常」这类
/// 查起来完全不像重号的怪现象。
///
/// 用法:把 <c>Interlocked.Increment(ref s_x)</c> 换成
/// <c>IdCounterStore.Next("文件.字段名", FLOOR)</c>,语义完全一致(返回 +1 之后的值),
/// 只是多了一步落盘。FLOOR 仍由调用方持有,存档里的值低于 FLOOR 时自动抬回 FLOOR,
/// 所以调整号段不会把旧的高水位带进新号段。
///
/// 落盘是同步写:调用点全是低频的面板 / GM 控制台操作(生成 NPC、建车、发物品),
/// 一个几百字节的 JSON 不值得为此引入延迟写入的复杂度。写失败只记日志、绝不影响发号。
/// </summary>
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<string, long> HighWater = Load();
internal static string FilePath { get; } =
PrivateServerConfigStore.ResolveProjectPath(Path.Combine("config", "id-counters.json"));
/// <summary>
/// 取 <paramref name="name"/> 这个号段的下一个号。
/// <paramref name="floor"/> 是号段下界(也就是原来那个 static 字段的初值)。
/// </summary>
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;
}
}
/// <summary>
/// 把水位抬到至少 <paramref name="value"/>(单调,永不回退)。
/// 用于「外部已经拿到一个号,后续发号不能撞上它」——例如恢复存档里某个 item 的 UniqueId。
/// </summary>
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();
}
}
/// <summary>当前高水位(已发出的最大号),供日志/面板查看。没有记录则返回 floor。</summary>
internal static long Peek(string name, long floor)
{
lock (Gate)
return HighWater.TryGetValue(name, out var stored) ? stored : floor;
}
private static Dictionary<string, long> Load()
{
var path = FilePath;
if (!File.Exists(path))
return [];
try
{
return JsonSerializer.Deserialize<Dictionary<string, long>>(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.
}
}
}
}