using game.PlayerDataStorage; using Newtonsoft.Json; using Newtonsoft.Json.Linq; static partial class Tests { static async Task RunCollectionSchemaTests() { await Run("M09-two-formats-conflicts-empty-and-strict", () => { var raw = new Dictionary { ["FishingBoxData"] = "{\"FishingBoxOpenNum\":{\"1\":2,\"3\":4}}" }; var modern = new Dictionary { ["bd"] = "1,2" }; var e = new PlayerKVSchemaDiagnostics(); var state = PlayerKVSaveSchema.DecodeFishingBox(raw,modern,e); Check(state != null && state.Values.Count == 1 && state.Values[1] == 2, "new replaces whole map"); modern["bd"] = ""; Check(PlayerKVSaveSchema.DecodeFishingBox(raw,modern,new()).Values.Count == 0, "present empty replaces old"); modern["bd"] = "1,3"; e = new(); Check(PlayerKVSaveSchema.DecodeFishingBox(raw,modern,e) == null && e.Count(9,PlayerKVSchemaError.ConflictingRepresentations) == 1, "conflict rejected"); foreach (var bad in new[] { "1,2;", "1,2,3", "1,2;01,3", "1,2.0", "2147483648,0", "null" }) { modern["bd"] = bad; Check(PlayerKVSaveSchema.DecodeFishingBox(raw,modern,new()) == null, "bad compact never falls back"); } modern["bd"] = "1,2"; foreach (var bad in new[] { "null", "{}", "{\"FishingBoxOpenNum\":null}", "{\"FishingBoxOpenNum\":{\"1\":2,\"01\":2}}", "{\"FishingBoxOpenNum\":{\"1\":\"2\"}}", "{\"FishingBoxOpenNum\":{\"1\":2.0}}" }) { raw["FishingBoxData"] = bad; Check(PlayerKVSaveSchema.DecodeFishingBox(raw,modern,new()) == null, "bad old never hidden"); } Check(PlayerKVSaveSchema.DecodeFishingBox(new Dictionary(),new Dictionary(),new()).Values.Count == 0,"missing empty"); return Task.CompletedTask; }); await Run("M10-whole-guide-replacement-and-conflict", () => { var raw = new Dictionary { ["GuideSaveData"] = "{\"A\":{\"guideSubIndex\":2,\"isFinish\":true,\"groupName\":\"A\"},\"B\":{\"guideSubIndex\":3,\"isFinish\":false,\"groupName\":\"B\"}}", ["GuideSaveDataNew"] = "A|2|1" }; var state = PlayerKVSaveSchema.DecodeGuides(raw,new()); Check(state.Values.Count == 1 && state.Values["A"].Index == 2 && state.Values["A"].Finished && state.Values["A"].Group == "A","new whole map selected"); foreach (var conflict in new[] { "A|3|1", "A|2|0" }) { raw["GuideSaveDataNew"] = conflict; var e = new PlayerKVSchemaDiagnostics(); Check(PlayerKVSaveSchema.DecodeGuides(raw,e) == null && e.Count(10,PlayerKVSchemaError.ConflictingRepresentations) == 1,"common field conflict"); } raw["GuideSaveDataNew"] = ""; Check(PlayerKVSaveSchema.DecodeGuides(raw,new()).Values.Count == 0,"new empty clears old groups"); foreach (var bad in new[] { "A|2|true", "A|2|2", "A|x|0", "A|2", "A|2|0;", "A|2|0;A|2|0" }) { raw["GuideSaveDataNew"] = bad; Check(PlayerKVSaveSchema.DecodeGuides(raw,new()) == null,"invalid compact rejected"); } raw["GuideSaveDataNew"] = "A|2|1"; foreach (var bad in new[] { "null", "{\"A\":null}", "{\"A\":{\"guideSubIndex\":2,\"isFinish\":1,\"groupName\":\"A\"}}", "{\"A\":{\"guideSubIndex\":2,\"isFinish\":true,\"groupName\":null}}" }) { raw["GuideSaveData"] = bad; Check(PlayerKVSaveSchema.DecodeGuides(raw,new()) == null,"invalid old not ignored"); } raw["GuideSaveData"] = "{\"A\":{\"guideSubIndex\":2,\"isFinish\":true,\"groupName\":\"B\"}}"; Check(PlayerKVSaveSchema.DecodeGuides(raw,new()) == null,"groupName participates in comparison"); Check(PlayerKVSaveSchema.DecodeGuides(new Dictionary(),new()).Values.Count == 0,"missing empty"); return Task.CompletedTask; }); await Run("M12-nested-signin-all-fields-required", () => { var valid = JObject.Parse("{\"index\":5,\"dailySignEvent\":{\"ID\":7,\"index\":2,\"lastID\":-1,\"time\":\"2026-09-23\"}}"); var raw = new Dictionary { ["MonthlySignEvent"] = valid.ToString(Formatting.None) }; var state = PlayerKVSaveSchema.DecodeMonthlySign(raw,new()); Check(state.IsPresent && state.Index == 5 && state.DailyId == 7 && state.DailyIndex == 2 && state.LastId == -1 && state.Time == "2026-09-23","all fields decoded"); foreach (var path in new[] { "index", "dailySignEvent", "dailySignEvent.ID", "dailySignEvent.index", "dailySignEvent.lastID", "dailySignEvent.time" }) foreach (var bad in new JToken[] { JValue.CreateNull(), new JArray() }) { var obj = (JObject)valid.DeepClone(); obj.SelectToken(path).Replace(bad); raw["MonthlySignEvent"] = obj.ToString(Formatting.None); Check(PlayerKVSaveSchema.DecodeMonthlySign(raw,new()) == null,"nested field null/shape rejected"); } foreach (var bad in new[] { "null", "{}", "{\"index\":1}", "{\"index\":1,\"dailySignEvent\":{}}" }) { raw["MonthlySignEvent"] = bad; Check(PlayerKVSaveSchema.DecodeMonthlySign(raw,new()) == null,"present malformed fails"); } Check(!PlayerKVSaveSchema.DecodeMonthlySign(new Dictionary(),new()).IsPresent,"missing explicit"); return Task.CompletedTask; }); } }