using System.Text.Json; using Ananta.Server.Configuration; using Ananta.Server.Protocol.Client4229938; namespace Ananta.Server.ClientData.Client4229938; /// /// 改装店改装件的落盘(save/vehicles/<pid>.json)。 /// /// 为什么需要:donor 最初把购买结果只放在 WorldEntryState 的内存字典里, /// 服务端一重启买过的件全没了(donor backlog #2 的原话)。买一次就要写一次盘 —— 没有可靠的 /// session-end 钩子,所以走「每次购买后立刻写」。 /// /// 开关 gameplay.repairShop.persistTuning(默认 false = 与 donor 默认一致): /// 关掉时本类只读不写,行为完全等同 donor 未接 store 时的样子。 /// /// 写盘用 tmp + move 的原子替换,避免写一半崩掉留下截断的 json。 /// internal static class VehicleTuningStore { private static readonly object Gate = new(); private static readonly JsonSerializerOptions Options = new() { WriteIndented = true }; private static string DirectoryPath => PrivateServerConfigStore.ResolveProjectPath(Path.Combine("save", "vehicles")); private static string PathFor(ulong pid) => Path.Combine(DirectoryPath, $"{pid}.json"); /// 把当前会话的改装状态写盘。开关关闭或 world 为空时直接返回。 internal static void Save(ulong pid, WorldEntryState? world) { if (!PrivateServerConfigStore.Current.Gameplay.RepairShop.PersistTuning) return; if (world is null) return; List vehicles; lock (world.SyncRoot) { var configs = new HashSet(); foreach (var key in world.VehiclePartsByConfig.Keys) configs.Add(key); foreach (var key in world.VehicleSuitByConfig.Keys) configs.Add(key); vehicles = new List(configs.Count); foreach (var configId in configs) { var parts = world.VehiclePartsByConfig.TryGetValue(configId, out var list) ? list.Select(p => new PartDto { Type = p.Type, ConfigId = p.ConfigId }).ToList() : []; vehicles.Add(new VehicleDto { ConfigId = configId, SuitId = world.VehicleSuitByConfig.GetValueOrDefault(configId), Parts = parts, }); } } try { lock (Gate) { var dir = DirectoryPath; System.IO.Directory.CreateDirectory(dir); var path = PathFor(pid); var tmp = path + ".tmp"; File.WriteAllText(tmp, JsonSerializer.Serialize(new SaveFile { Vehicles = vehicles }, Options)); File.Move(tmp, path, overwrite: true); } } catch (Exception ex) { Console.WriteLine($"[REPAIRSHOP] tuning save failed: {ex.Message}"); } } /// 进世界时把盘上的改装状态读回内存。开关关闭或文件不存在时什么也不做。 internal static void Load(ulong pid, WorldEntryState world) { if (!PrivateServerConfigStore.Current.Gameplay.RepairShop.PersistTuning) return; try { SaveFile? file; var path = PathFor(pid); lock (Gate) { if (!File.Exists(path)) return; file = JsonSerializer.Deserialize(File.ReadAllText(path)); } if (file?.Vehicles is null) return; lock (world.SyncRoot) { foreach (var v in file.Vehicles) { if (v.ConfigId == 0) continue; if (v.SuitId != 0) world.VehicleSuitByConfig[v.ConfigId] = v.SuitId; if (v.Parts is { Count: > 0 }) world.VehiclePartsByConfig[v.ConfigId] = v.Parts.Where(p => p.ConfigId != 0).Select(p => (p.Type, p.ConfigId)).ToList(); } } } catch (Exception ex) { Console.WriteLine($"[REPAIRSHOP] tuning load failed: {ex.Message}"); } } private sealed class SaveFile { public List Vehicles { get; set; } = []; } private sealed class VehicleDto { public uint ConfigId { get; set; } public uint SuitId { get; set; } public List Parts { get; set; } = []; } private sealed class PartDto { public uint Type { get; set; } public uint ConfigId { get; set; } } }