using game.PlayerDataStorage; using Newtonsoft.Json; using Newtonsoft.Json.Linq; static partial class Tests { static async Task RunActivitySchemaTests() { await Run("M15-activity-all-saved-fields-strict",() => { var obj=JObject.FromObject(new { FirstOpen=true,EventId=11,RankTierLevel=11,Finished=11,RedirectID=11,Score=11,ProcessedScore=11,step=11,uiStep=11,index=11,StartTime="2026-09-23T00:00:00.0000000Z",EndTime="2026-09-23T00:00:00.0000000Z",eliminateNumber=new[] { 7,9 },robotAccountList=new[] { 7,9 },InflationRate=1.5f }); Dictionary Raw(JObject value)=>new() { ["FishingChallengeData"]=value.ToString(Formatting.None) }; var state=PlayerKVSaveSchema.DecodeChallenge(Raw(obj),SchemaSnapshot(),new()); Check(state!=null && state.IsPresent && !state.IsExplicitNull,"valid complete object, not explicit null"); Check(state.FirstOpen==true,"typed FirstOpen"); Check(state.EventId==11,"typed EventId"); Check(state.RankTierLevel==11,"typed RankTierLevel"); Check(state.Finished==11,"typed Finished"); Check(state.RedirectID==11,"typed RedirectID"); Check(state.Score==11,"typed Score"); Check(state.ProcessedScore==11,"typed ProcessedScore"); Check(state.step==11,"typed step"); Check(state.uiStep==11,"typed uiStep"); Check(state.index==11,"typed index"); Check(state.StartTime==new DateTime(2026,9,23,0,0,0,DateTimeKind.Utc),"typed StartTime"); Check(state.EndTime==new DateTime(2026,9,23,0,0,0,DateTimeKind.Utc),"typed EndTime"); Check(state.eliminateNumber.SequenceEqual(new[] { 7,9 }),"typed eliminateNumber"); Check(state.robotAccountList.SequenceEqual(new[] { 7,9 }),"typed robotAccountList"); Check(state.InflationRate==1.5f,"typed InflationRate"); foreach(var property in obj.Properties()) { var bad=(JObject)obj.DeepClone(); bad.Remove(property.Name); Check(PlayerKVSaveSchema.DecodeChallenge(Raw(bad),SchemaSnapshot(),new())==null,"missing saved field rejects"); bad=(JObject)obj.DeepClone(); bad[property.Name]=JValue.CreateNull(); Check(PlayerKVSaveSchema.DecodeChallenge(Raw(bad),SchemaSnapshot(),new())==null,"null saved field rejects"); } var missing = PlayerKVSaveSchema.DecodeChallenge(new Dictionary(),SchemaSnapshot(),new()); Check(!missing.IsPresent && !missing.IsExplicitNull,"missing separate"); var empty = PlayerKVSaveSchema.DecodeChallenge(new Dictionary { ["FishingChallengeData"]="null" },SchemaSnapshot(),new()); Check(empty != null && empty.IsPresent && empty.IsExplicitNull,"explicit null is a present inactive challenge"); return Task.CompletedTask; }); await Run("M15-explicit-null-raw-and-strict-boundaries", async () => { foreach (var text in new[] { "null", " \r\n\tnull \t" }) { var raw = ModernRaw(); raw["FishingChallengeData"] = text; raw["outside-null-compat-probe"] = " untouched \r\n"; var original = new Dictionary(raw); var result = Aggregate(raw); Check(result.Verdict == PlayerKVSchemaVerdict.Valid, "explicit null aggregate valid"); Check(result.Data.Module(15).IsExplicitNull, "aggregate preserves explicit null marker"); Check(result.Data.Raw.Count == original.Count, "raw key count unchanged"); foreach (var pair in original) { Check(System.Text.Encoding.UTF8.GetBytes(result.Data.Raw[pair.Key]).SequenceEqual( System.Text.Encoding.UTF8.GetBytes(pair.Value)), "raw UTF8 bytes preserved including null whitespace"); Check(raw[pair.Key] == pair.Value, "decode did not edit input"); } raw["FishingChallengeData"] = "changed-after-decode"; Check(result.Data.Raw["FishingChallengeData"] == text, "decoded raw is a separate copy"); } Check(!Aggregate(ModernRaw()).Data.Raw.ContainsKey("FishingChallengeData"), "missing raw stays missing"); foreach (var text in new[] { null, "", " \t", "\"null\"", "0", "true", "[]", "[null]", "{", "null null", "\vnull\v", "\fnull\f", "\u0085null", "\u00a0null\u00a0", "\ufeffnull", "/*x*/null", "null//x", "null,", "null,\"x\":1", "null}" }) { var errors = new PlayerKVSchemaDiagnostics(); var state = PlayerKVSaveSchema.DecodeChallenge(new Dictionary { ["FishingChallengeData"] = text }, SchemaSnapshot(), errors); Check(state == null && errors.Count(15, PlayerKVSchemaError.Malformed) == 1, "only strict JSON null or complete object accepted"); } await Throws(() => { PlayerKVSaveSchema.Json("null"); return Task.CompletedTask; }); await Throws(() => { _ = new PlayerKVChallengeState(false,false,0,0,0,0,0,0,0,0,0,default,default,new List(),new List(),0,true); return Task.CompletedTask; }); }); await Run("M26-activity-all-saved-fields-strict",() => { var obj=JObject.FromObject(new { EventID=11,PinballMainID=11,SpinPoint=11,Scores=11,Multiple=11,Token=11,ConsumedToken=11,RewardIndex=11,Force=1.5f,LuckyValue=1.5f,InflationRate=1.5f }); Dictionary Raw(JObject value)=>new() { ["epbdk"]=value.ToString(Formatting.None) }; var state=PlayerKVSaveSchema.DecodePinball(Raw(obj),SchemaSnapshot(),new()); Check(state!=null && state.IsPresent,"valid complete state"); Check(state.EventID==11,"typed EventID"); Check(state.PinballMainID==11,"typed PinballMainID"); Check(state.SpinPoint==11,"typed SpinPoint"); Check(state.Scores==11,"typed Scores"); Check(state.Multiple==11,"typed Multiple"); Check(state.Token==11,"typed Token"); Check(state.ConsumedToken==11,"typed ConsumedToken"); Check(state.RewardIndex==11,"typed RewardIndex"); Check(state.Force==1.5f,"typed Force"); Check(state.LuckyValue==1.5f,"typed LuckyValue"); Check(state.InflationRate==1.5f,"typed InflationRate"); foreach(var property in obj.Properties()) { var bad=(JObject)obj.DeepClone(); bad.Remove(property.Name); Check(PlayerKVSaveSchema.DecodePinball(Raw(bad),SchemaSnapshot(),new())==null,"missing saved field rejects"); bad=(JObject)obj.DeepClone(); bad[property.Name]=JValue.CreateNull(); Check(PlayerKVSaveSchema.DecodePinball(Raw(bad),SchemaSnapshot(),new())==null,"null saved field rejects"); } Check(!PlayerKVSaveSchema.DecodePinball(new Dictionary(),SchemaSnapshot(),new()).IsPresent,"missing separate"); Check(PlayerKVSaveSchema.DecodePinball(new Dictionary { ["epbdk"]="null" },SchemaSnapshot(),new())==null,"null not missing"); return Task.CompletedTask; }); await Run("M27-activity-all-saved-fields-strict",() => { var obj=JObject.FromObject(new { EventID=11,ScratchMainID=11,ChainPackID=11,Token=11,ConsumedToken=11,RewardTaskConsumedToken=11,EventScratchRewardTaskID=11,EventScratchCardID=11,TotalConsumedToken=11,ChainPackProgress=11,magIndex=11,IsOver=true,LuckyValue=1.5f,InflationRate=1.5f,StampRewardList=new[] { 7,9 },StampRewardDict=new Dictionary { [7]=new { EventScratchCardID=8,Count=9 } } }); Dictionary Raw(JObject value)=>new() { ["estdk"]=value.ToString(Formatting.None) }; var state=PlayerKVSaveSchema.DecodeScratch(Raw(obj),SchemaSnapshot(),new()); Check(state!=null && state.IsPresent,"valid complete state"); Check(state.EventID==11,"typed EventID"); Check(state.ScratchMainID==11,"typed ScratchMainID"); Check(state.ChainPackID==11,"typed ChainPackID"); Check(state.Token==11,"typed Token"); Check(state.ConsumedToken==11,"typed ConsumedToken"); Check(state.RewardTaskConsumedToken==11,"typed RewardTaskConsumedToken"); Check(state.EventScratchRewardTaskID==11,"typed EventScratchRewardTaskID"); Check(state.EventScratchCardID==11,"typed EventScratchCardID"); Check(state.TotalConsumedToken==11,"typed TotalConsumedToken"); Check(state.ChainPackProgress==11,"typed ChainPackProgress"); Check(state.magIndex==11,"typed magIndex"); Check(state.IsOver==true,"typed IsOver"); Check(state.LuckyValue==1.5f,"typed LuckyValue"); Check(state.InflationRate==1.5f,"typed InflationRate"); Check(state.StampRewardList.SequenceEqual(new[] { 7,9 }),"typed StampRewardList"); Check(state.StampRewardDict[7].EventScratchCardID==8 && state.StampRewardDict[7].Count==9,"typed StampRewardDict"); foreach(var property in obj.Properties()) { var bad=(JObject)obj.DeepClone(); bad.Remove(property.Name); Check(PlayerKVSaveSchema.DecodeScratch(Raw(bad),SchemaSnapshot(),new())==null,"missing saved field rejects"); bad=(JObject)obj.DeepClone(); bad[property.Name]=JValue.CreateNull(); Check(PlayerKVSaveSchema.DecodeScratch(Raw(bad),SchemaSnapshot(),new())==null,"null saved field rejects"); } Check(!PlayerKVSaveSchema.DecodeScratch(new Dictionary(),SchemaSnapshot(),new()).IsPresent,"missing separate"); Check(PlayerKVSaveSchema.DecodeScratch(new Dictionary { ["estdk"]="null" },SchemaSnapshot(),new())==null,"null not missing"); return Task.CompletedTask; }); } }