test(upserver): 归档存档迁移与玩法闭环回归工具
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
141
CodexOutput/upserver/KVGameLoadTests/ActivitySchemaTests.cs
Normal file
141
CodexOutput/upserver/KVGameLoadTests/ActivitySchemaTests.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunActivitySchemaTests()
|
||||
{
|
||||
await Run("M15-activity-all-saved-fields-strict",() =>
|
||||
{
|
||||
var obj=JObject.FromObject(new { FirstOpen=true,EventId=11,RankTierLevel=11,Finished=11,RedirectID=11,Score=11,ProcessedScore=11,step=11,uiStep=11,index=11,StartTime="2026-09-23T00:00:00.0000000Z",EndTime="2026-09-23T00:00:00.0000000Z",eliminateNumber=new[] { 7,9 },robotAccountList=new[] { 7,9 },InflationRate=1.5f });
|
||||
Dictionary<string,string> Raw(JObject value)=>new() { ["FishingChallengeData"]=value.ToString(Formatting.None) };
|
||||
var state=PlayerKVSaveSchema.DecodeChallenge(Raw(obj),SchemaSnapshot(),new());
|
||||
Check(state!=null && state.IsPresent && !state.IsExplicitNull,"valid complete object, not explicit null");
|
||||
Check(state.FirstOpen==true,"typed FirstOpen");
|
||||
Check(state.EventId==11,"typed EventId");
|
||||
Check(state.RankTierLevel==11,"typed RankTierLevel");
|
||||
Check(state.Finished==11,"typed Finished");
|
||||
Check(state.RedirectID==11,"typed RedirectID");
|
||||
Check(state.Score==11,"typed Score");
|
||||
Check(state.ProcessedScore==11,"typed ProcessedScore");
|
||||
Check(state.step==11,"typed step");
|
||||
Check(state.uiStep==11,"typed uiStep");
|
||||
Check(state.index==11,"typed index");
|
||||
Check(state.StartTime==new DateTime(2026,9,23,0,0,0,DateTimeKind.Utc),"typed StartTime");
|
||||
Check(state.EndTime==new DateTime(2026,9,23,0,0,0,DateTimeKind.Utc),"typed EndTime");
|
||||
Check(state.eliminateNumber.SequenceEqual(new[] { 7,9 }),"typed eliminateNumber");
|
||||
Check(state.robotAccountList.SequenceEqual(new[] { 7,9 }),"typed robotAccountList");
|
||||
Check(state.InflationRate==1.5f,"typed InflationRate");
|
||||
foreach(var property in obj.Properties())
|
||||
{
|
||||
var bad=(JObject)obj.DeepClone(); bad.Remove(property.Name);
|
||||
Check(PlayerKVSaveSchema.DecodeChallenge(Raw(bad),SchemaSnapshot(),new())==null,"missing saved field rejects");
|
||||
bad=(JObject)obj.DeepClone(); bad[property.Name]=JValue.CreateNull();
|
||||
Check(PlayerKVSaveSchema.DecodeChallenge(Raw(bad),SchemaSnapshot(),new())==null,"null saved field rejects");
|
||||
}
|
||||
var missing = PlayerKVSaveSchema.DecodeChallenge(new Dictionary<string,string>(),SchemaSnapshot(),new());
|
||||
Check(!missing.IsPresent && !missing.IsExplicitNull,"missing separate");
|
||||
var empty = PlayerKVSaveSchema.DecodeChallenge(new Dictionary<string,string> { ["FishingChallengeData"]="null" },SchemaSnapshot(),new());
|
||||
Check(empty != null && empty.IsPresent && empty.IsExplicitNull,"explicit null is a present inactive challenge");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await Run("M15-explicit-null-raw-and-strict-boundaries", async () =>
|
||||
{
|
||||
foreach (var text in new[] { "null", " \r\n\tnull \t" })
|
||||
{
|
||||
var raw = ModernRaw();
|
||||
raw["FishingChallengeData"] = text;
|
||||
raw["outside-null-compat-probe"] = " untouched \r\n";
|
||||
var original = new Dictionary<string,string>(raw);
|
||||
var result = Aggregate(raw);
|
||||
Check(result.Verdict == PlayerKVSchemaVerdict.Valid, "explicit null aggregate valid");
|
||||
Check(result.Data.Module<PlayerKVChallengeState>(15).IsExplicitNull, "aggregate preserves explicit null marker");
|
||||
Check(result.Data.Raw.Count == original.Count, "raw key count unchanged");
|
||||
foreach (var pair in original)
|
||||
{
|
||||
Check(System.Text.Encoding.UTF8.GetBytes(result.Data.Raw[pair.Key]).SequenceEqual(
|
||||
System.Text.Encoding.UTF8.GetBytes(pair.Value)), "raw UTF8 bytes preserved including null whitespace");
|
||||
Check(raw[pair.Key] == pair.Value, "decode did not edit input");
|
||||
}
|
||||
raw["FishingChallengeData"] = "changed-after-decode";
|
||||
Check(result.Data.Raw["FishingChallengeData"] == text, "decoded raw is a separate copy");
|
||||
}
|
||||
Check(!Aggregate(ModernRaw()).Data.Raw.ContainsKey("FishingChallengeData"), "missing raw stays missing");
|
||||
foreach (var text in new[] { null, "", " \t", "\"null\"", "0", "true", "[]", "[null]", "{", "null null",
|
||||
"\vnull\v", "\fnull\f", "\u0085null", "\u00a0null\u00a0", "\ufeffnull", "/*x*/null", "null//x", "null,", "null,\"x\":1", "null}" })
|
||||
{
|
||||
var errors = new PlayerKVSchemaDiagnostics();
|
||||
var state = PlayerKVSaveSchema.DecodeChallenge(new Dictionary<string,string> { ["FishingChallengeData"] = text }, SchemaSnapshot(), errors);
|
||||
Check(state == null && errors.Count(15, PlayerKVSchemaError.Malformed) == 1, "only strict JSON null or complete object accepted");
|
||||
}
|
||||
await Throws<PlayerKVSchemaException>(() => { PlayerKVSaveSchema.Json("null"); return Task.CompletedTask; });
|
||||
await Throws<ArgumentException>(() =>
|
||||
{
|
||||
_ = new PlayerKVChallengeState(false,false,0,0,0,0,0,0,0,0,0,default,default,new List<int>(),new List<int>(),0,true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
await Run("M26-activity-all-saved-fields-strict",() =>
|
||||
{
|
||||
var obj=JObject.FromObject(new { EventID=11,PinballMainID=11,SpinPoint=11,Scores=11,Multiple=11,Token=11,ConsumedToken=11,RewardIndex=11,Force=1.5f,LuckyValue=1.5f,InflationRate=1.5f });
|
||||
Dictionary<string,string> Raw(JObject value)=>new() { ["epbdk"]=value.ToString(Formatting.None) };
|
||||
var state=PlayerKVSaveSchema.DecodePinball(Raw(obj),SchemaSnapshot(),new());
|
||||
Check(state!=null && state.IsPresent,"valid complete state");
|
||||
Check(state.EventID==11,"typed EventID");
|
||||
Check(state.PinballMainID==11,"typed PinballMainID");
|
||||
Check(state.SpinPoint==11,"typed SpinPoint");
|
||||
Check(state.Scores==11,"typed Scores");
|
||||
Check(state.Multiple==11,"typed Multiple");
|
||||
Check(state.Token==11,"typed Token");
|
||||
Check(state.ConsumedToken==11,"typed ConsumedToken");
|
||||
Check(state.RewardIndex==11,"typed RewardIndex");
|
||||
Check(state.Force==1.5f,"typed Force");
|
||||
Check(state.LuckyValue==1.5f,"typed LuckyValue");
|
||||
Check(state.InflationRate==1.5f,"typed InflationRate");
|
||||
foreach(var property in obj.Properties())
|
||||
{
|
||||
var bad=(JObject)obj.DeepClone(); bad.Remove(property.Name);
|
||||
Check(PlayerKVSaveSchema.DecodePinball(Raw(bad),SchemaSnapshot(),new())==null,"missing saved field rejects");
|
||||
bad=(JObject)obj.DeepClone(); bad[property.Name]=JValue.CreateNull();
|
||||
Check(PlayerKVSaveSchema.DecodePinball(Raw(bad),SchemaSnapshot(),new())==null,"null saved field rejects");
|
||||
}
|
||||
Check(!PlayerKVSaveSchema.DecodePinball(new Dictionary<string,string>(),SchemaSnapshot(),new()).IsPresent,"missing separate");
|
||||
Check(PlayerKVSaveSchema.DecodePinball(new Dictionary<string,string> { ["epbdk"]="null" },SchemaSnapshot(),new())==null,"null not missing");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await Run("M27-activity-all-saved-fields-strict",() =>
|
||||
{
|
||||
var obj=JObject.FromObject(new { EventID=11,ScratchMainID=11,ChainPackID=11,Token=11,ConsumedToken=11,RewardTaskConsumedToken=11,EventScratchRewardTaskID=11,EventScratchCardID=11,TotalConsumedToken=11,ChainPackProgress=11,magIndex=11,IsOver=true,LuckyValue=1.5f,InflationRate=1.5f,StampRewardList=new[] { 7,9 },StampRewardDict=new Dictionary<int,object> { [7]=new { EventScratchCardID=8,Count=9 } } });
|
||||
Dictionary<string,string> Raw(JObject value)=>new() { ["estdk"]=value.ToString(Formatting.None) };
|
||||
var state=PlayerKVSaveSchema.DecodeScratch(Raw(obj),SchemaSnapshot(),new());
|
||||
Check(state!=null && state.IsPresent,"valid complete state");
|
||||
Check(state.EventID==11,"typed EventID");
|
||||
Check(state.ScratchMainID==11,"typed ScratchMainID");
|
||||
Check(state.ChainPackID==11,"typed ChainPackID");
|
||||
Check(state.Token==11,"typed Token");
|
||||
Check(state.ConsumedToken==11,"typed ConsumedToken");
|
||||
Check(state.RewardTaskConsumedToken==11,"typed RewardTaskConsumedToken");
|
||||
Check(state.EventScratchRewardTaskID==11,"typed EventScratchRewardTaskID");
|
||||
Check(state.EventScratchCardID==11,"typed EventScratchCardID");
|
||||
Check(state.TotalConsumedToken==11,"typed TotalConsumedToken");
|
||||
Check(state.ChainPackProgress==11,"typed ChainPackProgress");
|
||||
Check(state.magIndex==11,"typed magIndex");
|
||||
Check(state.IsOver==true,"typed IsOver");
|
||||
Check(state.LuckyValue==1.5f,"typed LuckyValue");
|
||||
Check(state.InflationRate==1.5f,"typed InflationRate");
|
||||
Check(state.StampRewardList.SequenceEqual(new[] { 7,9 }),"typed StampRewardList");
|
||||
Check(state.StampRewardDict[7].EventScratchCardID==8 && state.StampRewardDict[7].Count==9,"typed StampRewardDict");
|
||||
foreach(var property in obj.Properties())
|
||||
{
|
||||
var bad=(JObject)obj.DeepClone(); bad.Remove(property.Name);
|
||||
Check(PlayerKVSaveSchema.DecodeScratch(Raw(bad),SchemaSnapshot(),new())==null,"missing saved field rejects");
|
||||
bad=(JObject)obj.DeepClone(); bad[property.Name]=JValue.CreateNull();
|
||||
Check(PlayerKVSaveSchema.DecodeScratch(Raw(bad),SchemaSnapshot(),new())==null,"null saved field rejects");
|
||||
}
|
||||
Check(!PlayerKVSaveSchema.DecodeScratch(new Dictionary<string,string>(),SchemaSnapshot(),new()).IsPresent,"missing separate");
|
||||
Check(PlayerKVSaveSchema.DecodeScratch(new Dictionary<string,string> { ["estdk"]="null" },SchemaSnapshot(),new())==null,"null not missing");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
83
CodexOutput/upserver/KVGameLoadTests/AggregateTests.cs
Normal file
83
CodexOutput/upserver/KVGameLoadTests/AggregateTests.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using game.PlayerDataStorage;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static PlayerKVSchemaResult Aggregate(Dictionary<string,string> raw=null,bool fresh=false,bool current=true,string version="synthetic-v1")
|
||||
{
|
||||
var prepared=new PlayerKVLoginPrepareResult(PlayerKVLoginPrepareError.None,"101",7,data:raw ?? ModernRaw());
|
||||
var account=new PlayerKVAccountDescriptor("101",7,17,fresh,SchemaSnapshot().NowUtc,SchemaSnapshot().NowUtc);
|
||||
return PlayerKVSaveSchema.Decode(prepared,account,"101",7,current,SchemaSnapshot(),version,Identity());
|
||||
}
|
||||
static async Task RunAggregateTests()
|
||||
{
|
||||
await Run("transaction-every-reset-replace-revocation-boundary",()=>
|
||||
{
|
||||
foreach(int boundary in Enumerable.Range(1,31)) foreach(int mode in new[]{0,1,2,3})
|
||||
{
|
||||
bool current=true;int notified=0;var values=new int[31];
|
||||
var modules=Enumerable.Range(1,31).Select(id=>(IPlayerKVLoadModule)new PlayerKVLoadModule(id,
|
||||
()=>{values[id-1]=0;if(id==boundary && mode==0)throw new Exception();if(id==boundary && mode==1)current=false;},
|
||||
_=>{values[id-1]=id;if(id==boundary && mode==2)current=false;if(id==boundary && mode==3)throw new Exception();}));
|
||||
var transaction=new PlayerKVGameLoadTransaction(modules,Environment.CurrentManagedThreadId,()=>{});
|
||||
var result=transaction.Apply(Aggregate().Data,()=>current,()=>true,()=>notified++);
|
||||
Check(result==(mode==0?PlayerKVApplyError.Poisoned:mode==3?PlayerKVApplyError.Apply:PlayerKVApplyError.Stale),"failure classification at every module");
|
||||
Check(notified==0 && values.All(v=>v==0) && transaction.AppliedModules==0,"no publication, all reset");
|
||||
if(mode==0)Check(transaction.Apply(Aggregate().Data,()=>true,()=>true,()=>{})==PlayerKVApplyError.Poisoned,"failed reset cannot reuse container");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await Run("aggregate-31-source-priority-no-partial-state",()=>
|
||||
{
|
||||
var raw=ModernRaw(); raw["extension"]="uninterpreted";
|
||||
var a=Aggregate(raw); Check(a.Verdict==PlayerKVSchemaVerdict.Valid && a.Data.ModuleCount==31,"all31 decoded");
|
||||
Check(a.Data.UnconsumedCount==1 && a.Data.UnconsumedTopLevelCount==1,"unknown top-level retained and counted");
|
||||
var extended=ModernRaw(); var inner=Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string,string>>(extended["NewPlayerDataDic"]);
|
||||
inner["unknown"]="opaque"; extended["NewPlayerDataDic"]=Newtonsoft.Json.JsonConvert.SerializeObject(inner);extended["Map_not_a_number"]="opaque";
|
||||
var counted=Aggregate(extended).Data;Check(counted.UnconsumedModernCount==1 && counted.UnconsumedTopLevelCount==1,"unknown modern and nonmatching map prefixes counted separately");
|
||||
raw.Clear(); Check(a.Data.Raw.Count==2 && a.Data.Module<PlayerKVPlayerState>(1).Core==a.Data.Module<PlayerKVCoreState>(2),"source copied, consistent shared immutable core");
|
||||
Check(Aggregate(new(),true).Verdict==PlayerKVSchemaVerdict.EmptyNew && Aggregate(new()).Verdict==PlayerKVSchemaVerdict.EmptyExisting,"S101 empty classification");
|
||||
Check(Aggregate(new(),true,false).Verdict==PlayerKVSchemaVerdict.Stale,"stale before empty");
|
||||
Check(Aggregate(version:"other").Verdict==PlayerKVSchemaVerdict.UnsupportedSchema,"version locked");
|
||||
Check(Aggregate(new(){["FootPrint"]="1"}).Verdict==PlayerKVSchemaVerdict.MissingCore,"optional data not core");
|
||||
raw=ModernRaw(); raw["EventPartnerGatherData"]="null"; raw["TapJoy_Diamond"]="bad";
|
||||
var bad=Aggregate(raw); Check(bad.Data==null && bad.Diagnostics.Count(18,PlayerKVSchemaError.Malformed)==1 && bad.Diagnostics.Count(31,PlayerKVSchemaError.Malformed)==1,"last module visited despite earlier optional failure");
|
||||
raw["NewPlayerDataDic"]="null"; Check(Aggregate(raw).Verdict==PlayerKVSchemaVerdict.Malformed,"outer parse failure");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await Run("transaction-all-reset-before-replace-failure-clears",()=>
|
||||
{
|
||||
var order=new List<string>(); var values=new int[31]; int failReplace=0,failReset=0,discard=0,notify=0;
|
||||
var modules=Enumerable.Range(1,31).Select(id=>(IPlayerKVLoadModule)new PlayerKVLoadModule(id,
|
||||
()=>{order.Add("R"+id); values[id-1]=0; if(failReset==id) throw new Exception();},
|
||||
_=>{order.Add("A"+id); values[id-1]=id; if(failReplace==id) throw new Exception();})).ToArray();
|
||||
var transaction=new PlayerKVGameLoadTransaction(modules,Environment.CurrentManagedThreadId,()=>discard++);
|
||||
Check(transaction.Apply(Aggregate().Data,()=>true,()=>true,()=>notify++)==PlayerKVApplyError.None,"apply succeeded");
|
||||
Check(order.Take(31).SequenceEqual(Enumerable.Range(1,31).Select(id=>"R"+id)) && order.Skip(31).SequenceEqual(Enumerable.Range(1,31).Select(id=>"A"+id)),"exact reset-then-replace sequence");
|
||||
Check(transaction.AppliedModules==31 && notify==1 && discard==0,"notification only complete");
|
||||
foreach(var failed in Enumerable.Range(1,31))
|
||||
{
|
||||
failReplace=failed; order.Clear();
|
||||
Check(transaction.Apply(Aggregate().Data,()=>true,()=>true,()=>notify++)==PlayerKVApplyError.Apply && values.All(v=>v==0),"partial materialization cleared");
|
||||
Check(order.TakeLast(31).SequenceEqual(Enumerable.Range(1,31).Select(id=>"R"+id)),"all resets attempted after failure");
|
||||
}
|
||||
failReplace=0; failReset=8;
|
||||
Check(transaction.Apply(Aggregate().Data,()=>true,()=>true,()=>notify++)==PlayerKVApplyError.Poisoned && transaction.Poisoned,"reset failure poisons");
|
||||
failReset=0; Check(transaction.Apply(Aggregate().Data,()=>true,()=>true,()=>notify++)==PlayerKVApplyError.Poisoned,"poison cannot retry");
|
||||
Check(notify==1,"no failure notifications");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await Run("transaction-notification-stale-sideeffect-and-main-thread",async()=>
|
||||
{
|
||||
bool current=true,clean=true; int resets=0;
|
||||
var modules=Enumerable.Range(1,31).Select(id=>(IPlayerKVLoadModule)new PlayerKVLoadModule(id,()=>resets++,_=>{})).ToArray();
|
||||
var transaction=new PlayerKVGameLoadTransaction(modules,Environment.CurrentManagedThreadId,()=>{});
|
||||
Check(transaction.Apply(Aggregate().Data,()=>current,()=>clean,()=>current=false)==PlayerKVApplyError.Stale,"notification revocation");
|
||||
current=true; Check(transaction.Apply(Aggregate().Data,()=>current,()=>clean,()=>clean=false)==PlayerKVApplyError.SideEffect,"notification sideeffect");
|
||||
clean=true; Check(transaction.Apply(Aggregate().Data,()=>current,()=>clean,()=>throw new Exception())==PlayerKVApplyError.Apply,"receiver failure");
|
||||
int before=resets;
|
||||
var other=new PlayerKVGameLoadTransaction(modules,-1,()=>{});
|
||||
Check(other.Apply(Aggregate().Data,()=>true,()=>true,()=>{})==PlayerKVApplyError.Input && resets==before,"wrong thread never mutates");
|
||||
await Throws<ArgumentException>(()=>{new PlayerKVGameLoadTransaction(modules.Take(30),Environment.CurrentManagedThreadId,()=>{});return Task.CompletedTask;});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunBattlePassSchemaTests()
|
||||
{
|
||||
await Run("M11-battle-pass-two-layouts-conflict-and-ticks",() =>
|
||||
{
|
||||
var raw=new Dictionary<string,string> { ["BattlePassRecordTask"]="1|2|3|4|1|5,6|7,8#9:10|11:12|13,14|15|16" };
|
||||
var state=PlayerKVSaveSchema.DecodeBattlePass(raw,SchemaSnapshot(),new());
|
||||
var r=state.Record; var t=state.Task;
|
||||
Check(r.ID==1 && r.Level==2 && r.Exp==3 && r.BuyVipToastIndex==4 && r.IsUnlockVip==1 &&
|
||||
r.Normal.SequenceEqual(new[] { 5,6 }) && r.Vip.SequenceEqual(new[] { 7,8 }) && r.LastDrops.Count==0 && r.Inflation==0,"all compact record fields");
|
||||
Check(t.Daily[9]==10 && t.Weekly[11]==12 && t.LoginDays.SequenceEqual(new[] { 13,14 }) &&
|
||||
t.DailyEnd.Ticks==15 && t.WeeklyEnd.Ticks==16,"all compact task fields");
|
||||
raw["BattlePassRecord"]=JsonConvert.SerializeObject(new { ID=1,Level=2,Exp=3,BuyVipToastIndex=4,IsUnlockVip=1,
|
||||
NormalRewardGet=new[] { 5,6 },VipRewardGet=new[] { 7,8 },LastDropIDs=new[] { 99 },InflationRate=2.5f });
|
||||
raw["BattleTask"]=JsonConvert.SerializeObject(new { dic_dailyTasks=new Dictionary<int,int> { [9]=10 },
|
||||
dic_weeklyTasks=new Dictionary<int,int> { [11]=12 },loginDays=new[] { 13,14 },DailyTaskEndTime=new DateTime(15),WeeklyTaskEndTime=new DateTime(16) });
|
||||
state=PlayerKVSaveSchema.DecodeBattlePass(raw,SchemaSnapshot(),new());
|
||||
Check(state!=null && state.Record.LastDrops.Count==0,"new whole record with absent old-only fields");
|
||||
var legacy=new Dictionary<string,string>(raw); legacy.Remove("BattlePassRecordTask");
|
||||
state=PlayerKVSaveSchema.DecodeBattlePass(legacy,SchemaSnapshot(),new());
|
||||
Check(state.Record.LastDrops.SequenceEqual(new[] { 99 }) && state.Record.Inflation==2.5f,"old-only fields retained when old selected");
|
||||
raw["BattlePassRecordTask"]="1|2|3|4|1|5,6|7,8#9:99|11:12|13,14|15|16";
|
||||
var e=new PlayerKVSchemaDiagnostics();
|
||||
Check(PlayerKVSaveSchema.DecodeBattlePass(raw,SchemaSnapshot(),e)==null && e.Count(11,PlayerKVSchemaError.ConflictingRepresentations)==1,"task conflict");
|
||||
foreach(var text in new[] { "1|2|3|4|1||#|||0|0#", "1|2|3|4|1|5,|#|||0|0", "1|2|3|4|1||#1:2,01:3|||0|0",
|
||||
"1|2|3|4|1||#|||-1|0", "1|2|3|4|1||#|||3155378976000000000|0", "null" })
|
||||
Check(PlayerKVSaveSchema.DecodeBattlePass(new Dictionary<string,string> { ["BattlePassRecordTask"]=text },SchemaSnapshot(),new())==null,"malformed compact/ticks reject");
|
||||
raw["BattlePassRecordTask"]="1|2|3|4|1|5,6|7,8#9:10|11:12|13,14|15|16";
|
||||
foreach(var key in new[] { "BattlePassRecord","BattleTask" })
|
||||
{
|
||||
var bad=new Dictionary<string,string>(raw) { [key]="null" };
|
||||
Check(PlayerKVSaveSchema.DecodeBattlePass(bad,SchemaSnapshot(),new())==null,"bad old not hidden by new");
|
||||
}
|
||||
state=PlayerKVSaveSchema.DecodeBattlePass(new Dictionary<string,string>(),SchemaSnapshot(),new());
|
||||
Check(state.Record.ID==0 && state.Record.Level==1 && state.Record.Normal.Count==0 && state.Record.Vip.Count==0 &&
|
||||
state.Record.LastDrops.Count==0 && state.Task.Daily.Count==0 && state.Task.Weekly.Count==0 && state.Task.LoginDays.Count==0 &&
|
||||
state.Task.DailyEnd==default && state.Task.WeeklyEnd==default,"missing constructor state");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunBuffTriggerSchemaTests()
|
||||
{
|
||||
await Run("M06-four-buff-shapes-strict-and-missing", () =>
|
||||
{
|
||||
const string value="{\"SortID\":1,\"sourceID\":2,\"buffID\":3,\"isEnd\":false,\"buffEndTime\":\"2026-09-24T00:00:00.0000000Z\"}";
|
||||
var raw=new Dictionary<string,string> { ["BuffDataList"]="{\"7\":"+value+"}",["TargetBuffDataList"]="["+value+"]",
|
||||
["FishDailyBuff"]=value,["GlobalBuffDataList"]="{\"7\":"+value+"}" };
|
||||
var state=PlayerKVSaveSchema.DecodeBuffs(raw,SchemaSnapshot(),new());
|
||||
foreach(var item in new[] { state.Map[7],state.Target[0],state.Daily,state.Global[7] })
|
||||
Check(item.SortID==1 && item.sourceID==2 && item.buffID==3 && !item.isEnd && item.buffEndTime.Day==24,"all saved buff fields");
|
||||
var absent=PlayerKVSaveSchema.DecodeBuffs(new Dictionary<string,string>(),SchemaSnapshot(),new());
|
||||
Check(absent.Map.Count==0 && absent.Target.Count==0 && absent.Global.Count==0 && absent.Daily==null,"all missing defaults");
|
||||
foreach(var key in raw.Keys)
|
||||
foreach(var bad in new[] { "null","{\"7\":null}","[null]","{}" })
|
||||
{
|
||||
// Empty maps are valid only for map containers.
|
||||
if(bad=="{}" && (key=="BuffDataList" || key=="GlobalBuffDataList")) continue;
|
||||
Check(PlayerKVSaveSchema.DecodeBuffs(new Dictionary<string,string>(raw) { [key]=bad },SchemaSnapshot(),new())==null,"bad present container fails");
|
||||
}
|
||||
foreach(var property in JObject.Parse(value).Properties())
|
||||
{
|
||||
var bad=JObject.Parse(value); bad[property.Name]=JValue.CreateNull();
|
||||
Check(PlayerKVSaveSchema.DecodeBuffs(new Dictionary<string,string> { ["FishDailyBuff"]=bad.ToString(Formatting.None) },SchemaSnapshot(),new())==null,"all required buff fields");
|
||||
}
|
||||
Check(PlayerKVSaveSchema.DecodeBuffs(new Dictionary<string,string> { ["BuffDataList"]="{\"7\":"+value+",\"07\":"+value+"}" },SchemaSnapshot(),new())==null,"normalized duplicate rejected");
|
||||
var allBad=new Dictionary<string,string>(raw); foreach(var key in raw.Keys) allBad[key]="null";
|
||||
var errors=new PlayerKVSchemaDiagnostics(); Check(PlayerKVSaveSchema.DecodeBuffs(allBad,SchemaSnapshot(),errors)==null && errors.TotalErrors==4,"all bad keys counted");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await Run("M13-trigger-gift-fixed-shapes", () =>
|
||||
{
|
||||
var raw=new Dictionary<string,string> {
|
||||
["TriggerPackDataList"]="[{\"ID\":1,\"index\":2,\"lastID\":3,\"vipLevel\":4,\"mapID\":5,\"time\":\"2026-09-24\"}]",
|
||||
["GiftVIPDData"]="{\"A\":{\"oldVipLevel\":1,\"curVipLevel\":2,\"timer\":3}}",
|
||||
["FixedTimeShopDic"]="{\"7\":{\"id\":8,\"endTime\":\"2026-09-24\"}}"
|
||||
};
|
||||
var state=PlayerKVSaveSchema.DecodeTriggers(raw,new()); var pack=state.Packs[0];
|
||||
Check(pack.ID==1 && pack.index==2 && pack.lastID==3 && pack.vipLevel==4 && pack.mapID==5 && pack.time=="2026-09-24","pack fields");
|
||||
Check(state.Gifts["A"].oldVipLevel==1 && state.Gifts["A"].curVipLevel==2 && state.Gifts["A"].timer==3 &&
|
||||
state.Fixed[7].id==8 && state.Fixed[7].endTime=="2026-09-24","gift and fixed fields");
|
||||
foreach(var key in raw.Keys)
|
||||
foreach(var bad in new[] { "null","[null]","{\"7\":null}","{\"A\":{}}" })
|
||||
Check(PlayerKVSaveSchema.DecodeTriggers(new Dictionary<string,string>(raw) { [key]=bad },new())==null,"invalid container or nested fails");
|
||||
var missing=PlayerKVSaveSchema.DecodeTriggers(new Dictionary<string,string>(),new());
|
||||
Check(missing.Packs.Count==0 && missing.Gifts.Count==0 && missing.Fixed.Count==0,"missing all empty");
|
||||
var allBad=new Dictionary<string,string>(raw); foreach(var key in raw.Keys) allBad[key]="null";
|
||||
var e=new PlayerKVSchemaDiagnostics(); Check(PlayerKVSaveSchema.DecodeTriggers(allBad,e)==null && e.TotalErrors==3,"all keys counted");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
58
CodexOutput/upserver/KVGameLoadTests/CampEventSchemaTests.cs
Normal file
58
CodexOutput/upserver/KVGameLoadTests/CampEventSchemaTests.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using game.PlayerDataStorage;
|
||||
using System.Globalization;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunCampEventSchemaTests()
|
||||
{
|
||||
await Run("M07-camp-strict-fields-and-copy", () =>
|
||||
{
|
||||
var raw=new Dictionary<string,string> { ["IsFix"]="True",["IsOpenSkyscraper"]="1",["CampDataDic"]="{\"CampId\":205}" };
|
||||
var state=PlayerKVSaveSchema.DecodeCamp(raw,new());
|
||||
Check(state.IsFix && state.Skyscraper=="1" && state.Values["CampId"]==205,"all raw fields");
|
||||
var missing=PlayerKVSaveSchema.DecodeCamp(new Dictionary<string,string>(),new());
|
||||
Check(!missing.IsFix && missing.Skyscraper=="0" && missing.Values.Count==0,"missing defaults");
|
||||
foreach(var key in raw.Keys)
|
||||
{
|
||||
foreach(var bad in new[] { "null",null,"[]","1.0" })
|
||||
Check(PlayerKVSaveSchema.DecodeCamp(new Dictionary<string,string>(raw) { [key]=bad },new())==null,"bad optional fails");
|
||||
}
|
||||
Check(PlayerKVSaveSchema.DecodeCamp(new Dictionary<string,string> { ["IsOpenSkyscraper"]="true" },new())==null,"exact sky flag");
|
||||
var source=new Dictionary<string,int> { ["CampId"]=205 };
|
||||
var copied=new PlayerKVCampState(true,"1",source); source.Clear();
|
||||
Check(copied.Values.Count==1,"independent map");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await Run("M20-event-pack-active-dormant-config", () =>
|
||||
{
|
||||
var events=new Dictionary<int,PlayerKVEventConfig> {
|
||||
[1]=new(100,"2026-09-24T00:00:00.0000000Z"),[2]=new(999,"2026-09-22T00:00:00.0000000Z"),
|
||||
[3]=new(200,"2026-09-24T00:00:00.0000000Z"),[4]=new(100,null)
|
||||
};
|
||||
var snapshot=new PlayerKVSchemaSnapshot("M20",Array.Empty<int>(),Array.Empty<int>(),new Dictionary<string,int>(),
|
||||
CultureInfo.InvariantCulture,new DateTime(2026,9,23,0,0,0,DateTimeKind.Utc),0,
|
||||
new Dictionary<int,int> { [100]=1,[200]=3,[4054001]=1 },events);
|
||||
events.Clear();
|
||||
Dictionary<string,string> Raw(int eventId,int redirect=0) => new() {
|
||||
["EventPackData"]=$"{{\"currentEventID\":{eventId},\"purchaseCount\":2,\"VIPLvlWhenEventActivate\":3,\"redirectID\":{redirect}}}"
|
||||
};
|
||||
var state=PlayerKVSaveSchema.DecodeEventPack(Raw(1),snapshot,new());
|
||||
Check(state.EventId==1 && state.PurchaseCount==2 && state.VipLevel==3 && state.RedirectId==4054001,"active old default normalized after copied config");
|
||||
foreach(var eventId in new[] { 0,2,999 })
|
||||
{
|
||||
state=PlayerKVSaveSchema.DecodeEventPack(Raw(eventId),snapshot,new());
|
||||
Check(state.EventId==eventId && state.RedirectId==0 && state.PurchaseCount==2,"dormant values retained");
|
||||
}
|
||||
foreach(var invalid in new[] { Raw(3),Raw(4),Raw(1,999) })
|
||||
{
|
||||
var e=new PlayerKVSchemaDiagnostics();
|
||||
Check(PlayerKVSaveSchema.DecodeEventPack(invalid,snapshot,e)==null && e.Count(20,PlayerKVSchemaError.UnsupportedSchema)==1,"config dereference failure explicit");
|
||||
}
|
||||
foreach(var bad in new[] { "null","{}","{\"currentEventID\":1,\"purchaseCount\":\"2\",\"VIPLvlWhenEventActivate\":3,\"redirectID\":0}" })
|
||||
Check(PlayerKVSaveSchema.DecodeEventPack(new Dictionary<string,string> { ["EventPackData"]=bad },snapshot,new())==null,"strict object");
|
||||
state=PlayerKVSaveSchema.DecodeEventPack(new Dictionary<string,string>(),snapshot,new());
|
||||
Check(state.EventId==0 && state.PurchaseCount==0 && state.VipLevel==0 && state.RedirectId==0,"missing all zero without normalization");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
59
CodexOutput/upserver/KVGameLoadTests/ClubPackSchemaTests.cs
Normal file
59
CodexOutput/upserver/KVGameLoadTests/ClubPackSchemaTests.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using game.PlayerDataStorage;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunClubPackSchemaTests()
|
||||
{
|
||||
await Run("M14-club-three-load-fields-only", () =>
|
||||
{
|
||||
var raw = new Dictionary<string,string> {
|
||||
["ClubSignin"] = "2026-09-23T00:00:00.0000000Z",
|
||||
["LureDonateData"] = "{\"DonateCountTime\":\"2026-09-22T00:00:00.0000000Z\",\"ClubLureDonateCount\":7,\"ClubLureRequestCDEnd\":\"2026-09-24T00:00:00.0000000Z\"}",
|
||||
["IsOpenClubRed"] = ""
|
||||
};
|
||||
var state = PlayerKVSaveSchema.DecodeClub(raw,SchemaSnapshot(),new());
|
||||
Check(state.SignTime.Value.Day == 23 && state.DonateTime.Day == 22 && state.DonateCount == 7 && state.RequestEnd.Day == 24 && !state.OpenRed,"all three load states");
|
||||
var missing = PlayerKVSaveSchema.DecodeClub(new Dictionary<string,string>(),SchemaSnapshot(),new());
|
||||
Check(missing.SignTime == null && missing.DonateTime == default && missing.DonateCount == 0 && missing.RequestEnd == default && missing.OpenRed,"missing exact zero state");
|
||||
raw["ClubSignin"] = "";
|
||||
Check(PlayerKVSaveSchema.DecodeClub(raw,SchemaSnapshot(),new()).SignTime == null,"empty signin supported");
|
||||
foreach (var key in raw.Keys.ToArray())
|
||||
{
|
||||
var bad = new Dictionary<string,string>(raw) { [key] = null };
|
||||
Check(PlayerKVSaveSchema.DecodeClub(bad,SchemaSnapshot(),new()) == null,"present null rejected");
|
||||
}
|
||||
foreach (var bad in new[] { "null", "{}", "{\"DonateCountTime\":0,\"ClubLureDonateCount\":1,\"ClubLureRequestCDEnd\":0}" })
|
||||
{
|
||||
raw["LureDonateData"] = bad;
|
||||
Check(PlayerKVSaveSchema.DecodeClub(raw,SchemaSnapshot(),new()) == null,"bad donation DTO rejected");
|
||||
}
|
||||
raw = new() { ["ClubSignin"] = "03/04/2026" };
|
||||
var errors = new PlayerKVSchemaDiagnostics();
|
||||
Check(PlayerKVSaveSchema.DecodeClub(raw,SchemaSnapshot("en-GB"),errors) == null &&
|
||||
errors.Count(14,PlayerKVSchemaError.UnsupportedSchema) == 1,"ambiguous date rejected");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await Run("M28-two-independent-four-slot-packs", () =>
|
||||
{
|
||||
var raw = new Dictionary<string,string> { ["ThanksGivingPackData"] = "1,2,3,4", ["ThanksGivingPackMigrationData"] = "5,6,7,8" };
|
||||
foreach (bool migration in new[] { false,true })
|
||||
{
|
||||
var state = PlayerKVSaveSchema.DecodeProgressPack(raw,migration,new());
|
||||
int offset = migration ? 4 : 0;
|
||||
Check(state.IsPresent && state.EventId == 1 + offset && state.RedirectId == 2 + offset &&
|
||||
state.TokenProgress == 3 + offset && state.ChainProgress == 4 + offset,"complete four slots");
|
||||
var key = migration ? "ThanksGivingPackMigrationData" : "ThanksGivingPackData";
|
||||
foreach (var bad in new[] { "", "null", "1,2,3", "1,2,3,4,5", "1,2,x,4", "1,2,3,2147483648", null })
|
||||
{
|
||||
var invalid = new Dictionary<string,string>(raw) { [key] = bad };
|
||||
Check(PlayerKVSaveSchema.DecodeProgressPack(invalid,migration,new()) == null,"bad present pack rejected");
|
||||
}
|
||||
var absent = PlayerKVSaveSchema.DecodeProgressPack(new Dictionary<string,string>(),migration,new());
|
||||
Check(!absent.IsPresent,"each missing is explicit");
|
||||
var zero = PlayerKVSaveSchema.DecodeProgressPack(new Dictionary<string,string> { [key] = "0,0,0,0" },migration,new());
|
||||
Check(zero.IsPresent && zero.EventId == 0 && zero.RedirectId == 0,"zero present not missing");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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<string,string> { ["FishingBoxData"] = "{\"FishingBoxOpenNum\":{\"1\":2,\"3\":4}}" };
|
||||
var modern = new Dictionary<string,string> { ["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<string,string>(),new Dictionary<string,string>(),new()).Values.Count == 0,"missing empty");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await Run("M10-whole-guide-replacement-and-conflict", () =>
|
||||
{
|
||||
var raw = new Dictionary<string,string> {
|
||||
["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<string,string>(),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<string,string> { ["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<string,string>(),new()).IsPresent,"missing explicit");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
190
CodexOutput/upserver/KVGameLoadTests/CoordinatorTests.cs
Normal file
190
CodexOutput/upserver/KVGameLoadTests/CoordinatorTests.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
sealed class MainLoop : SynchronizationContext
|
||||
{
|
||||
internal readonly BlockingCollection<(SendOrPostCallback,object)> Queue=new();
|
||||
public override void Post(SendOrPostCallback callback,object state) => Queue.Add((callback,state));
|
||||
}
|
||||
static Task OnMain(Func<Task> body) => Task.Run(()=>
|
||||
{
|
||||
var old=SynchronizationContext.Current; var loop=new MainLoop(); SynchronizationContext.SetSynchronizationContext(loop);
|
||||
try
|
||||
{
|
||||
var task=body();
|
||||
_=task.ContinueWith(_=>loop.Post(_=>{},null),TaskScheduler.Default);
|
||||
while(!task.IsCompleted) { var (callback,state)=loop.Queue.Take(); callback(state); }
|
||||
task.GetAwaiter().GetResult();
|
||||
}
|
||||
finally { SynchronizationContext.SetSynchronizationContext(old); }
|
||||
});
|
||||
sealed class LoadFixture
|
||||
{
|
||||
internal readonly PlayerKVShutdownRegistry Registry;
|
||||
internal readonly int[] Values=new int[31];
|
||||
internal int Auth,Gets,Sends,Notify,Discard,AuthDisposed,LeaseDisposed;
|
||||
internal bool Fresh=true,Unstarted=true,FailApply,NotifyViolation,AuthDisposeFails,LeaseDisposeFails;
|
||||
internal Action OnNotify;
|
||||
internal Func<Task<HttpResponseMessage>> GetOverride;
|
||||
internal Func<int,CancellationToken,Task> Clock;
|
||||
internal PlayerKVAccountLease Lease;
|
||||
internal PlayerKVGameLoadCoordinator Coordinator;
|
||||
internal LoadFixture(PlayerKVShutdownRegistry registry=null)
|
||||
{
|
||||
Registry=registry ?? new();
|
||||
var modules=Enumerable.Range(1,31).Select(id=>(IPlayerKVLoadModule)new PlayerKVLoadModule(id,
|
||||
()=>Values[id-1]=0,_=>{Values[id-1]=id;if(FailApply && id==18) throw new InvalidOperationException();})).ToArray();
|
||||
var transaction=new PlayerKVGameLoadTransaction(modules,Environment.CurrentManagedThreadId,()=>Discard++);
|
||||
Coordinator=new(transaction,()=>Unstarted,()=>{Notify++;OnNotify?.Invoke();if(NotifyViolation) PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Update);},()=>1,
|
||||
lease=>Lease=lease,()=> {var lease=Lease;Lease=null;return lease?.CloseAsync() ?? Task.CompletedTask;},Registry);
|
||||
}
|
||||
internal PlayerKVGameLoadCapability Capability(Dictionary<string,string> raw=null,bool realKV=false)
|
||||
{
|
||||
CookieContainer jar=null;
|
||||
return PlayerKVGameLoadCapability.CreateForValidation(Identity(),SchemaSnapshot(),Api,Path.Combine(Root,"load",Guid.NewGuid().ToString("N")),
|
||||
cookies=>{ jar=cookies;return new Handler((r,t)=>
|
||||
{
|
||||
Auth++; Check(r.RequestUri.AbsolutePath=="/account/Login","one auth route"); jar.Add(Api,new Cookie("sid","synthetic"));
|
||||
return Task.FromResult(Json(JsonConvert.SerializeObject(new{state=0,internalId=17,newCreate=Fresh,createTime="2026-01-01T00:00:00Z",svrTime="2026-01-02T00:00:00Z"})));
|
||||
},()=>{AuthDisposed++;if(AuthDisposeFails)throw new InvalidOperationException("synthetic dispose");});},
|
||||
cookies=>{Check(ReferenceEquals(cookies,jar),"KV cookie identity");if(realKV) return new HttpClientHandler();return new Handler((r,t)=>
|
||||
{Gets++;Check(r.RequestUri.AbsolutePath=="/playerdata/Get","no mutation route");return GetOverride?.Invoke() ?? Task.FromResult(Json(JsonConvert.SerializeObject(new{data=raw ?? ModernRaw()})));});},
|
||||
cookies=>{Check(ReferenceEquals(cookies,jar),"lease cookie identity");return new Handler((r,t)=>{Sends++;return Task.FromResult(Json("{}"));},()=>{LeaseDisposed++;if(LeaseDisposeFails)throw new InvalidOperationException("synthetic dispose");});},
|
||||
delay:Clock);
|
||||
}
|
||||
}
|
||||
static async Task RunCoordinatorTests()
|
||||
{
|
||||
await Run("coordinator-success-reload-revoke-and-one-auth",()=>OnMain(async()=>
|
||||
{
|
||||
var f=new LoadFixture();
|
||||
var first=await f.Coordinator.LoadAsync("101",f.Capability());
|
||||
Check(first.State==PlayerKVGameLoadState.LoadedForValidation && first.ModuleCount==31 && f.Coordinator.LoadedForValidation,"controlled success only");
|
||||
Check(f.Auth==1 && f.Gets==1 && f.Notify==1 && f.Values.All(v=>v>0),"one auth/get and actual transaction");
|
||||
var oldLease=f.Lease;
|
||||
var second=await f.Coordinator.LoadAsync("102",f.Capability());
|
||||
Check(second.State==PlayerKVGameLoadState.LoadedForValidation && second.Epoch>first.Epoch && f.Auth==2 && f.AuthDisposed==1 && !oldLease.IsCurrent,"previous account drained before reload");
|
||||
var close=f.Coordinator.Revoke(); Check(!f.Coordinator.LoadedForValidation && f.Values.All(v=>v==0),"synchronous invalidation and module clearing");
|
||||
Check(ReferenceEquals(close,f.Coordinator.Revoke()),"same close task"); await close;
|
||||
Check(f.Registry.Status==PlayerKVShutdownStatus.Ready && f.AuthDisposed==2 && f.LeaseDisposed==2,"all ownership drained");
|
||||
}));
|
||||
await Run("coordinator-schema-apply-and-sideeffect-failures",()=>OnMain(async()=>
|
||||
{
|
||||
foreach(var mode in new[]{0,1,2})
|
||||
{
|
||||
var f=new LoadFixture{FailApply=mode==1,NotifyViolation=mode==2};
|
||||
var result=await f.Coordinator.LoadAsync("101",f.Capability(mode==0 ? new Dictionary<string,string>() : null));
|
||||
Check(result.Error==(mode==0 ? PlayerKVGameLoadError.Schema : mode==1 ? PlayerKVGameLoadError.Apply : PlayerKVGameLoadError.SideEffect),"stage error preserved");
|
||||
Check(f.Values.All(v=>v==0) && !f.Coordinator.LoadedForValidation && f.AuthDisposed==1,"failure clears and closes");
|
||||
Check(f.Notify==(mode==2 ? 1 : 0),"notification cannot precede complete apply");
|
||||
}
|
||||
var rejected=new LoadFixture(); var real=await rejected.Coordinator.LoadAsync("101",rejected.Capability(realKV:true));
|
||||
Check(real.State==PlayerKVGameLoadState.Rejected && rejected.Auth==0 && rejected.Gets==0,"all handlers preflight before authentication");
|
||||
}));
|
||||
await Run("coordinator-cancel-during-notification-and-after-success",()=>OnMain(async()=>
|
||||
{
|
||||
using var cancel=new CancellationTokenSource(); var f=new LoadFixture(); f.OnNotify=()=>cancel.Cancel();
|
||||
var result=await f.Coordinator.LoadAsync("101",f.Capability(),cancel.Token);
|
||||
Check(result.Error==PlayerKVGameLoadError.Stale && !f.Coordinator.LoadedForValidation && f.Values.All(v=>v==0),"callback revocation clears after transaction unwinds");
|
||||
using var later=new CancellationTokenSource(); var g=new LoadFixture();
|
||||
Check((await g.Coordinator.LoadAsync("101",g.Capability(),later.Token)).State==PlayerKVGameLoadState.LoadedForValidation,"ready before later cancel");
|
||||
later.Cancel(); await g.Coordinator.Revoke();
|
||||
Check(!g.Coordinator.LoadedForValidation && g.Values.All(v=>v==0),"lifetime remains effective after result");
|
||||
}));
|
||||
await Run("coordinator-pending-close-cross-instance-and-late-prepare",()=>OnMain(async()=>
|
||||
{
|
||||
var response=new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var started=Signal(); var f=new LoadFixture(); f.GetOverride=()=>{started.TrySetResult(true);return response.Task;};
|
||||
var deadlines=new List<TaskCompletionSource<bool>>();
|
||||
var observedDurations=new List<int>();
|
||||
f.Clock=(ms,token)=>{var s=Signal();lock(deadlines){deadlines.Add(s);observedDurations.Add(ms);}token.Register(()=>s.TrySetCanceled(token));return s.Task;};
|
||||
using var cancel=new CancellationTokenSource(); var load=f.Coordinator.LoadAsync("101",f.Capability(),cancel.Token);
|
||||
await started.Task; cancel.Cancel();
|
||||
// Advance only the live observation clock; transport deliberately ignores cancellation.
|
||||
while(true)
|
||||
{
|
||||
await Task.Yield(); TaskCompletionSource<bool> deadline; lock(deadlines) deadline=deadlines.Last();
|
||||
if(!deadline.Task.IsCompleted) {deadline.TrySetResult(true);break;}
|
||||
}
|
||||
var result=await load;
|
||||
Check(result.Error==PlayerKVGameLoadError.ClosePending && f.Registry.Status==PlayerKVShutdownStatus.Pending && f.AuthDisposed==0,"deadline is not drainage");
|
||||
var g=new LoadFixture(f.Registry);int ticks=0;g.Clock=(ms,t)=>++ticks==1 ? Task.Delay(Timeout.Infinite,t) : Task.CompletedTask;
|
||||
var blocked=await g.Coordinator.LoadAsync("102",g.Capability());
|
||||
Check(blocked.Error==PlayerKVGameLoadError.PreviousClosePending && g.Auth==0,"new coordinator cannot bypass pending close");
|
||||
int observed=0;f.Clock=null;
|
||||
// Existing captured capability owns the quit clock, not a later mutable option.
|
||||
var quit=f.Coordinator.ObserveQuitAsync();
|
||||
while(!quit.IsCompleted){await Task.Yield();lock(deadlines) deadlines.Last().TrySetResult(true);observed++;}
|
||||
Check(await quit==PlayerKVShutdownStatus.Pending && f.AuthDisposed==0 && observed>=0,"quit observation is not force disposal");
|
||||
Check(observedDurations.Contains(10000) && observedDurations.Last()==5000,"captured login/quit observations are 10s/5s");
|
||||
response.TrySetResult(Json(JsonConvert.SerializeObject(new{data=ModernRaw()})));
|
||||
await f.Coordinator.Revoke();
|
||||
Check(f.AuthDisposed==1 && f.Notify==0 && f.Values.All(v=>v==0) && !f.Coordinator.LoadedForValidation,"late success cannot apply");
|
||||
}));
|
||||
await Run("guard-exclusive-across-await-and-all-points",async()=>
|
||||
{
|
||||
Check(PlayerKVGameLoadGuard.TryEnter(out var guard),"first guard");
|
||||
using(guard)
|
||||
{
|
||||
await Task.Yield(); Check(!PlayerKVGameLoadGuard.TryEnter(out _),"process lease crosses await");
|
||||
foreach(var point in Enum.GetValues<PlayerKVGuardPoint>()) Check(PlayerKVGameLoadGuard.Reject(point) && guard.Count(point)==1,"all points counted");
|
||||
await Throws<PlayerKVLoadSideEffectException>(()=>{PlayerKVGameLoadGuard.RequireLegacyAllowed(PlayerKVGuardPoint.ReadLocal);return Task.CompletedTask;});
|
||||
Check(guard.Violations==8,"total violations");
|
||||
}
|
||||
Check(!PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Update),"PF passthrough outside scope");
|
||||
});
|
||||
await Run("coordinator-gate-timeout-cancel-boundary-no-leak",()=>OnMain(async()=>
|
||||
{
|
||||
var f=new LoadFixture{Clock=(ms,t)=>Task.CompletedTask};
|
||||
Check((await f.Coordinator.LoadAsync("101",f.Capability())).Error==PlayerKVGameLoadError.GateBusy && f.Auth==0,"simultaneous free lock and elapsed deadline rejects");
|
||||
f.Clock=null;using var cancel=new CancellationTokenSource();cancel.Cancel();
|
||||
Check((await f.Coordinator.LoadAsync("101",f.Capability(),cancel.Token)).Error==PlayerKVGameLoadError.Stale,"cancelled gate");
|
||||
Check((await f.Coordinator.LoadAsync("101",f.Capability())).State==PlayerKVGameLoadState.LoadedForValidation,"rejected gate returned permit");await f.Coordinator.Revoke();
|
||||
var g=new LoadFixture();bool current=false;
|
||||
Check((await g.Coordinator.LoadAsync("102",g.Capability(),identityCurrent:()=>current)).State==PlayerKVGameLoadState.Rejected && g.Notify==0,"changed identity never applies");
|
||||
}));
|
||||
await Run("coordinator-occupied-gate-bounded-and-reusable",()=>OnMain(async()=>
|
||||
{
|
||||
var started=Signal();var response=new TaskCompletionSource<HttpResponseMessage>();var f=new LoadFixture();
|
||||
f.GetOverride=()=>{started.TrySetResult(true);return response.Task;};var first=f.Coordinator.LoadAsync("101",f.Capability());await started.Task;
|
||||
f.Clock=(ms,t)=>Task.CompletedTask;
|
||||
Check((await f.Coordinator.LoadAsync("102",f.Capability())).Error==PlayerKVGameLoadError.GateBusy && f.Auth==1,"busy gate never authenticates next account");
|
||||
response.SetResult(Json(Newtonsoft.Json.JsonConvert.SerializeObject(new{data=ModernRaw()})));Check((await first).State==PlayerKVGameLoadState.LoadedForValidation,"first untouched");await f.Coordinator.Revoke();
|
||||
f.Clock=null;f.GetOverride=null;Check((await f.Coordinator.LoadAsync("102",f.Capability())).State==PlayerKVGameLoadState.LoadedForValidation,"permit reusable");await f.Coordinator.Revoke();
|
||||
}));
|
||||
await Run("coordinator-all-prepare-evidence-under-cleanup-failure",()=>OnMain(async()=>
|
||||
{
|
||||
// Layered mapping probe: 4B1 independently exercises the preparation causes; this invokes the real rejection path for every historical error.
|
||||
var registry=new PlayerKVShutdownRegistry();registry.Register(Task.FromException(new InvalidOperationException("synthetic close")));
|
||||
var fixture=new LoadFixture(registry);
|
||||
var reject=typeof(PlayerKVGameLoadCoordinator).GetMethod("Reject",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic);
|
||||
foreach(var error in Enum.GetValues<PlayerKVLoginPrepareError>().Where(e=>e!=PlayerKVLoginPrepareError.None))
|
||||
{
|
||||
var prepared=new PlayerKVLoginPrepareResult(error,"101",7,httpStatus:401,exceptionType:"SyntheticPrepare",cleanupExceptionType:"SyntheticClose",
|
||||
originalError:error==PlayerKVLoginPrepareError.CloseFailed ? PlayerKVLoginPrepareError.AuthenticationFailed : error);
|
||||
var task=(Task<PlayerKVGameLoadResult>)reject.Invoke(fixture.Coordinator,new object[]{PlayerKVGameLoadError.Prepare,7L,fixture.Capability(),prepared,null});
|
||||
var result=await task;
|
||||
Check(result.CleanupFailed && result.OriginalError==PlayerKVGameLoadError.Prepare && result.Prepare.Error==error && result.Prepare.OriginalError==prepared.OriginalError,"all historical errors preserved under CloseFailed");
|
||||
Check(result.Prepare.HttpStatusCode==401 && result.Prepare.ExceptionType=="SyntheticPrepare" && result.Prepare.CleanupExceptionType=="SyntheticClose" && fixture.Auth==0,"stage metadata retained with zero new authentication");
|
||||
}
|
||||
}));
|
||||
}
|
||||
static async Task<int> IsolatedCleanupFailure(string mode)
|
||||
{
|
||||
await Run("coordinator-cleanup-failure-"+mode,()=>OnMain(async()=>
|
||||
{
|
||||
var f=new LoadFixture{FailApply=mode=="apply",LeaseDisposeFails=mode!="prepare",AuthDisposeFails=mode=="prepare"};
|
||||
var result=await f.Coordinator.LoadAsync("101",f.Capability(mode=="schema" ? new Dictionary<string,string>() : null,mode=="prepare"));
|
||||
var expected=mode=="prepare" ? PlayerKVGameLoadError.Prepare : mode=="schema" ? PlayerKVGameLoadError.Schema : PlayerKVGameLoadError.Apply;
|
||||
Check(result.Error==PlayerKVGameLoadError.CloseFailed && result.OriginalError==expected && result.CleanupFailed,"cleanup failure preserves original stage");
|
||||
Check(result.Prepare!=null && result.ModuleCount==0 && result.IntentCount==0 && !f.Coordinator.LoadedForValidation,"failure carries evidence only");
|
||||
var next=new LoadFixture(f.Registry);Check((await next.Coordinator.LoadAsync("102",next.Capability())).Error==PlayerKVGameLoadError.PreviousCloseFailed && next.Auth==0,"failure persists across coordinators");
|
||||
Check(!PlayerKVGameLoadGuard.TryEnter(out _),"failed cleanup cannot release guard");
|
||||
}));
|
||||
Console.WriteLine($"RESULT isolated mode={mode} failed={failures} assertions={assertions}");return failures==0?0:1;
|
||||
}
|
||||
}
|
||||
31
CodexOutput/upserver/KVGameLoadTests/CubeSchemaTests.cs
Normal file
31
CodexOutput/upserver/KVGameLoadTests/CubeSchemaTests.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
const string CubeJson="{\"TicketCount\":1,\"FirstOpen\":true,\"IsFinished\":true,\"EventId\":2,\"IsInGuide\":true,\"StartTime\":\"2026-09-22T00:00:00.0000000Z\",\"EndTime\":\"2026-09-24T00:00:00.0000000Z\",\"CurrentStagetId\":3,\"CollectItems\":[{\"ItemId\":4,\"Count\":5,\"MaxCount\":6,\"ItemTrueId\":7}],\"BoardData\":[8,9],\"BoardViewSize\":{\"x\":10,\"y\":11},\"CollectRewardsId\":[12,13],\"IsFinalRewardGet\":true,\"PieceDatas\":[[14,15],[16,17]],\"Cycle\":18,\"InflationRate\":1.5}";
|
||||
static async Task RunCubeSchemaTests()
|
||||
{
|
||||
await Run("M19-cube-all-fields-nested-collections",() =>
|
||||
{
|
||||
var obj=JObject.Parse(CubeJson);
|
||||
Dictionary<string,string> Raw(JObject value)=>new() { ["EventCubeMeltData"]=value.ToString() };
|
||||
var state=PlayerKVSaveSchema.DecodeCube(Raw(obj),SchemaSnapshot(),new());
|
||||
Check(state.IsPresent && state.TicketCount==1 && state.FirstOpen && state.IsFinished && state.EventId==2 && state.IsInGuide &&
|
||||
state.StartTime.Day==22 && state.EndTime.Day==24 && state.CurrentStagetId==3,"base fields");
|
||||
Check(state.CollectItems[0].ItemId==4 && state.CollectItems[0].Count==5 && state.CollectItems[0].MaxCount==6 &&
|
||||
state.CollectItems[0].ItemTrueId==7 && state.BoardData.SequenceEqual(new[] { 8,9 }),"items and board");
|
||||
Check(state.Width==10 && state.Height==11 && state.CollectRewardsId.SequenceEqual(new[] { 12,13 }) &&
|
||||
state.IsFinalRewardGet && state.Cycle==18 && state.InflationRate==1.5f,"dimensions and progress");
|
||||
Check(state.PieceDatas[0].SequenceEqual(new[] { 14,15 }) && state.PieceDatas[1].SequenceEqual(new[] { 16,17 }),"nested pieces");
|
||||
foreach(var path in obj.Properties().Select(p=>p.Name).Concat(new[] { "CollectItems[0]","CollectItems[0].Count","BoardViewSize.x","BoardViewSize.y","PieceDatas[0]","PieceDatas[0][0]" }))
|
||||
{
|
||||
var bad=(JObject)obj.DeepClone(); bad.SelectToken(path).Replace(JValue.CreateNull());
|
||||
Check(PlayerKVSaveSchema.DecodeCube(Raw(bad),SchemaSnapshot(),new())==null,"all nested nulls reject");
|
||||
}
|
||||
Check(!PlayerKVSaveSchema.DecodeCube(new Dictionary<string,string>(),SchemaSnapshot(),new()).IsPresent,"missing explicit");
|
||||
Check(PlayerKVSaveSchema.DecodeCube(new Dictionary<string,string> { ["EventCubeMeltData"]="null" },SchemaSnapshot(),new())==null,"null not missing");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
220
CodexOutput/upserver/KVGameLoadTests/D3SchemaTests.cs
Normal file
220
CodexOutput/upserver/KVGameLoadTests/D3SchemaTests.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
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");
|
||||
});
|
||||
}
|
||||
}
|
||||
44
CodexOutput/upserver/KVGameLoadTests/EventBaseSchemaTests.cs
Normal file
44
CodexOutput/upserver/KVGameLoadTests/EventBaseSchemaTests.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using game.PlayerDataStorage;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunEventBaseSchemaTests()
|
||||
{
|
||||
await Run("M05-frozen-time-conflicts-and-whole-replacement",() =>
|
||||
{
|
||||
var raw=new Dictionary<string,string> {
|
||||
["LimitedTimeEventDic"]="{\"1\":{\"id\":2,\"duration\":60.123456789,\"startTime\":\"2026-09-23T00:00:00.0000000Z\"},\"3\":{\"id\":4,\"duration\":60,\"startTime\":\"2026-09-22T00:00:00.0000000Z\"}}",
|
||||
["FixedTimeEventDic"]="{\"5\":{\"id\":6,\"endTime\":\"raw\"}}",
|
||||
["ETDropAfterDropDic"]="{\"1\":2,\"3\":4}",["RankTargetCount"]="{\"5\":6,\"7\":8}",
|
||||
["WheelLevelCount"]="{\"9\":10}",["TurntableRoll"]="11",["WheelOperateCount"]="12",["AllWheelOperateCount"]="13",["WheelLevelFreeCount"]="14",["PVPToken"]="15"
|
||||
};
|
||||
var modern=new Dictionary<string,string>{["ct"]="1:2",["rt"]="5:6"};
|
||||
var a=PlayerKVSaveSchema.DecodeEventBase(raw,modern,SchemaSnapshot(),new());
|
||||
Check(a!=null && a.Limited.Count==1 && a.Limited[1].Id==2 && a.Limited[1].Duration==60.123456789 && a.Fixed[5].id==6,"frozen filter double precision");
|
||||
var offsetSnapshot=new PlayerKVSchemaSnapshot("offset",Array.Empty<int>(),Array.Empty<int>(),new Dictionary<string,int>(),System.Globalization.CultureInfo.InvariantCulture,
|
||||
SchemaSnapshot().NowUtc,17,globalTimeOffset:9);
|
||||
var boundary=new Dictionary<string,string> { ["LimitedTimeEventDic"]="{\"1\":{\"id\":2,\"duration\":60,\"startTime\":\"2026-09-22T15:00:00.0000000Z\"}}" };
|
||||
Check(PlayerKVSaveSchema.DecodeEventBase(boundary,new Dictionary<string,string>(),offsetSnapshot,new()).Limited.Count==1 &&
|
||||
PlayerKVSaveSchema.DecodeEventBase(boundary,new Dictionary<string,string>(),SchemaSnapshot(),new()).Limited.Count==0,"captured game time offset used");
|
||||
Check(a.Target.Count==1 && a.Target[1]==2 && a.Rank.Count==1 && a.Rank[5]==6 && a.Wheel[9]==10,"whole new map wins");
|
||||
Check(a.Coupon==11 && a.Operate==12 && a.AllOperate==13 && a.Free==14 && a.PvpToken==15,"all scalars");
|
||||
foreach(var key in raw.Keys)
|
||||
Check(PlayerKVSaveSchema.DecodeEventBase(new Dictionary<string,string>(raw){[key]="null"},modern,SchemaSnapshot(),new())==null,"each bad key rejected");
|
||||
modern["ct"]="1:99";
|
||||
var e=new PlayerKVSchemaDiagnostics();
|
||||
Check(PlayerKVSaveSchema.DecodeEventBase(raw,modern,SchemaSnapshot(),e)==null && e.Count(5,PlayerKVSchemaError.ConflictingRepresentations)==1,"overlap mismatch fails");
|
||||
modern["ct"]=""; modern["rt"]="";
|
||||
a=PlayerKVSaveSchema.DecodeEventBase(raw,modern,SchemaSnapshot(),new());
|
||||
Check(a.Target.Count==0 && a.Rank.Count==0,"empty new replaces old");
|
||||
foreach(var invalid in new[]{"1:2,01:2","1:2,","null",null})
|
||||
{
|
||||
modern["ct"]=invalid;
|
||||
Check(PlayerKVSaveSchema.DecodeEventBase(raw,modern,SchemaSnapshot(),new())==null,"bad compact fails");
|
||||
}
|
||||
a=PlayerKVSaveSchema.DecodeEventBase(new Dictionary<string,string>(),new Dictionary<string,string>(),SchemaSnapshot(),new());
|
||||
Check(a.Limited.Count==0 && a.Fixed.Count==0 && a.Target.Count==0 && a.Rank.Count==0 && a.Wheel.Count==0 && a.Coupon==17 && a.Operate==0 && a.AllOperate==0 && a.Free==0 && a.PvpToken==0,"missing captured config default");
|
||||
Check(PlayerKVSaveSchema.DecodeEventBase(new Dictionary<string,string>{["LimitedTimeEventDic"]="{\"1\":{\"id\":2,\"duration\":-1,\"startTime\":\"bad\"}}"},new Dictionary<string,string>(),SchemaSnapshot(),new())==null,"expired candidate malformed time fails");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
98
CodexOutput/upserver/KVGameLoadTests/FishSchemaTests.cs
Normal file
98
CodexOutput/upserver/KVGameLoadTests/FishSchemaTests.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.Globalization;
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static PlayerKVSchemaSnapshot FishSnapshot(Dictionary<int,PlayerKVFishConfig> fish=null,Dictionary<int,PlayerKVFishCardConfig> cards=null) =>
|
||||
new("fish-v1",new[]{101},new[]{1,2},new Dictionary<string,int>(),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<float>()) });
|
||||
static Dictionary<string,string> FishRaw(string record="2|99|1|7|True") => new() { ["Map_5_1"]=JsonConvert.SerializeObject(new Dictionary<int,string>{[101]=record}) };
|
||||
static PlayerKVFishState FishDecode(Dictionary<string,string> raw,PlayerKVSchemaSnapshot snapshot=null,Dictionary<string,string> 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<string,string>(),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<int,PlayerKVFishConfig>{[101]=new(101,1,5,11,2,1,0)};
|
||||
var cards=new Dictionary<int,PlayerKVFishCardConfig>{[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<string,string>{["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<string,string>(),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<int>(),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<int,PlayerKVFishConfig>()))==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<string,string>{["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<string,string>(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<int,PlayerKVFishConfig>{[101]=new(101,1,6,11,1,1,0),[102]=new(102,2,6,11,1,1,0)};
|
||||
var raw=new Dictionary<string,string>{["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<string,string>(),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;
|
||||
});
|
||||
}
|
||||
}
|
||||
32
CodexOutput/upserver/KVGameLoadTests/KVGameLoadTests.csproj
Normal file
32
CodexOutput/upserver/KVGameLoadTests/KVGameLoadTests.csproj
Normal file
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework>
|
||||
<DefineConstants>UNITY_EDITOR</DefineConstants><ImplicitUsings>enable</ImplicitUsings><Nullable>disable</Nullable>
|
||||
<RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="ServiceHost/**/*.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVInitializationIntents.cs" Link="Production/PlayerKVInitializationIntents.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVGameLoadTransaction.cs" Link="Production/PlayerKVGameLoadTransaction.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVGameLoadGuard.cs" Link="Production/PlayerKVGameLoadGuard.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVControlledLoadCapability.cs" Link="Production/PlayerKVControlledLoadCapability.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVGameLoadCoordinator.cs" Link="Production/PlayerKVGameLoadCoordinator.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVSaveSchema.cs" Link="Production/PlayerKVSaveSchema.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVDecodedSave.cs" Link="Production/PlayerKVDecodedSave.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVShutdownRegistry.cs" Link="Production/PlayerKVShutdownRegistry.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVLoginContext.cs" Link="Production/PlayerKVLoginContext.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVLoginPrepareResult.cs" Link="Production/PlayerKVLoginPrepareResult.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVGameSessionBridge.cs" Link="Production/PlayerKVGameSessionBridge.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVModels.cs" Link="Production/PlayerKVModels.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVClient.cs" Link="Production/PlayerKVClient.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVSyncModels.cs" Link="Production/PlayerKVSyncModels.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVCacheStore.cs" Link="Production/PlayerKVCacheStore.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVSyncState.cs" Link="Production/PlayerKVSyncState.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVSyncCoordinator.cs" Link="Production/PlayerKVSyncCoordinator.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVSessionOwner.cs" Link="Production/PlayerKVSessionOwner.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVLocalReadResult.cs" Link="Production/PlayerKVLocalReadResult.cs" />
|
||||
<Compile Include="../../../Client/Assets/Scripts/Services/Account/S101AccountSession.cs" Link="Production/S101AccountSession.cs" />
|
||||
<Reference Include="Newtonsoft.Json"><HintPath>../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll</HintPath></Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
42
CodexOutput/upserver/KVGameLoadTests/LoginSchemaTests.cs
Normal file
42
CodexOutput/upserver/KVGameLoadTests/LoginSchemaTests.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using game.PlayerDataStorage;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static PlayerKVIdentitySnapshot Identity(bool fresh=false,string id="FF",PlayerKVIdentityChannel channel=PlayerKVIdentityChannel.none) =>
|
||||
new(id,fresh,channel,Array.Empty<PlayerKVIdentityChannel>());
|
||||
static PlayerKVLoginState LoginDecode(Dictionary<string,string> raw,PlayerKVIdentitySnapshot identity=null) =>
|
||||
PlayerKVSaveSchema.DecodeLogin(raw,identity ?? Identity(),new());
|
||||
static async Task RunLoginSchemaTests()
|
||||
{
|
||||
await Run("M31-MMTest-identity-boundaries",()=>
|
||||
{
|
||||
Check(LoginDecode(new(){["MMTest"]="MMFailParam#-1"}).MMTest==-1,"declared signed value retained");
|
||||
Check(LoginDecode(new(){["MMTest"]="MMFailParam#12"},Identity(true,"bad-id")).Normalizations.Count==0,"existing value does not use PF id");
|
||||
foreach(var text in new[]{"old#123","", "something"}) { var a=LoginDecode(new(){["MMTest"]=text}); Check(a.MMTest==0 && a.Normalizations["MMTest"]=="MMFailParam#0","old format normalized"); }
|
||||
Check(LoginDecode(new()).MMTest==0 && LoginDecode(new(),Identity(true)).MMTest==15,"PF newly created determines bucket");
|
||||
Check(LoginDecode(new(),Identity(false,"not-hex")).MMTest==0,"old PF profile ignores id format");
|
||||
foreach(var id in new[]{"",null," 1","-1","0xFF","10000000000000000","invalid"}) Check(LoginDecode(new(),Identity(true,id))==null,"invalid new PF id");
|
||||
foreach(var mm in new[]{null,"MMFailParam","MMFailParam#","MMFailParam#1#2","MMFailParam#2147483648","MMFailParam#1.0"}) Check(LoginDecode(new(){["MMTest"]=mm})==null,"malformed current layout");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await Run("M31-channels-url-balance-and-intents",async()=>
|
||||
{
|
||||
var input=new Dictionary<string,string>{["hwGoogle"]="1",["hwFacebook"]="0",["Apple"]="1",["EAccoutUrl"]="{\"synthetic\":\"avatar\"}",["TapJoy_Diamond"]="-3"};
|
||||
var a=LoginDecode(input,Identity(channel:PlayerKVIdentityChannel.hwVkid));
|
||||
Check(a.Linked.SequenceEqual(new[]{PlayerKVIdentityChannel.hwGoogle,PlayerKVIdentityChannel.Apple,PlayerKVIdentityChannel.hwVkid}) && a.TapJoyBalance==-3 && a.AccountUrls["synthetic"]=="avatar","merged private channels, url and balance");
|
||||
Check(a.Normalizations.Count==2 && a.Normalizations["hwVkid"]=="1" && !input.ContainsKey("MMTest"),"two normalized copies without input mutation");
|
||||
input.Clear(); Check(a.AccountUrls.Count==1 && a.Normalizations.Count==2,"independent immutable data");
|
||||
foreach(var key in new[]{"hwGoogle","hwFacebook","Apple","hwVkid"}) foreach(var value in new[]{null,"true","2","01"})
|
||||
Check(LoginDecode(new(){[key]=value},Identity(channel:PlayerKVIdentityChannel.hwVkid))==null,"flags exact 0/1");
|
||||
foreach(var key in new[]{"EAccoutUrl","TapJoy_Diamond"}) Check(LoginDecode(new(){[key]="null"})==null,"bad optional field not missing");
|
||||
var missing=LoginDecode(new()); Check(missing.AccountUrls.Count==0 && missing.TapJoyBalance==0 && missing.Linked.Count==0,"empty state");
|
||||
var intents=new PlayerKVInitializationIntents(7,PlayerKVIdentityChannel.hwVkid);
|
||||
intents.Add(7,"MMTest","MMFailParam#1"); intents.Add(7,"MMTest","MMFailParam#2"); intents.Add(7,"hwVkid","1");
|
||||
var captured=intents.Snapshot; Check(intents.Count==2 && captured["MMTest"]=="MMFailParam#2","dedupe and last value");
|
||||
await Throws<InvalidOperationException>(()=>{intents.Add(8,"MMTest","x");return Task.CompletedTask;});
|
||||
await Throws<InvalidOperationException>(()=>{intents.Add(7,"other","x");return Task.CompletedTask;});
|
||||
intents.Discard(); intents.Discard(); Check(intents.Discarded && intents.Count==0 && intents.DiscardedCount==2 && captured.Count==2,"discard idempotent, historical immutable view");
|
||||
await Throws<InvalidOperationException>(()=>{intents.Add(7,"MMTest","x");return Task.CompletedTask;});
|
||||
});
|
||||
}
|
||||
}
|
||||
43
CodexOutput/upserver/KVGameLoadTests/OfferSchemaTests.cs
Normal file
43
CodexOutput/upserver/KVGameLoadTests/OfferSchemaTests.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunOfferSchemaTests()
|
||||
{
|
||||
foreach(bool disco in new[] { false,true })
|
||||
await Run(disco ? "M17-offer-nested-shape-and-enum" : "M16-offer-nested-shape-and-state",() =>
|
||||
{
|
||||
var obj=JObject.FromObject(new { FirstOpen=true,EventId=7,StartTime="2026-09-22T00:00:00.0000000Z",EndTime="2026-09-24T00:00:00.0000000Z",
|
||||
ItemId=-1,LotteryNum=5,Items=new[] { new { Id=1,Item=2,Number=3,Status=2 } },RewardList=new[] { 8,9 },Finished=true,ChestReward=10 });
|
||||
if (disco) { obj.Remove("Finished"); obj.Remove("ChestReward"); }
|
||||
string key=disco ? "OfferDiscoPartyManager" : "OfferChainsChestData";
|
||||
Dictionary<string,string> Raw(JObject value)=>new() { [key]=value.ToString(Formatting.None) };
|
||||
var state=PlayerKVSaveSchema.DecodeOffer(Raw(obj),disco,SchemaSnapshot(),new());
|
||||
Check(state.IsPresent && state.FirstOpen && state.EventId==7 && state.StartTime.Day==22 && state.EndTime.Day==24 &&
|
||||
state.ItemId == -1 && state.LotteryNum==5,"all scalar fields");
|
||||
Check(state.Items.Count==1 && state.Items[0].Id==1 && state.Items[0].Item==2 && state.Items[0].Number==3 && state.Items[0].Status==2 &&
|
||||
state.RewardList.SequenceEqual(new[] { 8,9 }),"complete nested collections");
|
||||
Check(disco || (state.Finished && state.ChestReward==10),"chains only fields");
|
||||
Check(!PlayerKVSaveSchema.DecodeOffer(new Dictionary<string,string>(),disco,SchemaSnapshot(),new()).IsPresent,"missing explicit");
|
||||
var paths=obj.Properties().Select(p=>p.Name).Concat(new[] { "Items[0]","Items[0].Id","Items[0].Item","Items[0].Number","Items[0].Status","RewardList[0]" });
|
||||
foreach(var path in paths)
|
||||
{
|
||||
var bad=(JObject)obj.DeepClone(); bad.SelectToken(path).Replace(JValue.CreateNull());
|
||||
Check(PlayerKVSaveSchema.DecodeOffer(Raw(bad),disco,SchemaSnapshot(),new())==null,"all consumed null fields rejected");
|
||||
}
|
||||
foreach(var path in new[] { "EventId","Items[0].Id","Items[0].Status","RewardList[0]" })
|
||||
{
|
||||
var bad=(JObject)obj.DeepClone(); bad.SelectToken(path).Replace(new JValue(1.5));
|
||||
Check(PlayerKVSaveSchema.DecodeOffer(Raw(bad),disco,SchemaSnapshot(),new())==null,"integer coercion rejected");
|
||||
}
|
||||
if(disco)
|
||||
{
|
||||
var bad=(JObject)obj.DeepClone(); bad["Items"][0]["Status"]=3;
|
||||
Check(PlayerKVSaveSchema.DecodeOffer(Raw(bad),true,SchemaSnapshot(),new())==null,"undefined enum rejected");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
56
CodexOutput/upserver/KVGameLoadTests/PartnerSchemaTests.cs
Normal file
56
CodexOutput/upserver/KVGameLoadTests/PartnerSchemaTests.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static JObject PartnerFixture() => JObject.FromObject(new
|
||||
{
|
||||
FirstOpen=false, EventId=18, StartTime="2026-09-23T00:00:00.0000000Z", EndTime="2026-09-24T00:00:00.0000000Z",
|
||||
CurrentPoints=1,CurrentMiningProgress=2,Token=3,InvitedCount=4,TicketCount=5,
|
||||
CompletedNodesList=new[]{new[]{6,7},Array.Empty<int>()},BuilderPartnerScore=new[]{new{Item1=8,Item2=9}},
|
||||
PartnerBonus=new{Item1=1.25f,Item2="2026-09-23T01:00:00.0000000Z"},FreeSpinNum=10,
|
||||
Bots=new Dictionary<int,object>{[11]=new{Id="synthetic-bot",ScorePlayer=12,ScoreBot=13,Grade=14,TargetScore=15,WakeStartTime="2026-09-23T02:00:00.0000000Z",WakingSeconds=16}},
|
||||
IsRecvFinalReward=true,UIFinishedPartners=new[]{true,false},IsFinished=true,SpinState=2,InflationRate=2.5f,
|
||||
IsEnableAutoMatch=true,LastAutoMatchTime="2026-09-23T03:00:00.0000000Z"
|
||||
});
|
||||
static PlayerKVPartnerState PartnerDecode(JObject o,PlayerKVSchemaDiagnostics errors=null) => PlayerKVSaveSchema.DecodePartner(
|
||||
new Dictionary<string,string>{["EventPartnerGatherData"]=o.ToString(Formatting.None)},SchemaSnapshot(),errors ?? new());
|
||||
static async Task RunPartnerSchemaTests()
|
||||
{
|
||||
await Run("M18-all-fields-missing-deep-copy",()=>
|
||||
{
|
||||
var o=PartnerFixture(); var a=PartnerDecode(o);
|
||||
Check(a.Present && !a.FirstOpen && a.EventId==18 && a.StartTime.Day==23 && a.EndTime.Day==24,"identity and dates");
|
||||
Check(a.CurrentPoints==1 && a.CurrentMiningProgress==2 && a.Token==3 && a.InvitedCount==4 && a.TicketCount==5,"counts");
|
||||
Check(a.CompletedNodesList[0].SequenceEqual(new[]{6,7}) && a.CompletedNodesList[1].Count==0 && a.BuilderPartnerScore[0]==(8,9),"nested nodes and tuple");
|
||||
Check(a.BonusPercent==1.25f && a.BonusExpireUtc.Hour==1 && a.FreeSpinNum==10,"bonus tuple");
|
||||
var b=a.Bots[11]; Check(b.Id=="synthetic-bot" && b.ScorePlayer==12 && b.ScoreBot==13 && b.Grade==14 && b.TargetScore==15 && b.WakeStartTime.Hour==2 && b.WakingSeconds==16,"all bot data");
|
||||
Check(a.IsRecvFinalReward && a.UIFinishedPartners.SequenceEqual(new[]{true,false}) && a.IsFinished && a.SpinState==2 && a.InflationRate==2.5f && a.IsEnableAutoMatch && a.LastAutoMatchTime.Hour==3,"remaining scalar/list fields");
|
||||
o["CompletedNodesList"][0][0]=999; ((JObject)o["Bots"]).RemoveAll();
|
||||
Check(a.CompletedNodesList[0][0]==6 && a.Bots.Count==1,"detached input");
|
||||
var missing=PlayerKVSaveSchema.DecodePartner(new Dictionary<string,string>(),SchemaSnapshot(),new());
|
||||
Check(!missing.Present && missing.Bots.Count==0 && missing.CompletedNodesList.Count==0 && missing.BuilderPartnerScore.Count==0 && missing.UIFinishedPartners.Count==0,"missing is empty");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await Run("M18-present-strict-fields-and-nested-values",()=>
|
||||
{
|
||||
foreach(var property in PartnerFixture().Properties())
|
||||
{
|
||||
var o=PartnerFixture(); o.Remove(property.Name); Check(PartnerDecode(o)==null,"each field required");
|
||||
o=PartnerFixture(); o[property.Name]=JValue.CreateNull(); Check(PartnerDecode(o)==null,"each field rejects null");
|
||||
}
|
||||
foreach(var path in new[]{"PartnerBonus.Item1","PartnerBonus.Item2","BuilderPartnerScore[0].Item1","BuilderPartnerScore[0].Item2",
|
||||
"CompletedNodesList[0]","CompletedNodesList[0][0]","UIFinishedPartners[0]","Bots.11.Id","Bots.11.ScorePlayer","Bots.11.ScoreBot",
|
||||
"Bots.11.Grade","Bots.11.TargetScore","Bots.11.WakeStartTime","Bots.11.WakingSeconds"})
|
||||
{ var o=PartnerFixture(); o.SelectToken(path).Replace(JValue.CreateNull()); Check(PartnerDecode(o)==null,"nested null rejected"); }
|
||||
foreach(var spin in new[]{-1,3,int.MaxValue}) { var o=PartnerFixture(); o["SpinState"]=spin; Check(PartnerDecode(o)==null,"undefined enum rejected"); }
|
||||
var invalid=PartnerFixture(); invalid["PartnerBonus"]["Item2"]="not-a-date"; Check(PartnerDecode(invalid)==null,"invalid date");
|
||||
invalid=PartnerFixture(); invalid["InflationRate"]=double.MaxValue; Check(PartnerDecode(invalid)==null,"nonfinite float");
|
||||
invalid=PartnerFixture(); invalid["Token"]=(long)int.MaxValue+1; Check(PartnerDecode(invalid)==null,"int overflow");
|
||||
foreach(var raw in new[]{"null","[]","{\"EventId\":1,\"EventId\":2}"})
|
||||
{ var e=new PlayerKVSchemaDiagnostics(); Check(PlayerKVSaveSchema.DecodePartner(new Dictionary<string,string>{["EventPartnerGatherData"]=raw},SchemaSnapshot(),e)==null && e.Count(18,PlayerKVSchemaError.Malformed)==1,"strict root"); }
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
26
CodexOutput/upserver/KVGameLoadTests/PinataSchemaTests.cs
Normal file
26
CodexOutput/upserver/KVGameLoadTests/PinataSchemaTests.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunPinataSchemaTests()
|
||||
{
|
||||
await Run("M29-pinata-three-integer-record",() =>
|
||||
{
|
||||
var valid=JObject.Parse("{\"EventID\":1,\"PinataMainID\":2,\"PurchasedCount\":3}");
|
||||
Dictionary<string,string> Raw(JObject obj)=>new() { ["EventPinataPackData"]=obj.ToString() };
|
||||
var a=PlayerKVSaveSchema.DecodePinata(Raw(valid),new());
|
||||
Check(a.EventID==1 && a.PinataMainID==2 && a.PurchasedCount==3,"all scalars");
|
||||
foreach(var p in valid.Properties())
|
||||
foreach(var invalid in new JToken[] { JValue.CreateNull(),new JValue("1"),new JValue(1.5),new JValue(2147483648L) })
|
||||
{
|
||||
var bad=(JObject)valid.DeepClone(); bad[p.Name]=invalid;
|
||||
Check(PlayerKVSaveSchema.DecodePinata(Raw(bad),new())==null,"strict integer fields");
|
||||
}
|
||||
var missing=PlayerKVSaveSchema.DecodePinata(new Dictionary<string,string>(),new());
|
||||
Check(missing.EventID==0 && missing.PinataMainID==0 && missing.PurchasedCount==0,"all missing zeros");
|
||||
Check(PlayerKVSaveSchema.DecodePinata(new Dictionary<string,string> { ["EventPinataPackData"]="null" },new())==null,"null not missing");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
40
CodexOutput/upserver/KVGameLoadTests/PlayerSchemaTests.cs
Normal file
40
CodexOutput/upserver/KVGameLoadTests/PlayerSchemaTests.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunPlayerSchemaTests()
|
||||
{
|
||||
await Run("M01-player-additional-fields-and-ownership",() =>
|
||||
{
|
||||
var raw=ModernRaw(); raw["NewRodList"]="[7,8]"; raw["SeasonId"]="9"; raw["IsOpenAppraise"]=""; raw["InfiniteBuildingData"]="raw";
|
||||
var modern=new Dictionary<string,string> { ["newData"]=string.Join(",",CompleteCore),["vipL"]="-1,5|0,6|1,7" };
|
||||
var a=PlayerKVSaveSchema.DecodePlayer(raw,modern,SchemaSnapshot(),new());
|
||||
Check(a!=null && a.VipScores[-1]==5 && a.VipScores[0]==6 && a.VipScores[1]==7 &&
|
||||
a.NewRods.SequenceEqual(new[] { 7,8 }) && a.Season==9 && a.Appraise && a.Building=="raw","all extra fields");
|
||||
modern["vipL"]="";
|
||||
Check(a.Modern["vipL"]=="-1,5|0,6|1,7","modern copied");
|
||||
var b=PlayerKVSaveSchema.DecodePlayer(ModernRaw(),modern,SchemaSnapshot(),new());
|
||||
Check(b.NewRods.Count==0 && b.VipScores.Count==0 && b.Season==0 && !b.Appraise && b.Building=="0","missing building restores original zero default");
|
||||
foreach(var text in new[]{"", "0", "23,7"})
|
||||
{
|
||||
var existing=ModernRaw(); existing["InfiniteBuildingData"]=text;
|
||||
Check(PlayerKVSaveSchema.DecodePlayer(existing,modern,SchemaSnapshot(),new()).Building==text,"existing building string unchanged");
|
||||
}
|
||||
foreach(var vip in new[] { "0,1|00,2","0,1|","1,2,3","null",null })
|
||||
{
|
||||
modern["vipL"]=vip;
|
||||
Check(PlayerKVSaveSchema.DecodePlayer(raw,modern,SchemaSnapshot(),new())==null,"bad vip map rejects");
|
||||
}
|
||||
modern["vipL"]="";
|
||||
foreach(var key in new[] { "NewRodList","SeasonId","IsOpenAppraise","InfiniteBuildingData" })
|
||||
{
|
||||
var bad=new Dictionary<string,string>(raw) { [key]=null };
|
||||
Check(PlayerKVSaveSchema.DecodePlayer(bad,modern,SchemaSnapshot(),new())==null,"present null extra fails");
|
||||
}
|
||||
var missingCore=new Dictionary<string,string>();
|
||||
Check(PlayerKVSaveSchema.DecodePlayer(missingCore,new Dictionary<string,string>(),SchemaSnapshot(),new())==null,"no default account creation");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
54
CodexOutput/upserver/KVGameLoadTests/PotionSchemaTests.cs
Normal file
54
CodexOutput/upserver/KVGameLoadTests/PotionSchemaTests.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunPotionSchemaTests()
|
||||
{
|
||||
await Run("M25-potion-complete-nested-and-enums",() =>
|
||||
{
|
||||
var item=new { name="synthetic",index=3,isOver=true,DataType=2,ColorTypeList=new[] { 7,9 },ShelfTypeList=new[] { 0,1 },GridStateList=new[] { 0,5 },FromDisplayAreaIndexList=new[] { 1,2 } };
|
||||
var obj=JObject.FromObject(new { EventID=11,PotionID=11,StageID=11,Stage1TotalLevel=11,Stage2TotalLevel=11,LevelID=11,LevelIndex=11,CorrectNum=11,SubmitNum=11,Token=11,ConsumedToken=11,Rule=11,PreEventID=11,PrePotionID=11,PreStageID=11,PreLevelID=11,Round=11,FirstPlayedEventId=11,IsOver=true,Name="synthetic",InflationRate=1.5f,LevelTargetData=item,LevelDisplayData=item,LevelProcessedList=new[] { item } });
|
||||
Dictionary<string,string> Raw(JObject value)=>new() { ["epdk"]=value.ToString() };
|
||||
var state=PlayerKVSaveSchema.DecodePotion(Raw(obj),new());
|
||||
Check(state!=null && state.IsPresent,"present valid");
|
||||
Check(state.EventID==11,"EventID");
|
||||
Check(state.PotionID==11,"PotionID");
|
||||
Check(state.StageID==11,"StageID");
|
||||
Check(state.Stage1TotalLevel==11,"Stage1TotalLevel");
|
||||
Check(state.Stage2TotalLevel==11,"Stage2TotalLevel");
|
||||
Check(state.LevelID==11,"LevelID");
|
||||
Check(state.LevelIndex==11,"LevelIndex");
|
||||
Check(state.CorrectNum==11,"CorrectNum");
|
||||
Check(state.SubmitNum==11,"SubmitNum");
|
||||
Check(state.Token==11,"Token");
|
||||
Check(state.ConsumedToken==11,"ConsumedToken");
|
||||
Check(state.Rule==11,"Rule");
|
||||
Check(state.PreEventID==11,"PreEventID");
|
||||
Check(state.PrePotionID==11,"PrePotionID");
|
||||
Check(state.PreStageID==11,"PreStageID");
|
||||
Check(state.PreLevelID==11,"PreLevelID");
|
||||
Check(state.Round==11,"Round");
|
||||
Check(state.FirstPlayedEventId==11,"FirstPlayedEventId");
|
||||
Check(state.IsOver==true,"IsOver");
|
||||
Check(state.Name=="synthetic","Name");
|
||||
Check(state.InflationRate==1.5f,"InflationRate");
|
||||
foreach(var entry in new[] { state.LevelTargetData,state.LevelDisplayData,state.LevelProcessedList[0] })
|
||||
Check(entry.Name=="synthetic" && entry.Index==3 && entry.IsOver && entry.DataType==2 && entry.Colors.SequenceEqual(new[] { 7,9 }) &&
|
||||
entry.Shelves.SequenceEqual(new[] { 0,1 }) && entry.Grids.SequenceEqual(new[] { 0,5 }) && entry.FromDisplay.SequenceEqual(new[] { 1,2 }),"all nested fields");
|
||||
foreach(var path in obj.Properties().Select(p=>p.Name).Concat(new[] { "LevelTargetData.name","LevelTargetData.index","LevelTargetData.isOver","LevelTargetData.DataType",
|
||||
"LevelTargetData.ColorTypeList","LevelTargetData.ShelfTypeList","LevelTargetData.GridStateList","LevelTargetData.FromDisplayAreaIndexList","LevelProcessedList[0]" }))
|
||||
{
|
||||
var bad=(JObject)obj.DeepClone(); bad.SelectToken(path).Replace(JValue.CreateNull());
|
||||
Check(PlayerKVSaveSchema.DecodePotion(Raw(bad),new())==null,"all nested nulls rejected");
|
||||
}
|
||||
foreach(var path in new[] { "LevelTargetData.DataType","LevelTargetData.ShelfTypeList[0]","LevelTargetData.GridStateList[0]" })
|
||||
{
|
||||
var bad=(JObject)obj.DeepClone(); bad.SelectToken(path).Replace(new JValue(99));
|
||||
Check(PlayerKVSaveSchema.DecodePotion(Raw(bad),new())==null,"undefined enum rejected");
|
||||
}
|
||||
Check(!PlayerKVSaveSchema.DecodePotion(new Dictionary<string,string>(),new()).IsPresent,"missing distinct");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
46
CodexOutput/upserver/KVGameLoadTests/PrecompiledAudit.cs
Normal file
46
CodexOutput/upserver/KVGameLoadTests/PrecompiledAudit.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using System.Security.Cryptography;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
static class PrecompiledAudit
|
||||
{
|
||||
internal static int Run()
|
||||
{
|
||||
var root=Path.GetFullPath(Path.Combine(AppContext.BaseDirectory,"../../../../../.."));
|
||||
if(!Directory.Exists(Path.Combine(root,"Client/Assets"))) throw new InvalidOperationException("Project root required.");
|
||||
var files=new[]{"Client/Assets","Client/Packages","Client/LocalPackages","Client/Library/PackageCache"}.SelectMany(p=>Directory.Exists(Path.Combine(root,p)) ?
|
||||
Directory.EnumerateFiles(Path.Combine(root,p),"*.dll",SearchOption.AllDirectories) : Array.Empty<string>()).OrderBy(p=>p,StringComparer.Ordinal).ToArray();
|
||||
var rows=new List<object>(); int blocked=0,managed=0,native=0;
|
||||
foreach(var file in files)
|
||||
{
|
||||
string kind="managed"; var references=new List<string>(); var assemblies=new List<string>();
|
||||
using var stream=File.OpenRead(file);
|
||||
try
|
||||
{
|
||||
using var pe=new PEReader(stream);
|
||||
if(!pe.HasMetadata) { native++; kind="native"; }
|
||||
else
|
||||
{
|
||||
managed++; var reader=pe.GetMetadataReader();
|
||||
foreach(var h in reader.AssemblyReferences)
|
||||
{ var name=reader.GetString(reader.GetAssemblyReference(h).Name); if(name=="HotUpdate") assemblies.Add(name); }
|
||||
foreach(var h in reader.MemberReferences)
|
||||
{
|
||||
var member=reader.GetMemberReference(h); string name=reader.GetString(member.Name);
|
||||
if(name!="BuilderCapacity" || member.Parent.Kind!=HandleKind.TypeReference) continue;
|
||||
var t=reader.GetTypeReference((TypeReferenceHandle)member.Parent);
|
||||
if(reader.GetString(t.Namespace)=="EventPartnerGather" && reader.GetString(t.Name)=="PartnerInfoManager") references.Add("EventPartnerGather.PartnerInfoManager.BuilderCapacity");
|
||||
}
|
||||
if(references.Count>0) blocked++;
|
||||
}
|
||||
}
|
||||
catch(BadImageFormatException) { kind="unreadable-PE"; blocked++; }
|
||||
rows.Add(new { path=Path.GetRelativePath(root,file).Replace('\\','/'),sha256=Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(file))),kind,fieldReferences=references,hotUpdateReferences=assemblies });
|
||||
}
|
||||
var report=new { success=blocked==0,scope="Assets-Packages-LocalPackages-PackageCache-dlls",total=files.Length,managed,native,blocked,files=rows };
|
||||
File.WriteAllText(Path.Combine(root,"CodexOutput/upserver/KVGameLoadTests/checkpoint2-D4-precompiled-audit.json"),JsonConvert.SerializeObject(report,Formatting.Indented));
|
||||
Console.WriteLine($"PRECOMPILED total={files.Length} managed={managed} native={native} blocked={blocked}");
|
||||
return blocked==0 ? 0 : 1;
|
||||
}
|
||||
}
|
||||
304
CodexOutput/upserver/KVGameLoadTests/Program.cs
Normal file
304
CodexOutput/upserver/KVGameLoadTests/Program.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using game.Account;
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static readonly Uri Api = new("http://synthetic.invalid/");
|
||||
static readonly string RunId = DateTime.UtcNow.ToString("yyyyMMddHHmmss") + "_" + Guid.NewGuid().ToString("N");
|
||||
static readonly string Root = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../runs", RunId));
|
||||
static readonly List<object> Results = new();
|
||||
static int assertions, failures;
|
||||
static async Task<int> Main(string[] args)
|
||||
{
|
||||
if(args.Length==1 && args[0]=="--audit-precompiled") return PrecompiledAudit.Run();
|
||||
if(args.Length==2 && args[0]=="--cleanup-failure") return await IsolatedCleanupFailure(args[1]);
|
||||
Directory.CreateDirectory(Root);
|
||||
await Run("B2-C1-metadata-one-auth-shared-cookie", SingleAuthentication);
|
||||
await Run("B2-C1-lease-once-before-after-close", Admission);
|
||||
await Run("B2-C1-denied-routes-zero-send", Routes);
|
||||
await Run("B2-C1-real-handler-rejected", RealHandlerRejected);
|
||||
await Run("B2-C1-cyclic-handler-rejected-without-recursive-dispose", CyclicHandlerRejected);
|
||||
await Run("B2-C1-active-send-drains-before-session", Drain);
|
||||
await Run("B2-C1-reentrant-cancel-same-task", Reentry);
|
||||
await Run("B2-C1-dispose-failure-retains-session", DisposeFailure);
|
||||
await Run("B2-C1-issuance-close-race", IssuanceRace);
|
||||
await Run("B2-C1-factory-revokes-context", FactoryRevocation);
|
||||
await Run("B2-C1-stale-success-body-discarded", StaleResponse);
|
||||
await Run("B2-C1-401-and-redirect-evidence", Responses);
|
||||
await Run("B2-C1-registry-timeout-then-real-drain", RegistryTimeout);
|
||||
await Run("B2-C1-registry-failure-retained", RegistryFailure);
|
||||
await Run("B2-C1-registry-fails-before-other-drains", RegistryEarlyFailure);
|
||||
await Run("B2-C1-new-failure-wakes-existing-observer", RegistryNewFailure);
|
||||
await Run("B2-C1-registry-registration-during-await", RegistryRegistration);
|
||||
await Run("B2-C1-registry-invalid-cancel", RegistryCancellation);
|
||||
await Run("B2-C1-process-registry-cross-caller", ProcessBarrier);
|
||||
await RunSchemaTests();
|
||||
await RunD3Tests();
|
||||
await RunCollectionSchemaTests();
|
||||
await RunClubPackSchemaTests();
|
||||
await RunCampEventSchemaTests();
|
||||
await RunOfferSchemaTests();
|
||||
await RunBuffTriggerSchemaTests();
|
||||
await RunActivitySchemaTests();
|
||||
await RunPinataSchemaTests();
|
||||
await RunCubeSchemaTests();
|
||||
await RunBattlePassSchemaTests();
|
||||
await RunPotionSchemaTests();
|
||||
await RunPlayerSchemaTests();
|
||||
await RunShopSchemaTests();
|
||||
await RunEventBaseSchemaTests();
|
||||
await RunFishSchemaTests();
|
||||
await RunPartnerSchemaTests();
|
||||
await RunLoginSchemaTests();
|
||||
await RunAggregateTests();
|
||||
await RunCoordinatorTests();
|
||||
var report = new { runId = RunId, checkpoint = "4-validation", failures, assertions, accessedFormalServer = false, tests = Results };
|
||||
await File.WriteAllTextAsync(Path.Combine(Root, "result.json"), JsonConvert.SerializeObject(report, Formatting.Indented));
|
||||
Console.WriteLine($"RESULT tests={Results.Count} failed={failures} assertions={assertions} run={RunId}");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
static async Task Run(string name, Func<Task> test)
|
||||
{
|
||||
var before = assertions;
|
||||
try { await test().WaitAsync(TimeSpan.FromSeconds(30)); Results.Add(new { name, passed = true, assertions = assertions - before }); Console.WriteLine("PASS " + name); }
|
||||
catch (Exception ex) { failures++; Results.Add(new { name, passed = false, error = ex.GetType().Name }); Console.WriteLine("FAIL " + name + " " + ex); }
|
||||
}
|
||||
static void Check(bool value, string message) { Interlocked.Increment(ref assertions); if (!value) throw new InvalidOperationException(message); }
|
||||
sealed class CyclicHandler : DelegatingHandler { }
|
||||
static async Task CyclicHandlerRejected()
|
||||
{
|
||||
var first = new CyclicHandler();
|
||||
var second = new CyclicHandler();
|
||||
first.InnerHandler = second;
|
||||
second.InnerHandler = first;
|
||||
var capability = PlayerKVControlledLoadCapability.CreateForValidation(_ => first);
|
||||
await Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
capability.CreateHandler(new CookieContainer());
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
// No send occurred. Break the rejected graph before its owner disposes it.
|
||||
second.InnerHandler = new Handler((_, _) => throw new InvalidOperationException("Must not send."));
|
||||
first.Dispose();
|
||||
}
|
||||
static TaskCompletionSource<bool> Signal() => new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
static HttpResponseMessage Json(string text, HttpStatusCode code = HttpStatusCode.OK) => new(code) { Content = new StringContent(text, Encoding.UTF8, "application/json") };
|
||||
static async Task Throws<T>(Func<Task> action) where T : Exception
|
||||
{ try { await action(); } catch (T) { Check(true, "expected exception"); return; } throw new InvalidOperationException("Expected " + typeof(T).Name); }
|
||||
sealed class Handler : HttpMessageHandler
|
||||
{
|
||||
readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> send;
|
||||
readonly Action dispose;
|
||||
internal Handler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> send, Action dispose = null)
|
||||
{ this.send = send; this.dispose = dispose; }
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage r, CancellationToken t) => send(r, t);
|
||||
protected override void Dispose(bool disposing) { if (disposing) dispose?.Invoke(); base.Dispose(disposing); }
|
||||
}
|
||||
sealed class Fixture
|
||||
{
|
||||
internal readonly List<string> Order = new();
|
||||
internal int Auth, Gets, BorrowedSends, SessionDisposes, LeaseDisposes;
|
||||
internal bool Current = true, FailLeaseDispose;
|
||||
internal Action DuringFactory;
|
||||
internal CookieContainer Jar;
|
||||
internal Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> Borrowed = (_, _) => Task.FromResult(Json("{}"));
|
||||
internal PlayerKVLoginContext Context;
|
||||
internal void Record(string x) { lock (Order) Order.Add(x); }
|
||||
internal Fixture()
|
||||
{
|
||||
Context = new PlayerKVLoginContext("101", Path.Combine(Root, "cache", Guid.NewGuid().ToString("N")), 7,
|
||||
_ => Current, Api,
|
||||
() => new S101AccountSession(Api, jar =>
|
||||
{
|
||||
Jar = jar;
|
||||
return new Handler((r, t) =>
|
||||
{
|
||||
Check(r.RequestUri.AbsolutePath == "/account/Login", "one synthetic auth route");
|
||||
Auth++;
|
||||
jar.Add(Api, new Cookie("sid", "synthetic"));
|
||||
return Task.FromResult(Json("{\"state\":0,\"internalId\":17,\"newCreate\":true,\"createTime\":\"2026-01-01T00:00:00Z\",\"svrTime\":\"2026-01-02T00:00:00Z\"}"));
|
||||
}, () => { SessionDisposes++; Record("session"); });
|
||||
}),
|
||||
() => new PlayerKVGameSessionBridge(jar =>
|
||||
{
|
||||
Check(ReferenceEquals(Jar, jar), "bridge shares cookie identity");
|
||||
return new Handler((r, t) =>
|
||||
{
|
||||
Check(r.RequestUri.AbsolutePath == "/playerdata/Get", "get only, zero KV mutation");
|
||||
Gets++; return Task.FromResult(Json("{\"data\":{}}"));
|
||||
}, () => Record("bridge"));
|
||||
}));
|
||||
}
|
||||
internal PlayerKVControlledLoadCapability Capability() => PlayerKVControlledLoadCapability.CreateForValidation(jar =>
|
||||
{
|
||||
Check(ReferenceEquals(Jar, jar), "borrower shares exact cookie identity");
|
||||
DuringFactory?.Invoke();
|
||||
return new Handler((r, t) => { BorrowedSends++; return Borrowed(r, t); }, () =>
|
||||
{ LeaseDisposes++; Record("lease"); if (FailLeaseDispose) throw new IOException("synthetic cleanup fault"); });
|
||||
});
|
||||
internal async Task<PlayerKVAccountLease> PreparedLease()
|
||||
{
|
||||
Check((await Context.PrepareAsync()).Prepared, "prepared");
|
||||
Check(Context.TryAcquireAccountLease(Capability(), out var lease), "lease admitted");
|
||||
return lease;
|
||||
}
|
||||
}
|
||||
static async Task SingleAuthentication()
|
||||
{
|
||||
var f = new Fixture(); var lease = await f.PreparedLease();
|
||||
var d = f.Context.AccountDescriptor;
|
||||
Check(d.PlayerId == "101" && d.Epoch == 7 && d.InternalId == 17 && d.NewlyCreated, "metadata belongs to authentication");
|
||||
Check(d.CreateTimeUtc.Year == 2026 && d.ServerTimeUtc.Day == 2, "metadata time");
|
||||
Check((await lease.SendAsync(HttpMethod.Get, "__kv_validation/ok")).IsCurrent, "borrowed send");
|
||||
Check(f.Auth == 1 && f.Gets == 1 && f.BorrowedSends == 1, "no second login");
|
||||
await f.Context.CloseAsync();
|
||||
Check(string.Join(",", f.Order) == "lease,bridge,session", "borrower drains before bridge and session");
|
||||
Check(f.Context.AccountDescriptor == null && !lease.IsCurrent, "closed admission revoked");
|
||||
}
|
||||
static async Task Admission()
|
||||
{
|
||||
var f = new Fixture(); Check(!f.Context.TryAcquireAccountLease(f.Capability(), out _), "before prepared rejected");
|
||||
var lease = await f.PreparedLease();
|
||||
Check(!f.Context.TryAcquireAccountLease(f.Capability(), out _), "one lease only");
|
||||
Check(!f.Context.TryAcquireAccountLease(null, out _), "capability required");
|
||||
var close = f.Context.CloseAsync(); Check(!f.Context.TryAcquireAccountLease(f.Capability(), out _), "close synchronous");
|
||||
await close;
|
||||
Check(!(await lease.SendAsync(HttpMethod.Get, "__kv_validation/ok")).IsCurrent && f.BorrowedSends == 0, "after close no transport");
|
||||
}
|
||||
static async Task Routes()
|
||||
{
|
||||
var f = new Fixture(); var l = await f.PreparedLease();
|
||||
foreach (var route in new[] { "account/Login", "playerdata/Get", "playerdata/Update", "http://synthetic.invalid/__kv_validation/ok", "//synthetic.invalid/x", "__kv_validation/../ok", "__kv_validation/ok?x=1", "%5f%5fkv_validation/ok", "__kv_validation/ok#f" })
|
||||
await Throws<ArgumentException>(() => l.SendAsync(HttpMethod.Get, route));
|
||||
Check(f.BorrowedSends == 0, "rejected routes never sent"); await f.Context.CloseAsync();
|
||||
}
|
||||
static async Task RealHandlerRejected()
|
||||
{
|
||||
var f = new Fixture(); await f.Context.PrepareAsync();
|
||||
await Throws<InvalidOperationException>(() =>
|
||||
{ f.Context.TryAcquireAccountLease(PlayerKVControlledLoadCapability.CreateForValidation(_ => new HttpClientHandler()), out _); return Task.CompletedTask; });
|
||||
Check(f.BorrowedSends == 0, "real handler never sends"); await f.Context.CloseAsync();
|
||||
}
|
||||
static async Task Drain()
|
||||
{
|
||||
var f = new Fixture(); var entered = Signal(); var release = Signal(); var cancelled = Signal();
|
||||
f.Borrowed = async (_, token) => { token.Register(() => cancelled.TrySetResult(true)); entered.TrySetResult(true); await release.Task; f.Record("send-ended"); return Json("late"); };
|
||||
var l = await f.PreparedLease(); var send = l.SendAsync(HttpMethod.Get, "__kv_validation/ok"); await entered.Task;
|
||||
var close = f.Context.CloseAsync(); await cancelled.Task;
|
||||
Check(!close.IsCompleted && f.SessionDisposes == 0 && f.LeaseDisposes == 0, "no premature dispose despite cancellation");
|
||||
release.TrySetResult(true); var response = await send; await close;
|
||||
Check(!response.IsCurrent && response.Body == null, "late response withheld");
|
||||
Check(string.Join(",", f.Order) == "send-ended,lease,bridge,session", "full resource order");
|
||||
}
|
||||
static async Task Reentry()
|
||||
{
|
||||
var f = new Fixture(); var entered = Signal(); var release = Signal(); var callback = Signal(); Task reentered = null;
|
||||
f.Borrowed = async (_, token) => { token.Register(() => { reentered = f.Context.CloseAsync(); callback.TrySetResult(true); }); entered.TrySetResult(true); await release.Task; return Json("{}"); };
|
||||
var l = await f.PreparedLease(); var send = l.SendAsync(HttpMethod.Get, "__kv_validation/ok"); await entered.Task;
|
||||
var close = f.Context.CloseAsync(); await callback.Task;
|
||||
Check(ReferenceEquals(close, reentered), "cancellation reentry same task, no cycle");
|
||||
release.TrySetResult(true); await send; await close;
|
||||
}
|
||||
static async Task DisposeFailure()
|
||||
{
|
||||
var f = new Fixture { FailLeaseDispose = true }; var l = await f.PreparedLease();
|
||||
var close = f.Context.CloseAsync(); await Throws<IOException>(() => close);
|
||||
Check(f.SessionDisposes == 0 && !f.Order.Contains("bridge"), "failed borrower cleanup prevents owner disposal");
|
||||
Check(ReferenceEquals(close, f.Context.CloseAsync()) && !f.Context.TryAcquireAccountLease(f.Capability(), out _), "failure cannot reopen");
|
||||
var registry = new PlayerKVShutdownRegistry(); registry.Register(close, f.Context);
|
||||
Check(registry.Status == PlayerKVShutdownStatus.Failed, "fault retained");
|
||||
var retained = (Dictionary<Task, PlayerKVLoginContext>)typeof(PlayerKVShutdownRegistry)
|
||||
.GetField("pending", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(registry)!;
|
||||
Check(ReferenceEquals(retained[close], f.Context), "failed owner retained beyond caller lifetime");
|
||||
}
|
||||
static async Task IssuanceRace()
|
||||
{
|
||||
for (int i = 0; i < 24; i++)
|
||||
{
|
||||
var f = new Fixture(); await f.Context.PrepareAsync(); var go = Signal();
|
||||
var issue = Task.Run(async () => { await go.Task; return f.Context.TryAcquireAccountLease(f.Capability(), out _); });
|
||||
var close = Task.Run(async () => { await go.Task; await f.Context.CloseAsync(); });
|
||||
go.TrySetResult(true); await Task.WhenAll(issue, close);
|
||||
Check(f.SessionDisposes == 1 && !f.Context.IsCurrent, "race closes session once");
|
||||
if (issue.Result) Check(f.LeaseDisposes == 1, "published borrower observed by close");
|
||||
}
|
||||
}
|
||||
static async Task FactoryRevocation()
|
||||
{
|
||||
var f = new Fixture(); await f.Context.PrepareAsync(); f.DuringFactory = () => { _ = f.Context.CloseAsync(); };
|
||||
Check(!f.Context.TryAcquireAccountLease(f.Capability(), out var l) && l == null, "reentrant factory cannot publish");
|
||||
await f.Context.CloseAsync(); Check(f.LeaseDisposes == 1 && f.SessionDisposes == 1, "candidate still cleaned");
|
||||
}
|
||||
static async Task StaleResponse()
|
||||
{
|
||||
var f = new Fixture(); f.Borrowed = (_, _) => { f.Current = false; return Task.FromResult(Json("should-not-escape")); };
|
||||
var l = await f.PreparedLease(); var r = await l.SendAsync(HttpMethod.Get, "__kv_validation/ok");
|
||||
Check(!r.IsCurrent && r.Body == null && r.StatusCode == null, "200 does not restore old epoch"); await f.Context.CloseAsync();
|
||||
}
|
||||
static async Task Responses()
|
||||
{
|
||||
var f = new Fixture(); var l = await f.PreparedLease();
|
||||
f.Borrowed = (_, _) => Task.FromResult(Json("{}", HttpStatusCode.Unauthorized));
|
||||
Check((await l.SendAsync(HttpMethod.Get, "__kv_validation/unauthorized")).StatusCode == 401, "401 evidence for version-aware caller");
|
||||
f.Borrowed = (_, _) => Task.FromResult(Json("{}", HttpStatusCode.Redirect));
|
||||
await Throws<InvalidOperationException>(() => l.SendAsync(HttpMethod.Get, "__kv_validation/ok")); await f.Context.CloseAsync();
|
||||
}
|
||||
static async Task RegistryTimeout()
|
||||
{
|
||||
var registry = new PlayerKVShutdownRegistry(); var closing = Signal(); var deadline = Signal(); registry.Register(closing.Task);
|
||||
var observe = registry.ObserveAsync(10000, delay: (_, _) => deadline.Task); deadline.TrySetResult(true);
|
||||
Check(await observe == PlayerKVShutdownStatus.Pending && !closing.Task.IsCompleted, "timeout not force-close");
|
||||
closing.TrySetResult(true); await closing.Task;
|
||||
Check(registry.Status == PlayerKVShutdownStatus.Ready, "real completion opens barrier");
|
||||
}
|
||||
static async Task RegistryFailure()
|
||||
{
|
||||
var registry = new PlayerKVShutdownRegistry(); var fail = Signal(); registry.Register(fail.Task); fail.TrySetException(new IOException("synthetic"));
|
||||
Check(await registry.ObserveAsync(10000) == PlayerKVShutdownStatus.Failed, "fault classified");
|
||||
registry.Register(Task.CompletedTask); Check(registry.Status == PlayerKVShutdownStatus.Failed, "new task cannot erase failure");
|
||||
Check(!typeof(PlayerKVShutdownRegistry).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Any(m => m.Name == "Clear" || m.Name == "Reset"), "no reset API");
|
||||
}
|
||||
static async Task RegistryRegistration()
|
||||
{
|
||||
var registry = new PlayerKVShutdownRegistry(); var first = Signal(); var second = Signal(); var deadline = Signal();
|
||||
registry.Register(first.Task); var observe = registry.ObserveAsync(10000, delay: (_, _) => deadline.Task);
|
||||
registry.Register(second.Task); first.TrySetResult(true); deadline.TrySetResult(true);
|
||||
Check(await observe == PlayerKVShutdownStatus.Pending, "newly registered work not missed"); second.TrySetResult(true);
|
||||
}
|
||||
static async Task RegistryEarlyFailure()
|
||||
{
|
||||
var registry = new PlayerKVShutdownRegistry(); var fail = Signal(); var slow = Signal(); var deadline = Signal();
|
||||
registry.Register(fail.Task); registry.Register(slow.Task);
|
||||
var observe = registry.ObserveAsync(10000, delay: (_, _) => deadline.Task);
|
||||
fail.TrySetException(new IOException("synthetic"));
|
||||
Check(await observe == PlayerKVShutdownStatus.Failed && !slow.Task.IsCompleted && !deadline.Task.IsCompleted,
|
||||
"failure reported without waiting for unrelated cleanup or observation deadline");
|
||||
slow.TrySetResult(true);
|
||||
}
|
||||
static async Task RegistryNewFailure()
|
||||
{
|
||||
var registry = new PlayerKVShutdownRegistry(); var slow = Signal(); var deadline = Signal();
|
||||
registry.Register(slow.Task); var observe = registry.ObserveAsync(10000, delay: (_, _) => deadline.Task);
|
||||
registry.Register(Task.FromException(new IOException("synthetic")));
|
||||
Check(await observe == PlayerKVShutdownStatus.Failed && !slow.Task.IsCompleted && !deadline.Task.IsCompleted,
|
||||
"new root failure wakes existing observer"); slow.TrySetResult(true);
|
||||
}
|
||||
static async Task RegistryCancellation()
|
||||
{
|
||||
var registry = new PlayerKVShutdownRegistry(); var close = Signal(); registry.Register(close.Task);
|
||||
Check(await registry.ObserveAsync(0) == PlayerKVShutdownStatus.Input, "invalid timeout Input");
|
||||
using var cts = new CancellationTokenSource(); var observed = registry.ObserveAsync(10000, cts.Token); cts.Cancel();
|
||||
Check(await observed == PlayerKVShutdownStatus.Cancelled && !close.Task.IsCompleted, "observer cancellation does not cancel cleanup"); close.TrySetResult(true);
|
||||
}
|
||||
static async Task ProcessBarrier()
|
||||
{
|
||||
var firstRoot = PlayerKVShutdownRegistry.Process; var close = Signal(); firstRoot.Register(close.Task);
|
||||
var secondRoot = PlayerKVShutdownRegistry.Process;
|
||||
Check(ReferenceEquals(firstRoot, secondRoot) && secondRoot.Status == PlayerKVShutdownStatus.Pending, "new caller same process barrier");
|
||||
close.TrySetResult(true); Check(await secondRoot.ObserveAsync(1000) == PlayerKVShutdownStatus.Ready, "drained across caller replacement");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#if KV_EXIT_REGRESSION
|
||||
namespace game
|
||||
{
|
||||
// Boundary double for the actual QuitAct. Real UserService is verified by the Unity service host.
|
||||
public class PlayFabUserService : IUserService
|
||||
{
|
||||
internal int Revocations,Logouts;
|
||||
internal readonly TaskCompletionSource<bool> Observing=new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
internal readonly TaskCompletionSource<bool> Observed=new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public void Logout() { Logouts++; }
|
||||
internal Task CloseKVForValidationAsync() { Revocations++; return Task.CompletedTask; }
|
||||
internal Task ObserveKVQuitAsync() { Observing.TrySetResult(true); return Observed.Task; }
|
||||
}
|
||||
}
|
||||
static class ControlledExitProbe
|
||||
{
|
||||
[System.Runtime.CompilerServices.ModuleInitializer]
|
||||
internal static void Run()
|
||||
{
|
||||
foreach(bool logout in new[]{true,false})
|
||||
{
|
||||
GameCore.Timer.Reset();
|
||||
var user=new game.PlayFabUserService(); var hub=new Hub(); var ads=new Ads();
|
||||
var act=new game.QuitAct { userService=user,rtService=hub,config=new Config(),adsService=ads,soundMgr=new SoundMgr() };
|
||||
game.QuitStage.Logout=logout;
|
||||
// Module initialization cannot block on code executing in the same module on another thread.
|
||||
// Complete the boundary doubles up front; the historical async Hub cases still run unchanged.
|
||||
hub.Gate.TrySetResult(true); user.Observed.TrySetResult(true);
|
||||
var task=act.StartAsync(); task.GetAwaiter().GetResult();
|
||||
if(user.Revocations!=1 || user.Logouts!=(logout ? 1 : 0) || !user.Observing.Task.IsCompleted) throw new Exception("KV exit boundary was skipped.");
|
||||
if(!ads.Released || GameCore.Timer.Destroyed!=1) throw new Exception("Scene exit must continue after observation.");
|
||||
Console.WriteLine("PASS actual QuitAct controlled revoke / Hub / bounded-observer boundary, logout="+logout);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
153
CodexOutput/upserver/KVGameLoadTests/SchemaTests.cs
Normal file
153
CodexOutput/upserver/KVGameLoadTests/SchemaTests.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
33
CodexOutput/upserver/KVGameLoadTests/SealDelivery.ps1
Normal file
33
CodexOutput/upserver/KVGameLoadTests/SealDelivery.ps1
Normal file
@@ -0,0 +1,33 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$root=(Resolve-Path (Join-Path $PSScriptRoot '../../..')).Path
|
||||
Set-Location $root
|
||||
& "$PSScriptRoot/VerifyBoundary.ps1"
|
||||
if($LASTEXITCODE -ne 0){throw 'Boundary verification failed'}
|
||||
$paths=[Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
||||
$original=Get-Content 'CodexOutput/upserver/阶段4B2方案受审基线-修订2-2026-09-23.json' -Raw | ConvertFrom-Json
|
||||
foreach($f in $original.files){[void]$paths.Add($f.path)}
|
||||
foreach($file in Get-ChildItem 'Client/Assets/Scripts/Services/PlayerData/PlayerKV*' -File){[void]$paths.Add($file.FullName.Substring($root.Length+1).Replace('\','/'))}
|
||||
foreach($path in @('Client/Assets/Scripts/Services/UserService.KVLoad.cs','Client/Assets/Editor/KVGameLoadValidation.cs','Client/Assets/Scripts/EventPartnerGather/PartnerInfoManager.cs')){
|
||||
[void]$paths.Add($path);if(Test-Path -LiteralPath ($path+'.meta')){[void]$paths.Add($path+'.meta')}
|
||||
}
|
||||
foreach($file in Get-ChildItem $PSScriptRoot -Recurse -File){
|
||||
$relative=$file.FullName.Substring($root.Length+1).Replace('\','/')
|
||||
if($relative -notmatch '/(bin|obj|runs|unity-cache|\.appdata)/' -and $file.Extension -in @('.cs','.csproj','.targets','.ps1')){[void]$paths.Add($relative)}
|
||||
}
|
||||
$logs=@('checkpoint4-release-offline.log','checkpoint4-release-full.log','checkpoint4-final-unity-cases.log','checkpoint4-final4-compile.log','checkpoint4-cleanup-final-prepare.log','checkpoint4-cleanup-final-schema.log','checkpoint4-cleanup-final-apply.log','checkpoint4-service-release-false.log','checkpoint4-service-release-true.log','checkpoint4-noneditor-factories.log','checkpoint4-regression-KVLoginContextTests.log','checkpoint4-regression-KVGameBridgeTests.log','checkpoint4-regression-KVContractTests.log','checkpoint4-regression-KVSyncTests.log','checkpoint4-regression-KVSyncTests-serial.log','checkpoint4-regression-KVSyncCoordinatorTests.log','checkpoint4-regression-KVSyncCoordinatorReviewTests.log','checkpoint4-regression-KVStage3CTests.log','checkpoint4-regression-KVLiveTests.log','checkpoint4-network-build-final.log','checkpoint3-network-regression.log','checkpoint3-minimal-login-regression.log','checkpoint3-exit-final-regression.log','checkpoint2-D4-unity-real.log','checkpoint2-D4-unity-hot-real.log','checkpoint2-D5-rounding-real.log','checkpoint2-D4-imported-dependencies-final.log')
|
||||
$privacyFlags=@()
|
||||
foreach($name in $logs){
|
||||
$path="CodexOutput/upserver/KVGameLoadTests/$name";[void]$paths.Add($path)
|
||||
$text=Get-Content -LiteralPath $path -Raw
|
||||
if($text -match '(?i)sid=|Authorization:\s*Bearer\s+\S+|Cookie:\s*\S+|C:\\Users\\LB'){$privacyFlags+=$path}
|
||||
}
|
||||
foreach($name in @('checkpoint4-boundary-and-evidence.json','checkpoint2-D4-precompiled-audit.json','checkpoint2-D4-dependency-conclusion.json','checkpoint2-D4-source-references.txt','baseline/ClubService.cs.txt','runs/20260923144334_75379546c3404789b03c7a27ddea7784/result.json')){[void]$paths.Add("CodexOutput/upserver/KVGameLoadTests/$name")}
|
||||
foreach($name in @('阶段4B2交付与验收矩阵-2026-09-23.md','阶段4B2限定代码自审-2026-09-23.md','其他AI审查提示词-4B2实施-2026-09-23.md','阶段4B2实施记录送审快照-2026-09-23.md','阶段4B2当前TodoItem-2026-09-23.json','阶段4B2字段与模块加载合同-正式版3-2026-09-23.md','阶段4B2检查点2配置与派生审查裁决-2026-09-23.md','阶段4B2检查点2配置隔离与鱼数据派生独立审查-2026-09-23.md','阶段4B2方案受审基线-修订2-2026-09-23.json','阶段4B2检查点2配置与派生差异受审基线-2026-09-23.json')){[void]$paths.Add("CodexOutput/upserver/$name")}
|
||||
$files=@(foreach($path in ($paths|Sort-Object)){
|
||||
if(!(Test-Path -LiteralPath $path -PathType Leaf)){throw "Missing artifact: $path"}
|
||||
[ordered]@{path=$path;sha256=(Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash}
|
||||
})
|
||||
if($privacyFlags.Count){$privacyFlags;throw 'Review log privacy scan requires inspection'}
|
||||
$head=git rev-parse HEAD
|
||||
[ordered]@{schemaVersion=1;phase='4B2-implementation';status='awaiting-independent-implementation-review';generatedAt=(Get-Date).ToString('o');gitHead=$head;gitCommitCreated=$false;formalRequests=0;stageClosed=$false;B3Started=$false;privacy=@{reviewLogs=$logs.Count;credentialOrUserPathFlags=$privacyFlags.Count;scope='bounded pattern scan of selected review logs; synthetic fixtures, no real account data'};note='Shared entry/status/TODO/live logs are excluded; immutable review snapshot included. Author verification is not independent approval.';files=$files} | ConvertTo-Json -Depth 7 | Set-Content "$PSScriptRoot/checkpoint4-delivery-hashes.json"
|
||||
Write-Output "Sealed $($files.Count) files; review-log privacy flags=$($privacyFlags.Count)."
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Reflection;
|
||||
using game.PlayerDataStorage;
|
||||
int checkedTypes=0;
|
||||
foreach(var type in new[]{typeof(PlayerKVGameLoadCapability),typeof(PlayerKVControlledLoadCapability)})
|
||||
{
|
||||
if(type.GetMethod("CreateForValidation",BindingFlags.Static|BindingFlags.Public|BindingFlags.NonPublic)!=null || type.IsPublic ||
|
||||
type.GetConstructors(BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance).Any(c=>!c.IsPrivate))
|
||||
{Console.WriteLine("FAIL player build capability construction surface");return 1;}
|
||||
checkedTypes++;
|
||||
}
|
||||
Console.WriteLine($"PASS non-editor production capability factories absent types={checkedTypes}; compile probe only, no Unity player/package produced");
|
||||
return 0;
|
||||
163
CodexOutput/upserver/KVGameLoadTests/ServiceHost/Program.cs
Normal file
163
CodexOutput/upserver/KVGameLoadTests/ServiceHost/Program.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using game;
|
||||
using game.Account;
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
using PlayFab.ClientModels;
|
||||
using tysdk;
|
||||
|
||||
static class Probe
|
||||
{
|
||||
static int assertions,groups;
|
||||
static void Check(bool condition,string label){assertions++;if(!condition)throw new Exception(label);}
|
||||
sealed class Loop:SynchronizationContext
|
||||
{ internal BlockingCollection<(SendOrPostCallback,object)> Queue=new();public override void Post(SendOrPostCallback c,object s)=>Queue.Add((c,s)); }
|
||||
static int Main() { try { RunHost(); return 0; } catch(Exception ex) { Console.WriteLine(ex); return 1; } }
|
||||
static void RunHost()
|
||||
{
|
||||
var loop=new Loop();SynchronizationContext.SetSynchronizationContext(loop);
|
||||
var task=Run();_=task.ContinueWith(_=>loop.Post(_=>{},null),TaskScheduler.Default);
|
||||
while(!task.IsCompleted){var(c,s)=loop.Queue.Take();c(s);}task.GetAwaiter().GetResult();
|
||||
#if UNITY_ANDROID
|
||||
const string platform="ANDROID+AGG";
|
||||
#else
|
||||
const string platform="NON_ANDROID";
|
||||
#endif
|
||||
Console.WriteLine($"RESULT service-host {platform} groups={groups} assertions={assertions} formalRequests=0");
|
||||
}
|
||||
sealed class Handler:HttpMessageHandler
|
||||
{
|
||||
readonly Func<HttpRequestMessage,Task<HttpResponseMessage>> send;
|
||||
internal Handler(Func<HttpRequestMessage,Task<HttpResponseMessage>> send)=>this.send=send;
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage r,CancellationToken t)=>send(r);
|
||||
}
|
||||
static Task<HttpResponseMessage> Json(object value,HttpStatusCode status=HttpStatusCode.OK)=>Task.FromResult(new HttpResponseMessage(status){Content=new StringContent(JsonConvert.SerializeObject(value))});
|
||||
static readonly Uri Api=new("http://synthetic.invalid/");
|
||||
static Config Configuration(){var c=new Config();c.Set("EventApiURL",Api.AbsoluteUri);c.Set("DEBUG","1");return c;}
|
||||
static readonly string[] Core={"3","4","4","5","6","7","101","2026-09-23T00:00:00Z","-1","8","9","9","1","0","10","11","12","13","14"};
|
||||
static Dictionary<string,string> Raw()=>new(){["NewPlayerDataDic"]=JsonConvert.SerializeObject(new{newData=string.Join(",",Core)}),["TapJoy_Diamond"]="9"};
|
||||
static PlayerKVSchemaSnapshot Snapshot()=>new("stub-v1",new[]{101},new[]{1,2},new Dictionary<string,int>{{"None",0}},CultureInfo.InvariantCulture,new DateTime(2026,9,23,0,0,0,DateTimeKind.Utc),17);
|
||||
static PlayerKVIdentitySnapshot Identity()=>new("FF",false,PlayerKVIdentityChannel.hwGuest,Array.Empty<PlayerKVIdentityChannel>());
|
||||
static object Private(object target,string method,params object[] args)=>target.GetType().GetMethod(method,BindingFlags.Instance|BindingFlags.NonPublic,null,args.Select(a=>a.GetType()).ToArray(),null).Invoke(target,args);
|
||||
static void Pass(string name){groups++;Console.WriteLine("PASS "+name);}
|
||||
static async Task Run()
|
||||
{
|
||||
foreach(bool agg in new[]{false,true})
|
||||
{
|
||||
var config=Configuration();PlayFabUserService user=agg?new AggUserService():new PlayFabUserService();user.config=config;user.CustomId="101";
|
||||
foreach(var backend in new[]{"kv","KV","unknown","",null})
|
||||
{
|
||||
config.Set("SaveBackend",backend);Effects.Clear();
|
||||
Check(await user.Login(null)==ELoginStep.Api,"public backend gate");
|
||||
Check(user.KVLastError==(backend=="kv"?PlayerKVGameLoadError.BackendNotEnabled:PlayerKVGameLoadError.BackendUnsupported),"typed rejection");
|
||||
Check(Effects.Total==0,"before all SDK/event access");
|
||||
}
|
||||
}
|
||||
Pass("public-backend-rejection-before-AGG-shortcut");
|
||||
var pf=new PlayFabMgr();Private(pf,"Awake");
|
||||
var data=new Dictionary<string,UserDataRecord>{{"MMTest",new(){Value="MMFailParam#4",LastUpdated=DateTime.UtcNow}},{"TapJoy_Diamond",new(){Value="7",LastUpdated=DateTime.UtcNow}}};
|
||||
pf.CompareDatas(data,"synthetic");pf.UpdateUserDataValue("local","value",false);
|
||||
Check(pf.GetLocalData("local")=="value","default PF local write unchanged");
|
||||
Check(await pf.SyncDataImmidiate("remote","value"),"default PF immediate result unchanged");
|
||||
Pass("default-PF-storage-passthrough");
|
||||
foreach(bool agg in new[]{false,true})
|
||||
{
|
||||
var config=Configuration();PlayFabUserService user=agg?new AggUserService():new PlayFabUserService();user.config=config;user.CustomId="101";
|
||||
var server=new CustomServerMgr(config,_=>new Handler(r=>Json(new{})));user.customServerMgr=server;
|
||||
using var session=new S101AccountSession(Api,jar=>new Handler(r=>{jar.Add(Api,new Cookie("sid","synthetic"));return Json(new{state=0,internalId=17,newCreate=false,createTime="2026-01-01T00:00:00Z",svrTime="2026-01-01T00:00:00Z"});}));
|
||||
Check((await session.LoginAsync("101")).Success,"PF synthetic account");
|
||||
typeof(PlayFabUserService).GetField("apiSession",BindingFlags.NonPublic|BindingFlags.Instance).SetValue(user,session);
|
||||
typeof(PlayFabUserService).GetProperty("InternalId").SetValue(user,(uint)17);PlayFab.PlayFabClientAPI.Logged=true;
|
||||
int progress=0;Check(await user.Login(v=>progress=v)==ELoginStep.Done && progress==(agg?3:2),"default PF authorized shortcut");
|
||||
config.Set("SaveBackend","kv");Effects.Clear();Check(await user.Login(null)==ELoginStep.Api && Effects.Total==0,"authorized cannot bypass backend gate");
|
||||
typeof(PlayFabUserService).GetField("apiSession",BindingFlags.NonPublic|BindingFlags.Instance).SetValue(user,null);
|
||||
}
|
||||
Pass("default-PF-and-AGG-shortcuts");
|
||||
GContext.container.Values[typeof(ISettingService)]=new Setting();GContext.container.Values[typeof(IChatService)]=new Chat();
|
||||
GContext.container.Values[typeof(PlayerData)]=new PlayerData();GContext.container.Values[typeof(MMTService)]=new MMTService();
|
||||
asap.playfab.async.PlayFabClientAsyncAPI.Result=new LoginResult{PlayFabId="FF",NewlyCreated=false,
|
||||
EntityToken=new EntityTokenResponse{Entity=new EntityKey()},InfoResultPayload=new GetPlayerCombinedInfoResultPayload{UserData=data,AccountInfo=new UserAccountInfo{Created=DateTime.UtcNow},PlayerProfile=new PlayerProfileModel{DisplayName="Synthetic",AvatarUrl="synthetic",Locations=new(){new LocationModel()}}}};
|
||||
// Invalid scheme ensures legacy API stage fails before HttpClient; no real endpoint is contacted.
|
||||
var normalConfig=Configuration();normalConfig.Set("EventApiURL","ftp://synthetic.invalid/");
|
||||
var normal=new PlayFabUserService{config=normalConfig,CustomId="101",customServerMgr=new CustomServerMgr(normalConfig,_=>new Handler(r=>throw new Exception("no HTTP")))};
|
||||
PlayFab.PlayFabClientAPI.Logged=false;Check(await normal.Login(null)==ELoginStep.Api,"normal PF reaches API boundary");
|
||||
Check(((PlayerData)GContext.container.Values[typeof(PlayerData)]).Loads==1 && normal.MMTestForKVValidation==4,"real PF init after adjusted time");
|
||||
var hotConfig=Configuration();hotConfig.Set("EventApiURL","ftp://synthetic.invalid/");
|
||||
var hot=new AggUserService{config=hotConfig,customServerMgr=new CustomServerMgr(hotConfig,_=>new Handler(r=>throw new Exception("no HTTP")))};
|
||||
var sdk=new TaskCompletionSource<LoginInfo>();TYSdkFacade.IsLoggedIn=false;TYSdkFacade.LoginOverride=()=>sdk.Task;PlayFab.PlayFabClientAPI.Logged=false;
|
||||
var pending=hot.Login(null);hotConfig.Set("SaveBackend","kv");sdk.SetResult(new(){isSuccess=true,userId=101});
|
||||
Check(await pending==ELoginStep.Api && hot.KVLastError==PlayerKVGameLoadError.None,"captured PF branch continues to API failure");
|
||||
Check(((PlayerData)GContext.container.Values[typeof(PlayerData)]).Loads==2,"AGG real PF init window");TYSdkFacade.LoginOverride=null;
|
||||
Pass("PF-original-init-order-and-backend-capture");
|
||||
UnityEngine.GameObject.Found=new TapjoyUnity.Internal.TapjoyComponent();Effects.Clear();
|
||||
typeof(PlayFabUserService).GetMethod("InitTapJoy",BindingFlags.Static|BindingFlags.NonPublic).Invoke(null,null);
|
||||
#if UNITY_ANDROID
|
||||
Check(Effects.Helpers==2,"PF Android creates and initializes helper");
|
||||
#else
|
||||
Check(Effects.Helpers==0,"PF non-Android no helper");
|
||||
#endif
|
||||
Pass("PF-original-TapJoy-platform-branch");
|
||||
UniRx.Stream<EvtAPISvrUnauthorized>.Handlers=null;
|
||||
var unauthorizedUser=new PlayFabUserService{config=normalConfig,CustomId="101",customServerMgr=new CustomServerMgr(normalConfig,_=>new Handler(r=>throw new Exception("no HTTP")))};
|
||||
typeof(PlayFabUserService).GetProperty("InternalId").SetValue(unauthorizedUser,(uint)17);PlayFab.PlayFabClientAPI.Logged=true;
|
||||
int version=unauthorizedUser.customServerMgr.SessionVersion;
|
||||
GContext.Publish(new EvtAPISvrUnauthorized{SessionVersion=version-1});Check(unauthorizedUser.InternalId==17,"PF stale 401 ignored");
|
||||
GContext.Publish(new EvtAPISvrUnauthorized{SessionVersion=version});
|
||||
Check(unauthorizedUser.InternalId==0 && unauthorizedUser.customServerMgr.SessionVersion>version,"PF current 401 retains old reauthentication branch, invalid API prevents HTTP");
|
||||
UniRx.Stream<EvtAPISvrUnauthorized>.Handlers=null;
|
||||
Pass("PF-current-and-stale-401-branches");
|
||||
Private(pf,"OnDestroy"); typeof(PlayFabMgr).GetField("_instance",BindingFlags.Static|BindingFlags.NonPublic).SetValue(null,null);
|
||||
var fresh=new PlayFabMgr();Check(fresh.IsUnstartedForKVLoad,"fresh PF component");
|
||||
Check(PlayerKVGameLoadGuard.TryEnter(out var guard),"guard entered");
|
||||
using(guard)
|
||||
{
|
||||
Effects.Clear();
|
||||
foreach(Action attempt in new Action[]{()=>fresh.CompareDatas(data,"x"),()=>fresh.GetLocalData("x"),()=>fresh.UpdateUserDataValue("x","y"),()=>fresh.UpdateUserDataValue("x","y",false)})
|
||||
{try{attempt();throw new Exception("guard missed");}catch(PlayerKVLoadSideEffectException){assertions++;}}
|
||||
Check(!await fresh.SyncDataImmidiate("x","y"),"immediate rejected");
|
||||
Private(fresh,"SyncData");Private(fresh,"Awake");Private(fresh,"OnApplicationPause",true);Private(fresh,"OnApplicationPause",false);Private(fresh,"OnApplicationQuit");Private(fresh,"OnDestroy");Private(fresh,"UpdateUserDataValue",CancellationToken.None);
|
||||
Check(Effects.Total==0 && fresh.IsUnstartedForKVLoad && guard.Violations==12,"all PF entrances reject before buffer/Prefs/SDK");
|
||||
}
|
||||
Pass("all-real-PF-guard-entrances");await Controlled();
|
||||
}
|
||||
sealed class Chat:IChatService {public void WaitingForLogin(){} }
|
||||
sealed class Setting:ISettingService {public string Lang=>"en";}
|
||||
static async Task Controlled()
|
||||
{
|
||||
int auth=0,get=0,borrow=0,unauthorizedEvents=0;Dictionary<string,string> raw=Raw();
|
||||
Task<HttpResponseMessage> delayedBorrow=null;
|
||||
var config=Configuration();var user=new AggUserService(PlayerKVControlledLoadCapability.CreateForValidation(_=>new Handler(r=>throw new Exception()))){config=config,CustomId="101"};
|
||||
var server=new CustomServerMgr(config,_=>new Handler(r=>throw new Exception("PF transport forbidden")));
|
||||
server.KVUnauthorized+=_=>unauthorizedEvents++;
|
||||
var modules=Enumerable.Range(1,31).Select(id=>(IPlayerKVLoadModule)new PlayerKVLoadModule(id,()=>{if(id==31)user.ResetForKVLoad();},decoded=>{if(id==31)user.ReplaceFromKVDecoded(decoded.Module<PlayerKVLoginState>(31),decoded.Epoch,decoded.Raw);})).ToArray();
|
||||
user.ConfigureKVForValidation(new PlayerKVGameLoadTransaction(modules,Environment.CurrentManagedThreadId,()=>user.KVIntents?.Discard()),new PlayFabMgr(),server,()=>true);
|
||||
PlayerKVGameLoadCapability Capability()=>PlayerKVGameLoadCapability.CreateForValidation(Identity(),Snapshot(),Api,Path.Combine(Path.GetTempPath(),"ft-kv-service-probe",Guid.NewGuid().ToString("N")),
|
||||
jar=>new Handler(r=>{auth++;jar.Add(Api,new Cookie("sid","synthetic"));return Json(new{state=0,internalId=17,newCreate=false,createTime="2026-01-01T00:00:00Z",svrTime="2026-01-01T00:00:00Z"});}),
|
||||
jar=>new Handler(r=>{get++;return Json(new{data=raw});}),jar=>new Handler(r=>{borrow++;return delayedBorrow ?? Json(new{},HttpStatusCode.Unauthorized);}));
|
||||
Effects.Clear();
|
||||
foreach(var invalid in new[]{"0","999","15001","bad","1.5"})
|
||||
{config.Set("KVLoginCloseObserveMs",invalid);Check((await user.LoadKVForValidationAsync(Capability())).Error==PlayerKVGameLoadError.Input,"invalid option rejected");Check(auth==0 && user.KVReceiverCountForValidation==0,"invalid no send/subscription leak");}
|
||||
config.Values.Remove("KVLoginCloseObserveMs");
|
||||
var result=await user.LoadKVForValidationAsync(Capability());
|
||||
Check(result.State==PlayerKVGameLoadState.LoadedForValidation && user.KVLoadedForValidation,"real service controlled success: "+result.Error);
|
||||
Check(auth==1 && get==1 && user.KVNotificationCount==1 && user.KVReceiverCountForValidation==0,"one auth/Get/windowed receiver");
|
||||
Check(!user.IsLogin && !user.IsAuthorized && user.InternalId==0 && user.KVTapJoyBalance==9 && Effects.Total==0,"zero SDK/Prefs/reward/UI/helpers/events in KV");
|
||||
var intents=user.KVIntents;await server.SendKVForValidationAsync("__kv_validation/unauthorized");await user.CloseKVForValidationAsync();
|
||||
Check(!user.KVLoadedForValidation && user.KVIntents==null && intents.Discarded && borrow==1 && auth==1,"401 revokes without reauth");
|
||||
raw.Clear();var failed=await user.LoadKVForValidationAsync(Capability());
|
||||
Check(failed.Error==PlayerKVGameLoadError.Schema && user.KVReceiverCountForValidation==0 && user.KVNotificationCount==1,"failure removes receiver without notification");
|
||||
Check(Effects.Total==0,"all failure paths zero effects");
|
||||
raw=Raw();Check((await user.LoadKVForValidationAsync(Capability())).State==PlayerKVGameLoadState.LoadedForValidation,"new epoch after successful cleanup");
|
||||
var late=new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);delayedBorrow=late.Task;
|
||||
var oldRequest=server.SendKVForValidationAsync("__kv_validation/unauthorized");user.Logout();Check(!user.KVLoadedForValidation,"logout synchronously revoked");
|
||||
late.SetResult(new HttpResponseMessage(HttpStatusCode.Unauthorized){Content=new StringContent("{}")});
|
||||
Check(!(await oldRequest).IsCurrent,"late 401 body discarded");await user.CloseKVForValidationAsync();
|
||||
Check(unauthorizedEvents==1 && auth==3 && Effects.Total==0 && user.KVIntents==null,"late 401 never reauthenticates or publishes another unauthorized event");
|
||||
Pass("real-Agg-User-CustomServer-coordinator-KV-chain");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings><Nullable>disable</Nullable>
|
||||
<DefineConstants>$(DefineConstants);UNITY_EDITOR;SERVICE_HOST</DefineConstants>
|
||||
<DefineConstants Condition="'$(AndroidProbe)'=='true'">$(DefineConstants);UNITY_ANDROID;AGG</DefineConstants>
|
||||
<DefineConstants Condition="'$(PlayerProbe)'=='true'">SERVICE_HOST</DefineConstants>
|
||||
<NoWarn>$(NoWarn);CS0436;CS1998</NoWarn>
|
||||
<RestoreConfigFile>$(MSBuildThisFileDirectory)../../MinimalLoginTests/NuGet.Config</RestoreConfigFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="FactoryProbe.cs" Condition="'$(PlayerProbe)'!='true'" />
|
||||
<Compile Remove="Program.cs" Condition="'$(PlayerProbe)'=='true'" />
|
||||
<Compile Include="../../../../Client/Assets/Scripts/Services/PlayerData/PlayerKV*.cs" Exclude="../../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVEditorValidationHost.cs" />
|
||||
<Compile Include="../../../../Client/Assets/Scripts/Services/Account/S101AccountSession.cs" />
|
||||
<Compile Include="../../../../Client/Assets/Scripts/Services/UserService.cs" />
|
||||
<Compile Include="../../../../Client/Assets/Scripts/Services/UserService.KVLoad.cs" />
|
||||
<Compile Include="../../../../Client/Assets/Scripts/Services/AggUserService.cs" />
|
||||
<Compile Include="../../../../Client/Assets/Scripts/Core/PlayFabMgr.cs" />
|
||||
<Compile Include="../../../../Client/Assets/Scripts/Core/CustomServerMgr.cs" />
|
||||
<Reference Include="PlayFab"><HintPath>../../../../Client/Library/ScriptAssemblies/PlayFab.dll</HintPath></Reference>
|
||||
<Reference Include="Newtonsoft.Json"><HintPath>../../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll</HintPath></Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
139
CodexOutput/upserver/KVGameLoadTests/ServiceHost/Stubs.cs
Normal file
139
CodexOutput/upserver/KVGameLoadTests/ServiceHost/Stubs.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
// Only SDK/engine/business dependencies are replaced. Four service sources are linked unchanged.
|
||||
using PlayFab.ClientModels;
|
||||
using Newtonsoft.Json;
|
||||
// Legacy desktop fallback names AndroidDeviceId on CustomID request (unreachable with validated CustomId).
|
||||
// Keep this compatibility DTO local; do not silently change the pre-existing PF branch in production.
|
||||
namespace PlayFab.ClientModels { public class LoginWithCustomIDRequest { public string CustomId,AndroidDeviceId;public bool CreateAccount;public GetPlayerCombinedInfoRequestParams InfoRequestParameters; } }
|
||||
static class Effects
|
||||
{
|
||||
internal static int SDK,Prefs,Events,Subscriptions,Helpers,Rewards,UI,Timers,Account;
|
||||
internal static void Clear() { SDK=Prefs=Events=Subscriptions=Helpers=Rewards=UI=Timers=Account=0; }
|
||||
internal static int Total => SDK+Prefs+Events+Subscriptions+Helpers+Rewards+UI+Timers+Account;
|
||||
}
|
||||
namespace UnityEngine
|
||||
{
|
||||
public class Object { public static implicit operator bool(Object value)=>value!=null; public static void DontDestroyOnLoad(Object o){} }
|
||||
public class GameObject:Object
|
||||
{
|
||||
public static object Found;
|
||||
public static T FindObjectOfType<T>() where T:class =>Found as T;
|
||||
public T AddComponent<T>() where T:new(){Effects.Helpers++;return new T();}
|
||||
}
|
||||
public class MonoBehaviour:Object { public GameObject gameObject=new(); public T GetComponent<T>() where T:class=>null; }
|
||||
public static class Debug { public static void Log(object o){} public static void LogWarning(object o){} public static void LogError(object o){} }
|
||||
public static class Application { public static string version="synthetic",identifier="101",persistentDataPath=Path.GetTempPath(); }
|
||||
public static class SystemInfo { public static string deviceUniqueIdentifier="101",deviceModel="stub"; }
|
||||
public static class PlayerPrefs
|
||||
{
|
||||
public static readonly Dictionary<string,string> Data=new();
|
||||
public static string GetString(string key,string fallback=""){Effects.Prefs++;return Data.GetValueOrDefault(key,fallback);}
|
||||
public static void SetString(string key,string value){Effects.Prefs++;Data[key]=value;}
|
||||
public static void Save(){Effects.Prefs++;}
|
||||
}
|
||||
}
|
||||
namespace asap.core
|
||||
{
|
||||
public class InjectAttribute:Attribute{}
|
||||
public interface IConfig { T Get<T>(string key,T fallback); void Set(string key,object value); }
|
||||
public static class Awaiters { public static Task NextFrame=>Task.CompletedTask; }
|
||||
}
|
||||
sealed class Config:asap.core.IConfig
|
||||
{
|
||||
internal readonly Dictionary<string,object> Values=new();
|
||||
public T Get<T>(string key,T fallback)=>Values.TryGetValue(key,out var v)?(T)v:fallback;
|
||||
public void Set(string key,object value)=>Values[key]=value;
|
||||
}
|
||||
namespace UniRx
|
||||
{
|
||||
public sealed class Stream<T>
|
||||
{ internal static Action<T> Handlers; public IDisposable Subscribe(Action<T> action){Effects.Subscriptions++;Handlers+=action;return new Subscription(()=>Handlers-=action);} }
|
||||
public sealed class Subscription:IDisposable { readonly Action action; public Subscription(Action action)=>this.action=action;public void Dispose()=>action(); }
|
||||
}
|
||||
public static class GContext
|
||||
{
|
||||
public static readonly Container container=new();
|
||||
public static UniRx.Stream<T> OnEvent<T>()=>new();
|
||||
public static void Publish<T>(T value){Effects.Events++;UniRx.Stream<T>.Handlers?.Invoke(value);}
|
||||
}
|
||||
public sealed class Container
|
||||
{ public readonly Dictionary<Type,object> Values=new();public T Resolve<T>()=> (T)Values[typeof(T)]; }
|
||||
public static class GameDebug { public static void LogError(string s,params object[] a){} }
|
||||
public static class ZZTimeHelper { public static DateTime UtcNow()=>new(2026,9,23,0,0,0,DateTimeKind.Utc); public static bool Adjusted; public static void AdjustTime(DateTime t)=>Adjusted=true;public static void WathForcusChange(){} }
|
||||
public static class GConstant { public const string K_Event_API_URL="EventApiURL",K_AssetVersion="AssetVersion",Yidun_GooglePlayChannel="stub",Yidun_AndroidBusinessId="stub",K_PlayFab_Title="PFTitle",V_Debug_PF_Title="stub",K_PlayFab_TimeOut="PFTimeout"; }
|
||||
namespace LitJson { public static class JsonMapper { public static string ToJson(object o)=>JsonConvert.SerializeObject(o);public static T ToObject<T>(string s)=>JsonConvert.DeserializeObject<T>(s); } }
|
||||
namespace GameCore { }
|
||||
namespace YiDun.Protector { public class YiDunManager { } }
|
||||
namespace PlayFab
|
||||
{
|
||||
public static class PlayFabClientAPI
|
||||
{
|
||||
public static bool Logged;
|
||||
public static bool IsClientLoggedIn(){Effects.SDK++;return Logged;}
|
||||
public static void ForgetAllCredentials(){Effects.SDK++;Logged=false;}
|
||||
public static void UpdateUserData(UpdateUserDataRequest r,Action<UpdateUserDataResult> ok,Action<PlayFabError> fail){Effects.SDK++;ok?.Invoke(new());}
|
||||
}
|
||||
public static class PlayFabSettings { public static string TitleId;public static int RequestTimeout; }
|
||||
}
|
||||
namespace PlayFab.Internal { public static class PlayFabWebRequest { public static void SkipCertificateValidation(){Effects.SDK++;} } }
|
||||
namespace asap.playfab.async
|
||||
{
|
||||
public static class PlayFabClientAsyncAPI
|
||||
{
|
||||
internal static LoginResult Result;
|
||||
internal static Func<Task<LoginResult>> LoginOverride;
|
||||
public static Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> call)=>call();
|
||||
public static Task<LoginResult> LoginWithCustomIDAsync(LoginWithCustomIDRequest r){Effects.SDK++;PlayFab.PlayFabClientAPI.Logged=Result!=null;return LoginOverride?.Invoke() ?? Task.FromResult(Result);}
|
||||
public static Task<LoginResult> LoginWithAndroidDeviceIDAsync(LoginWithAndroidDeviceIDRequest r)=>LoginWithCustomIDAsync(new());
|
||||
public static Task<GetPlayerProfileResult> GetPlayerProfileAsync(GetPlayerProfileRequest r){Effects.SDK++;return Task.FromResult(new GetPlayerProfileResult{PlayerProfile=new PlayerProfileModel{Locations=new(){new LocationModel()}}});}
|
||||
public static Task<UpdateUserTitleDisplayNameResult> UpdateUserTitleDisplayNameAsync(UpdateUserTitleDisplayNameRequest r){Effects.SDK++;return Task.FromResult(new UpdateUserTitleDisplayNameResult{DisplayName=r.DisplayName});}
|
||||
public static Task<EmptyResponse> UpdateAvatarUrlAsync(UpdateAvatarUrlRequest r){Effects.SDK++;return Task.FromResult(new EmptyResponse());}
|
||||
public static Task<GetUserDataResult> GetUserDataAsync(GetUserDataRequest r){Effects.SDK++;return Task.FromResult(new GetUserDataResult{Data=new()});}
|
||||
}
|
||||
public static class PlayFabProfilesAsyncAPI
|
||||
{ public static Task<PlayFab.ProfilesModels.SetProfileLanguageResponse> SetProfileLanguageAsync(PlayFab.ProfilesModels.SetProfileLanguageRequest r){Effects.SDK++;return Task.FromResult(new PlayFab.ProfilesModels.SetProfileLanguageResponse());} }
|
||||
}
|
||||
namespace tysdk
|
||||
{
|
||||
public enum EAccoutType {hwGoogle,hwFacebook,hwGuest,hwVkid,Apple,none}
|
||||
public class LoginInfo { public bool isSuccess; public int userId;public string StrUserId=>userId.ToString(); }
|
||||
public sealed class TYSdkFacade
|
||||
{
|
||||
public sealed class AccountInfo {public EAccoutType channel;public string userName,avatar;public int userId;public List<EAccoutType> linkedAccout=new();}
|
||||
public static AccountInfo TYAccountInfo=new(){userId=101,channel=EAccoutType.hwGuest};
|
||||
public static bool IsLoggedIn;
|
||||
public static TYSdkFacade Instance=new();
|
||||
internal static Func<Task<LoginInfo>> LoginOverride;
|
||||
public Task<LoginInfo> LoginBySns(){Effects.SDK++;return LoginOverride?.Invoke() ?? Task.FromResult(new LoginInfo{isSuccess=true,userId=101});}
|
||||
public Task<LoginInfo> LoginByToken()=>LoginBySns();public Task<LoginInfo> Login(EAccoutType type)=>LoginBySns();
|
||||
public void Logout(){Effects.SDK++;IsLoggedIn=false;}
|
||||
public Task<bool> LinkCheck(EAccoutType t){Effects.SDK++;return Task.FromResult(false);}
|
||||
public Task<(int code,int userId)> LinkAccount(EAccoutType t){Effects.SDK++;return Task.FromResult((1,0));}
|
||||
}
|
||||
}
|
||||
public static class ChannelManager { public static bool IsRustoreStore=>false; }
|
||||
public class AFHelper { public static AFHelper Instance=new();public bool GetDLReferer(out string pid,out string uid){pid=uid=null;return false;} }
|
||||
public static class GEvent
|
||||
{
|
||||
public sealed class Event:IDisposable { public Event AddContent(string k,object v)=>this;public void Dispose(){} }
|
||||
public static Event GameEvent(string name){Effects.Events++;return new();}
|
||||
public static Event LoginEvent()=>GameEvent("login");public static Event CompleteRegistrationEvent()=>GameEvent("registration");
|
||||
public static void SetProp(string k,object v){Effects.Events++;}public static void SetUserProp(string k,object v,bool b){Effects.Events++;}
|
||||
public static void SetProps(Dictionary<string,object> p){Effects.Events++;}public static void SetUserProps(Dictionary<string,object> p){Effects.Events++;}
|
||||
public static void OnLogin(string a,string b){Effects.Events++;}
|
||||
}
|
||||
namespace game
|
||||
{
|
||||
public interface IChatService { void WaitingForLogin(); }
|
||||
public interface ISettingService { string Lang {get;} }
|
||||
public class MMTService { public string Init(int value){Effects.Events++;return value.ToString();} }
|
||||
public class PlayerData { public int lv=1;public int Loads;public void LoadPlayerData(IDictionary<string,string> data){if(!ZZTimeHelper.Adjusted)throw new Exception("PF time not adjusted");Loads++;} }
|
||||
public class ChangeAvatarEvent{}
|
||||
public class GetClubPlayInfoEvent { public string id,name,avatarUrl; }
|
||||
public class ItemData {public int id,count;public ulong curCount;}
|
||||
public enum RewardType {NormalOne}
|
||||
public class ShowData {public ShowData(){}public ShowData(List<ItemData> items,RewardType t){Effects.UI++;}}
|
||||
public class PlayerItemData {public int GetItemCount(int id)=>0;public void AddItem(ItemData i,object o){Effects.Rewards++;}}
|
||||
public class TapjoyHelper:UnityEngine.MonoBehaviour {public void Init(int balance,Action<string,int> earn,Action<string,int> changed){Effects.Helpers++;}}
|
||||
}
|
||||
namespace TapjoyUnity.Internal { public class TapjoyComponent:UnityEngine.MonoBehaviour{} }
|
||||
|
||||
47
CodexOutput/upserver/KVGameLoadTests/ShopSchemaTests.cs
Normal file
47
CodexOutput/upserver/KVGameLoadTests/ShopSchemaTests.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunShopSchemaTests()
|
||||
{
|
||||
await Run("M04-all-fields-strict-and-missing",() =>
|
||||
{
|
||||
var daily="{\"ID\":1,\"index\":2,\"lastID\":3,\"time\":\"raw\"}";
|
||||
var raw=new Dictionary<string,string> {
|
||||
["ShopPackBuyCount"]="{\"x\":1}",["ShopAllPackBuyCount"]="{\"2\":3}",["ShopGiftBuyCount"]="{\"4\":5}",
|
||||
["Voucher"]="6",["ChainPackDataNew"]=daily,["TurntableAdData"]=daily,
|
||||
["VIPAdPackData"]="{\"ID\":7,\"index\":8,\"day\":9,\"cdTime\":\"raw\",\"isOpen\":true}",
|
||||
["BuyData"]="[1.5,2.5]",["AdvertAllCount"]="{\"-2\":10}",["DiamondShopRefreshCount"]="11",
|
||||
["DiamondShopRefreshTime"]="2026-09-23T00:00:00.0000000Z",
|
||||
["DiamondShopItemRecords"]="{\"12\":{\"ItemID\":13,\"Discount\":0.5,\"hasBought\":1}}",
|
||||
["BlackFridayCouponDate"]="2026,9,23,1,2,3,14,15,1"
|
||||
};
|
||||
var a=PlayerKVSaveSchema.DecodeShop(raw,SchemaSnapshot(),new());
|
||||
Check(a!=null && a.Daily["x"]==1 && a.All[2]==3 && a.Gift[4]==5 && a.Voucher==6,"counts");
|
||||
Check(a.Chain.Id==1 && a.Chain.Index==2 && a.Chain.LastId==3 && a.Chain.Time=="raw" && a.Turntable.LastId==3,"daily");
|
||||
Check(a.Vip.Id==7 && a.Vip.Index==8 && a.Vip.Day==9 && a.Vip.Open && a.Vip.Time=="raw","advert");
|
||||
Check(a.Buy.SequenceEqual(new[]{1.5f,2.5f}) && a.Advert[-2]==10 && a.RefreshCount==11 && a.RefreshTime.Year==2026,"saved shop fields");
|
||||
Check(a.Items[12].Id==13 && a.Items[12].Discount==.5f && a.Items[12].Bought==1 && a.Coupon.EventId==14 && a.Coupon.RedirectId==15 && a.Coupon.Bought,"nested values");
|
||||
foreach(var key in raw.Keys)
|
||||
{
|
||||
var bad=new Dictionary<string,string>(raw) { [key]="null" };
|
||||
Check(PlayerKVSaveSchema.DecodeShop(bad,SchemaSnapshot(),new())==null,"present bad key rejects");
|
||||
bad[key]=null;
|
||||
Check(PlayerKVSaveSchema.DecodeShop(bad,SchemaSnapshot(),new())==null,"present null rejects");
|
||||
}
|
||||
var errors=new PlayerKVSchemaDiagnostics();
|
||||
Check(PlayerKVSaveSchema.DecodeShop(raw.ToDictionary(p=>p.Key,p=>"null"),SchemaSnapshot(),errors)==null && errors.TotalErrors==13,"all fields diagnosed");
|
||||
var b=PlayerKVSaveSchema.DecodeShop(new Dictionary<string,string>(),SchemaSnapshot(),new());
|
||||
Check(b.Daily.Count==0 && b.All.Count==0 && b.Gift.Count==0 && b.Advert.Count==0 && b.Items.Count==0 && b.Buy.Count==0 &&
|
||||
b.Chain==null && b.Turntable==null && b.Vip==null && b.Voucher==0 && b.RefreshCount==0 && b.RefreshTime==default && b.Coupon.ActivateTime==default && !b.Coupon.Bought,"missing clears all");
|
||||
foreach(var coupon in new[] { "2026,9,23,1,2,3,1,2", "2026,2,30,1,2,3,1", "2026,9,23,1,2,3,1,2,2", "2026,9,23,1,2,3,1,2,1,0" })
|
||||
Check(PlayerKVSaveSchema.DecodeShop(new Dictionary<string,string>{["BlackFridayCouponDate"]=coupon},SchemaSnapshot(),new())==null,"bad coupon shape/date/bool");
|
||||
b=PlayerKVSaveSchema.DecodeShop(new Dictionary<string,string>{["BlackFridayCouponDate"]="2026,9,23,1,2,3,1"},SchemaSnapshot(),new());
|
||||
Check(b.Coupon.EventId==1 && b.Coupon.RedirectId==0 && !b.Coupon.Bought,"legacy seven-slot coupon");
|
||||
foreach(var key in new[] { "BuyData","DiamondShopItemRecords" })
|
||||
Check(PlayerKVSaveSchema.DecodeShop(new Dictionary<string,string>{[key]=key=="BuyData" ? "[1e999]" : "{\"1\":null}"},SchemaSnapshot(),new())==null,"invalid nested numeric");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
40
CodexOutput/upserver/KVGameLoadTests/VerifyBoundary.ps1
Normal file
40
CodexOutput/upserver/KVGameLoadTests/VerifyBoundary.ps1
Normal file
@@ -0,0 +1,40 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$root=(Resolve-Path (Join-Path $PSScriptRoot '../../..')).Path
|
||||
Set-Location $root
|
||||
$baseline=Get-Content 'CodexOutput/upserver/阶段4B2方案受审基线-修订2-2026-09-23.json' -Raw | ConvertFrom-Json
|
||||
$partial=Get-Content "$PSScriptRoot/checkpoint2-partial-final-verification.json" -Raw | ConvertFrom-Json
|
||||
$allowed=[Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
||||
foreach($item in $partial.checks){if($item.authorizedChange){[void]$allowed.Add($item.path)}}
|
||||
foreach($path in @('DataCenter/DataCenter.PlayerFishData.cs','DataCenter/EventPartnerGatherManager.cs','Services/UserService.cs','Services/AggUserService.cs','Core/CustomServerMgr.cs','Core/PlayFabMgr.cs','Stages/QuitAct.cs','EventPartnerGather/PartnerInfoManager.cs')){[void]$allowed.Add("Client/Assets/Scripts/$path")}
|
||||
$checks=@(foreach($item in $baseline.files){$hash=(Get-FileHash -LiteralPath $item.path -Algorithm SHA256).Hash; [ordered]@{path=$item.path;baseline=$item.sha256;actual=$hash;unchanged=$hash -eq $item.sha256;authorizedChange=$allowed.Contains($item.path)}})
|
||||
$outside=@($checks | Where-Object { !$_.unchanged -and !$_.authorizedChange })
|
||||
$club=Get-Content 'Client/Assets/Scripts/Services/ClubService.cs' -Raw
|
||||
$clubBase=Get-Content "$PSScriptRoot/baseline/ClubService.cs.txt" -Raw
|
||||
$begin=$club.IndexOf(' // KV loading owns these three fields only;')
|
||||
$end=$club.IndexOf(' public int ClubLureDonateCount', $begin)
|
||||
$stripped=$club.Remove($begin,$end-$begin).Replace("`r`n","`n")
|
||||
$clubOnlyKV=$stripped.Replace(" LureDonateData lureDonateData;`n`n"," LureDonateData lureDonateData;`n") -ceq $clubBase.Replace("`r`n","`n")
|
||||
$sourcePaths=@($allowed | Where-Object { $_.EndsWith('.cs') })
|
||||
$sourcePaths+=@(Get-ChildItem 'Client/Assets/Scripts/Services/PlayerData/PlayerKV*.cs' | ForEach-Object { $_.FullName.Substring($root.Length+1).Replace('\','/') })
|
||||
$sourcePaths+=@('Client/Assets/Scripts/Services/UserService.KVLoad.cs','Client/Assets/Editor/KVGameLoadValidation.cs')
|
||||
$methods=@();$flags=@()
|
||||
foreach($path in ($sourcePaths | Sort-Object -Unique)){
|
||||
$text=Get-Content -LiteralPath $path -Raw
|
||||
foreach($match in [regex]::Matches($text,'\b(?:internal|public|private)\s+(?:override\s+|virtual\s+)?void\s+(ResetForKVLoad|ReplaceFromKVDecoded)\s*\([^)]*\)\s*\{')){
|
||||
$start=$match.Index+$match.Length;$depth=1;$end=$start
|
||||
while($end -lt $text.Length -and $depth -gt 0){if($text[$end] -eq '{'){$depth++};if($text[$end] -eq '}'){$depth--};$end++}
|
||||
$body=$text.Substring($start,$end-$start)
|
||||
# This is a bounded direct-call audit, not a substitute for constructor/helper inspection or runtime counters.
|
||||
$hits=@([regex]::Matches($body,'\b(?:PlayerPrefs|PlayFabClientAPI|PlayFabClientAsyncAPI|TYSdkFacade|GEvent|Task\.Delay|Observable|Timer)\b|\b(?:Publish|Subscribe|SaveData|UpdateUserDataValue|LoadData|InitData|Init|ClearData|AddGiftFaceUIForce|AddItem)\s*\(') | ForEach-Object Value)
|
||||
$record=[ordered]@{path=$path;method=$match.Groups[1].Value;line=1+([regex]::Matches($text.Substring(0,$match.Index),"`n")).Count;directForbiddenCalls=$hits}
|
||||
$methods+=$record;if($hits.Count){$flags+=$record}
|
||||
}
|
||||
}
|
||||
$compile=Get-Content "$PSScriptRoot/checkpoint4-final4-compile.log" -Raw | ConvertFrom-Json
|
||||
$localCases=(Get-Content "$PSScriptRoot/checkpoint4-final-unity-cases.log" -Raw | ConvertFrom-Json).Result | ConvertFrom-Json | Where-Object caseId -ne 'B2-20'
|
||||
$full=(Get-Content "$PSScriptRoot/checkpoint4-release-full.log" -Raw | ConvertFrom-Json).Result | ConvertFrom-Json
|
||||
$result=[ordered]@{generatedAt=(Get-Date).ToString('o');baselineCount=$checks.Count;unchanged=@($checks | Where-Object unchanged).Count;authorizedChanges=@($checks|Where-Object{!$_.unchanged -and $_.authorizedChange}).Count;outside=$outside;clubOnlyAddedKVBlock=$clubOnlyKV;directAuditMethodCount=$methods.Count;directAuditFlags=$flags;compile=@{success=$compile.Success;errors=$compile.ErrorCount;warnings=$compile.WarningCount;newKVWarnings=@($compile.Warnings|Where-Object{$_.File -match 'PlayerKV|KVGameLoadValidation|UserService.KVLoad'}).Count};unityLocalCases=@($localCases);unityFull=$full;unityAssertions=($localCases.assertions | Measure-Object -Sum).Sum+$full.assertions;checks=$checks;methods=$methods}
|
||||
$result | ConvertTo-Json -Depth 8 | Set-Content "$PSScriptRoot/checkpoint4-boundary-and-evidence.json"
|
||||
[ordered]@{baselineCount=$checks.Count;outside=$outside.Count;clubOnlyKV=$clubOnlyKV;auditMethods=$methods.Count;directFlags=$flags.Count;unityAssertions=$result.unityAssertions;fullB220=$full.fullB220} | ConvertTo-Json
|
||||
if($outside.Count -gt 0 -or !$clubOnlyKV -or $flags.Count -gt 0 -or !$full.success){exit 1}
|
||||
exit 0
|
||||
915
CodexOutput/upserver/KVGameLoadTests/baseline/ClubService.cs.txt
Normal file
915
CodexOutput/upserver/KVGameLoadTests/baseline/ClubService.cs.txt
Normal file
@@ -0,0 +1,915 @@
|
||||
using asap.core;
|
||||
using cfg;
|
||||
using GameCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace game
|
||||
{
|
||||
public class ClubStateChangeEvent
|
||||
{
|
||||
public int state;
|
||||
public bool joinClub;
|
||||
}
|
||||
public class ClubSttingUpdateEvent
|
||||
{
|
||||
}
|
||||
public class GetClubPlayInfoEvent
|
||||
{
|
||||
public string id;
|
||||
public string name;
|
||||
public string avatarUrl;
|
||||
}
|
||||
public class LureDonateData
|
||||
{
|
||||
public DateTime DonateCountTime { get; set; }
|
||||
public int ClubLureDonateCount { get; set; }//时间加次数
|
||||
public DateTime ClubLureRequestCDEnd { get; set; }
|
||||
}
|
||||
public class ClubService //: IClubService
|
||||
{
|
||||
[Inject]
|
||||
public IUserService userService { get; set; }
|
||||
[Inject]
|
||||
public ICustomServerMgr customServerMgr { get; set; }
|
||||
[Inject]
|
||||
public IChatService chatService { get; set; }
|
||||
public bool IsOpenClubJoined = false;
|
||||
public bool IsOpenClubRed = true;
|
||||
|
||||
public int socialTipforUnlocked;
|
||||
public ClubInfo myClubInfo { get; private set; }
|
||||
//public bool isAdm { get; private set; }
|
||||
public EMemberRole memberRole { get; private set; } = EMemberRole.Member;
|
||||
public DateTime? signTime { get; private set; }
|
||||
|
||||
LureDonateData lureDonateData;
|
||||
public int ClubLureDonateCount => lureDonateData.ClubLureDonateCount;
|
||||
public DateTime ClubLureRequestCDEnd => lureDonateData.ClubLureRequestCDEnd;
|
||||
|
||||
public Dictionary<long, string> internalIdToPlayerId = new Dictionary<long, string>();
|
||||
public ClubItemData ClubItemData;
|
||||
public List<SocialInfoItemData> clubPlayerInfos { get; private set; }
|
||||
public List<EventPopupData> eventPopupDatas { get; private set; } = new List<EventPopupData>();
|
||||
public EventPopupData eventPopupDataClub { get; private set; }
|
||||
public int OpenPanelType = 0;
|
||||
|
||||
public async void OpenFishingSocialPanel(int type)
|
||||
{
|
||||
OpenPanelType = type;
|
||||
await UIManager.Instance.ShowUI(UITypes.FishingSocialPanel);
|
||||
OpenPanelType = 0;
|
||||
}
|
||||
|
||||
public bool CanAccepted
|
||||
{
|
||||
get
|
||||
{
|
||||
return memberRole == EMemberRole.Owner
|
||||
|| memberRole == EMemberRole.Vice
|
||||
|| memberRole == EMemberRole.Elder;
|
||||
}
|
||||
}
|
||||
public bool ApplyState { get; private set; }
|
||||
public bool CanSetting
|
||||
{
|
||||
get
|
||||
{
|
||||
return memberRole == EMemberRole.Owner || memberRole == EMemberRole.Vice;
|
||||
}
|
||||
}
|
||||
public void Init()
|
||||
{
|
||||
if (lureDonateData == null)
|
||||
{
|
||||
lureDonateData = new LureDonateData();
|
||||
lureDonateData.DonateCountTime = ZZTimeHelper.UtcNow().UtcNowOffset();
|
||||
lureDonateData.ClubLureDonateCount = 0;
|
||||
lureDonateData.ClubLureRequestCDEnd = ZZTimeHelper.UtcNow().UtcNowOffset();
|
||||
}
|
||||
if (IsOpenClubJoined)
|
||||
{
|
||||
SetClubRed();
|
||||
}
|
||||
}
|
||||
void SetClubRed()
|
||||
{
|
||||
if (myClubInfo == null)
|
||||
{
|
||||
RedPointManager.Instance.SetRedPointState(RedPointName.Home_Club, IsOpenClubRed);
|
||||
}
|
||||
else
|
||||
{
|
||||
var timer = ZZTimeHelper.UtcNow().UtcNowOffset();
|
||||
bool isSign = signTime?.DayOfYear != timer.DayOfYear;
|
||||
bool isCanLure = lureDonateData != null && lureDonateData.ClubLureRequestCDEnd <= timer;
|
||||
RedPointManager.Instance.SetRedPointState(RedPointName.Home_Club, isCanLure || isSign);
|
||||
}
|
||||
}
|
||||
public void SetLureDonateData(string value)
|
||||
{
|
||||
lureDonateData = Newtonsoft.Json.JsonConvert.DeserializeObject<LureDonateData>(value);
|
||||
}
|
||||
void SaveLureDonateData()
|
||||
{
|
||||
PlayFabMgr.Instance.UpdateUserDataValue("LureDonateData", Newtonsoft.Json.JsonConvert.SerializeObject(lureDonateData));
|
||||
}
|
||||
//刷新体力求助次数
|
||||
public void RefreshLureDonate()
|
||||
{
|
||||
if (lureDonateData.DonateCountTime.Date != ZZTimeHelper.UtcNow().UtcNowOffset().Date)
|
||||
{
|
||||
lureDonateData.DonateCountTime = ZZTimeHelper.UtcNow().UtcNowOffset();
|
||||
lureDonateData.ClubLureDonateCount = 0;
|
||||
SaveLureDonateData();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOpenClubRed()
|
||||
{
|
||||
if (IsOpenClubRed)
|
||||
{
|
||||
IsOpenClubRed = false;
|
||||
SetClubRed();
|
||||
PlayFabMgr.Instance.UpdateUserDataValue("IsOpenClubRed", "1");
|
||||
}
|
||||
}
|
||||
public bool IsClubJoined()
|
||||
{
|
||||
return myClubInfo != null;
|
||||
}
|
||||
public void SetClubSignin(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
signTime = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
signTime = DateTime.Parse(value);
|
||||
}
|
||||
}
|
||||
public async void GetSelfClub()
|
||||
{
|
||||
if (!userService.IsAuthorized || string.IsNullOrEmpty(userService.ApiPlayerId))
|
||||
return;
|
||||
var sessionVersion = customServerMgr.SessionVersion;
|
||||
GetSelfClubRequest getSelfClubRequest = new GetSelfClubRequest()
|
||||
{
|
||||
playerId = userService.ApiPlayerId,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("GetSelfClub", getSelfClubRequest);
|
||||
if (sessionVersion != customServerMgr.SessionVersion) return;
|
||||
if (string.IsNullOrWhiteSpace(json)) return;
|
||||
GetSelfClubResp response;
|
||||
try
|
||||
{
|
||||
var body = JObject.Parse(json);
|
||||
if (!body.TryGetValue("clubInfo", out var club) ||
|
||||
(club.Type != JTokenType.Null && club.Type != JTokenType.Object)) return;
|
||||
response = body.ToObject<GetSelfClubResp>();
|
||||
if (response == null) return;
|
||||
if (club.Type == JTokenType.Object &&
|
||||
(response.clubInfo == null || response.clubInfo.id == 0 ||
|
||||
body["role"]?.Type != JTokenType.Integer || !Enum.IsDefined(typeof(EMemberRole), response.role))) return;
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonException) { return; }
|
||||
catch (OverflowException) { return; }
|
||||
myClubInfo = response.clubInfo;
|
||||
memberRole = myClubInfo == null ? EMemberRole.Member : response.role;
|
||||
internalIdToPlayerId = response.playerIDs ?? new Dictionary<long, string>();
|
||||
if (myClubInfo != null)
|
||||
{
|
||||
SetClubPlayerProfile(response.clubPlayerProfiles);
|
||||
chatService.GetConversationList((long)myClubInfo.id);
|
||||
}
|
||||
SetClubRed();
|
||||
MyClubInfoChange();
|
||||
}
|
||||
void MyClubInfoChange(bool joinClub = false)
|
||||
{
|
||||
GContext.Publish(new ClubStateChangeEvent() { state = 0, joinClub = joinClub });
|
||||
}
|
||||
public async Task<int> GetOnlineUsers()
|
||||
{
|
||||
HashSet<long> longs = await chatService.GetOnlineUsers(internalIdToPlayerId.Keys.ToList());
|
||||
return longs == null ? 0 : longs.Count;
|
||||
}
|
||||
public void Refresh()
|
||||
{
|
||||
ClubInfoMap.Clear();
|
||||
}
|
||||
Dictionary<string, List<ClubInfo>> ClubInfoMap = new Dictionary<string, List<ClubInfo>>();
|
||||
public async Task<List<ClubInfo>> GetClubs(string clubName)
|
||||
{
|
||||
if (ClubInfoMap.ContainsKey(clubName))
|
||||
{
|
||||
await Awaiters.NextFrame;
|
||||
return ClubInfoMap[clubName];
|
||||
}
|
||||
GetClubsRequest getClubsRequest = new GetClubsRequest()
|
||||
{
|
||||
clubName = clubName,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("GetClubs", getClubsRequest);
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
GetClubsResp clubDatas = Newtonsoft.Json.JsonConvert.DeserializeObject<GetClubsResp>(json);
|
||||
ClubInfoMap[clubName] = clubDatas.clubInfo;
|
||||
return clubDatas.clubInfo;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsDecimalPlayerId(string id) =>
|
||||
long.TryParse(id, System.Globalization.NumberStyles.None,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var number) && number > 0;
|
||||
|
||||
private bool TryGetClubSession(out string playerId, out int version)
|
||||
{
|
||||
playerId = userService.ApiPlayerId;
|
||||
version = customServerMgr.SessionVersion;
|
||||
return userService.IsAuthorized && IsDecimalPlayerId(playerId);
|
||||
}
|
||||
|
||||
private bool IsClubSessionCurrent(string playerId, int version) =>
|
||||
version == customServerMgr.SessionVersion && userService.IsAuthorized && playerId == userService.ApiPlayerId;
|
||||
|
||||
private static bool TryReadClubResponse<T>(string json, out T result, params int[] acceptedCodes) where T : class
|
||||
{
|
||||
result = null;
|
||||
if (string.IsNullOrWhiteSpace(json)) return false;
|
||||
try
|
||||
{
|
||||
var body = Newtonsoft.Json.Linq.JObject.Parse(json);
|
||||
var code = body["code"];
|
||||
if (code?.Type != Newtonsoft.Json.Linq.JTokenType.Integer || !acceptedCodes.Contains(code.Value<int>())) return false;
|
||||
result = body.ToObject<T>();
|
||||
return result != null;
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonException) { return false; }
|
||||
catch (OverflowException) { return false; }
|
||||
}
|
||||
|
||||
public async Task<List<SocialInfoItemData>> GetClubDetails(ulong clubID, bool applyState = false)
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return null;
|
||||
ApplyState = false;
|
||||
GetClubDetailsRequest getClubDetailsRequest = new GetClubDetailsRequest()
|
||||
{
|
||||
id = clubID,
|
||||
playerId = apiPlayerId,
|
||||
applyState = applyState,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("GetClubDetails", getClubDetailsRequest);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return null;
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
GetClubDetailsResp clubDetails = Newtonsoft.Json.JsonConvert.DeserializeObject<GetClubDetailsResp>(json);
|
||||
if (clubDetails.playerIDs == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
List<SocialInfoItemData> clubPlayerInfos = new List<SocialInfoItemData>();
|
||||
if (myClubInfo != null && clubID == myClubInfo.id)
|
||||
{
|
||||
internalIdToPlayerId = clubDetails.playerIDs;
|
||||
//internalIds = clubDetails.internalIds;
|
||||
}
|
||||
List<long> longs1 = clubDetails.playerIDs.Keys.ToList();
|
||||
HashSet<long> longs = await chatService.GetOnlineUsers(longs1);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return null;
|
||||
List<ClubPlayerProfile> clubPlayerProfiles = clubDetails.clubPlayerProfiles;
|
||||
SetClubPlayerProfile(clubPlayerProfiles);
|
||||
|
||||
for (int i = 0; i < clubPlayerProfiles.Count; i++)
|
||||
{
|
||||
ClubPlayerProfile clubPlayerProfile = clubPlayerProfiles[i];
|
||||
SocialInfoItemData clubPlayerInfo = new SocialInfoItemData();
|
||||
clubPlayerInfo.playFabId = clubPlayerProfile.playerId;
|
||||
userService.GetPlayInfo(clubPlayerInfo.playFabId);
|
||||
clubPlayerInfo.lv = userService.GetPlayerLv(clubPlayerInfo.playFabId);
|
||||
//clubPlayerInfo.rank = i + 1;
|
||||
clubPlayerInfo.isOnline = longs != null && longs.Contains(clubPlayerProfile.internalId);
|
||||
clubPlayerInfo.role = clubPlayerProfile.role;
|
||||
|
||||
if (clubPlayerProfile.lastLoginTime != null)
|
||||
{
|
||||
clubPlayerInfo.lastLoginTime = (DateTime)clubPlayerProfile.lastLoginTime;
|
||||
}
|
||||
clubPlayerInfos.Add(clubPlayerInfo);
|
||||
}
|
||||
|
||||
clubPlayerInfos.Sort((x, y) =>
|
||||
{
|
||||
//if (y.isOnline != x.isOnline)
|
||||
//{
|
||||
// return y.isOnline.CompareTo(x.isOnline);
|
||||
//}
|
||||
//else
|
||||
{
|
||||
return y.lv.CompareTo(x.lv);
|
||||
}
|
||||
});
|
||||
ApplyState = clubDetails.applyState;
|
||||
return clubPlayerInfos;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<ClubInfo> CreateClub(ClubSettingData clubSettingData)
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return null;
|
||||
var internalId = userService.InternalId;
|
||||
var request = new CreateClubRequest { playerId = apiPlayerId, clubSettingData = clubSettingData };
|
||||
var json = await customServerMgr.ClubRequest("CreateClub", request);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return null;
|
||||
// Code 1 means already a member; returning a club would charge the create UI again.
|
||||
if (!TryReadClubResponse<CreateClubResp>(json, out var response, 0) || response.clubInfo == null || response.clubInfo.id == 0)
|
||||
return null;
|
||||
memberRole = EMemberRole.Owner;
|
||||
myClubInfo = response.clubInfo;
|
||||
internalIdToPlayerId[internalId] = apiPlayerId;
|
||||
ShowPromote();
|
||||
chatService.GetConversationList((long)myClubInfo.id);
|
||||
MyClubInfoChange(true);
|
||||
return myClubInfo;
|
||||
}
|
||||
public async void ShowPromote()
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return;
|
||||
GameObject go = await UIManager.Instance.ShowUI(UITypes.ClubPromoteNoticePopupPanel);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion) || go == null) return;
|
||||
var ClubPromoteNoticePopupPanel = go.GetComponent<ClubPromoteNoticePopupPanel>();
|
||||
ClubPromoteNoticePopupPanel.SetRole(memberRole);
|
||||
}
|
||||
public async Task<bool> UpdateClub(ClubSettingData clubSettingData)
|
||||
{
|
||||
UpdateClubRequest updateClubRequest = new UpdateClubRequest()
|
||||
{
|
||||
id = myClubInfo.id,
|
||||
clubSettingData = clubSettingData,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("UpdateClub", updateClubRequest);
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
UpdateClubResp createClubResp = Newtonsoft.Json.JsonConvert.DeserializeObject<UpdateClubResp>(json);
|
||||
if (createClubResp.code == 0)
|
||||
{
|
||||
myClubInfo.clubSettingData = clubSettingData;
|
||||
GContext.Publish(new ClubSttingUpdateEvent());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> ApplyClub(ulong clubID)
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return false;
|
||||
ApplyClubRequest applyClubRequest = new ApplyClubRequest()
|
||||
{
|
||||
playerId = apiPlayerId,
|
||||
//entityKey = userService.entityKey.Id,
|
||||
id = clubID,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("ApplyClub", applyClubRequest);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return false;
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
ApplyClubResp applyClubResp = Newtonsoft.Json.JsonConvert.DeserializeObject<ApplyClubResp>(json);
|
||||
if (applyClubResp.code == 0)
|
||||
{
|
||||
memberRole = EMemberRole.Member;
|
||||
ToastPanel.Show(LocalizationMgr.GetText("UI_ToastPanel_41"));
|
||||
myClubInfo = applyClubResp.clubInfo;
|
||||
internalIdToPlayerId = applyClubResp.playerIDs;
|
||||
SetClubPlayerProfile(applyClubResp.clubPlayerProfiles);
|
||||
//internalIds = applyClubResp.internalIds;
|
||||
chatService.GetConversationList((long)myClubInfo.id);
|
||||
MyClubInfoChange(true);
|
||||
return true;
|
||||
}
|
||||
else if (applyClubResp.code == 1)
|
||||
{
|
||||
ToastPanel.Show(LocalizationMgr.GetText("UI_ToastPanel_44"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ToastPanel.Show(LocalizationMgr.GetText("UI_ToastPanel_45"));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async void HandleApply(bool isAgree, string applyId)
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return;
|
||||
if (applyId == "" && isAgree) return;
|
||||
if (applyId != "" && !IsDecimalPlayerId(applyId)) return;
|
||||
if (myClubInfo == null) return;
|
||||
if (isAgree && myClubInfo.curMembers >= myClubInfo.maxMembers)
|
||||
{
|
||||
//人数已满
|
||||
return;
|
||||
}
|
||||
HandleApplyRequest handleApplyRequest = new HandleApplyRequest()
|
||||
{
|
||||
playerId = applyId,
|
||||
//entityKey = entityID,
|
||||
isAgree = isAgree,
|
||||
id = myClubInfo.id,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("HandleApply", handleApplyRequest);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return;
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
if (!TryReadClubResponse<HandleApplyResp>(json, out var handleApplyResp, applyId == "" ? 4 : 0) || myClubInfo == null) return;
|
||||
if (handleApplyResp.code == 0 || handleApplyResp.code == 4)
|
||||
{
|
||||
if (applyId == "")
|
||||
{
|
||||
clubPlayerInfos.Clear();
|
||||
RedPointManager.Instance.SetRedPointState(RedPointName.ApplyList, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < clubPlayerInfos.Count; i++)
|
||||
{
|
||||
if (clubPlayerInfos[i].playFabId == applyId)
|
||||
{
|
||||
clubPlayerInfos.RemoveAt(i);
|
||||
RedPointManager.Instance.SetRedPointState(RedPointName.ApplyList, clubPlayerInfos.Count > 0
|
||||
&& CanAccepted);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isAgree) myClubInfo.curMembers++;
|
||||
GContext.Publish(new ClubSttingUpdateEvent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void GetApplyList()
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return;
|
||||
if (myClubInfo == null) return;
|
||||
GetApplyListRequest getApplyListRequest = new GetApplyListRequest()
|
||||
{
|
||||
playerId = apiPlayerId,
|
||||
id = myClubInfo.id,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("GetApplyList", getApplyListRequest);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return;
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
GetApplyListResp getApplyListResp = Newtonsoft.Json.JsonConvert.DeserializeObject<GetApplyListResp>(json);
|
||||
clubPlayerInfos = new List<SocialInfoItemData>();
|
||||
SetClubPlayerProfile(getApplyListResp.clubPlayerProfiles);
|
||||
for (int i = 0; i < getApplyListResp.userIDList.Count; i++)
|
||||
{
|
||||
SocialInfoItemData clubPlayerInfo = new SocialInfoItemData();
|
||||
//clubPlayerInfo.rank = i + 1;
|
||||
//clubPlayerInfo.entityKey = getApplyListResp.entityKeys[i];
|
||||
clubPlayerInfo.playFabId = getApplyListResp.userIDList[i];
|
||||
userService.GetPlayInfo(clubPlayerInfo.playFabId);
|
||||
clubPlayerInfo.lv = userService.GetPlayerLv(clubPlayerInfo.playFabId);
|
||||
clubPlayerInfos.Add(clubPlayerInfo);
|
||||
}
|
||||
clubPlayerInfos.Sort((x, y) => y.lv.CompareTo(x.lv));
|
||||
RedPointManager.Instance.SetRedPointState(RedPointName.ApplyList, clubPlayerInfos.Count > 0 && CanAccepted);
|
||||
}
|
||||
}
|
||||
void SetClubPlayerProfile(List<ClubPlayerProfile> clubPlayerProfiles)
|
||||
{
|
||||
if (clubPlayerProfiles == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var item in clubPlayerProfiles)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.displayName) || !string.IsNullOrEmpty(item.avatar))
|
||||
{
|
||||
userService.SetPlayInfo(item.playerId, item.displayName, item.avatar);
|
||||
userService.SetPlayerLv(item.playerId, item.level);
|
||||
}
|
||||
}
|
||||
}
|
||||
public async void QuitClub(string appointId = "0")
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return;
|
||||
if (!string.IsNullOrEmpty(appointId) && appointId != "0" && (!IsDecimalPlayerId(appointId) || appointId == apiPlayerId)) return;
|
||||
var request = new QuitClubRequest { playerId = apiPlayerId, appointId = appointId };
|
||||
var json = await customServerMgr.ClubRequest("QuitClub", request);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return;
|
||||
if (!TryReadClubResponse<QuitClubResp>(json, out var response, 0, 1)) return;
|
||||
chatService.OnQuit();
|
||||
myClubInfo = null;
|
||||
ClearLikeCursorIds();
|
||||
ToastPanel.Show(LocalizationMgr.GetText("UI_ToastPanel_46"));
|
||||
MyClubInfoChange();
|
||||
}
|
||||
public async void Demote(string playerId)
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return;
|
||||
if (!IsDecimalPlayerId(playerId) || playerId == apiPlayerId) return;
|
||||
if (myClubInfo == null) return;
|
||||
DemoteClubRequest demoteClubRequest = new DemoteClubRequest()
|
||||
{
|
||||
culbId = myClubInfo.id,
|
||||
demoteId = playerId,
|
||||
playerId = apiPlayerId,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("DemoteClub", demoteClubRequest);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return;
|
||||
}
|
||||
public async void Promote(string playerId, EMemberRole role)
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return;
|
||||
if (!IsDecimalPlayerId(playerId) || playerId == apiPlayerId) return;
|
||||
if (myClubInfo == null) return;
|
||||
PromoteClubRequest promoteClubRequest = new PromoteClubRequest()
|
||||
{
|
||||
culbId = myClubInfo.id,
|
||||
promoteId = playerId,
|
||||
playerId = apiPlayerId,
|
||||
role = role,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("PromoteClub", promoteClubRequest);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return;
|
||||
}
|
||||
public async void Kickout(string playerId)
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return;
|
||||
if (!IsDecimalPlayerId(playerId) || playerId == apiPlayerId) return;
|
||||
if (myClubInfo == null) return;
|
||||
KickoutClubRequest kickoutClubRequest = new KickoutClubRequest()
|
||||
{
|
||||
culbId = myClubInfo.id,
|
||||
kickoutId = playerId,
|
||||
playerId = apiPlayerId,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("KickoutClub", kickoutClubRequest);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return;
|
||||
if (!TryReadClubResponse<KickoutClubResp>(json, out var response, 0) || myClubInfo == null) return;
|
||||
myClubInfo.curMembers = Math.Max(0, myClubInfo.curMembers - 1);
|
||||
GContext.Publish(new ClubSttingUpdateEvent());
|
||||
//if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
//{
|
||||
// KickoutClubResp quitClubResp = Newtonsoft.Json.JsonConvert.DeserializeObject<KickoutClubResp>(json);
|
||||
//}
|
||||
}
|
||||
public async void ReadClubEvent()
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return;
|
||||
ReadClubEventRequest readClubEventRequest = new ReadClubEventRequest()
|
||||
{
|
||||
playerId = apiPlayerId,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("ReadClubEvent", readClubEventRequest);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return;
|
||||
//if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
//{
|
||||
// ReadClubEventResp readClubEventResp = Newtonsoft.Json.JsonConvert.DeserializeObject<ReadClubEventResp>(json);
|
||||
//}
|
||||
}
|
||||
private int? signingSession;
|
||||
public async Task ClubSingin()
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return;
|
||||
if (myClubInfo == null) return;
|
||||
if (signingSession == sessionVersion || signTime?.Date == ZZTimeHelper.UtcNow().UtcNowOffset().Date) return;
|
||||
signingSession = sessionVersion;
|
||||
try
|
||||
{
|
||||
ClubSinginRequest clubSinginRequest = new ClubSinginRequest()
|
||||
{
|
||||
playerId = apiPlayerId,
|
||||
id = myClubInfo.id,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("ClubSingin", clubSinginRequest);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return;
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
if (!TryReadClubResponse<ClubSinginResp>(json, out var clubSinginResp, 0)) return;
|
||||
signTime = ZZTimeHelper.UtcNow().UtcNowOffset();
|
||||
PlayFabMgr.Instance.UpdateUserDataValue("ClubSignin", signTime.Value.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||
if (clubSinginResp.code == 0)
|
||||
{
|
||||
chatService.SendSystemMessage($"{MessageChatType.SI}_{apiPlayerId}");
|
||||
//签到成功奖励
|
||||
GContext.container.Resolve<PlayerItemData>().AddItemByDrop(GContext.container.Resolve<cfg.Tables>().TbServerConfig.ClubSignReward, null);
|
||||
GContext.Publish(new ShowData());
|
||||
#if AGG
|
||||
using (var e = GEvent.GameEvent("guild_sign"))
|
||||
{
|
||||
e.AddContent("guild_id", myClubInfo.id)
|
||||
.AddContent("drop_list", GContext.container.Resolve<cfg.Tables>().TbServerConfig.ClubSignReward);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
SetClubRed();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (signingSession == sessionVersion) signingSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void SendSystemMessage2(int index)
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out _)) return;
|
||||
chatService.SendSystemMessage2($"{apiPlayerId}_{index}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取被赠送的照片 以及其他事件
|
||||
/// </summary>
|
||||
///
|
||||
int energyAddCount = 0;
|
||||
public int GetEnergyAddCount()
|
||||
{
|
||||
int count = energyAddCount;
|
||||
energyAddCount = 0;
|
||||
return count;
|
||||
}
|
||||
public async void GetServiceEvent()
|
||||
{
|
||||
if (!TryGetClubSession(out var apiPlayerId, out var sessionVersion)) return;
|
||||
GetPictureRequest getPictureRequest = new GetPictureRequest()
|
||||
{
|
||||
playerId = apiPlayerId,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("GetPictureList", getPictureRequest);
|
||||
if (!IsClubSessionCurrent(apiPlayerId, sessionVersion)) return;
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
GetPictureResp getPictureResp = Newtonsoft.Json.JsonConvert.DeserializeObject<GetPictureResp>(json);
|
||||
energyAddCount = 0;
|
||||
//var pictureDatas = getPictureResp.pictureDataList;
|
||||
//if (pictureDatas != null && pictureDatas.Count > 0)
|
||||
//{
|
||||
// List<int> pictureIDs = new List<int>();
|
||||
// foreach (var pictureData in pictureDatas)
|
||||
// {
|
||||
// pictureIDs.Add(pictureData.pictureID);
|
||||
// }
|
||||
// GContext.container.Resolve<AlbumData>().AddPictureProgress(pictureIDs);
|
||||
//}
|
||||
eventPopupDatas = getPictureResp.eventPopupDatas;
|
||||
if (eventPopupDatas != null && eventPopupDatas.Count > 0)
|
||||
{
|
||||
IReportService reportService = GContext.container.Resolve<IReportService>();
|
||||
var playerData = GContext.container.Resolve<PlayerData>();
|
||||
var tables = GContext.container.Resolve<Tables>();
|
||||
//CampData campData = GContext.container.Resolve<CampData>();
|
||||
foreach (var eventPopupData in eventPopupDatas)
|
||||
{
|
||||
if (eventPopupData.type == ReportDataType.Heist)
|
||||
{
|
||||
long content = long.Parse(eventPopupData.content);
|
||||
if (content < 0)
|
||||
{
|
||||
content = -content;
|
||||
}
|
||||
//=====新规则=====
|
||||
long newContent = content / 100;
|
||||
long rangeT = content % 100;
|
||||
float range = Mathf.Clamp(rangeT, 5, 15) * 0.1f;
|
||||
|
||||
|
||||
int m = Mathf.Min(tables.TbGlobalConfig.BottleCashParamLimit, (int)newContent);
|
||||
float mg = (1 + playerData.benefitsCashMag) * (1 + playerData.benefitsCashMagLimited);
|
||||
m = Mathf.RoundToInt(m * mg * range);
|
||||
|
||||
//=====新规则=====
|
||||
eventPopupData.content = m.ToString();
|
||||
}
|
||||
|
||||
if (eventPopupData.IsRead == false)
|
||||
{
|
||||
if (eventPopupData.type == ReportDataType.Energy)
|
||||
{
|
||||
#if AGG
|
||||
using (var e = GEvent.GameEvent("guild_request_lures_reward"))
|
||||
{
|
||||
e.AddContent("donate_userid_list", eventPopupData.content)
|
||||
.AddContent("reward_hook", GContext.container.Resolve<cfg.Tables>().TbServerConfig.ClubLureDonateReward);
|
||||
if (myClubInfo != null)
|
||||
{
|
||||
e.AddContent("guild_id", myClubInfo.id);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
//事件弹窗
|
||||
//添加体力
|
||||
playerData.SetEnergy(playerData.Energy + GContext.container.Resolve<cfg.Tables>().TbServerConfig.ClubLureDonateReward);
|
||||
energyAddCount++;
|
||||
}
|
||||
else if (eventPopupData.type == ReportDataType.Club)
|
||||
{
|
||||
//工会事件,只在工会界面调用,弹窗展示之后才设置为已读
|
||||
eventPopupDataClub = eventPopupData;
|
||||
}
|
||||
else if (eventPopupData.type == ReportDataType.Aquarium)
|
||||
{
|
||||
reportService.AddReport(new ReportData()
|
||||
{
|
||||
type = eventPopupData.type,
|
||||
id = eventPopupData.redirectID,
|
||||
playFabId = eventPopupData.playerID,
|
||||
content = eventPopupData.content,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
//可能扣钱
|
||||
if (eventPopupData.type == ReportDataType.Heist && playerData.gold > 0)
|
||||
{
|
||||
int content = int.Parse(eventPopupData.content);
|
||||
|
||||
reportService.AddReport(new ReportData()
|
||||
{
|
||||
type = eventPopupData.type,
|
||||
id = eventPopupData.redirectID,
|
||||
playFabId = eventPopupData.playerID,
|
||||
content = eventPopupData.content,
|
||||
});
|
||||
|
||||
ulong gold = (ulong)content;
|
||||
if (gold >= playerData.gold)
|
||||
{
|
||||
playerData.SetGold(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
playerData.SetGold(playerData.gold - gold);
|
||||
}
|
||||
}
|
||||
else if (eventPopupData.type == ReportDataType.Bomb)
|
||||
{
|
||||
reportService.AddReport(new ReportData()
|
||||
{
|
||||
type = eventPopupData.type,
|
||||
id = eventPopupData.redirectID,
|
||||
playFabId = eventPopupData.playerID,
|
||||
content = eventPopupData.content,
|
||||
});
|
||||
GContext.container.Resolve<CampDataMM>().SetIsFix(true);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
if (energyAddCount > 0)
|
||||
{
|
||||
GContext.container.Resolve<IFaceUIService>().AddFaceCustomUIData(UITypes.EventReportPopupPanel, true);
|
||||
}
|
||||
CurRewardQCount curRewardQCount = new CurRewardQCount();
|
||||
GContext.Publish(curRewardQCount);
|
||||
int faceCount = GContext.container.Resolve<IFaceUIService>().GetFaceUICount();
|
||||
if (curRewardQCount.count <= 0 && faceCount <= 0)
|
||||
{
|
||||
ShowHomeReport();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public void ShowHomeReport()
|
||||
{
|
||||
ReportData reportData = GContext.container.Resolve<IReportService>().GetReportData();
|
||||
if (reportData != null)
|
||||
{
|
||||
GContext.Publish(reportData);
|
||||
}
|
||||
}
|
||||
//消息点赞相关
|
||||
List<long> LikeCursorIds;
|
||||
string LikeCursorIdsKey = "LikeCursorIds";
|
||||
void ClearLikeCursorIds()
|
||||
{
|
||||
LikeCursorIds = null;
|
||||
PlayerPrefs.DeleteKey(LikeCursorIdsKey);
|
||||
}
|
||||
public async void SendLike(long cursorId)
|
||||
{
|
||||
ClubLikeRequest clubLikeRequest = new ClubLikeRequest()
|
||||
{
|
||||
cursorId = cursorId,
|
||||
id = myClubInfo.id,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("ClubLike", clubLikeRequest);
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
ClubLikeResp clubSinginResp = Newtonsoft.Json.JsonConvert.DeserializeObject<ClubLikeResp>(json);
|
||||
LikeCursorIds.Add(cursorId);
|
||||
PlayerPrefs.SetString(LikeCursorIdsKey, Newtonsoft.Json.JsonConvert.SerializeObject(LikeCursorIds));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool, int)> GetLikeCount(long cursorId)
|
||||
{
|
||||
GetLikeCountRequest LikeCountRequest = new GetLikeCountRequest()
|
||||
{
|
||||
cursorId = cursorId,
|
||||
id = myClubInfo.id,
|
||||
};
|
||||
LikeCursorIds = Newtonsoft.Json.JsonConvert.DeserializeObject<List<long>>(PlayerPrefs.GetString(LikeCursorIdsKey, "[]"));
|
||||
bool isLike = LikeCursorIds.Contains(cursorId);
|
||||
int count = 0;
|
||||
if (isLike)
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
string json = await customServerMgr.ClubRequest("GetLikeCount", LikeCountRequest);
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
GetLikeCountResp clubSinginResp = Newtonsoft.Json.JsonConvert.DeserializeObject<GetLikeCountResp>(json);
|
||||
count = clubSinginResp.count;
|
||||
}
|
||||
return (isLike, count);
|
||||
}
|
||||
//体力求助相关
|
||||
//聊天消息
|
||||
public async Task<bool> SendHelpEnergy()
|
||||
{
|
||||
//messageID
|
||||
long messageID = await chatService.SendHelpEnergyMessage(1001);
|
||||
if (messageID > 0)
|
||||
{
|
||||
#if AGG
|
||||
using (var e = GEvent.GameEvent("guild_request_lures"))
|
||||
{
|
||||
e.AddContent("guild_id", myClubInfo.id);
|
||||
}
|
||||
#endif
|
||||
//记录今日请求 体力互助
|
||||
lureDonateData.ClubLureRequestCDEnd = ZZTimeHelper.UtcNow().UtcNowOffset().
|
||||
AddHours(GContext.container.Resolve<cfg.Tables>().TbServerConfig.ClubLureRequestCd);
|
||||
SaveLureDonateData();
|
||||
SetClubRed();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
//服务器消息状态
|
||||
public async Task<string> SendHelpOthersEnergy(string playerId, long cursorId)
|
||||
{
|
||||
ClubHelpEnergyRequest clubHelpEnergyRequest = new ClubHelpEnergyRequest()
|
||||
{
|
||||
donateId = userService.UserId,
|
||||
cursorId = cursorId,
|
||||
playerId = playerId,
|
||||
id = myClubInfo.id,
|
||||
};
|
||||
lureDonateData.ClubLureDonateCount++;
|
||||
string json = await customServerMgr.ClubRequest("ClubHelpEnergy", clubHelpEnergyRequest);
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
#if AGG
|
||||
using (var e = GEvent.GameEvent("guild_donate_lures"))
|
||||
{
|
||||
e.AddContent("guild_id", myClubInfo.id)
|
||||
.AddContent("to_user_id", playerId)
|
||||
;
|
||||
}
|
||||
#endif
|
||||
ClubHelpEnergyResp clubHelpEnergyResp = Newtonsoft.Json.JsonConvert.DeserializeObject<ClubHelpEnergyResp>(json);
|
||||
SaveLureDonateData();
|
||||
return clubHelpEnergyResp.idContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
lureDonateData.ClubLureDonateCount--;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
public async Task<string> GetHelpEnergyState(long cursorId)
|
||||
{
|
||||
GetHelpEnergyStatusRequest helpEnergyRequest = new GetHelpEnergyStatusRequest()
|
||||
{
|
||||
cursorId = cursorId,
|
||||
id = myClubInfo.id,
|
||||
};
|
||||
string json = await customServerMgr.ClubRequest("GetHelpEnergyStatus", helpEnergyRequest);
|
||||
if (!string.IsNullOrEmpty(json) && json != "[]")
|
||||
{
|
||||
GetHelpEnergyStatusResp helpEnergyResp = Newtonsoft.Json.JsonConvert.DeserializeObject<GetHelpEnergyStatusResp>(json);
|
||||
return helpEnergyResp.idContent;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
16
CodexOutput/upserver/KVGameLoadTests/snapshot-comparers.inc
Normal file
16
CodexOutput/upserver/KVGameLoadTests/snapshot-comparers.inc
Normal file
@@ -0,0 +1,16 @@
|
||||
static bool Same(PlayerKVPlayerState a,PlayerKVPlayerState b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.Core,b.Core) && SameDictionary(a.Modern,b.Modern,(x,y)=>Same(x,y)) && SameDictionary(a.VipScores,b.VipScores,(x,y)=>Same(x,y)) && SameList(a.NewRods,b.NewRods,(x,y)=>Same(x,y)) && Same(a.Season,b.Season) && Same(a.Appraise,b.Appraise) && Same(a.Building,b.Building));
|
||||
static bool Same(PlayerKVShopState a,PlayerKVShopState b) => ReferenceEquals(a,b) || (a!=null && b!=null && SameDictionary(a.Daily,b.Daily,(x,y)=>Same(x,y)) && SameDictionary(a.All,b.All,(x,y)=>Same(x,y)) && SameDictionary(a.Gift,b.Gift,(x,y)=>Same(x,y)) && SameDictionary(a.Advert,b.Advert,(x,y)=>Same(x,y)) && SameDictionary(a.Items,b.Items,(x,y)=>Same(x,y)) && SameList(a.Buy,b.Buy,(x,y)=>Same(x,y)) && Same(a.Voucher,b.Voucher) && Same(a.RefreshCount,b.RefreshCount) && Same(a.RefreshTime,b.RefreshTime) && Same(a.Chain,b.Chain) && Same(a.Turntable,b.Turntable) && Same(a.Vip,b.Vip) && Same(a.Coupon,b.Coupon));
|
||||
static bool Same(PlayerKVEventBaseState a,PlayerKVEventBaseState b) => ReferenceEquals(a,b) || (a!=null && b!=null && SameDictionary(a.Limited,b.Limited,(x,y)=>Same(x,y)) && SameDictionary(a.Fixed,b.Fixed,(x,y)=>Same(x,y)) && SameDictionary(a.Target,b.Target,(x,y)=>Same(x,y)) && SameDictionary(a.Rank,b.Rank,(x,y)=>Same(x,y)) && SameDictionary(a.Wheel,b.Wheel,(x,y)=>Same(x,y)) && Same(a.Coupon,b.Coupon) && Same(a.Operate,b.Operate) && Same(a.AllOperate,b.AllOperate) && Same(a.Free,b.Free) && Same(a.PvpToken,b.PvpToken));
|
||||
static bool Same(PlayerKVCampState a,PlayerKVCampState b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.IsFix,b.IsFix) && Same(a.Skyscraper,b.Skyscraper) && SameDictionary(a.Values,b.Values,(x,y)=>Same(x,y)));
|
||||
static bool Same(PlayerKVClubState a,PlayerKVClubState b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.SignTime,b.SignTime) && Same(a.DonateTime,b.DonateTime) && Same(a.DonateCount,b.DonateCount) && Same(a.RequestEnd,b.RequestEnd) && Same(a.OpenRed,b.OpenRed));
|
||||
static bool Same(PlayerKVEventPackState a,PlayerKVEventPackState b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.EventId,b.EventId) && Same(a.PurchaseCount,b.PurchaseCount) && Same(a.VipLevel,b.VipLevel) && Same(a.RedirectId,b.RedirectId));
|
||||
static bool Same(PlayerKVPiggyBankState a,PlayerKVPiggyBankState b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.IsPresent,b.IsPresent) && Same(a.currentEventID,b.currentEventID) && Same(a.purchaseCount,b.purchaseCount) && Same(a.VIPLvlWhenEventActivate,b.VIPLvlWhenEventActivate) && Same(a.savingProgress,b.savingProgress) && Same(a.redirectID,b.redirectID) && Same(a.doNeedTrigger,b.doNeedTrigger) && Same(a.isTriggered,b.isTriggered) && Same(a.inflation,b.inflation));
|
||||
static bool Same(PlayerKVBargainState a,PlayerKVBargainState b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.IsPresent,b.IsPresent) && Same(a.currentEventID,b.currentEventID) && Same(a.redirectID,b.redirectID) && Same(a.VIPLvlWhenEventActivate,b.VIPLvlWhenEventActivate) && Same(a.purchaseCount,b.purchaseCount) && Same(a.savingProgress,b.savingProgress) && Same(a.discountLvl,b.discountLvl) && Same(a.visualProgress,b.visualProgress) && Same(a.visualDiscountLvl,b.visualDiscountLvl) && Same(a.rodID,b.rodID) && Same(a.lastPackIDBought,b.lastPackIDBought) && Same(a.packID,b.packID) && Same(a.doNeedTrigger,b.doNeedTrigger) && Same(a.isTriggered,b.isTriggered) && Same(a.inflation,b.inflation) && SameList(a.selections,b.selections,(x,y)=>Same(x,y)));
|
||||
static bool Same(PlayerKVShootingRangeState a,PlayerKVShootingRangeState b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.IsPresent,b.IsPresent) && Same(a.EventId,b.EventId) && Same(a.CycleId,b.CycleId) && Same(a.RedirectId,b.RedirectId) && Same(a.AmmoCount,b.AmmoCount) && Same(a.CurrentStageIdx,b.CurrentStageIdx) && Same(a.ChainProgress,b.ChainProgress) && Same(a.RewardFlag,b.RewardFlag) && Same(a.TargetFlag,b.TargetFlag) && Same(a.HasBeenOpened,b.HasBeenOpened) && Same(a.Inflation,b.Inflation));
|
||||
static bool Same(PlayerKVProgressPackState a,PlayerKVProgressPackState b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.IsPresent,b.IsPresent) && Same(a.EventId,b.EventId) && Same(a.RedirectId,b.RedirectId) && Same(a.TokenProgress,b.TokenProgress) && Same(a.ChainProgress,b.ChainProgress));
|
||||
static bool Same(PlayerKVShopItemValue a,PlayerKVShopItemValue b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.Id,b.Id) && Same(a.Discount,b.Discount) && Same(a.Bought,b.Bought));
|
||||
static bool Same(PlayerKVDailyValue a,PlayerKVDailyValue b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.Id,b.Id) && Same(a.Index,b.Index) && Same(a.LastId,b.LastId) && Same(a.Time,b.Time));
|
||||
static bool Same(PlayerKVAdvertValue a,PlayerKVAdvertValue b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.Id,b.Id) && Same(a.Index,b.Index) && Same(a.Day,b.Day) && Same(a.Time,b.Time) && Same(a.Open,b.Open));
|
||||
static bool Same(PlayerKVCouponState a,PlayerKVCouponState b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.ActivateTime,b.ActivateTime) && Same(a.EventId,b.EventId) && Same(a.RedirectId,b.RedirectId) && Same(a.Bought,b.Bought));
|
||||
static bool Same(PlayerKVLimitedValue a,PlayerKVLimitedValue b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.Id,b.Id) && Same(a.Duration,b.Duration) && Same(a.StartTime,b.StartTime));
|
||||
static bool Same(PlayerKVFixedTimeValue a,PlayerKVFixedTimeValue b) => ReferenceEquals(a,b) || (a!=null && b!=null && Same(a.id,b.id) && Same(a.endTime,b.endTime));
|
||||
Reference in New Issue
Block a user