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

154 lines
12 KiB
C#

using System.Globalization;
using game.PlayerDataStorage;
using Newtonsoft.Json;
static partial class Tests
{
static readonly string[] CompleteCore = { "3", "18446744073709551615", "4", "5", "6", "7", "101",
"2026-09-23T00:00:00.0000000Z", "-1", "8", "18446744073709551614", "9", "1", "0", "10", "11", "12", "13", "14" };
static PlayerKVSchemaSnapshot SchemaSnapshot(string culture = "en-US") => new("synthetic-v1", new[] { 101 }, new[] { 1, 2 },
new Dictionary<string, int> { ["None"] = 0, ["Fish"] = 1, ["Another"] = 2 }, CultureInfo.GetCultureInfo(culture),
new DateTime(2026, 9, 23, 0, 0, 0, DateTimeKind.Utc), 17);
static Dictionary<string, string> ModernRaw(string[] slots = null) => new()
{ ["NewPlayerDataDic"] = JsonConvert.SerializeObject(new Dictionary<string, string> { ["newData"] = string.Join(",", slots ?? CompleteCore) }) };
static PlayerKVCoreState Core(Dictionary<string, string> raw, out PlayerKVSchemaDiagnostics errors, string culture = "en-US")
{
errors = new(); var modern = PlayerKVSaveSchema.DecodeModern(raw, errors);
return PlayerKVSaveSchema.DecodeCore(raw, modern, SchemaSnapshot(culture), errors);
}
static PlayerKVAchievementState Achievement(Dictionary<string, string> raw, Dictionary<string, string> modern,
out PlayerKVSchemaDiagnostics errors)
{ errors = new(); return PlayerKVSaveSchema.DecodeAchievements(raw, modern, SchemaSnapshot(), errors); }
static async Task RunSchemaTests()
{
await Run("schema-primitive-json-and-finite-float", async () =>
{
Check(PlayerKVSaveSchema.Single("1.25") == 1.25f, "invariant finite number accepted");
foreach (var value in new[] { "NaN", "Infinity", "-Infinity", "1e999", "1,000" })
await Throws<PlayerKVSchemaException>(() => { PlayerKVSaveSchema.Single(value); return Task.CompletedTask; });
var token = PlayerKVSaveSchema.Json("{\"x\":[true,false,null,-0,1.25e2,\"a\\u0041\\n\"]}");
Check(token["x"].Count() == 6, "RFC literals numbers and escapes accepted by syntax layer");
Check(PlayerKVSaveSchema.StringMap("{\"x\":\"a\\u0041\"}")["x"] == "aA", "string escapes materialized");
foreach (var value in new[] { "[1,]", "01", "1.", ".1", "+1", "1e", "{\"x\":1e999}" })
await Throws<PlayerKVSchemaException>(() => { PlayerKVSaveSchema.Json(value); return Task.CompletedTask; });
});
await Run("M01-M02-modern-19-slots-currency-aliases", () =>
{
var state = Core(ModernRaw(), out var e);
Check(state != null && e.TotalErrors == 0, "complete modern accepted");
Check(state.Unsigned(1) == ulong.MaxValue && state.Unsigned(10) == ulong.MaxValue - 1, "unsigned values not rounded");
Check(state.Integer(4) == 6 && state.Integer(11) == 9 && state.Integer(8) == -1, "pearl, diamond and signed bit mask distinct");
Check(state.Integer(16) == 12 && state.Integer(17) == 13 && state.Integer(18) == 14, "all small game counts captured");
Check(state.EnergyTime.Kind == DateTimeKind.Utc, "roundtrip UTC retained"); return Task.CompletedTask;
});
await Run("M01-M02-legacy-complete-partial-and-conflict", () =>
{
string[] keys = { "Lv", "Gold", "vipAllScore", "Energy", "Diamond", "Magnification", "EquipRodID", "LastEnergyRecoveryTime",
"RewardNewMap", "EnergyRM", "GoldRM", "DiamondTrue", "CurrentMapId", "LastMapId", "FishingFrequency", "AllFishingFrequency",
"HeistGameCount", "BombGameCount", "FishHuntGameCount" };
var raw = keys.Select((k, i) => (k, v: CompleteCore[i])).ToDictionary(x => x.k, x => x.v);
Check(Core(raw, out var e) != null && e.TotalErrors == 0, "complete legacy accepted");
raw.Remove("Gold"); Check(Core(raw, out e) == null && e.Count(1, PlayerKVSchemaError.MissingCore) == 1, "cannot compose partial core");
raw["NewPlayerDataDic"] = ModernRaw()["NewPlayerDataDic"];
Check(Core(raw, out e) != null, "consistent overlapping legacy accepted");
raw["Diamond"] = "7"; Check(Core(raw, out e) == null && e.Count(1, PlayerKVSchemaError.ConflictingRepresentations) == 1, "partial legacy conflict rejected");
return Task.CompletedTask;
});
await Run("M01-M02-every-slot-malformed-and-boundaries", () =>
{
for (int i = 0; i < 19; i++)
{
var slots = (string[])CompleteCore.Clone(); slots[i] = "invalid";
Check(Core(ModernRaw(slots), out var e) == null && e.TotalErrors > 0, "invalid consumed slot rejected");
}
foreach (int count in new[] { 0, 18, 20 })
Check(Core(ModernRaw(Enumerable.Repeat("1", count).ToArray()), out _) == null, "exact length required");
var negative = (string[])CompleteCore.Clone(); negative[3] = "-1";
Check(Core(ModernRaw(negative), out _) == null, "energy cannot be clamped");
negative[3] = "0"; negative[0] = "0";
Check(Core(ModernRaw(negative), out _) == null, "level at least one");
var unknown = (string[])CompleteCore.Clone(); unknown[6] = "999";
Check(Core(ModernRaw(unknown), out var errors) == null && errors.Count(1, PlayerKVSchemaError.UnsupportedSchema) == 1, "unknown rod no fallback");
unknown = (string[])CompleteCore.Clone(); unknown[12] = "999";
Check(Core(ModernRaw(unknown), out _) == null, "map references captured table"); return Task.CompletedTask;
});
await Run("M01-modern-outer-strict-json-and-alias-conflicts", () =>
{
foreach (var text in new[] { "null", "[]", "{\"a\":null}", "{\"a\":1}", "{\"a\":\"x\",\"a\":\"y\"}", "{}{}", "/*comment*/{}", "{\"a\":NaN}",
"{'a':'b'}", "{a:\"b\"}", "{\"a\":\"b\",}", "{\"a\":\"\\x41\"}", "{\"a\":\"unterminated}" })
{
Check(Core(new() { ["NewPlayerDataDic"] = text }, out var e) == null && e.TotalErrors > 0, "outer format rejected");
}
var modern = new Dictionary<string, string> { ["newData"] = string.Join(",", CompleteCore), ["pearl"] = "77" };
Check(Core(new() { ["NewPlayerDataDic"] = JsonConvert.SerializeObject(modern) }, out var err) == null &&
err.Count(1, PlayerKVSchemaError.ConflictingRepresentations) == 1, "nested alias checked"); return Task.CompletedTask;
});
await Run("M01-date-culture-ambiguity-and-snapshot-copy", () =>
{
var slots = (string[])CompleteCore.Clone(); slots[7] = "01/02/2026";
Check(Core(ModernRaw(slots), out var e, "en-GB") == null && e.Count(1, PlayerKVSchemaError.UnsupportedSchema) == 1, "ambiguous day/month rejected");
var rods = new List<int> { 101 }; var conditions = new Dictionary<string, int> { ["Fish"] = 1 };
var culture = new CultureInfo("en-US"); var snapshot = new PlayerKVSchemaSnapshot("v1", rods, new[] { 1 }, conditions, culture, DateTime.MinValue, 17);
rods.Clear(); conditions.Clear(); culture.DateTimeFormat.ShortDatePattern = "dd/MM/yyyy";
Check(snapshot.HasRod(101) && snapshot.Condition("Fish") == 1 && snapshot.Culture.IsReadOnly, "snapshot owns copies");
Check(snapshot.InitWheelTicket == 17 && snapshot.Version == "v1", "M05 default comes from captured snapshot"); return Task.CompletedTask;
});
await Run("M08-three-representations-and-priority", () =>
{
var raw = new Dictionary<string, string> { ["AchievementData"] = "{\"Fish\":{\"Id\":2,\"Target\":18446744073709551615,\"day\":3}}" };
var modern = new Dictionary<string, string>();
var state = Achievement(raw, modern, out var e); Check(state.Values[1].Target == ulong.MaxValue && e.TotalErrors == 0, "object exact ulong");
modern["AD"] = "{\"Fish\":\"2|18446744073709551615|3\"}";
modern["ad"] = "1:2|18446744073709551615|3";
state = Achievement(raw, modern, out e); Check(state.Values.Count == 1 && e.TotalErrors == 0, "all agree");
modern["ad"] = ""; state = Achievement(raw, modern, out e);
Check(state.Values.Count == 0 && e.TotalErrors == 0, "whole newer empty set replaces old, not merged");
Check(Achievement(new(), new(), out e).Values.Count == 0, "missing explicit empty state"); return Task.CompletedTask;
});
await Run("M08-AD-strict-values-no-fallback", () =>
{
foreach (var text in new[] { "null", "{\"Fish\":null}", "{\"Fish\":{\"Id\":1,\"Target\":2,\"day\":3}}", "{\"Fish\":\"1|2\"}",
"{\"Fish\":\"1|2|3|4\"}", "{\"Fish\":\"1|18446744073709551616|3\"}", "{\"Fish\":\"1|-1|3\"}",
"{\"Fish\":\"1|2|3\",\"1\":\"1|2|3\"}", "{\"Fish\":\"1|2|3\",\"Fish\":\"1|2|3\"}", "{\"unknown\":\"1|2|3\"}" })
{
var state = Achievement(new(), new() { ["AD"] = text, ["ad"] = "1:1|2|3" }, out var e);
Check(state == null && e.Count(8, PlayerKVSchemaError.Malformed) > 0, "bad AD cannot be hidden by new representation");
}
Check(Achievement(new(), new() { ["AD"] = "{}" }, out var empty).Values.Count == 0 && empty.TotalErrors == 0, "empty object valid");
return Task.CompletedTask;
});
await Run("M08-object-token-types-compact-bad-records-and-conflict", () =>
{
foreach (var text in new[] { "{\"1\":{\"Id\":\"1\",\"Target\":2,\"day\":3}}", "{\"1\":{\"Id\":1.0,\"Target\":2,\"day\":3}}", "{\"1\":{\"Id\":1,\"Target\":2}}" })
Check(Achievement(new() { ["AchievementData"] = text }, new(), out _) == null, "no loose coercion/default");
foreach (var text in new[] { "1:1|2|3;", ";1:1|2|3", "1:1|2|3;Fish:1|2|3", "1:1|2", "unknown:1|2|3" })
Check(Achievement(new(), new() { ["ad"] = text }, out _) == null, "malformed compact rejected");
Check(Achievement(new(), new() { ["AD"] = "{\"1\":\"1|2|3\"}", ["ad"] = "1:1|3|3" }, out var e) == null &&
e.Count(8, PlayerKVSchemaError.ConflictingRepresentations) == 1, "conflicting targets rejected"); return Task.CompletedTask;
});
await Run("M23-full-unsigned-domain-missing-and-invalid", () =>
{
foreach (ulong value in new[] { 0UL, 255UL, 1UL << 31, 1UL << 32, ulong.MaxValue })
{
var e = new PlayerKVSchemaDiagnostics(); var state = PlayerKVSaveSchema.DecodeFootPrint(new Dictionary<string, string> { ["FootPrint"] = value.ToString(CultureInfo.InvariantCulture) }, e);
Check(state.Bits == value && e.TotalErrors == 0, "full bits retained in decoded value");
}
Check(PlayerKVSaveSchema.DecodeFootPrint(new Dictionary<string, string>(), new()).Bits == 0, "missing empty state");
foreach (var text in new[] { null, "", "-1", "18446744073709551616", "1.0", "1e3" })
{
var e = new PlayerKVSchemaDiagnostics(); Check(PlayerKVSaveSchema.DecodeFootPrint(new Dictionary<string, string> { ["FootPrint"] = text }, e) == null && e.TotalErrors == 1, "bad bits rejected");
}
return Task.CompletedTask;
});
await Run("schema-diagnostics-32-stable-aggregate-all", () =>
{
var e = new PlayerKVSchemaDiagnostics();
for (int i = 50; i >= 0; i--) e.Add(i % 2 == 0 ? 8 : 1, i, PlayerKVSchemaError.Malformed);
Check(e.TotalErrors == 51 && e.Details.Count == 32 && e.OmittedDetails == 19, "all errors counted with bounded storage");
Check(e.Count(8, PlayerKVSchemaError.Malformed) == 26 && e.Count(1, PlayerKVSchemaError.Malformed) == 25, "module code aggregate complete");
Check(e.Details.First().Module == 1 && e.Details.First().Field == 1 && e.Details.Last().Module == 8 && e.Details.Last().Field == 12, "stable module/field ordering");
return Task.CompletedTask;
});
}
}