Files
ft/CodexOutput/upserver/KVGameLoadTests/ActivitySchemaTests.cs

142 lines
10 KiB
C#

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<string,string> 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<string,string>(),SchemaSnapshot(),new());
Check(!missing.IsPresent && !missing.IsExplicitNull,"missing separate");
var empty = PlayerKVSaveSchema.DecodeChallenge(new Dictionary<string,string> { ["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<string,string>(raw);
var result = Aggregate(raw);
Check(result.Verdict == PlayerKVSchemaVerdict.Valid, "explicit null aggregate valid");
Check(result.Data.Module<PlayerKVChallengeState>(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<string,string> { ["FishingChallengeData"] = text }, SchemaSnapshot(), errors);
Check(state == null && errors.Count(15, PlayerKVSchemaError.Malformed) == 1, "only strict JSON null or complete object accepted");
}
await Throws<PlayerKVSchemaException>(() => { PlayerKVSaveSchema.Json("null"); return Task.CompletedTask; });
await Throws<ArgumentException>(() =>
{
_ = new PlayerKVChallengeState(false,false,0,0,0,0,0,0,0,0,0,default,default,new List<int>(),new List<int>(),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<string,string> 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<string,string>(),SchemaSnapshot(),new()).IsPresent,"missing separate");
Check(PlayerKVSaveSchema.DecodePinball(new Dictionary<string,string> { ["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<int,object> { [7]=new { EventScratchCardID=8,Count=9 } } });
Dictionary<string,string> 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<string,string>(),SchemaSnapshot(),new()).IsPresent,"missing separate");
Check(PlayerKVSaveSchema.DecodeScratch(new Dictionary<string,string> { ["estdk"]="null" },SchemaSnapshot(),new())==null,"null not missing");
return Task.CompletedTask;
});
}
}