93 lines
7.6 KiB
C#
93 lines
7.6 KiB
C#
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<cfg.ConditionType>().ToDictionary(n=>n,n=>(int)Enum.Parse<cfg.ConditionType>(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<int[]>(),v["WeightMagList"].ToObject<float[]>())));
|
|
}
|
|
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<Dictionary<string,string>>();
|
|
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<PlayerKVIdentityChannel>());
|
|
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.");
|
|
}
|
|
}
|