using game.PlayerDataStorage; using Newtonsoft.Json; using Newtonsoft.Json.Linq; static partial class Tests { static async Task RunOfferSchemaTests() { foreach(bool disco in new[] { false,true }) await Run(disco ? "M17-offer-nested-shape-and-enum" : "M16-offer-nested-shape-and-state",() => { var obj=JObject.FromObject(new { FirstOpen=true,EventId=7,StartTime="2026-09-22T00:00:00.0000000Z",EndTime="2026-09-24T00:00:00.0000000Z", ItemId=-1,LotteryNum=5,Items=new[] { new { Id=1,Item=2,Number=3,Status=2 } },RewardList=new[] { 8,9 },Finished=true,ChestReward=10 }); if (disco) { obj.Remove("Finished"); obj.Remove("ChestReward"); } string key=disco ? "OfferDiscoPartyManager" : "OfferChainsChestData"; Dictionary Raw(JObject value)=>new() { [key]=value.ToString(Formatting.None) }; var state=PlayerKVSaveSchema.DecodeOffer(Raw(obj),disco,SchemaSnapshot(),new()); Check(state.IsPresent && state.FirstOpen && state.EventId==7 && state.StartTime.Day==22 && state.EndTime.Day==24 && state.ItemId == -1 && state.LotteryNum==5,"all scalar fields"); Check(state.Items.Count==1 && state.Items[0].Id==1 && state.Items[0].Item==2 && state.Items[0].Number==3 && state.Items[0].Status==2 && state.RewardList.SequenceEqual(new[] { 8,9 }),"complete nested collections"); Check(disco || (state.Finished && state.ChestReward==10),"chains only fields"); Check(!PlayerKVSaveSchema.DecodeOffer(new Dictionary(),disco,SchemaSnapshot(),new()).IsPresent,"missing explicit"); var paths=obj.Properties().Select(p=>p.Name).Concat(new[] { "Items[0]","Items[0].Id","Items[0].Item","Items[0].Number","Items[0].Status","RewardList[0]" }); foreach(var path in paths) { var bad=(JObject)obj.DeepClone(); bad.SelectToken(path).Replace(JValue.CreateNull()); Check(PlayerKVSaveSchema.DecodeOffer(Raw(bad),disco,SchemaSnapshot(),new())==null,"all consumed null fields rejected"); } foreach(var path in new[] { "EventId","Items[0].Id","Items[0].Status","RewardList[0]" }) { var bad=(JObject)obj.DeepClone(); bad.SelectToken(path).Replace(new JValue(1.5)); Check(PlayerKVSaveSchema.DecodeOffer(Raw(bad),disco,SchemaSnapshot(),new())==null,"integer coercion rejected"); } if(disco) { var bad=(JObject)obj.DeepClone(); bad["Items"][0]["Status"]=3; Check(PlayerKVSaveSchema.DecodeOffer(Raw(bad),true,SchemaSnapshot(),new())==null,"undefined enum rejected"); } return Task.CompletedTask; }); } }