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

221 lines
16 KiB
C#

using System.Globalization;
using game.PlayerDataStorage;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
static partial class Tests
{
static PlayerKVSchemaSnapshot PackSnapshot(IDictionary<int,int> packs = null) => new("D3", new[] { 101 }, new[] { 1 },
new Dictionary<string,int>(), CultureInfo.InvariantCulture, new DateTime(2026,9,23), 0,
packs ?? new Dictionary<int,int> { [4055001] = 2, [300] = 3 });
static async Task RunD3Tests()
{
await Run("M21-strict-complete-value-state", () =>
{
var obj = JObject.FromObject(new { currentEventID = 11, purchaseCount = 11, VIPLvlWhenEventActivate = 11, savingProgress = 11, redirectID = 0, doNeedTrigger = true, isTriggered = true, inflation = 1.5f });
var raw = new Dictionary<string,string> { ["PiggyBankPackData"] = obj.ToString(Formatting.None) };
var e = new PlayerKVSchemaDiagnostics();
var state = PlayerKVSaveSchema.DecodePiggyBank(raw, PackSnapshot(), e);
Check(state != null && state.IsPresent && e.TotalErrors == 0, "present complete record");
Check(state.currentEventID == 11, "typed currentEventID retained");
Check(state.purchaseCount == 11, "typed purchaseCount retained");
Check(state.VIPLvlWhenEventActivate == 11, "typed VIPLvlWhenEventActivate retained");
Check(state.savingProgress == 11, "typed savingProgress retained");
Check(state.redirectID == 4055001, "typed redirectID retained");
Check(state.doNeedTrigger == true, "typed doNeedTrigger retained");
Check(state.isTriggered == true, "typed isTriggered retained");
Check(state.inflation == 1.5f, "typed inflation retained");
var missing = PlayerKVSaveSchema.DecodePiggyBank(new Dictionary<string,string>(), PackSnapshot(), new PlayerKVSchemaDiagnostics());
Check(!missing.IsPresent, "missing distinguished");
Check(missing.currentEventID == 0, "missing currentEventID empty");
Check(missing.purchaseCount == 0, "missing purchaseCount empty");
Check(missing.VIPLvlWhenEventActivate == 0, "missing VIPLvlWhenEventActivate empty");
Check(missing.savingProgress == 0, "missing savingProgress empty");
Check(missing.redirectID == 0, "missing redirectID empty");
Check(missing.doNeedTrigger == false, "missing doNeedTrigger empty");
Check(missing.isTriggered == false, "missing isTriggered empty");
Check(missing.inflation == 0, "missing inflation empty");
foreach (var property in obj.Properties().ToArray())
{
var bad = (JObject)obj.DeepClone(); bad.Remove(property.Name);
raw["PiggyBankPackData"] = bad.ToString(Formatting.None);
Check(PlayerKVSaveSchema.DecodePiggyBank(raw, PackSnapshot(), new PlayerKVSchemaDiagnostics()) == null, "every field required");
foreach (var invalid in new JToken[] { JValue.CreateNull(), new JValue("coercion"), new JObject() })
{
bad = (JObject)obj.DeepClone(); bad[property.Name] = invalid;
raw["PiggyBankPackData"] = bad.ToString(Formatting.None);
Check(PlayerKVSaveSchema.DecodePiggyBank(raw, PackSnapshot(), new PlayerKVSchemaDiagnostics()) == null, "no coercion or null");
}
}
foreach (var invalid in new[] { "null", "[]", "{}", "{", "{\\\"duplicate\\\":1,\\\"duplicate\\\":2}" })
{
raw["PiggyBankPackData"] = invalid;
e = new PlayerKVSchemaDiagnostics();
Check(PlayerKVSaveSchema.DecodePiggyBank(raw, PackSnapshot(), e) == null && e.Count(21, PlayerKVSchemaError.Malformed) == 1, "invalid optional is an error");
}
return Task.CompletedTask;
});
await Run("M22-strict-complete-value-state", () =>
{
var obj = JObject.FromObject(new { currentEventID = 11, redirectID = 300, VIPLvlWhenEventActivate = 11, purchaseCount = 11, savingProgress = 11, discountLvl = 11, visualProgress = 11, visualDiscountLvl = 11, rodID = 11, lastPackIDBought = 11, packID = 11, doNeedTrigger = true, isTriggered = true, inflation = 1.5f, selections = new[] { 7, 9 } });
var raw = new Dictionary<string,string> { ["GeneralEventPackData"] = obj.ToString(Formatting.None) };
var e = new PlayerKVSchemaDiagnostics();
var state = PlayerKVSaveSchema.DecodeBargain(raw, PackSnapshot(), e);
Check(state != null && state.IsPresent && e.TotalErrors == 0, "present complete record");
Check(state.currentEventID == 11, "typed currentEventID retained");
Check(state.redirectID == 300, "typed redirectID retained");
Check(state.VIPLvlWhenEventActivate == 11, "typed VIPLvlWhenEventActivate retained");
Check(state.purchaseCount == 11, "typed purchaseCount retained");
Check(state.savingProgress == 11, "typed savingProgress retained");
Check(state.discountLvl == 11, "typed discountLvl retained");
Check(state.visualProgress == 11, "typed visualProgress retained");
Check(state.visualDiscountLvl == 11, "typed visualDiscountLvl retained");
Check(state.rodID == 11, "typed rodID retained");
Check(state.lastPackIDBought == 11, "typed lastPackIDBought retained");
Check(state.packID == 11, "typed packID retained");
Check(state.doNeedTrigger == true, "typed doNeedTrigger retained");
Check(state.isTriggered == true, "typed isTriggered retained");
Check(state.inflation == 1.5f, "typed inflation retained");
Check(state.selections.SequenceEqual(new[] { 7, 9 }), "typed selections retained");
var missing = PlayerKVSaveSchema.DecodeBargain(new Dictionary<string,string>(), PackSnapshot(), new PlayerKVSchemaDiagnostics());
Check(!missing.IsPresent, "missing distinguished");
Check(missing.currentEventID == 0, "missing currentEventID empty");
Check(missing.redirectID == 0, "missing redirectID empty");
Check(missing.VIPLvlWhenEventActivate == 0, "missing VIPLvlWhenEventActivate empty");
Check(missing.purchaseCount == 0, "missing purchaseCount empty");
Check(missing.savingProgress == 0, "missing savingProgress empty");
Check(missing.discountLvl == 0, "missing discountLvl empty");
Check(missing.visualProgress == 0, "missing visualProgress empty");
Check(missing.visualDiscountLvl == 0, "missing visualDiscountLvl empty");
Check(missing.rodID == 0, "missing rodID empty");
Check(missing.lastPackIDBought == 0, "missing lastPackIDBought empty");
Check(missing.packID == 0, "missing packID empty");
Check(missing.doNeedTrigger == false, "missing doNeedTrigger empty");
Check(missing.isTriggered == false, "missing isTriggered empty");
Check(missing.inflation == 0, "missing inflation empty");
Check(missing.selections.Count == 0, "missing selections empty");
foreach (var property in obj.Properties().ToArray())
{
var bad = (JObject)obj.DeepClone(); bad.Remove(property.Name);
raw["GeneralEventPackData"] = bad.ToString(Formatting.None);
Check(PlayerKVSaveSchema.DecodeBargain(raw, PackSnapshot(), new PlayerKVSchemaDiagnostics()) == null, "every field required");
foreach (var invalid in new JToken[] { JValue.CreateNull(), new JValue("coercion"), new JObject() })
{
if (property.Name == "selections" && invalid.Type == JTokenType.Null) continue;
bad = (JObject)obj.DeepClone(); bad[property.Name] = invalid;
raw["GeneralEventPackData"] = bad.ToString(Formatting.None);
Check(PlayerKVSaveSchema.DecodeBargain(raw, PackSnapshot(), new PlayerKVSchemaDiagnostics()) == null, "no coercion or null");
}
}
foreach (var invalid in new[] { "null", "[]", "{}", "{", "{\\\"duplicate\\\":1,\\\"duplicate\\\":2}" })
{
raw["GeneralEventPackData"] = invalid;
e = new PlayerKVSchemaDiagnostics();
Check(PlayerKVSaveSchema.DecodeBargain(raw, PackSnapshot(), e) == null && e.Count(22, PlayerKVSchemaError.Malformed) == 1, "invalid optional is an error");
}
return Task.CompletedTask;
});
await Run("M22-null-empty-list-preserves-fields-and-strictness", () =>
{
var obj = JObject.FromObject(new { currentEventID = 1, redirectID = 300, VIPLvlWhenEventActivate = 2, purchaseCount = 3,
savingProgress = 4, discountLvl = 5, visualProgress = 6, visualDiscountLvl = 7, rodID = 8, lastPackIDBought = 9,
packID = 10, doNeedTrigger = true, isTriggered = false, inflation = 1.25f });
foreach (var selection in new JToken[] { JValue.CreateNull(), new JArray(), new JArray(7, 9) })
{
obj["selections"] = selection;
var text = obj.ToString(Formatting.None);
var raw = new Dictionary<string,string> { ["GeneralEventPackData"] = text };
var errors = new PlayerKVSchemaDiagnostics();
var state = PlayerKVSaveSchema.DecodeBargain(raw, PackSnapshot(), errors);
Check(state != null && state.IsPresent && errors.TotalErrors == 0, "all three list representations accepted");
Check(state.currentEventID == 1 && state.redirectID == 300 && state.VIPLvlWhenEventActivate == 2 && state.purchaseCount == 3 &&
state.savingProgress == 4 && state.discountLvl == 5 && state.visualProgress == 6 && state.visualDiscountLvl == 7 &&
state.rodID == 8 && state.lastPackIDBought == 9 && state.packID == 10 && state.doNeedTrigger && !state.isTriggered &&
state.inflation == 1.25f, "all fourteen other fields retained");
Check(selection.Type == JTokenType.Null ? state.selections == null : state.selections.SequenceEqual(selection.Values<int>()), "null is not empty list");
Check(raw["GeneralEventPackData"] == text, "raw string unchanged");
var wrongType = new PlayerKVSchemaDiagnostics();
Check(PlayerKVSaveSchema.DecodeBargain(raw, PackSnapshot(new Dictionary<int,int> { [300] = 2 }), wrongType) == null &&
wrongType.Count(22, PlayerKVSchemaError.UnsupportedSchema) == 1, "null does not bypass pack type check");
}
foreach (var selection in new JToken[] { new JArray(JValue.CreateNull()), new JArray(1.5), new JArray("7"),
new JArray(2147483648L), new JValue(false), new JValue("null"), new JObject() })
{
obj["selections"] = selection;
var errors = new PlayerKVSchemaDiagnostics();
Check(PlayerKVSaveSchema.DecodeBargain(new Dictionary<string,string> { ["GeneralEventPackData"] = obj.ToString(Formatting.None) },
PackSnapshot(), errors) == null && errors.Count(22, PlayerKVSchemaError.Malformed) == 1, "invalid list remains malformed");
}
var nullable = new PlayerKVBargainState(true,0,300,0,0,0,0,0,0,0,0,0,false,false,0,null);
Check(nullable.selections == null && nullable.IsPresent, "DTO null remains null");
var source = new List<int> { 7, 9 };
var copied = new PlayerKVBargainState(true,0,300,0,0,0,0,0,0,0,0,0,false,false,0,source);
source[0] = 99;
Check(copied.selections.SequenceEqual(new[] { 7,9 }) && copied.selections is not List<int>, "non-null DTO remains an independent read-only copy");
Check(!PlayerKVBargainState.Missing().IsPresent && PlayerKVBargainState.Missing().selections.Count == 0, "missing pack keeps original empty list contract");
return Task.CompletedTask;
});
await Run("M24-strict-complete-value-state", () =>
{
var obj = JObject.FromObject(new { EventId = 11, CycleId = 11, RedirectId = 11, AmmoCount = 11, CurrentStageIdx = 11, ChainProgress = 11, RewardFlag = long.MaxValue, TargetFlag = long.MinValue, HasBeenOpened = true, Inflation = 1.5f });
var raw = new Dictionary<string,string> { ["EventShootingRangeData"] = obj.ToString(Formatting.None) };
var e = new PlayerKVSchemaDiagnostics();
var state = PlayerKVSaveSchema.DecodeShootingRange(raw, PackSnapshot(), e);
Check(state != null && state.IsPresent && e.TotalErrors == 0, "present complete record");
Check(state.EventId == 11, "typed EventId retained");
Check(state.CycleId == 11, "typed CycleId retained");
Check(state.RedirectId == 11, "typed RedirectId retained");
Check(state.AmmoCount == 11, "typed AmmoCount retained");
Check(state.CurrentStageIdx == 11, "typed CurrentStageIdx retained");
Check(state.ChainProgress == 11, "typed ChainProgress retained");
Check(state.RewardFlag == long.MaxValue, "typed RewardFlag retained");
Check(state.TargetFlag == long.MinValue, "typed TargetFlag retained");
Check(state.HasBeenOpened == true, "typed HasBeenOpened retained");
Check(state.Inflation == 1.5f, "typed Inflation retained");
var missing = PlayerKVSaveSchema.DecodeShootingRange(new Dictionary<string,string>(), PackSnapshot(), new PlayerKVSchemaDiagnostics());
Check(!missing.IsPresent, "missing distinguished");
Check(missing.EventId == 0, "missing EventId empty");
Check(missing.CycleId == 0, "missing CycleId empty");
Check(missing.RedirectId == 0, "missing RedirectId empty");
Check(missing.AmmoCount == 0, "missing AmmoCount empty");
Check(missing.CurrentStageIdx == 0, "missing CurrentStageIdx empty");
Check(missing.ChainProgress == 0, "missing ChainProgress empty");
Check(missing.RewardFlag == 0, "missing RewardFlag empty");
Check(missing.TargetFlag == 0, "missing TargetFlag empty");
Check(missing.HasBeenOpened == false, "missing HasBeenOpened empty");
Check(missing.Inflation == 0, "missing Inflation empty");
foreach (var property in obj.Properties().ToArray())
{
var bad = (JObject)obj.DeepClone(); bad.Remove(property.Name);
raw["EventShootingRangeData"] = bad.ToString(Formatting.None);
Check(PlayerKVSaveSchema.DecodeShootingRange(raw, PackSnapshot(), new PlayerKVSchemaDiagnostics()) == null, "every field required");
foreach (var invalid in new JToken[] { JValue.CreateNull(), new JValue("coercion"), new JObject() })
{
bad = (JObject)obj.DeepClone(); bad[property.Name] = invalid;
raw["EventShootingRangeData"] = bad.ToString(Formatting.None);
Check(PlayerKVSaveSchema.DecodeShootingRange(raw, PackSnapshot(), new PlayerKVSchemaDiagnostics()) == null, "no coercion or null");
}
}
foreach (var invalid in new[] { "null", "[]", "{}", "{", "{\\\"duplicate\\\":1,\\\"duplicate\\\":2}" })
{
raw["EventShootingRangeData"] = invalid;
e = new PlayerKVSchemaDiagnostics();
Check(PlayerKVSaveSchema.DecodeShootingRange(raw, PackSnapshot(), e) == null && e.Count(24, PlayerKVSchemaError.Malformed) == 1, "invalid optional is an error");
}
return Task.CompletedTask;
});
await Run("D3-fixed-config-copy-and-signed-width", async () =>
{
var source = new Dictionary<int,int> { [4055001] = 2, [300] = 3 };
var snapshot = PackSnapshot(source); source.Clear();
Check(snapshot.HasPack(4055001) && snapshot.HasPackType(300,3), "configuration captured copy");
foreach (var text in new[] { "9223372036854775808", "-9223372036854775809", "1.0", "\"1\"" })
await Throws<PlayerKVSchemaException>(() => { PlayerKVSaveSchema.Signed64(PlayerKVSaveSchema.Json(text)); return Task.CompletedTask; });
var items = new List<int> { 7 };
var state = new PlayerKVBargainState(true, 0,300,0,0,0,0,0,0,0,0,0,false,false,0,items);
items.Clear(); Check(state.selections.Count == 1, "DTO owns collection");
Check(state.selections is not List<int>, "collection is not mutable list");
});
}
}