using System.Globalization; using game.PlayerDataStorage; using Newtonsoft.Json; static partial class Tests { static PlayerKVSchemaSnapshot FishSnapshot(Dictionary fish=null,Dictionary cards=null) => new("fish-v1",new[]{101},new[]{1,2},new Dictionary(),CultureInfo.InvariantCulture,SchemaSnapshot().NowUtc,17, fish:fish ?? new() { [101]=new(101,1,5,11,2,1,.5f),[102]=new(102,2,6,12,2,1,.5f) }, cards:cards ?? new() { [11]=new(11,1,new[]{10},new[]{.25f,.5f}),[12]=new(12,1,new[]{20},Array.Empty()) }); static Dictionary FishRaw(string record="2|99|1|7|True") => new() { ["Map_5_1"]=JsonConvert.SerializeObject(new Dictionary{[101]=record}) }; static PlayerKVFishState FishDecode(Dictionary raw,PlayerKVSchemaSnapshot snapshot=null,Dictionary modern=null) => PlayerKVSaveSchema.DecodeFish(raw,modern ?? new(),snapshot ?? FishSnapshot(),new()); static async Task RunFishSchemaTests() { await Run("M03-D5-raw-before-derived-and-config-snapshot",() => { var a=FishDecode(FishRaw()); Check(a.RawMaps[1][101].Level==2 && a.RawMaps[1][101].Weight==99 && a.RawMaps[1][101].Cards==1,"raw values retained"); Check(a.Maps[1][101].Level==1 && a.Maps[1][101].Weight==4.5f && a.Maps[1][101].Cards==10 && a.Cards[11]==10,"weight before level cap, cards raised"); Check(ReferenceEquals(a.Fish[101],a.Maps[1][101]) && a.Fish[101].Number==7 && a.Fish[101].New,"pure index coherence"); var r=FishRaw(); r["Map_1"]="{\"101\":{\"Level\":2,\"MaxG\":98,\"TotalCard\":1,\"TotalNum\":7,\"NewFish\":true}}"; var errors=new PlayerKVSchemaDiagnostics(); Check(PlayerKVSaveSchema.DecodeFish(r,new Dictionary(),FishSnapshot(),errors)==null && errors.Count(3,PlayerKVSchemaError.ConflictingRepresentations)==1,"different raw weights cannot normalize into equality"); r["Map_1"]=r["Map_1"].Replace("98","99"); Check(FishDecode(r)!=null,"matching formats accepted"); r["Map_5_1"]="{}"; Check(FishDecode(r).Maps[1].Count==0,"new empty replaces old map"); var required=new[]{10}; var weights=new[]{.5f}; var configs=new Dictionary{[101]=new(101,1,5,11,2,1,0)}; var cards=new Dictionary{[11]=new(11,1,required,weights)}; var snapshot=FishSnapshot(configs,cards); required[0]=99; weights[0]=99; configs.Clear(); cards.Clear(); a=FishDecode(FishRaw("1|99|1|7|false"),snapshot); Check(a.Fish[101].Weight==3 && a.Fish[101].Cards==10,"input configuration collections copied"); return Task.CompletedTask; }); await Run("M03-D5-weight-directions-quality-and-invalid-config",() => { Check(FishDecode(FishRaw("1|.125|1|7|false")).Fish[101].Weight==.125f,"P3-3 low weight not raised"); Check(FishDecode(FishRaw("0|-2|-3|-4|false")).Fish[101].Weight==-2,"finite negatives not banned"); Check(FishDecode(FishRaw("-1|99|-3|-4|false")).Fish[101].Level==-1,"nonpositive level preserved"); var high=new Dictionary{["Map_5_2"]="{\"102\":\"5|99|1|7|False\"}"}; var a=FishDecode(high); Check(a.Fish[102].Level==1 && a.Fish[102].Weight==99 && a.Cards[12]==20,"quality6 skips weight but caps level"); foreach(var record in new[]{"3|99|1|7|true","2147483647|99|1|7|true"}) Check(FishDecode(FishRaw(record))==null,"weight index checked before cap"); foreach(var fish in new[]{new PlayerKVFishConfig(101,2,5,11,2,1,0),new(101,1,5,999,2,1,0),new(101,1,5,11,float.MaxValue,2,0),new(101,1,5,11,float.NaN,1,0)}) { var e=new PlayerKVSchemaDiagnostics(); Check(PlayerKVSaveSchema.DecodeFish(FishRaw(),new Dictionary(),FishSnapshot(new(){[101]=fish}),e)==null && e.Count(3,PlayerKVSchemaError.UnsupportedSchema)==1,"invalid config pre-apply rejection"); } foreach(var card in new[]{new PlayerKVFishCardConfig(11,0,new[]{10},new[]{0f,0f}),new(11,1,Array.Empty(),new[]{0f,0f}),new(11,1,new[]{10},new[]{0f,float.PositiveInfinity})}) Check(FishDecode(FishRaw(),FishSnapshot(cards:new(){[11]=card}))==null,"card bounds and finite weights"); Check(FishDecode(FishRaw(),FishSnapshot(fish:new Dictionary()))==null,"unknown fish rejected"); return Task.CompletedTask; }); await Run("M03-D5-all-fields-accessories-and-luck",() => { var raw=FishRaw(); var intKeys=new[]{"RodData","RodLevelData","FishSwimToMap","FishSwimToMapCount","MapProgress","HonorLevelData"}; foreach(var k in intKeys) raw[k]="{\"-1\":7}"; raw["InitRodAccessoriesData"]="{\"GloveID\":1,\"RodToSkin\":{\"2\":3},\"GloveIds\":[4],\"SkinIds\":[5],\"GloveNewIds\":[6],\"SkinNewIds\":[7]}"; raw["LuckDataDic"]="{\"InitFishLuck\":1.25,\"InitBotLuck\":2.5,\"BotCount\":3,\"unknown\":4}"; var modern=new Dictionary{["LDD"]=raw["LuckDataDic"],["ld"]="1.25|2.5|3"}; var a=FishDecode(raw,modern:modern); Check(new[]{a.Rods,a.RodLevels,a.Swim,a.SwimCount,a.Progress,a.Honor}.All(m=>m[-1]==7),"six maps"); var acc=a.Accessories; Check(acc.Glove==1 && acc.RodSkin[2]==3 && acc.Gloves[0]==4 && acc.Skins[0]==5 && acc.NewGloves[0]==6 && acc.NewSkins[0]==7,"all six accessories fields"); Check(a.Luck.Count==3 && !a.Luck.ContainsKey("unknown"),"highest priority whole luck replaces"); foreach(var key in raw.Keys) { var bad=new Dictionary(raw){[key]="null"}; Check(FishDecode(bad,modern:modern)==null,"present bad rejected"); bad[key]=null; Check(FishDecode(bad,modern:modern)==null,"present null rejected"); } foreach(var invalid in new[]{"1|2|3.5","1|2|2147483648","1|2|-2147483904","NaN|2|3","1|2|3|4",null}) Check(FishDecode(new(),modern:new(){["ld"]=invalid})==null,"bad compact or lossy BotCount"); Check(FishDecode(new(),modern:new(){["ld"]="0|0|-2147483648"}).Luck["BotCount"]==int.MinValue,"exact lower bound allowed"); modern["ld"]="1.25|2.5|4"; var e=new PlayerKVSchemaDiagnostics(); Check(PlayerKVSaveSchema.DecodeFish(raw,modern,FishSnapshot(),e)==null && e.Count(3,PlayerKVSchemaError.ConflictingRepresentations)>0,"luck conflicts detected"); a=FishDecode(new()); Check(a.Maps.Count==0 && a.Fish.Count==0 && a.Cards.Count==0 && a.Accessories==null && a.Luck.Count==0 && a.Rods.Count==0,"missing explicit empty"); return Task.CompletedTask; }); await Run("M03-D5-map-layout-compatibility-and-order",() => { var a=FishDecode(FishRaw("2|99|ignored||compat|1|7|True|tail")); Check(a.RawMaps[1][101].Compatibility.SequenceEqual(new[]{"ignored","","compat","tail"}),"all four ignored slots retained"); foreach(var record in new[]{"2|99|1|7","2|99|1|7|true|extra","2|NaN|1|7|true","2|99|1.5|7|true","2|99|1|7|1"}) Check(FishDecode(FishRaw(record))==null,"malformed consumed slots"); foreach(var key in new[]{"Map_5_01","Map_0","Map_5_0","Map_3","Map_2147483648"}) Check(FishDecode(new(){[key]="{}"})==null,"invalid map suffix"); Check(FishDecode(new(){["Map_other"]="not-json"})!=null,"nonmatching keys unconsumed"); var fish=new Dictionary{[101]=new(101,1,6,11,1,1,0),[102]=new(102,2,6,11,1,1,0)}; var raw=new Dictionary{["Map_5_1"]="{\"101\":\"0|1|1|1|true\"}",["Map_5_2"]="{\"102\":\"0|1|2|1|true\"}"}; foreach(var r in new[]{raw,raw.Reverse().ToDictionary(p=>p.Key,p=>p.Value)}) { var e=new PlayerKVSchemaDiagnostics(); Check(PlayerKVSaveSchema.DecodeFish(r,new Dictionary(),FishSnapshot(fish),e)==null && e.Count(3,PlayerKVSchemaError.UnsupportedSchema)==1,"card conflict order independent"); } raw["Map_5_2"]="{\"102\":\"0|1|1|1|true\"}"; Check(FishDecode(raw,FishSnapshot(fish)).Cards[11]==1,"matching shared card quantity allowed"); Check(FishDecode(new(){["Map_5_1"]="{\"101\":\"0|1|1|1|true\",\"0101\":\"0|1|1|1|true\"}"})==null,"normalized duplicate fish ids"); return Task.CompletedTask; }); } }