#if UNITY_EDITOR && !SERVICE_HOST using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using game.Account; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace game.PlayerDataStorage { internal static class PlayerKVSaveRouteEditorValidationHost { static int running; // Retain a failed cleanup's owner for diagnosis; never silently restart over it. static object failedLifetime; internal static async Task RunAsync(string caseId) { if (caseId != "fixed-route") throw new ArgumentException("Unknown fixed validation case."); if (Interlocked.CompareExchange(ref running, 1, 0) != 0 || failedLifetime != null) throw new InvalidOperationException("Validation already running or cleanup failed."); var run = new Run(); try { return await run.Execute(); } finally { try { await run.Close(); } catch { failedLifetime = run; throw; } finally { Interlocked.Exchange(ref running, 0); } } } sealed class Run { readonly Uri api = new Uri("http://kv-route.invalid/"); readonly List sessions = new List(); readonly Dictionary> remote = new Dictionary> { ["101"] = new Dictionary { ["k"] = "A", ["empty"] = "" }, ["102"] = new Dictionary { ["k"] = "B" } }; readonly string cache = Path.GetFullPath(Path.Combine(UnityEngine.Application.dataPath, "../../CodexOutput/upserver/KVSaveRouteTests/unity-runs", DateTime.UtcNow.ToString("yyyyMMddHHmmss") + "_" + Guid.NewGuid().ToString("N"))); PlayerKVGameSessionBridge bridge; PlayerKVGameLoadGuard guard; int assertions, auth, gets, writes; long epoch = 1; void Check(bool value) { assertions++; if (!value) throw new InvalidOperationException("Fixed route assertion " + assertions); } static HttpResponseMessage Json(object value) => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonConvert.SerializeObject(value)) }; sealed class SyntheticHandler : HttpMessageHandler { readonly Func> send; internal SyntheticHandler(Func> send) { this.send = send; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken token) => send(request); } HttpMessageHandler KV(CookieContainer jar) { var id = jar.GetCookies(api)["sid"].Value; return new SyntheticHandler(async request => { Check(request.RequestUri.Host == "kv-route.invalid" && request.RequestUri.Port == 80); var path = request.RequestUri.AbsolutePath; Check(path == "/playerdata/Get" || path == "/playerdata/Set"); var body = JObject.Parse(await request.Content.ReadAsStringAsync()); if (path == "/playerdata/Get") { gets++; return Json(new { data = remote[id] }); } writes++; foreach (var p in ((JObject)body["data"]).Properties()) remote[id][p.Name] = (string)p.Value; return Json(new { }); }); } async Task Open(string id) { var session = new S101AccountSession(api, jar => new SyntheticHandler(request => { Check(request.RequestUri.Host == "kv-route.invalid" && request.RequestUri.AbsolutePath == "/account/Login"); auth++; jar.Add(api, new Cookie("sid", id)); return Task.FromResult(Json(new { state = 0, internalId = int.Parse(id), newCreate = false, createTime = "2026-01-01T00:00:00Z", svrTime = "2026-01-01T00:00:00Z" })); })); sessions.Add(session); Check((await session.LoginAsync(id)).Success); var opened = await bridge.OpenAsync(session, cache, id, api, epoch, e => e == epoch); Check(opened.Success); return opened.Handle; } internal async Task Execute() { // Never disturb a guard held by another validation or a controlled login. Check(PlayerKVGameLoadGuard.TryEnter(out guard)); guard.Dispose(); guard = null; Directory.CreateDirectory(cache); bridge = new PlayerKVGameSessionBridge(KV); var a = await Open("101"); var present = PlayFabMgr.ReadKV(a, "k"); Check(present.Success && present.Found == true && present.Value == "A"); Check(PlayFabMgr.ReadKV(a, "missing").Found == false); Check(PlayFabMgr.ReadKV(a, "empty").Found == true && PlayFabMgr.ReadKV(a, "empty").Value == ""); Check(PlayFabMgr.ReadKV(a, null).Found == null); var edit = PlayFabMgr.WriteKV(a, "k", "remote"); Check(edit.Accepted && edit.Ticket != null && writes == 0); var local = PlayFabMgr.WriteKV(a, "k", "local", false); Check(local.Accepted && local.Ticket == null); Check(PlayFabMgr.ReadKV(a, "k").Value == "local"); var flush = await PlayFabMgr.FlushKVAsync(a); Check(flush.Confirmed == 1 && remote["101"]["k"] == "remote"); Check((await PlayFabMgr.WaitForKVTicketAsync(a, edit.Ticket)).Status == PlayerKVTicketStatus.Confirmed); await bridge.CloseAsync(); epoch++; var b = await Open("102"); var writesBefore = writes; Check(PlayFabMgr.ReadKV(a, "k").Error == PlayerKVKeyReadError.StaleOrClosing); Check(!PlayFabMgr.WriteKV(a, "k", "late").Accepted); Check((await PlayFabMgr.FlushKVAsync(a)).Blocked == 1); Check((await PlayFabMgr.WaitForKVTicketAsync(b, edit.Ticket)).Status == PlayerKVTicketStatus.Blocked); Check(PlayFabMgr.ReadKV(b, "k").Value == "B" && writes == writesBefore); Check(PlayerKVGameLoadGuard.TryEnter(out guard)); Check(PlayFabMgr.ReadKV(null, null).Error == PlayerKVKeyReadError.LoadGuardActive); Check(PlayFabMgr.WriteKV(null, null, null).StateResult.Error == PlayerKVChangeError.Blocked); Check((await PlayFabMgr.FlushKVAsync(b)).TransportError == PlayerKVError.Cancelled); Check((await PlayFabMgr.WaitForKVTicketAsync(b, null)).TransportError == PlayerKVError.Cancelled); Check(guard.Violations == 4 && guard.Count(PlayerKVGuardPoint.ReadLocal) == 1 && guard.Count(PlayerKVGuardPoint.Update) == 1 && guard.Count(PlayerKVGuardPoint.Sync) == 2); Check(auth == 2 && gets == 2 && writes == 1); await Close(); return JsonConvert.SerializeObject(new { success = true, assertions, auth, gets, writes, formalRequests = 0, restored = true, cache }); } internal async Task Close() { if (bridge != null) await bridge.CloseAsync(); foreach (var session in sessions) session.Dispose(); sessions.Clear(); guard?.Dispose(); guard = null; } } } } #endif