221 lines
16 KiB
C#
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");
|
|
});
|
|
}
|
|
}
|