using game.Account; using game.PlayerDataStorage; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Globalization; using System.Security.Cryptography; using System.Text; internal static class Import102 { const string Root="CodexOutput/upserver/KVGameplayLoopTests/"; const string Api="http://s101.local:18080/"; static string Hash(byte[] value)=>Convert.ToHexString(SHA256.HashData(value)); static string Hash(string value)=>Hash(Encoding.UTF8.GetBytes(value)); static JToken Read(string path) { using var reader=new JsonTextReader(new StringReader(File.ReadAllText(path))){DateParseHandling=DateParseHandling.None}; return JToken.ReadFrom(reader); } static void Save(string path,object value) { var bytes=Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value,Formatting.Indented)); using var file=new FileStream(path,FileMode.CreateNew,FileAccess.Write,FileShare.Read); file.Write(bytes); file.Flush(true); } static PlayerKVSchemaSnapshot Snapshot(DateTime now) { JArray Table(string name)=>(JArray)Read("cfgData/"+name+".json"); var global=Table("tbglobalconfig")[0]; return new PlayerKVSchemaSnapshot("kv-loop",Table("tbroddata").Select(v=>(int)v["ID"]),Table("tbmapdata").Select(v=>(int)v["ID"]), Enum.GetNames().ToDictionary(n=>n,n=>(int)Enum.Parse(n)),CultureInfo.InvariantCulture, now,(int)global["InitWheelTicket"],Table("tbeventpackmanager").ToDictionary(v=>(int)v["ID"],v=>(int)v["PackType"]), Table("tbfishingevent").ToDictionary(v=>(int)v["ID"],v=>new PlayerKVEventConfig((int)v["RedirectID"], (string)v["TimeDefinition"]["$type"]=="LimitedTime"?(string)v["TimeDefinition"]["EndTime"]:null)),(int)global["GlobalTimeOffset"], Table("tbfishdata").ToDictionary(v=>(int)v["ID"],v=>new PlayerKVFishConfig((int)v["ID"],(int)v["AreaID"],(int)v["Quality"],(int)v["FishCardID"],(float)v["BaseWeight"],(float)v["PointExtraParam"],(float)v["WeightRandom"])), Table("tbfishcard").ToDictionary(v=>(int)v["ID"],v=>new PlayerKVFishCardConfig((int)v["ID"],(int)v["MaxLevel"],v["CardsRequiredList"].ToObject(),v["WeightMagList"].ToObject()))); } internal static async Task Run() { var proposal=Read(Root+"102-import-proposal.json"); if((string)proposal["status"]!="proposal-not-executed" || (string)proposal["api"]!=Api || (string)proposal["playerId"]!="102" || (bool)proposal["transformValues"] || (int)proposal["writeCount"]!=45) throw new InvalidOperationException("Approved proposal mismatch."); var sourcePath=(string)proposal["source"]; if(Hash(File.ReadAllBytes(sourcePath))!="827F36084648449CEFAFB96B3CE62093DF70B8A87E33A526DA5B03A9C135A377") throw new InvalidOperationException("Original source changed."); var source=Read(sourcePath); if((string)source["customId"]!="102" || (string)source["titleId"]!="108EE8" || (string)source["source"]!="PlayFab") throw new InvalidOperationException("Source identity mismatch."); var raw=source["data"].ToObject>(); var entries=(JArray)proposal["entries"]; if(raw.Count!=45 || entries.Count!=45 || entries.Select(v=>(string)v["key"]).Distinct().Count()!=45 || entries.Any(v=>!raw.TryGetValue((string)v["key"],out var value) || value==null || Hash(value)!=(string)v["valueSha256"] || Encoding.UTF8.GetByteCount(value)!=(int)v["utf8Bytes"])) throw new InvalidOperationException("45 original values differ from reviewed proposal."); var claim=Root+"import-102.claimed"; if(File.Exists(claim)) throw new InvalidOperationException("Import already attempted; inspect evidence, never replay."); var folder=Path.GetFullPath(Root+"imports/"+DateTime.UtcNow.ToString("yyyyMMddTHHmmssZ")); Directory.CreateDirectory(folder); using var session=new S101AccountSession(new Uri(Api)); var login=await session.LoginAsync("102"); if(!login.Success || session.NewlyCreated || session.PlayerId!="102") throw new InvalidOperationException("Existing 102 login failed: "+login.Error); Save(Path.Combine(folder,"account.json"),new{session.PlayerId,session.InternalId,session.CreateTimeUtc,session.ServerTimeUtc,session.NewlyCreated}); using var http=new HttpClient(new HttpClientHandler{CookieContainer=session.Cookies,UseCookies=true,AllowAutoRedirect=false}){Timeout=TimeSpan.FromSeconds(20)}; var kv=new PlayerKVClient(http,session.ApiBaseUri); var before=await kv.GetAsync(null); if(!before.Success) throw new InvalidOperationException("Fresh baseline Get failed: "+before.Error); Save(Path.Combine(folder,"before.json"),new{playerId="102",api=Api,at=DateTime.UtcNow,data=before.Data}); if(before.Data.Count!=0) throw new InvalidOperationException("102 is no longer empty; not overwriting."); var snapshot=Snapshot(session.ServerTimeUtc); // Candidate validation only: real account/time, no game handle, no claim that these are existing KV values. var candidate=new PlayerKVLoginPrepareResult(PlayerKVLoginPrepareError.None,"102",1,ownerId:"preimport-candidate",data:raw); var account=new PlayerKVAccountDescriptor("102",1,session.InternalId,false,session.CreateTimeUtc,session.ServerTimeUtc); var identity=new PlayerKVIdentitySnapshot((string)source["playFabId"],false,PlayerKVIdentityChannel.none,Array.Empty()); var decoded=PlayerKVSaveSchema.Decode(candidate,account,"102",1,true,snapshot,snapshot.Version,identity); Save(Path.Combine(folder,"candidate-schema.json"),new{candidateOnly=true,realAccountAndServerTime=true,verdict=decoded.Verdict.ToString(),errors=decoded.Diagnostics.TotalErrors,details=decoded.Diagnostics.Details}); if(decoded.Verdict!=PlayerKVSchemaVerdict.Valid) throw new InvalidOperationException("Candidate not valid with real account/server time."); Save(claim,new{folder,playerId="102",api=Api,keys=raw.Keys.OrderBy(k=>k),at=DateTime.UtcNow}); var set=await kv.SetAsync(raw); // One request only. No retry path. Save(Path.Combine(folder,"set-result.json"),set); var after=await kv.GetAsync(null); // Observe even if Set reports Unknown/failure. if(!after.Success) { Save(Path.Combine(folder,"get-failure.json"),new{after.Error,after.StatusCode}); throw new InvalidOperationException("Cannot confirm import; inspect, never retry."); } var path=Path.Combine(folder,"after.json"); Save(path,new{playerId="102",api=Api,at=DateTime.UtcNow,data=after.Data}); var matrix=raw.OrderBy(p=>p.Key).Select(p=>new{key=p.Key,present=after.Data.ContainsKey(p.Key),valueEqual=after.Data.TryGetValue(p.Key,out var value)&&value==p.Value, expectedHash=Hash(p.Value),actualHash=after.Data.TryGetValue(p.Key,out var actual)?Hash(actual):null}).ToArray(); var extra=after.Data.Keys.Except(raw.Keys,StringComparer.Ordinal).OrderBy(k=>k).ToArray(); var verified=matrix.All(v=>v.present&&v.valueEqual)&&extra.Length==0; Save(Path.Combine(folder,"verification.json"),new{verified,matrix,extra,set.Success,set.Error,set.MayHaveCommitted,afterBackup=path,sha256=Hash(File.ReadAllBytes(path)),formalRequests=4,setRequests=1,pfWrites=0}); Console.WriteLine(JsonConvert.SerializeObject(new{verified,folder,afterBackup=path,keys=after.Data.Count,setRequests=1,formalRequests=4,pfWrites=0})); if(!verified || !set.Success) throw new InvalidOperationException("Import response or values require review; do not start game or resend."); } }