using game.PlayerDataStorage; static partial class Tests { static async Task RunClubPackSchemaTests() { await Run("M14-club-three-load-fields-only", () => { var raw = new Dictionary { ["ClubSignin"] = "2026-09-23T00:00:00.0000000Z", ["LureDonateData"] = "{\"DonateCountTime\":\"2026-09-22T00:00:00.0000000Z\",\"ClubLureDonateCount\":7,\"ClubLureRequestCDEnd\":\"2026-09-24T00:00:00.0000000Z\"}", ["IsOpenClubRed"] = "" }; var state = PlayerKVSaveSchema.DecodeClub(raw,SchemaSnapshot(),new()); Check(state.SignTime.Value.Day == 23 && state.DonateTime.Day == 22 && state.DonateCount == 7 && state.RequestEnd.Day == 24 && !state.OpenRed,"all three load states"); var missing = PlayerKVSaveSchema.DecodeClub(new Dictionary(),SchemaSnapshot(),new()); Check(missing.SignTime == null && missing.DonateTime == default && missing.DonateCount == 0 && missing.RequestEnd == default && missing.OpenRed,"missing exact zero state"); raw["ClubSignin"] = ""; Check(PlayerKVSaveSchema.DecodeClub(raw,SchemaSnapshot(),new()).SignTime == null,"empty signin supported"); foreach (var key in raw.Keys.ToArray()) { var bad = new Dictionary(raw) { [key] = null }; Check(PlayerKVSaveSchema.DecodeClub(bad,SchemaSnapshot(),new()) == null,"present null rejected"); } foreach (var bad in new[] { "null", "{}", "{\"DonateCountTime\":0,\"ClubLureDonateCount\":1,\"ClubLureRequestCDEnd\":0}" }) { raw["LureDonateData"] = bad; Check(PlayerKVSaveSchema.DecodeClub(raw,SchemaSnapshot(),new()) == null,"bad donation DTO rejected"); } raw = new() { ["ClubSignin"] = "03/04/2026" }; var errors = new PlayerKVSchemaDiagnostics(); Check(PlayerKVSaveSchema.DecodeClub(raw,SchemaSnapshot("en-GB"),errors) == null && errors.Count(14,PlayerKVSchemaError.UnsupportedSchema) == 1,"ambiguous date rejected"); return Task.CompletedTask; }); await Run("M28-two-independent-four-slot-packs", () => { var raw = new Dictionary { ["ThanksGivingPackData"] = "1,2,3,4", ["ThanksGivingPackMigrationData"] = "5,6,7,8" }; foreach (bool migration in new[] { false,true }) { var state = PlayerKVSaveSchema.DecodeProgressPack(raw,migration,new()); int offset = migration ? 4 : 0; Check(state.IsPresent && state.EventId == 1 + offset && state.RedirectId == 2 + offset && state.TokenProgress == 3 + offset && state.ChainProgress == 4 + offset,"complete four slots"); var key = migration ? "ThanksGivingPackMigrationData" : "ThanksGivingPackData"; foreach (var bad in new[] { "", "null", "1,2,3", "1,2,3,4,5", "1,2,x,4", "1,2,3,2147483648", null }) { var invalid = new Dictionary(raw) { [key] = bad }; Check(PlayerKVSaveSchema.DecodeProgressPack(invalid,migration,new()) == null,"bad present pack rejected"); } var absent = PlayerKVSaveSchema.DecodeProgressPack(new Dictionary(),migration,new()); Check(!absent.IsPresent,"each missing is explicit"); var zero = PlayerKVSaveSchema.DecodeProgressPack(new Dictionary { [key] = "0,0,0,0" },migration,new()); Check(zero.IsPresent && zero.EventId == 0 && zero.RedirectId == 0,"zero present not missing"); } return Task.CompletedTask; }); } }