diff --git a/CodexOutput/upserver/B3DesignAudit/B3DesignAudit.csproj b/CodexOutput/upserver/B3DesignAudit/B3DesignAudit.csproj new file mode 100644 index 000000000..f1081d6e7 --- /dev/null +++ b/CodexOutput/upserver/B3DesignAudit/B3DesignAudit.csproj @@ -0,0 +1,11 @@ + + + Exenet9.0 + enabledisable + $(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config + + + $(MSBuildSDKsPath)/../Roslyn/bincore/Microsoft.CodeAnalysis.dll + $(MSBuildSDKsPath)/../Roslyn/bincore/Microsoft.CodeAnalysis.CSharp.dll + + diff --git a/CodexOutput/upserver/B3DesignAudit/Program.cs b/CodexOutput/upserver/B3DesignAudit/Program.cs new file mode 100644 index 000000000..f9ed62bd5 --- /dev/null +++ b/CodexOutput/upserver/B3DesignAudit/Program.cs @@ -0,0 +1,81 @@ +// Design inventory only. Parses source; never loads or executes game code. +using System.Text.Json; +using System.Security.Cryptography; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +var root = Path.GetFullPath(args[0]); +var output = Path.GetFullPath(args[1]); +var configs = new Dictionary { + ["editor-standalone"] = new[]{"UNITY_EDITOR","UNITY_STANDALONE_WIN"}, + ["editor-android-agg"] = new[]{"UNITY_EDITOR","UNITY_ANDROID","AGG"}, + ["player-android-agg"] = new[]{"UNITY_ANDROID","AGG"}, + ["player-ios-agg"] = new[]{"UNITY_IOS","AGG"}, + ["player-standalone"] = new[]{"UNITY_STANDALONE_WIN"} +}; +var calls = new Dictionary(); +var refs = new Dictionary>(); +var files = new Dictionary(); +foreach (var path in Directory.EnumerateFiles(Path.Combine(root,"Client/Assets/Scripts"),"*.cs",SearchOption.AllDirectories).Order()) { + var rel = Path.GetRelativePath(root,path).Replace('\\','/'); + var source = File.ReadAllText(path); + files[rel] = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))); + foreach (var config in configs) { + var tree = CSharpSyntaxTree.ParseText(source,new CSharpParseOptions(preprocessorSymbols:config.Value)); + var node = tree.GetRoot(); + foreach (var inv in node.DescendantNodes().OfType()) { + string name = inv.Expression switch { + MemberAccessExpressionSyntax m => m.Name.Identifier.ValueText, + MemberBindingExpressionSyntax m => m.Name.Identifier.ValueText, + SimpleNameSyntax s => s.Identifier.ValueText, + _ => "" + }; + int line=tree.GetLineSpan(inv.Span).StartLinePosition.Line+1; + if(!refs.TryGetValue(name,out var refsForName)) refs[name]=refsForName=new(); + refsForName.Add(rel+":"+line); + var full = inv.ToString(); + var conditional=inv.Ancestors().OfType().FirstOrDefault(); + var receiver = inv.Expression.ToString()+" "+conditional?.Expression.ToString(); + bool facade=receiver.Contains("PlayFabMgr") && new[]{"GetLocalData","UpdateUserDataValue","SyncDataImmidiate","CompareDatas"}.Contains(name); + bool sdk=receiver.Contains("PlayFabClient") && (name.Contains("UserData")); + // Include unqualified internal facade calls, which the old text scan omitted. + bool internalFacade=rel.EndsWith("/Core/PlayFabMgr.cs") && new[]{"GetLocalData","UpdateUserDataValue","SyncDataImmidiate","CompareDatas","SyncData"}.Contains(name); + if(!facade && !sdk && !internalFacade) continue; + string key=rel+":"+line+":"+inv.SpanStart; + if(calls.TryGetValue(key,out var previous)){previous.Configs.Add(config.Key);continue;} + var member=inv.Ancestors().FirstOrDefault(n=>n is BaseMethodDeclarationSyntax || n is PropertyDeclarationSyntax); + string memberName=member switch { + MethodDeclarationSyntax m => m.Identifier.ValueText, + ConstructorDeclarationSyntax c => c.Identifier.ValueText, + PropertyDeclarationSyntax p => p.Identifier.ValueText, + _=>"" + }; + var type=inv.Ancestors().OfType().FirstOrDefault()?.Identifier.ValueText; + var e=new Entry { Path=rel,Line=line,Member=type+"."+memberName,MemberName=memberName, + MemberLine=member==null?line:tree.GetLineSpan(member.Span).StartLinePosition.Line+1, + Kind=sdk?"sdk":internalFacade?"facade-internal":"facade-call",Method=name, + Arguments=inv.ArgumentList.Arguments.Select(x=>x.ToString()).ToArray(), + CallbackAncestor=inv.Ancestors().Any(x=>x is AnonymousFunctionExpressionSyntax), + AwaitLines=member?.DescendantNodes().OfType().Select(x=>tree.GetLineSpan(x.Span).StartLinePosition.Line+1).ToArray()??Array.Empty(), + MemberSource=member?.ToString()??full, Configs=new(){config.Key}, Source=full }; + calls[key]=e; + } + } +} +var ordered=calls.Values.OrderBy(x=>x.Path).ThenBy(x=>x.Line).ToArray(); +int i=0; +foreach(var entry in ordered){entry.Id="B3-C"+(++i).ToString("D3");entry.CallerNameCandidates=refs.GetValueOrDefault(entry.MemberName)?.Order().ToArray()??Array.Empty();} +Directory.CreateDirectory(output); +File.WriteAllText(Path.Combine(output,"calls.json"),JsonSerializer.Serialize(ordered,new JsonSerializerOptions{WriteIndented=true})); +File.WriteAllText(Path.Combine(output,"source-hashes.json"),JsonSerializer.Serialize(files,new JsonSerializerOptions{WriteIndented=true})); +Console.WriteLine(JsonSerializer.Serialize(new {calls=ordered.Length,files=ordered.Select(x=>x.Path).Distinct().Count(),sourceFiles=files.Count,groups=ordered.GroupBy(x=>x.Kind).ToDictionary(g=>g.Key,g=>g.Count())})); + +class Entry { + public string Id{get;set;} public string Path{get;set;} public int Line{get;set;} + public string Member{get;set;} public string MemberName{get;set;} public int MemberLine{get;set;} + public string Kind{get;set;} public string Method{get;set;} public string[] Arguments{get;set;} + public bool CallbackAncestor{get;set;} public int[] AwaitLines{get;set;} + public List Configs{get;set;} public string[] CallerNameCandidates{get;set;} + public string MemberSource{get;set;} public string Source{get;set;} +} diff --git a/CodexOutput/upserver/B3DesignAudit/render_inventory.py b/CodexOutput/upserver/B3DesignAudit/render_inventory.py new file mode 100644 index 000000000..9170e41cc --- /dev/null +++ b/CodexOutput/upserver/B3DesignAudit/render_inventory.py @@ -0,0 +1,89 @@ +"""Render design facts only; does not modify or execute game code.""" +import json +from pathlib import Path + +base = Path(__file__).resolve().parent +calls = json.loads((base / 'calls.json').read_text(encoding='utf-8-sig')) +def assign(c): + n = int(c['Id'].split('C')[-1]) + if c['Path'].endswith('/Core/PlayFabMgr.cs'): + return 'A保留旧区;C另审生命周期', 'PF原流程;本次不改' + if c['Method'] == 'SyncDataImmidiate' or n == 38: + return '4C消费合同;B先审归属', '消费/等待;不得KV启用' + if n == 190: + return '身份/外围专项保留PF', '读取他人Lv;不转本人KV' + if n in (168, 183, 184, 185, 186, 187, 188, 189): + return 'B归属+C初始化发布', '登录/渠道/GM;不复用PF初始化' + if n in (126, 194): + return 'B调试入口边界', '默认禁止KV调试写;不默授权限' + if n in (1, 2, 3, 127, 128, 129): + return 'B仓库与事件上游详审', '泛型/继承;捕获异常默认值须拆开' + if n in (213, 214): + return 'B渠道回调+身份专项', 'await后UI/奖励也须旧epoch拒绝' + if n in (63,64,65,66,67,151,152,171,176,182): + return 'B归属;奖励/支付结果另裁决', '先变内存/奖励风险;禁自动重试' + return 'B逐业务完整详审', '同步末端不等于上游无延后回调' + +header = '''# 阶段4B3调用事实与分期附件 + +日期:2026-09-24。配套[分期与4B3A方案](阶段4B3分期与4B3A固定存档入口方案-待评审-2026-09-24.md)。 + +**本附件是语法调用事实和阶段归属,不是4B3B完成的回调隔离合同。** 所有B/C行当前均禁止凭此编码;它们既不进入A白名单,也不获得KV运行资格。A只新增四个尚无业务调用者的静态入口。 + +## 1. 可复现盘点和限制 + +使用本地.NET 9.0.310自带Roslyn,读取Client/Assets/Scripts全部2101个.cs文件,仅解析语法,不编译或执行游戏。五种条件符号合并去重:Editor Standalone、Editor Android+AGG、Player Android+AGG、Player iOS+AGG、Player Standalone。223个位置/81文件:外部facade 212、facade内部8、SDK3。原文本234行/84文件还包含注释、声明及B2配置引用,统计口径不同。 + +`B3DesignAudit/calls.json`逐项含源行、所在类型/方法/属性、完整参数、方法体、局部await行、编译条件、按方法名匹配的上游位置。`source-hashes.json`封存2101个源文件hash。工具源码也纳入受审基线,运行仅产生设计分析数据。 + +**重要限制**:CallerNameCandidates是名称候选,可能把不同类型的Save/Load混在一起;数量0也不能证明无UnityEvent/方法组/反射调用。AwaitLines只观察包含本调用的方法;空列表不是整条链同步的证据。未枚举的自定义编译符号和包内/持久化UnityEvent另需B审查。此处不输出未经语义验证的“全部上游已覆盖”结论。 + +可复现命令(项目内NuGet配置,无新包):`dotnet run --project CodexOutput/upserver/B3DesignAudit/B3DesignAudit.csproj -- C:/projects/ft C:/projects/ft/CodexOutput/upserver/B3DesignAudit`。送审后复跑需另给输出目录,不覆盖受审快照。 + +## 2. 已人工核对的调用链与设计约束 + +| 位置 | 已确认链/副作用 | 捕获和取消的待审合同 | +|---|---|---| +| C050 SavePlayerData | 17个按名候选,方法自身先Publish(this),再改NewPlayerDataDic并从Fish/Achievement/Box/Event读取;remote参数可传递 | 必须从最早内存变更者起捕获,不能到此方法末端才验epoch;需要保护整个跨模块序列化对象集合 | +| C038/C223 RewardTurntableRoll→ConsumeOnPlayfab | Panel:530传false;方法先Publish、扣券、更新过渡数据与红点,后localOnly保存;随后立即远端bool | B只隔离归属;4C设计未知结果与防重入;不能把两次保存当事务 | +| C051 OpenAppraisePopupPanel | FishingRewardPanel:963调用,await ShowUI后改IsOpenAppraise并存档 | 从打开UI前捕获,await后先检查;关闭旧面板和取消等待仍不能替代epoch检查 | +| C063~067 OnBuy/Replenishment→BuySuccess | OnBuy含SDK/支付await;Replenishment亦进入BuySuccess及奖励/事件 | 支付开始时绑定账号及业务单据,不能补单回调取当前账号;B处理归属,支付恢复语义另审 | +| C151/152 OnBuySuccess | LuckyADTaskPopupPanel:87将方法组交给Item.Init;即使方法内无await也有延后来源 | 在Item回调注册处捕获;面板退出退订/失效;进入回调即检查,早于领奖/计数 | +| C171 ClubSingin | 自身已有API身份/版本捕获与await后检查,随后signTime、保存、聊天及奖励 | 复用现有API防护并证明与KV Owner/epoch绑定;避免削弱既有ClubService边界 | +| C176 OnReceiveLinkAward;C182 GetSurveyCheckAnswer | 先AddItem/Publish后标记领取并保存;Survey catch会吞掉异常继续由调用者操作 | 防止“只看保存结果”方案;失败恢复/奖励契约单独裁决,不能写失败后重发奖励 | +| C001~003 EventDataRepository | ActivityManager:34注册单例;Activity.Data/AEventDataManager有Repository.Load??new T、Save后Publish、RefreshData中AutoSaveOnCreate | 仓库与上游默认值链一起改才有效;账号关闭必须使旧事件与实例归属失效 | +| C127~129 game.AEventDataManager | 构造时Subscribe(TargetEvent),Dispose释放;GetCacheData从全局facade读、缺档new T | 注册订阅时绑定账号归属,旧事件入队仍应拒绝;继承类和公开Data可变引用须另外追踪 | +| C213 OnLinkSns | 四个按钮方法await OnLinkSns后仍改按钮/发奖;OnLinkSns内部还有账号切换UI await | 外层及内层同一操作归属;不能仅在UpdateUserDataValue前检查;finally关闭等待UI也须限制原面板实例 | +| C190 GetPlayLv | 请求PlayFabId=参数playFabId,Keys=[Lv],写PlayLvs[playFabId]缓存 | 属他人PF资料;不接本人KV。个人身份切换及缓存清理另审 | +| C177/178 ShowNotice | 属性get惰性读缓存,setter会写存档 | 账号换代必须清旧缓存或绑定新实例;无await也不能默认为账号无关设置 | + +## 3. 逐调用位置与拟归属 + +以下“局部await”列只描述所在方法语法。每行的上下文原文与上游候选保存在同ID JSON。key为源码表达式,不把泛型名/变量/插值key误记为一个固定键。 + +| ID | 源文件:行(相对Client/Assets/Scripts) | 所在方法/属性 | 调用 / key表达式 | 局部await | 拟归属 | 处理约束 | +|---|---|---|---|---|---|---| +''' +def cell(x): + return str(x).replace('|', '|').replace('\r', '').replace('\n', ' ') +rows=[] +classified=[] +for c in calls: + phase,note=assign(c) + args=c['Arguments'] + key=args[0] if args else '—' + # Full SDK request remains in JSON; keep the table readable. + if c['Kind']=='sdk': key='见JSON完整请求' + rows.append('| '+' | '.join(map(cell,[c['Id'],c['Path'].replace('Client/Assets/Scripts/','')+':'+str(c['Line']),c['Member'],c['Method']+' / '+key,','.join(map(str,c['AwaitLines'])) or '无(仅局部)',phase,note]))+' |') + classified.append(dict(id=c['Id'],path=c['Path'],line=c['Line'],phaseProposal=phase,implementationApproved=False,upstreamIsolationComplete=False)) +tail=''' + +## 4. B3B完整合同的进入条件 + +本轮不把B3B未知项隐藏到实施阶段。B方案送审前,每个拟启用的业务入口必须给出已验证的上游调用/事件注册位置、身份捕获点、首个字段/集合修改点、取消/退订点、共享对象生命周期、localOnly/奖励副作用、错误处置以及对应验收用例。整个B单元的启用白名单必须闭合;任何新增文件先入方案,不能从本81文件统计推导允许批量改动。 + +在此之前,旧业务路径维持原PF;公共kv登录仍BackendNotEnabled。主方案A验收不以该待完成工作为已解决。外部应对“可先实施A”单独裁决,若不同意则退回分期。 +''' +(base.parent/'阶段4B3调用事实与分期附件-2026-09-24.md').write_text(header+'\n'.join(rows)+tail,encoding='utf-8') +(base/'classification.json').write_text(json.dumps(classified,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') +print(json.dumps({'renderedCalls':len(calls),'allImplementationApproved':False})) diff --git a/CodexOutput/upserver/ClubRegression/ClubRegression.csproj b/CodexOutput/upserver/ClubRegression/ClubRegression.csproj new file mode 100644 index 000000000..178760a4e --- /dev/null +++ b/CodexOutput/upserver/ClubRegression/ClubRegression.csproj @@ -0,0 +1 @@ +Exenet9.0enable$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll diff --git a/CodexOutput/upserver/ClubRegression/Program.cs b/CodexOutput/upserver/ClubRegression/Program.cs new file mode 100644 index 000000000..b1ba687af --- /dev/null +++ b/CodexOutput/upserver/ClubRegression/Program.cs @@ -0,0 +1,49 @@ +void Assert(bool ok){if(!ok)throw new Exception("Club regression failed");} +int passed=0; +foreach(var body in new string[]{null,"","[]","{}","bad","{\"code\":\"0\"}","{\"code\":2}","{\"code\":1,\"clubInfo\":{\"id\":8}}","{\"code\":0}","{\"code\":0,\"clubInfo\":{\"id\":0}}"}){ + var s=new Subject();s.customServerMgr.Response=body;var old=s.myClubInfo; + Assert(await s.CreateClub(new ClubSettingData())==null && ReferenceEquals(old,s.myClubInfo) && s.Effects==0 && s.chatService.Effects==0);passed++; +} +foreach(var body in new string[]{null,"","[]","{}","bad","{\"code\":\"0\"}","{\"code\":2}"}){ + var s=new Subject();s.customServerMgr.Response=body;var old=s.myClubInfo;var toasts=ToastPanel.Count; + s.QuitClub();Assert(ReferenceEquals(old,s.myClubInfo) && s.Effects==0 && s.chatService.Effects==0 && ToastPanel.Count==toasts);passed++; +} +{ + var s=new Subject();s.customServerMgr.Response="{\"code\":0,\"clubInfo\":{\"id\":8}}"; + Assert((await s.CreateClub(new ClubSettingData())).id==8 && ((CreateClubRequest)s.customServerMgr.Body).playerId=="101" && s.internalIdToPlayerId[42]=="101" && s.memberRole==EMemberRole.Owner);passed++; +} +foreach(var code in new[]{0,1}){var s=new Subject();s.customServerMgr.Response=$"{{\"code\":{code}}}";s.QuitClub("102");Assert(s.myClubInfo==null && s.chatService.Effects==1 && ((QuitClubRequest)s.customServerMgr.Body).appointId=="102" && ((QuitClubRequest)s.customServerMgr.Body).playerId=="101");passed++;} +{ + var s=new Subject();var old=s.myClubInfo;s.customServerMgr.Gate=new();var pending=s.CreateClub(new ClubSettingData());s.customServerMgr.SessionVersion++;s.userService.ApiPlayerId="102";s.customServerMgr.Gate.SetResult("{\"code\":0,\"clubInfo\":{\"id\":8}}");Assert(await pending==null && ReferenceEquals(old,s.myClubInfo) && s.Effects==0);passed++; +} +{ + var s=new Subject();var old=s.myClubInfo;s.customServerMgr.Gate=new();s.QuitClub();s.customServerMgr.SessionVersion++;s.customServerMgr.Gate.SetResult("{\"code\":0}");Assert(ReferenceEquals(old,s.myClubInfo) && s.Effects==0);passed++; +} +{ + var s=new Subject();s.userService.IsAuthorized=false;Assert(await s.CreateClub(new ClubSettingData())==null);s.QuitClub();s.Demote("102");s.Promote("102",EMemberRole.Elder);s.Kickout("102");Assert(s.customServerMgr.Calls==0);passed++; +} +{ + var s=new Subject();s.QuitClub("101");s.QuitClub("HEX");s.Demote("101");s.Promote("HEX",EMemberRole.Elder);s.Kickout("101");Assert(s.customServerMgr.Calls==0);s.Demote("102");Assert(((DemoteClubRequest)s.customServerMgr.Body).demoteId=="102" && ((DemoteClubRequest)s.customServerMgr.Body).playerId=="101");passed++; +} +{ + var s=new Subject();s.customServerMgr.Response="{\"code\":2}";s.Kickout("102");Assert(s.myClubInfo.curMembers==3);s.customServerMgr.Response="{\"code\":0}";s.Kickout("102");Assert(s.myClubInfo.curMembers==2);passed++; +} + +{ + var s=new Subject();s.customServerMgr.Response="{\"code\":4}";s.HandleApply(false,"");Assert(s.clubPlayerInfos.Count==0 && s.myClubInfo.curMembers==3);passed++; + s=new Subject();s.customServerMgr.Response="{\"code\":0}";s.HandleApply(false,"");Assert(s.clubPlayerInfos.Count==1);passed++; + s=new Subject();s.HandleApply(true,"");Assert(s.customServerMgr.Calls==0);passed++; +} +{ + var s=new Subject();s.customServerMgr.Gate=new();var rewards=PlayerItemData.Rewards;var writes=PlayFabMgr.Instance.Writes; + var first=s.ClubSingin();await s.ClubSingin();Assert(s.customServerMgr.Calls==1 && s.signTime==null); + s.customServerMgr.Gate.SetResult("{\"code\":0}");await first;await s.ClubSingin();Assert(s.customServerMgr.Calls==1 && PlayerItemData.Rewards==rewards+1 && PlayFabMgr.Instance.Writes==writes+1);passed++; +} +{ + var s=new Subject();var rewards=PlayerItemData.Rewards;s.customServerMgr.Response=null;await s.ClubSingin();Assert(s.signTime==null && PlayerItemData.Rewards==rewards); + s.customServerMgr.Response="{\"code\":0}";await s.ClubSingin();Assert(s.signTime!=null && s.customServerMgr.Calls==2 && PlayerItemData.Rewards==rewards+1);passed++; +} +{ + var s=new Subject();var rewards=PlayerItemData.Rewards;var writes=PlayFabMgr.Instance.Writes;s.customServerMgr.Gate=new();var old=s.ClubSingin();s.customServerMgr.SessionVersion++;s.userService.ApiPlayerId="102";s.customServerMgr.Gate.SetResult("{\"code\":0}");await old;Assert(s.signTime==null && PlayerItemData.Rewards==rewards && PlayFabMgr.Instance.Writes==writes);passed++; +} +Console.WriteLine($"PASS {passed} club cases: failure preserves state, explicit business success, decimal self/target identity, stale responses, no duplicate creation charge path"); diff --git a/CodexOutput/upserver/ClubRegression/generate.py b/CodexOutput/upserver/ClubRegression/generate.py new file mode 100644 index 000000000..2a9567c5c --- /dev/null +++ b/CodexOutput/upserver/ClubRegression/generate.py @@ -0,0 +1,42 @@ +from pathlib import Path +s=Path('Client/Assets/Scripts/Services/ClubService.cs').read_text(encoding='utf-8-sig') +models=Path('Client/Assets/Scripts/Services/ClubModel.cs').read_text(encoding='utf-8-sig') +def block(text,marker): + start=text.index(marker);a=text.index('{',start);depth=1;b=a+1 + while depth: + depth+=(text[b]=='{')-(text[b]=='}');b+=1 + return text[start:b] +helper=s[s.index(' private static bool IsDecimalPlayerId'):s.index(' public async Task> GetClubDetails')] +methods='\n'.join(block(s,m) for m in ['public async Task CreateClub(', 'public async void QuitClub(', 'public async void Demote(', 'public async void Promote(', 'public async void Kickout(', 'public async void HandleApply(', 'public async Task ClubSingin(']) +dtos='\n'.join(block(models,'public class '+name+'\n') for name in ['ClubInfo','ClubSettingData','CreateClubRequest','CreateClubResp','QuitClubRequest','QuitClubResp','DemoteClubRequest','PromoteClubRequest','KickoutClubRequest','KickoutClubResp','HandleApplyRequest','HandleApplyResp','ClubSinginRequest','ClubSinginResp']) +code='''// Generated from production methods; do not edit. Engine and transport are controlled test doubles. +using System;using System.Collections.Generic;using System.ComponentModel;using System.Linq;using System.Threading.Tasks;using Newtonsoft.Json.Linq; +public enum EMemberRole:byte {Owner,Vice,Member,Elder} +'''+dtos+''' +public class Subject { + public User userService=new();public Server customServerMgr=new();public Chat chatService=new();public ClubInfo myClubInfo=new(){id=9,curMembers=3};public EMemberRole memberRole=EMemberRole.Member;public Dictionary internalIdToPlayerId=new();public int Effects; + private int? signingSession; public DateTime? signTime; public List clubPlayerInfos=new(){new Person{playFabId="102"}}; public bool CanAccepted=true; void SetClubRed(){} void ShowPromote(){Effects++;}void MyClubInfoChange(bool join=false){Effects++;}void ClearLikeCursorIds(){Effects++;} +'''+helper+methods+''' +} +public class User{public string UserId="8BDE34F732DD4210";public string ApiPlayerId="101";public bool IsAuthorized=true;public uint InternalId=42;} +public class Server{public int SessionVersion;public int Calls;public object Body;public string Response;public TaskCompletionSource Gate;public Task ClubRequest(string path,object data){Calls++;Body=data;return Gate?.Task??Task.FromResult(Response);}} +public class Chat{public int Effects;public void SendSystemMessage(string text){Effects++;}public void OnQuit(){Effects++;}public void GetConversationList(long id){Effects++;}} +public class ClubSttingUpdateEvent{} public static class GContext {public static Container container=new();public static int Events;public static void Publish(object value){Events++;}} +public static class ToastPanel{public static int Count;public static void Show(string text){Count++;}} +public static class LocalizationMgr{public static string GetText(string key)=>key;} +''' +Path('CodexOutput/upserver/ClubRegression/Generated.cs').write_text(code) + +p=Path('CodexOutput/upserver/ClubRegression/Generated.cs') +with p.open('a') as f:f.write(""" +public class Person{public string playFabId;} +public enum RedPointName {ApplyList} +public class RedPointManager {public static RedPointManager Instance=new();public void SetRedPointState(RedPointName n,bool b){}} +public static class ZZTimeHelper {public static DateTime UtcNow()=>new DateTime(2026,9,21);public static DateTime UtcNowOffset(this DateTime time)=>time;} +public class PlayFabMgr {public static PlayFabMgr Instance=new();public int Writes;public void UpdateUserDataValue(string k,string v){Writes++;}} +public enum MessageChatType {SI} +public class Container {public T Resolve() where T:new()=>new T();} +public class PlayerItemData {public static int Rewards;public void AddItemByDrop(int id,object ctx){Rewards++;}} +public class ShowData{} +namespace cfg {public class Tables {public Config TbServerConfig=new();}public class Config{public int ClubSignReward=1;}} +""") diff --git a/CodexOutput/upserver/ExitTests/ExitTests.csproj b/CodexOutput/upserver/ExitTests/ExitTests.csproj new file mode 100644 index 000000000..8ccbb5bc7 --- /dev/null +++ b/CodexOutput/upserver/ExitTests/ExitTests.csproj @@ -0,0 +1 @@ +Exenet9.0enable$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config diff --git a/CodexOutput/upserver/ExitTests/Program.cs b/CodexOutput/upserver/ExitTests/Program.cs new file mode 100644 index 000000000..5932315a8 --- /dev/null +++ b/CodexOutput/upserver/ExitTests/Program.cs @@ -0,0 +1,30 @@ +using game; +void Assert(bool ok){if(!ok)throw new Exception("Exit assertion failed");} +foreach(var logout in new[]{true,false}) foreach(var failure in new[]{true,false}) { + GameCore.Timer.Reset(); + var user=new User();var hub=new Hub();var ads=new Ads(); + var act=new QuitAct{userService=user,rtService=hub,config=new Config(),adsService=ads,soundMgr=new SoundMgr()}; + QuitStage.Logout=logout;var pending=act.StartAsync(); + Assert(!pending.IsCompleted && user.Logouts==(logout?1:0) && user.Valid==!logout && !QuitStage.Logout); + user.DeliverOldHttpAndLogin();Assert(user.Applied==(logout?0:2)); + if(failure)hub.Gate.SetException(new Exception("Stop failed"));else hub.Gate.SetResult(true); + await pending;Assert(ads.Released && GameCore.Timer.Destroyed==1 && GameCore.Timer.Cancelled==1); + await act.StartAsync();Assert(user.Logouts==(logout?1:0)); + Console.WriteLine($"PASS actual QuitAct: logout={logout}, stopFailure={failure}, identity invalidated before stop wait and no duplicate logout"); +} +class User:IUserService {public bool Valid=true;public int Logouts,Applied;public void Logout(){Valid=false;Logouts++;} public void DeliverOldHttpAndLogin(){if(Valid)Applied+=2;} } +class Hub:IRTService {public TaskCompletionSource Gate=new();public Task ReleaseAsync()=>Gate.Task;} +class Ads:IAdsService {public bool Released;public void Release(){Released=true;}} +class Config:asap.core.IConfig{public void Rest(){}} +public interface IRTService{Task ReleaseAsync();}public interface IAdsService{void Release();} +namespace asap.core { public class InjectAttribute:Attribute{} public interface IConfig{void Rest();} } +namespace game {public interface IUserService{void Logout();}public static class QuitStage{public static bool Logout;} } +namespace Game {public class AGameAct {protected virtual void OnDestroy(){} protected virtual void OnStarted(){}public virtual Task StartAsync()=>Task.FromResult(true);public void Invoke(string s,float n){} } } +namespace GameCore {public static class Timer{public static int Cancelled,Destroyed;public static void Reset(){Cancelled=Destroyed=0;}public static void CancelAllRegisteredTimers(){Cancelled++;}public static void Destroy(){if(Cancelled!=Destroyed+1)throw new Exception("Timer destroyed before cancellation");Destroyed++;}} } +namespace UnityEngine {public class GameObject {public static void Destroy(object x){}public static void DestroyImmediate(object x){}public static GameObject Find(string x)=>null;}public static class Debug{public static void LogException(Exception e){}}} +public class SoundMgr {public void Release(){}} +public static class LoadingScreen{public static void Hide(){}} +public class UIManager{public static UIManager Instance=new();public UnityEngine.GameObject gameObject=new();} +public class GameEventMgr{public static GameEventMgr Instance=new();public void Clear(){}} +public class RedPointManager{public static RedPointManager Instance=new();public void Init(){}} +public static class RootCtx{public static void DestoryRootCtx(){}} diff --git a/CodexOutput/upserver/Gameplay101Validation/guide-live-inspect.cs.txt b/CodexOutput/upserver/Gameplay101Validation/guide-live-inspect.cs.txt new file mode 100644 index 000000000..f1bd99fe0 --- /dev/null +++ b/CodexOutput/upserver/Gameplay101Validation/guide-live-inspect.cs.txt @@ -0,0 +1,11 @@ +using asap.core; +using GameCore; +using game; +if(!GContext.Ready)return "GContext not ready; "+KVGameplayLoopSetup.State(); +var u=GContext.container.Resolve(); +var p=GContext.container.Resolve(); +var g=GContext.container.Resolve(); +var cfg=GContext.container.Resolve(); +var e=GContext.container.Resolve(); +GuideSaveData potion;g.GuideSaveDataDic.TryGetValue("EventPotion01",out potion); +return new {loggedIn=u.IsLogin, account=u.CustomId, internalId=u.InternalId,backend=cfg.Get("SaveBackend",""),api=cfg.Get(GConstant.K_Event_API_URL,""),level=p.lv,gold=p.gold,energy=p.Energy,guideGroup=g.curGroupDefine==null?null:g.curGroupDefine.groupName.ToString(),guideNode=g.curDefine!=null,guideSavePointer=g.curSaveData!=null,potionEvent=e.GetEvent(4,5),potionSaved=potion==null?"missing":potion.groupName+"|"+potion.guideSubIndex+"|"+potion.isFinish,state=KVGameplayLoopSetup.State()}.ToString(); diff --git a/CodexOutput/upserver/Gameplay101Validation/guide-recovered-values.cs.txt b/CodexOutput/upserver/Gameplay101Validation/guide-recovered-values.cs.txt new file mode 100644 index 000000000..3ab539c13 --- /dev/null +++ b/CodexOutput/upserver/Gameplay101Validation/guide-recovered-values.cs.txt @@ -0,0 +1,18 @@ +using asap.core; +using GameCore; +using game; +using LitJson; +if(!GContext.Ready || GContext.container==null) return "Bootstrap pending; "+KVGameplayLoopSetup.State(); +var u=GContext.container.Resolve(); +if(!u.IsLogin) return "Login pending; "+KVGameplayLoopSetup.State(); +var p=GContext.container.Resolve(); +var f=GContext.container.Resolve(); +var guide=GContext.container.Resolve(); +var source=SimpleJSON.JSON.Parse(System.IO.File.ReadAllText("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260928T092418Z/101.json"))["data"]; +var mismatches=new System.Collections.Generic.List(); +foreach(var key in source.Keys)if(!string.Equals(PlayFabMgr.Instance.GetLocalData(key),source[key].Value,System.StringComparison.Ordinal))mismatches.Add(key); +var rawMap=JsonMapper.ToObject>(PlayFabMgr.Instance.GetLocalData("Map_5_102")); +var saved=guide.GuideSaveDataDic["EventPotion01"]; +var api=GContext.container.Resolve().ApiBaseUri.ToString(); +bool valuesPass=u.CustomId=="101" && api=="http://s101.local:18080/" && (ulong)p.gold==36552317UL && (int)p.Energy==7300 && p.GetEnergyRM()==7236 && rawMap["10231"]=="6|9.23|54|5|False" && System.Math.Abs(f.GetDataMaxWeight(10231)-9.23f)<0.001f && f.GetDataLevel(10231)==6 && saved.guideSubIndex==4 && !saved.isFinish && guide.curDefine==null && guide.curGroupDefine==null && guide.curSaveData==null; +return JsonMapper.ToJson(new {valuesPass,account=u.CustomId,internalId=u.InternalId,api,gold=(ulong)p.gold,energy=(int)p.Energy,energyRM=p.GetEnergyRM(),core=p.PlayerDataDic["newData"],fishId=10231,fishRaw=rawMap["10231"],liveMaxWeight=f.GetDataMaxWeight(10231),liveLevel=f.GetDataLevel(10231),liveNewFish=f.GetDataNewFish(10231),guide=saved.groupName+"|"+saved.guideSubIndex+"|"+saved.isFinish,comparedKeys=source.Count,mismatches,state=KVGameplayLoopSetup.State()}); diff --git a/CodexOutput/upserver/Gameplay101Validation/guide-regressions.cs.txt b/CodexOutput/upserver/Gameplay101Validation/guide-regressions.cs.txt new file mode 100644 index 000000000..648897d77 --- /dev/null +++ b/CodexOutput/upserver/Gameplay101Validation/guide-regressions.cs.txt @@ -0,0 +1,4 @@ +var consume=await KVGameplayLoopSetup.RunValidationAsync("fishing-consume"); +var guides=await KVGameLoadValidation.RunAsync("M09-M10-M12"); +var full=await KVGameLoadValidation.RunAsync("B2-20"); +return "consume="+consume+"\nguides="+guides+"\nfull="+full; diff --git a/CodexOutput/upserver/Gameplay101Validation/inspect-source101.cs.txt b/CodexOutput/upserver/Gameplay101Validation/inspect-source101.cs.txt new file mode 100644 index 000000000..db7b2aa98 --- /dev/null +++ b/CodexOutput/upserver/Gameplay101Validation/inspect-source101.cs.txt @@ -0,0 +1 @@ +return KVGameplayLoopSetup.InspectPFBackup("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/pf-source-backups/20260924T041802Z/101.json"); \ No newline at end of file diff --git a/CodexOutput/upserver/Gameplay101Validation/prepare-guide-fixed-v3.cs.txt b/CodexOutput/upserver/Gameplay101Validation/prepare-guide-fixed-v3.cs.txt new file mode 100644 index 000000000..efeea53b6 --- /dev/null +++ b/CodexOutput/upserver/Gameplay101Validation/prepare-guide-fixed-v3.cs.txt @@ -0,0 +1,4 @@ +if(UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || UnityEditor.EditorApplication.isCompiling || UnityEditor.EditorApplication.isUpdating) throw new System.InvalidOperationException("Idle required"); +if(System.IO.File.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/permit.json")) throw new System.InvalidOperationException("Active permit exists"); +using(var p=System.Diagnostics.Process.GetCurrentProcess()){var id=p.Id+"_"+p.StartTime.ToUniversalTime().Ticks;if(System.IO.File.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/run-control/"+id+".claimed") || System.IO.Directory.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/runs/"+id))throw new System.InvalidOperationException("Process already used");} +return KVGameplayLoopSetup.PrepareFishingRewardPermit("101","C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260928T091527Z/101.json",new string[]{"AquariumEventDataNewKey","BattlePassRecordTask","BombGameCount","BuffDataList","BuyData","ChainPackDataNew","DeferredRewardStashService","epbdk","epdk","Event1v1BattleSlapData","EventBingo","EventBoostPackData","EventBossFightData","EventCubeMeltData","EventLuckGameRecord","EventLuckMagicPlayfabData","EventLuckyTowerData","EventOfferDevourFishData","EventPackData","EventPartnerStatueData","EventTransitionTokenDic","FishHuntGameCount","FishingChallengeData","FixedTimeShopDic","GeneralEventPackData","GuideSaveDataNew","IsOpenAppraise","IsOpenClubRed","LastPaymentTime","Leadboard_LastContinent","LuckyTaskADKey","Map_5_101","Map_5_102","MiniBPKeyNew","MMTest","MonthlySignEvent","NewPlayerDataDic","NewRodList","OfferChainsChestData","OfferDiscoPartyManager","OfferStoryChain","OfferSweepPackData","Pack1A1Data","Pack1A2Data","PiggyBankPackData","RodData","RodLevelData","SeasonStampData","ShopPackBuyCount","SoloDataKey","TargetBuffDataList","TransitionData","TriggerPackDataList","VIPAdPackData","WelcomeBack"}); \ No newline at end of file diff --git a/CodexOutput/upserver/Gameplay101Validation/prepare-guide-recovery-v1.cs.txt b/CodexOutput/upserver/Gameplay101Validation/prepare-guide-recovery-v1.cs.txt new file mode 100644 index 000000000..b0cb48ab9 --- /dev/null +++ b/CodexOutput/upserver/Gameplay101Validation/prepare-guide-recovery-v1.cs.txt @@ -0,0 +1,4 @@ +if(UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || UnityEditor.EditorApplication.isCompiling || UnityEditor.EditorApplication.isUpdating) throw new System.InvalidOperationException("Idle required"); +if(System.IO.File.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/permit.json")) throw new System.InvalidOperationException("Active permit exists"); +using(var p=System.Diagnostics.Process.GetCurrentProcess()){var id=p.Id+"_"+p.StartTime.ToUniversalTime().Ticks;if(System.IO.File.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/run-control/"+id+".claimed") || System.IO.Directory.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/runs/"+id))throw new System.InvalidOperationException("Process already used");} +return KVGameplayLoopSetup.PreparePermit("101","C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260928T092418Z/101.json",new string[]{"AquariumEventDataNewKey","BattlePassRecordTask","BombGameCount","BuffDataList","BuyData","ChainPackDataNew","DeferredRewardStashService","epbdk","epdk","Event1v1BattleSlapData","EventBingo","EventBoostPackData","EventBossFightData","EventCubeMeltData","EventLuckGameRecord","EventLuckMagicPlayfabData","EventLuckyTowerData","EventOfferDevourFishData","EventPackData","EventPartnerStatueData","EventTransitionTokenDic","FishHuntGameCount","FishingChallengeData","FixedTimeShopDic","GeneralEventPackData","GuideSaveDataNew","IsOpenAppraise","IsOpenClubRed","LastPaymentTime","Leadboard_LastContinent","LuckyTaskADKey","Map_5_101","Map_5_102","MiniBPKeyNew","MMTest","MonthlySignEvent","NewPlayerDataDic","NewRodList","OfferChainsChestData","OfferDiscoPartyManager","OfferStoryChain","OfferSweepPackData","Pack1A1Data","Pack1A2Data","PiggyBankPackData","RodData","RodLevelData","SeasonStampData","ShopPackBuyCount","SoloDataKey","TargetBuffDataList","TransitionData","TriggerPackDataList","VIPAdPackData","WelcomeBack"}); \ No newline at end of file diff --git a/CodexOutput/upserver/Gameplay101Validation/prepare-manual-v3.cs.txt b/CodexOutput/upserver/Gameplay101Validation/prepare-manual-v3.cs.txt new file mode 100644 index 000000000..c50149d79 --- /dev/null +++ b/CodexOutput/upserver/Gameplay101Validation/prepare-manual-v3.cs.txt @@ -0,0 +1,4 @@ +if(UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || UnityEditor.EditorApplication.isCompiling || UnityEditor.EditorApplication.isUpdating) throw new System.InvalidOperationException("Idle required"); +if(System.IO.File.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/permit.json")) throw new System.InvalidOperationException("Active permit exists"); +using(var p=System.Diagnostics.Process.GetCurrentProcess()){var id=p.Id+"_"+p.StartTime.ToUniversalTime().Ticks;if(System.IO.File.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/run-control/"+id+".claimed") || System.IO.Directory.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/runs/"+id))throw new System.InvalidOperationException("Process already used");} +return KVGameplayLoopSetup.PrepareFishingRewardPermit("101","C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260928T081406Z/101.json",new string[]{"AquariumEventDataNewKey","BattlePassRecordTask","BombGameCount","BuffDataList","BuyData","ChainPackDataNew","DeferredRewardStashService","epbdk","epdk","Event1v1BattleSlapData","EventBingo","EventBoostPackData","EventBossFightData","EventCubeMeltData","EventLuckGameRecord","EventLuckMagicPlayfabData","EventLuckyTowerData","EventOfferDevourFishData","EventPackData","EventPartnerStatueData","EventTransitionTokenDic","FishHuntGameCount","FishingChallengeData","FixedTimeShopDic","GeneralEventPackData","GuideSaveDataNew","IsOpenAppraise","IsOpenClubRed","LastPaymentTime","Leadboard_LastContinent","LuckyTaskADKey","Map_5_101","Map_5_102","MiniBPKeyNew","MMTest","MonthlySignEvent","NewPlayerDataDic","NewRodList","OfferChainsChestData","OfferDiscoPartyManager","OfferStoryChain","OfferSweepPackData","Pack1A1Data","Pack1A2Data","PiggyBankPackData","RodData","RodLevelData","SeasonStampData","ShopPackBuyCount","SoloDataKey","TargetBuffDataList","TransitionData","TriggerPackDataList","VIPAdPackData","WelcomeBack"}); \ No newline at end of file diff --git a/CodexOutput/upserver/Gameplay101Validation/prepare-v3.cs.txt b/CodexOutput/upserver/Gameplay101Validation/prepare-v3.cs.txt new file mode 100644 index 000000000..4965644f3 --- /dev/null +++ b/CodexOutput/upserver/Gameplay101Validation/prepare-v3.cs.txt @@ -0,0 +1,4 @@ +if(UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || UnityEditor.EditorApplication.isCompiling || UnityEditor.EditorApplication.isUpdating) throw new System.InvalidOperationException("Idle required"); +if(System.IO.File.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/permit.json")) throw new System.InvalidOperationException("Active permit exists"); +using(var p=System.Diagnostics.Process.GetCurrentProcess()){var id=p.Id+"_"+p.StartTime.ToUniversalTime().Ticks;if(System.IO.File.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/run-control/"+id+".claimed") || System.IO.Directory.Exists("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/runs/"+id))throw new System.InvalidOperationException("Process already used");} +return KVGameplayLoopSetup.PrepareFishingRewardPermit("101","C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260928T075611Z/101.json",new string[]{"AquariumEventDataNewKey","BattlePassRecordTask","BombGameCount","BuffDataList","BuyData","ChainPackDataNew","DeferredRewardStashService","epbdk","epdk","Event1v1BattleSlapData","EventBingo","EventBoostPackData","EventBossFightData","EventCubeMeltData","EventLuckGameRecord","EventLuckMagicPlayfabData","EventLuckyTowerData","EventOfferDevourFishData","EventPackData","EventPartnerStatueData","EventTransitionTokenDic","FishHuntGameCount","FishingChallengeData","FixedTimeShopDic","GeneralEventPackData","GuideSaveDataNew","IsOpenAppraise","IsOpenClubRed","LastPaymentTime","Leadboard_LastContinent","LuckyTaskADKey","Map_5_101","Map_5_102","MiniBPKeyNew","MMTest","MonthlySignEvent","NewPlayerDataDic","NewRodList","OfferChainsChestData","OfferDiscoPartyManager","OfferStoryChain","OfferSweepPackData","Pack1A1Data","Pack1A2Data","PiggyBankPackData","RodData","RodLevelData","SeasonStampData","ShopPackBuyCount","SoloDataKey","TargetBuffDataList","TransitionData","TriggerPackDataList","VIPAdPackData","WelcomeBack"}); \ No newline at end of file diff --git a/CodexOutput/upserver/Gameplay101Validation/reconcile-guide-final.ps1 b/CodexOutput/upserver/Gameplay101Validation/reconcile-guide-final.ps1 new file mode 100644 index 000000000..54bfc7f5e --- /dev/null +++ b/CodexOutput/upserver/Gameplay101Validation/reconcile-guide-final.ps1 @@ -0,0 +1,30 @@ +$ErrorActionPreference='Stop' +$root='C:/projects/ft/CodexOutput/upserver' +$rounds=@('20260928T091527Z','20260928T092018Z','20260928T092245Z','20260928T092418Z','20260928T092948Z') +function Compare-Values($before,$after){ + $names=[Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + foreach($p in $before.PSObject.Properties){[void]$names.Add($p.Name)} + foreach($p in $after.PSObject.Properties){[void]$names.Add($p.Name)} + $differences=@(foreach($key in $names){ + $a=$before.PSObject.Properties[$key];$b=$after.PSObject.Properties[$key] + if($null -eq $a -or $null -eq $b -or ![string]::Equals([string]$a.Value,[string]$b.Value,[StringComparison]::Ordinal)){$key} + }) + [ordered]@{beforeKeys=@($before.PSObject.Properties).Count;afterKeys=@($after.PSObject.Properties).Count;differentKeys=$differences;ordinalEqual=$differences.Count -eq 0} +} +$comparison=@() +foreach($account in @('101','102')){ + $data=@($rounds|ForEach-Object {(Get-Content -LiteralPath "$root/KVGameplayLoopTests/backups/$_/$account.json" -Raw|ConvertFrom-Json).data}) + for($i=1;$i -lt $rounds.Count;$i++){$comparison+=[ordered]@{account=$account;before=$rounds[$i-1];after=$rounds[$i];comparison=(Compare-Values $data[$i-1] $data[$i])}} +} +$runFacts=@() +foreach($prefix in @('55088','130200')){ + $run=Get-ChildItem "$root/KVGameplayLoopTests/runs" -Directory -Filter ($prefix+'_*') + $events=@(Get-Content ($run.FullName+'/events.jsonl')|ForEach-Object {$_|ConvertFrom-Json}) + $state=Get-Content (Get-ChildItem ($run.FullName+'/cache') -Recurse -Filter state.json).FullName -Raw|ConvertFrom-Json + $final=(Get-Content "$root/KVGameplayLoopTests/backups/20260928T092948Z/101.json" -Raw|ConvertFrom-Json).data + $runFacts+=[ordered]@{run=$run.Name;pending=@($state.pending).Count;activeBatch=$state.activeBatch;unknownBatch=$state.unknownBatch;cacheVsFinal=(Compare-Values $state.baseline $final);counts=@($events|Group-Object kind|ForEach-Object {[ordered]@{kind=$_.Name;count=$_.Count}});closed=($events|Where-Object kind -eq 'closed').at} +} +$result=[ordered]@{createdUtc=[DateTime]::UtcNow.ToString('o');comparisons=$comparison;runs=$runFacts;activePermit=Test-Path "$root/KVGameplayLoopTests/permit.json";claims=@(Get-ChildItem "$root/KVGameplayLoopTests/run-control" -Filter '*.claimed'|ForEach-Object Name);scope='Ordinal comparison of local captured data; event counts are application records, not HTTP request totals'} +$result|ConvertTo-Json -Depth 12|Set-Content "$root/Gameplay101Validation/guide-final-reconciliation.json" -Encoding utf8 +$result.comparisons|ConvertTo-Json -Depth 7 +$result.runs|ConvertTo-Json -Depth 7 diff --git a/CodexOutput/upserver/HomeExitTests/HomeExitTests.csproj b/CodexOutput/upserver/HomeExitTests/HomeExitTests.csproj new file mode 100644 index 000000000..8333f5f9e --- /dev/null +++ b/CodexOutput/upserver/HomeExitTests/HomeExitTests.csproj @@ -0,0 +1 @@ +Exenet9.0enable$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config diff --git a/CodexOutput/upserver/HomeExitTests/Program.cs b/CodexOutput/upserver/HomeExitTests/Program.cs new file mode 100644 index 000000000..297fa21a7 --- /dev/null +++ b/CodexOutput/upserver/HomeExitTests/Program.cs @@ -0,0 +1,29 @@ +void Check(bool value,string message){if(!value)throw new Exception(message);} +foreach(var scenario in new[]{"normal","destroyed","replaced"}){ + GContext.container=new Container();Awaiters.Gate=new();var old=GContext.container;var panel=new Subject(); + var pending=panel.LastShowPanel();Check(!pending.IsCompleted,"expected delay"); + if(scenario=="destroyed")panel.Destroyed=true; + if(scenario=="replaced")GContext.container=new Container(); + Awaiters.Gate.SetResult(true);await pending; + Check(old.Face.Shows==(scenario=="normal"?1:0),"old flow applied after exit"); + if(scenario=="replaced")Check(GContext.container.Face.Shows==0,"old callback opened new account UI"); + Check(Debug.Errors==0,"callback logged an error"); + Console.WriteLine("PASS real LastShowPanel body: "+scenario); +} +public partial class Subject{ + public bool Destroyed; + public static bool operator ==(Subject a,Subject b)=>ReferenceEquals(b,null)?ReferenceEquals(a,null)||a.Destroyed:ReferenceEquals(a,b); + public static bool operator !=(Subject a,Subject b)=>!(a==b); + public override bool Equals(object other)=>ReferenceEquals(this,other); + public override int GetHashCode()=>base.GetHashCode(); +} +public static class GContext{public static Container container=new();} +public class Container{ + public Face Face=new();public BuffDataCenter Buff=new(); + public T Resolve()=>(T)(object)(typeof(T)==typeof(BuffDataCenter)?(object)Buff:Face); +} +public class BuffDataCenter{public List showFishBuffIds=new();} +public interface IFaceUIService{void ShowPostFaceUI();} +public class Face:IFaceUIService{public int Shows;public void ShowPostFaceUI()=>Shows++;} +public static class Awaiters{public static TaskCompletionSource Gate;public static Task Seconds(float n)=>Gate.Task;} +public static class Debug{public static int Errors;public static void LogError(object e)=>Errors++;} diff --git a/CodexOutput/upserver/HomeExitTests/generate.py b/CodexOutput/upserver/HomeExitTests/generate.py new file mode 100644 index 000000000..df00fae57 --- /dev/null +++ b/CodexOutput/upserver/HomeExitTests/generate.py @@ -0,0 +1,7 @@ +from pathlib import Path +s=Path('Client/Assets/Scripts/UI/Home/HomePanel.cs').read_text(encoding='utf-8-sig') +start=s.index('async void LastShowPanel()'); a=s.index('{',start); n=1; end=a+1 +while n: + n+=(s[end]=='{')-(s[end]=='}'); end+=1 +method=s[start:end].replace('async void LastShowPanel()', 'public async Task LastShowPanel()') +Path('CodexOutput/upserver/HomeExitTests/Generated.cs').write_text('using System;using System.Threading.Tasks;public partial class Subject {\n'+method+'\n}',encoding='utf-8') diff --git a/CodexOutput/upserver/HomeLoginTests/HomeLoginTests.csproj b/CodexOutput/upserver/HomeLoginTests/HomeLoginTests.csproj new file mode 100644 index 000000000..178760a4e --- /dev/null +++ b/CodexOutput/upserver/HomeLoginTests/HomeLoginTests.csproj @@ -0,0 +1 @@ +Exenet9.0enable$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll diff --git a/CodexOutput/upserver/HomeLoginTests/Program.cs b/CodexOutput/upserver/HomeLoginTests/Program.cs new file mode 100644 index 000000000..488c5cbca --- /dev/null +++ b/CodexOutput/upserver/HomeLoginTests/Program.cs @@ -0,0 +1,17 @@ +void Assert(bool ok,string name){if(!ok)throw new Exception(name);} +foreach(var json in new string[]{null,"","[]","{}","bad","{\"clubInfo\":false}","{\"clubInfo\":{\"id\":0}}","{\"clubInfo\":{\"id\":8}}"}){ + var s=new Subject();var old=s.myClubInfo;s.customServerMgr.Response=json;s.GetSelfClub();Assert(ReferenceEquals(old,s.myClubInfo)&&s.Effects==0,"Invalid response must preserve known state: "+json); +} +{ + var s=new Subject();s.userService.IsAuthorized=false;s.GetSelfClub();Assert(s.customServerMgr.Calls==0,"No unauthorized request"); +} +{ + var s=new Subject();s.customServerMgr.Response="{\"clubInfo\":null}";s.GetSelfClub();Assert(s.myClubInfo==null && s.customServerMgr.Request.playerId=="101","Explicit no club and correct identity"); +} +{ + var s=new Subject();var old=s.myClubInfo;s.customServerMgr.Gate=new();s.GetSelfClub();s.customServerMgr.SessionVersion++;s.customServerMgr.Gate.SetResult("{\"clubInfo\":null}");Assert(ReferenceEquals(old,s.myClubInfo)&&s.Effects==0,"Old response ignored"); +} +{ + var s=new Subject();s.customServerMgr.Response="{\"clubInfo\":{\"id\":8},\"role\":2,\"playerIDs\":{\"42\":\"101\"}}";s.GetSelfClub();Assert(s.myClubInfo.id==8 && s.memberRole==EMemberRole.Member && s.chatService.Calls==1 && s.internalIdToPlayerId[42]=="101","Valid member response applied"); +} +Console.WriteLine("PASS home GetSelfClub: identity, unauthorized, failed/missing responses, explicit no-club, old response isolation"); diff --git a/CodexOutput/upserver/HomeLoginTests/generate.py b/CodexOutput/upserver/HomeLoginTests/generate.py new file mode 100644 index 000000000..ca4dd9131 --- /dev/null +++ b/CodexOutput/upserver/HomeLoginTests/generate.py @@ -0,0 +1,17 @@ +from pathlib import Path +def block(s,marker): + start=s.index(marker);a=s.index('{',start);n=1;b=a+1 + while n:n+=(s[b]=='{')-(s[b]=='}');b+=1 + return s[start:b] +s=Path('Client/Assets/Scripts/Services/ClubService.cs').read_text(encoding='utf-8-sig');m=Path('Client/Assets/Scripts/Services/ClubModel.cs').read_text(encoding='utf-8-sig') +code='using System;using System.Collections.Generic;using System.ComponentModel;using System.Threading.Tasks;using Newtonsoft.Json.Linq;\n' +code+=block(m,'public enum EMemberRole')+'\n'+'\n'.join(block(m,'public class '+x+'\n') for x in ['ClubInfo','ClubSettingData','GetSelfClubRequest','GetSelfClubResp','ClubPlayerProfile']) +code+=''' +public class Subject {public User userService=new();public Server customServerMgr=new();public Chat chatService=new();public ClubInfo myClubInfo=new(){id=9};public EMemberRole memberRole=EMemberRole.Owner;public Dictionary internalIdToPlayerId=new();public int Effects;void SetClubPlayerProfile(List p){Effects++;}void SetClubRed(){Effects++;}void MyClubInfoChange(){Effects++;} +'''+block(s,'public async void GetSelfClub()')+'\n}\n' +code+=''' +public class User {public bool IsAuthorized=true;public string ApiPlayerId="101";} +public class Server {public int SessionVersion,Calls;public string Response;public GetSelfClubRequest Request;public TaskCompletionSource Gate;public Task ClubRequest(string path,object data){Calls++;Request=(GetSelfClubRequest)data;return Gate?.Task??Task.FromResult(Response);}} +public class Chat{public int Calls;public void GetConversationList(long id){Calls++;}} +''' +Path('CodexOutput/upserver/HomeLoginTests/Generated.cs').write_text(code) diff --git a/CodexOutput/upserver/KVContractTests/KVContractTests.csproj b/CodexOutput/upserver/KVContractTests/KVContractTests.csproj new file mode 100644 index 000000000..aef6e7f08 --- /dev/null +++ b/CodexOutput/upserver/KVContractTests/KVContractTests.csproj @@ -0,0 +1,16 @@ + + + Exe + net9.0 + enable + disable + $(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config + + + + + + ../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll + + + diff --git a/CodexOutput/upserver/KVContractTests/Program.cs b/CodexOutput/upserver/KVContractTests/Program.cs new file mode 100644 index 000000000..d6fa8ae28 --- /dev/null +++ b/CodexOutput/upserver/KVContractTests/Program.cs @@ -0,0 +1,175 @@ +using System.Net; +using System.Text; +using game.PlayerDataStorage; +using Newtonsoft.Json.Linq; + +var passed = 0; +async Task Check(string name, Func action) +{ + await action(); + Console.WriteLine("PASS " + name); + passed++; +} +void Assert(bool ok, string message) { if (!ok) throw new Exception(message); } +async Task<(PlayerKVResult result, Probe handler)> Get(string body, int status = 200) +{ + var handler = new Probe(body, status); + using var http = new HttpClient(handler); + var result = await new PlayerKVClient(http, new Uri("http://example.test/api")).GetAsync(); + return (result, handler); +} + +await Check("Get preserves string/empty/JSON values and full-read request", async () => { + var (r, h) = await Get("{\"data\":{\"empty\":\"\",\"json\":\"{\\\"v\\\":1}\"}}"); + Assert(r.Success && r.Data["empty"] == "" && r.Data["json"] == "{\"v\":1}", "Values changed"); + Assert(h.Url == "http://example.test/api/playerdata/Get" && h.Body == "{}", "Wrong route/body"); +}); +await Check("Empty data is valid", async () => Assert((await Get("{\"data\":{}}")).result.Success, "Empty rejected")); +foreach (var body in new[] { "", "null", "{}", "{\"data\":null}", "{\"data\":[]}", "{\"data\":{\"x\":1}}", "{\"data\":{\"x\":null}}", "not-json" }) + await Check("Invalid Get response " + body, async () => { + var (r, _) = await Get(body); + Assert(!r.Success && r.Error == PlayerKVError.Protocol && r.Data == null, "Invalid response accepted"); + }); +await Check("Filtered read preserves keys", async () => { + var h = new Probe("{\"data\":{}}"); using var http = new HttpClient(h); + await new PlayerKVClient(http, new Uri("http://example.test/")).GetAsync(new[] { "missing", "Lv" }); + Assert(JObject.Parse(h.Body)["keys"].Count() == 2, "Missing keys"); +}); +await Check("Set/Delete accept empty 200 and preserve payload", async () => { + var h = new Probe(""); using var http = new HttpClient(h); + var kv = new PlayerKVClient(http, new Uri("http://example.test/")); + var r = await kv.SetAsync(new Dictionary { ["key"] = "{\"v\":1}" }); + Assert(r.Success && h.Url.EndsWith("/Set") && (string)JObject.Parse(h.Body)["data"]["key"] == "{\"v\":1}", "Set failed"); + Assert((await kv.DeleteAsync(new[] { "key" })).Success && h.Url.EndsWith("/Delete"), "Delete failed"); +}); +foreach (var status in new[] { 401, 403, 400, 500, 302 }) + await Check("HTTP failure " + status, async () => { + var (r, _) = await Get("", status); + Assert(!r.Success && r.StatusCode == status && r.Error == (status == 401 || status == 403 ? PlayerKVError.Unauthorized : PlayerKVError.Http), "Wrong HTTP result"); + }); +await Check("Invalid write/key rejected before HTTP", async () => { + var h = new Probe(""); using var http = new HttpClient(h); + var kv = new PlayerKVClient(http, new Uri("http://example.test/")); + Assert((await kv.SetAsync(null)).Error == PlayerKVError.Validation, "Null write"); + Assert((await kv.SetAsync(new Dictionary { ["x"] = null })).Error == PlayerKVError.Validation, "Null value"); + Assert((await kv.GetAsync(new[] { new string('a',129) })).Error == PlayerKVError.Validation, "Long key"); + Assert((await kv.DeleteAsync(null)).Error == PlayerKVError.Validation && h.Calls == 0, "Unexpected request"); +}); +foreach (var mode in new[] { "network", "timeout", "cancel" }) + await Check(mode + " failure", async () => { + var h = new Probe("") { Mode = mode }; using var http = new HttpClient(h); + using var cts = new CancellationTokenSource(); if(mode == "cancel") cts.Cancel(); + var r = await new PlayerKVClient(http,new Uri("http://example.test/")).GetAsync(null,cts.Token); + var expected = mode == "network" ? PlayerKVError.Network : mode == "timeout" ? PlayerKVError.Timeout : PlayerKVError.Cancelled; + Assert(!r.Success && r.Error == expected, "Wrong exception mapping"); + }); +foreach (var body in new[] { "{\"data\":{}} {}", "{\"data\":{\"x\":\"a\",\"x\":\"b\"}}", "{\"data\":{},\"data\":{}}", "{\"data\":{\"x\":true}}" }) + await Check("Reject ambiguous or non-string response", async () => { + var (r, _) = await Get(body); + Assert(r.Error == PlayerKVError.Protocol && r.Data == null, "Ambiguous data accepted"); + }); +await Check("Date-shaped values remain strings; key comparison is ordinal", async () => { + var (r, _) = await Get("{\"data\":{\"A\":\"2026-09-22T00:00:00Z\",\"a\":\"different\"}}"); + Assert(r.Success && r.Data["A"] == "2026-09-22T00:00:00Z" && r.Data["a"] == "different", "Value coercion"); + try { ((IDictionary)r.Data).Add("mutated", "x"); throw new Exception("Mutable result"); } + catch (NotSupportedException) { } +}); +await Check("Empty operations preserve server authentication and full-read semantics", async () => { + var h = new Probe("",401); using var http = new HttpClient(h);var kv=new PlayerKVClient(http,new Uri("http://example.test/")); + Assert((await kv.GetAsync(Array.Empty())).Error==PlayerKVError.Unauthorized && h.Body=="{}", "Empty Get"); + Assert((await kv.SetAsync(new Dictionary())).Error==PlayerKVError.Unauthorized && h.Body=="{\"data\":{}}", "Empty Set bypassed auth"); + Assert((await kv.DeleteAsync(Array.Empty())).Error==PlayerKVError.Unauthorized && h.Body=="{\"keys\":[]}" && h.Calls==3, "Empty Delete bypassed auth"); +}); +await Check("128-char keys and empty strings allowed; empty/null/129-char keys rejected", async () => { + var h=new Probe("");using var http=new HttpClient(h);var kv=new PlayerKVClient(http,new Uri("http://example.test/")); + Assert((await kv.SetAsync(new Dictionary{{new string('k',128),""}})).Success,"Valid boundary rejected"); + foreach(var key in new[]{"",null,new string('k',129)}) { + Assert((await kv.GetAsync(new[]{key})).Error==PlayerKVError.Validation,"Invalid read key"); + Assert((await kv.DeleteAsync(new[]{key})).Error==PlayerKVError.Validation,"Invalid delete key"); + } + Assert(h.Calls==1,"Validation sent requests"); +}); +foreach(var action in new[]{"Get","Set","Delete"}) + await Check(action+" payload is snapshotted before caller mutation",async()=>{ + var h=new DeferredProbe();using var http=new HttpClient(h);var kv=new PlayerKVClient(http,new Uri("http://example.test/api/")); + var keys=new List{"original"};var data=new Dictionary{{"original","old"}}; + var pending=action=="Get"?kv.GetAsync(keys):action=="Set"?kv.SetAsync(data):kv.DeleteAsync(keys); + keys[0]="changed";data["original"]="new";data["added"]="extra"; + h.Gate.SetResult(true);var r=await pending; + Assert(r.Success && h.Body.Contains("original") && !h.Body.Contains("changed") && !h.Body.Contains("extra") && !h.Body.Contains("new"),"Mutable request leaked"); + Assert(h.Url=="http://example.test/api/playerdata/"+action,"Wrong absolute route"); + }); +foreach(var action in new[]{"Set","Delete"}) + foreach(var failure in new[]{"network","timeout","http"}) + await Check(action+" "+failure+" is not retried and has unknown commit outcome",async()=>{ + var h=new Probe("",failure=="http"?500:200){Mode=failure};using var http=new HttpClient(h);var kv=new PlayerKVClient(http,new Uri("http://example.test/")); + var r=action=="Set"?await kv.SetAsync(new Dictionary{{"x","y"}}):await kv.DeleteAsync(new[]{"x"}); + Assert(!r.Success && r.MayHaveCommitted && r.Data==null && h.Calls==1,"Unsafe failure/retry semantics"); + }); +await Check("Pre-cancelled write never dispatches; in-flight cancellation may have committed",async()=>{ + using var cts=new CancellationTokenSource();cts.Cancel();var h=new DeferredProbe();using var http=new HttpClient(h);var kv=new PlayerKVClient(http,new Uri("http://example.test/")); + var before=await kv.DeleteAsync(new[]{"x"},cts.Token); + Assert(before.Error==PlayerKVError.Cancelled && !before.MayHaveCommitted && h.Calls==0,"Pre-cancel dispatch"); + using var running=new CancellationTokenSource();var task=kv.SetAsync(new Dictionary{{"x","y"}},running.Token); + running.Cancel();h.Gate.SetResult(true);var after=await task; + Assert(after.Error==PlayerKVError.Cancelled && after.MayHaveCommitted && h.Calls==1,"Cancelled mutation reported success"); +}); +await Check("Cancelled read cannot publish a late successful result",async()=>{ + using var cts=new CancellationTokenSource();var h=new DeferredProbe();using var http=new HttpClient(h);var kv=new PlayerKVClient(http,new Uri("http://example.test/")); + var task=kv.GetAsync(null,cts.Token);cts.Cancel();h.Gate.SetResult(true);var r=await task; + Assert(r.Error==PlayerKVError.Cancelled && r.Data==null && !r.MayHaveCommitted,"Late read published"); +}); +await Check("Adapter preserves injected client configuration and ownership; disposes messages",async()=>{ + var h=new DeferredProbe();using var http=new HttpClient(h){BaseAddress=new Uri("http://other.test/")};http.DefaultRequestHeaders.Add("X-Test","preserve"); + var kv=new PlayerKVClient(http,new Uri("http://example.test/"));var task=kv.GetAsync();h.Gate.SetResult(true);await task; + Assert(http.BaseAddress.AbsoluteUri=="http://other.test/" && http.DefaultRequestHeaders.GetValues("X-Test").Single()=="preserve","Shared configuration mutated"); + Assert(h.ResponseBody.Disposed,"Response not disposed"); + try{await h.Request.Content.ReadAsStringAsync();throw new Exception("Request not disposed");}catch(ObjectDisposedException){} + using var stillAlive=await http.GetAsync("probe");Assert(stillAlive.IsSuccessStatusCode,"Injected client disposed"); +}); +await Check("Disposed owner transport returns cancellation without logging payload",async()=>{ + var http=new HttpClient(new Probe(""));var kv=new PlayerKVClient(http,new Uri("http://example.test/"));http.Dispose(); + Assert((await kv.GetAsync()).Error==PlayerKVError.Cancelled,"Disposed owner failed classification"); +}); +await Check("Reject invalid base URIs before any request",()=>{ + using var http=new HttpClient(new Probe("")); + foreach(var uri in new Uri[]{null,new Uri("relative",UriKind.Relative),new Uri("ftp://example.test/"),new Uri("http://example.test/?q=x"),new Uri("http://example.test/#x"),new Uri("http://user:secret@example.test/")}) { + try{new PlayerKVClient(http,uri);throw new Exception("Invalid base allowed");}catch(ArgumentException){} + } + return Task.CompletedTask; +}); +Console.WriteLine($"Passed {passed} tests"); + +sealed class Probe : HttpMessageHandler +{ + public string Url, Body, Mode; + public int Calls; + readonly string response; + readonly int status; + public Probe(string response, int status = 200) { this.response=response; this.status=status; } + protected override async Task SendAsync(HttpRequestMessage req, CancellationToken token) + { + Calls++; Url=req.RequestUri.AbsoluteUri; Body=await req.Content.ReadAsStringAsync(); + if(Mode == "network") throw new HttpRequestException("simulated"); + if(Mode == "timeout" || Mode == "cancel") throw new TaskCanceledException(); + return new HttpResponseMessage((HttpStatusCode)status) { Content=new StringContent(response,Encoding.UTF8,"application/json") }; + } +} + +sealed class DeferredProbe : HttpMessageHandler +{ + public readonly TaskCompletionSource Gate=new(); + public int Calls;public string Body,Url;public HttpRequestMessage Request;public TrackedContent ResponseBody; + protected override async Task SendAsync(HttpRequestMessage request,CancellationToken token) + { + Calls++;Request=request;await Gate.Task; // deliberately returns even if the caller cancelled + Url=request.RequestUri.AbsoluteUri;Body=request.Content==null?"":await request.Content.ReadAsStringAsync(); + ResponseBody=new TrackedContent(Url.EndsWith("/Get")?"{\"data\":{}}":""); + return new HttpResponseMessage(HttpStatusCode.OK){Content=ResponseBody}; + } +} +sealed class TrackedContent(string body) : StringContent(body) +{ + public bool Disposed; + protected override void Dispose(bool disposing){Disposed=true;base.Dispose(disposing);} +} diff --git a/CodexOutput/upserver/KVFishingConsumeTests/ControlSlice.cs.txt b/CodexOutput/upserver/KVFishingConsumeTests/ControlSlice.cs.txt new file mode 100644 index 000000000..56325f7e5 --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/ControlSlice.cs.txt @@ -0,0 +1,55 @@ +using System; using System.Linq; using System.Threading.Tasks; +public class ControlSlice { +public bool isActiveAndEnabled=true; public int Continues,LegacyCalls; +public FishingData fishingData=new(); public Tables _tables=new(); +public game.PlayerDataStorage.PlayerKVGameplayLoop.FishingAction kvFishingAction=new(); +public void Run()=>ConsumeBait(); public Task RunAwaited()=>ConsumeBaitOnKVAsync(); +void WaitForFishBiteED()=>Continues++; void DoConsumeBait()=>LegacyCalls++; +bool KVFishingCurrent() + { + if(!game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) return true; + return this!=null && isActiveAndEnabled && game.PlayerDataStorage.PlayerKVGameplayLoop.FishingCurrent(kvFishingAction); + } +async Task ConsumeBaitOnKVAsync() + { + try + { + if(!KVFishingCurrent()) return; + if(!game.PlayerDataStorage.PlayerKVGameplayLoop.ClaimFishingConsumption(kvFishingAction)) return; + if(fishingData.fishItem==null || !fishingData.IsGeneralFish() || fishingData.IsFree || fishingData.IsRetry || + fishingData.IsDuel || fishingData.IsPackItemFish() || fishingData.curFishData==null) + throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Only one ordinary paid fishing action is permitted."); + if(Application.internetReachability==NetworkReachability.NotReachable) + throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Fishing stopped before consumption: offline."); + if(_tables.TbFishingEvent.DataList.Any(e=>e.ConditionList.Any(c=>c.Condition==ConditionType.ConsumeEnergy))) + throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("ConsumeEnergy activity activation requires separate review."); + var player=GContext.container.Resolve(); + int mag=player.magnification; + if(mag<0 || mag>=_tables.TbEnergyDef.DataList.Count) + throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Invalid fishing magnification."); + int fish=fishingData.curFishData.ID, rod=fishingData.RodId, map=player.lastMapId; + int cost=_tables.TbEnergyDef.DataList[mag].Mag; + await player.ConsumeEnergyForKVLoopAsync(cost,kvFishingAction); + if(!KVFishingCurrent()) return; + if(fishingData.curFishData==null || fishingData.curFishData.ID!=fish || fishingData.RodId!=rod || + player.lastMapId!=map || (int)player.magnification!=mag || fishingData.IsFree || fishingData.IsRetry || fishingData.IsDuel) + throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Fishing context changed during confirmation."); + game.PlayerDataStorage.PlayerKVGameplayLoop.ContinueFishing(kvFishingAction,WaitForFishBiteED); + } + catch(Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); } + } +private void ConsumeBait() + { +#if UNITY_EDITOR && !SERVICE_HOST + if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) { _=ConsumeBaitOnKVAsync(); return; } +#endif + if (fishingData.IsFree) + { + WaitForFishBiteED(); + } + else + { + DoConsumeBait(); + } + } +} diff --git a/CodexOutput/upserver/KVFishingConsumeTests/ControlTests.cs.txt b/CodexOutput/upserver/KVFishingConsumeTests/ControlTests.cs.txt new file mode 100644 index 000000000..f19ebf3ba --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/ControlTests.cs.txt @@ -0,0 +1,56 @@ +using Loop=game.PlayerDataStorage.PlayerKVGameplayLoop; +int checks=0; +void Check(bool ok,string text){checks++;if(!ok)throw new Exception(text);} +ControlSlice Fresh(){Loop.Enabled=true;Loop.Ready=true;Loop.Faults=0;GContext.container.Player=new();Application.internetReachability=NetworkReachability.Reachable;return new();} +var s=Fresh();var player=GContext.container.Player;var first=s.RunAwaited(); +Check(s.Continues==0 && player.Calls==1,"actual method waits for response"); +s.Run();await s.RunAwaited();Check(player.Calls==1 && s.Continues==0,"duplicate consume and callback cannot repeat"); +player.Response.SetResult();await first;Check(s.Continues==1,"actual success continuation exactly once"); +await s.RunAwaited();Check(s.Continues==1 && player.Calls==1,"post-success duplicate suppressed"); +foreach(var kind in new[]{"free","retry","duel","pack","special","missing-fish","missing-item","offline","activity","bad-mag"}){ + s=Fresh();player=GContext.container.Player; + switch(kind){ + case "free":s.fishingData.IsFree=true;break;case "retry":s.fishingData.IsRetry=true;break; + case "duel":s.fishingData.IsDuel=true;break;case "pack":s.fishingData.Pack=true;break; + case "special":s.fishingData.General=false;break;case "missing-fish":s.fishingData.curFishData=null;break; + case "missing-item":s.fishingData.fishItem=null;break;case "offline":Application.internetReachability=NetworkReachability.NotReachable;break; + case "activity":s._tables.TbFishingEvent.DataList.Add(new());break;case "bad-mag":player.magnification=-1;break;} + await s.RunAwaited();Check(player.Calls==0 && s.Continues==0 && Loop.Faults>0,"pre-consumption gate "+kind); +} +foreach(var kind in new[]{"fish","rod","map","mag","free","retry","duel","disabled","cancelled","fault"}){ + s=Fresh();player=GContext.container.Player;var task=s.RunAwaited(); + switch(kind){case "fish":s.fishingData.curFishData.ID++;break;case "rod":s.fishingData.RodId++;break; + case "map":player.lastMapId++;break;case "mag":player.magnification++;break;case "free":s.fishingData.IsFree=true;break; + case "retry":s.fishingData.IsRetry=true;break;case "duel":s.fishingData.IsDuel=true;break; + case "disabled":s.isActiveAndEnabled=false;break;case "cancelled":s.kvFishingAction.Cancelled=true;break; + case "fault":Loop.Ready=false;break;} + player.Response.SetResult();await task;Check(player.Calls==1 && s.Continues==0,"no stale continuation "+kind); +} +s=Fresh();player=GContext.container.Player;var failed=s.RunAwaited();player.Response.SetException(new InvalidOperationException("Unknown"));await failed; +Check(s.Continues==0 && player.Calls==1 && !Loop.Ready,"async exception observed, no retry"); +s=Fresh();Loop.Enabled=false;s.Run();Check(s.LegacyCalls==1 && GContext.container.Player.Calls==0,"default paid PF path unchanged"); +s.fishingData.IsFree=true;s.Run();Check(s.Continues==1,"default free PF path unchanged"); +Console.WriteLine($"PASS {checks} assertions; compiled actual source methods; visual continuation and storage boundaries are spies; no network"); +public enum NetworkReachability{NotReachable,Reachable} +public static class Application{public static NetworkReachability internetReachability;} +public enum ConditionType{ConsumeEnergy} +public sealed class Fish{public int ID=1;} +public sealed class FishingData{public object fishItem=new();public Fish curFishData=new();public int RodId=1;public bool IsFree,IsRetry,IsDuel,Pack;public bool General=true;public bool IsGeneralFish()=>General;public bool IsPackItemFish()=>Pack;} +public sealed class EnergyDef{public int Mag=3;} +public sealed class EnergyTable{public List DataList=new(){new()};} +public sealed class ConditionRule{public ConditionType Condition=ConditionType.ConsumeEnergy;} +public sealed class Activity{public List ConditionList=new(){new()};} +public sealed class ActivityTable{public List DataList=new();} +public sealed class Tables{public EnergyTable TbEnergyDef=new();public ActivityTable TbFishingEvent=new();} +public sealed class PlayerData{public int Calls,magnification,lastMapId;public TaskCompletionSource Response=new();public async Task ConsumeEnergyForKVLoopAsync(int n,Loop.FishingAction a){Calls++;await Response.Task;a.Confirmed=true;}} +public sealed class Container{public PlayerData Player=new();public T Resolve()=>(T)(object)Player;} +public static class GContext{public static Container container=new();} +namespace game.PlayerDataStorage{public static class PlayerKVGameplayLoop{ + public static bool Enabled,Ready;public static int Faults; + public sealed class FishingAction{public bool Consuming,Cancelled,Confirmed,Continued;} + public static bool FishingCurrent(FishingAction a)=>Ready && !a.Cancelled; + public static bool ClaimFishingConsumption(FishingAction a){if(a.Consuming)return false;a.Consuming=true;return true;} + public static InvalidOperationException Stop(string m){Fault(m);return new(m);} + public static void Fault(string m){Ready=false;Faults++;} + public static void ContinueFishing(FishingAction a,Action next){if(!FishingCurrent(a)||!a.Confirmed)throw Stop("invalid");if(a.Continued)return;a.Continued=true;next();} +}} diff --git a/CodexOutput/upserver/KVFishingConsumeTests/ControlTests.csproj b/CodexOutput/upserver/KVFishingConsumeTests/ControlTests.csproj new file mode 100644 index 000000000..a99e17200 --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/ControlTests.csproj @@ -0,0 +1,4 @@ + + Exenet9.0falseenableUNITY_EDITORfalse$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config + ../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll + diff --git a/CodexOutput/upserver/KVFishingConsumeTests/LegacyRed.cs.txt b/CodexOutput/upserver/KVFishingConsumeTests/LegacyRed.cs.txt new file mode 100644 index 000000000..b4bb46f2b --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/LegacyRed.cs.txt @@ -0,0 +1,22 @@ +using Newtonsoft.Json; +var slice=new LegacySlice(); slice.Run(); +int failed=0; +void Check(bool ok,string name) { Console.WriteLine((ok?"PASS ":"FAIL ")+name); if(!ok)failed++; } +Check(slice.Continues==0,"must not continue while original save response is pending"); +var sent=JsonConvert.DeserializeObject>(PlayFabMgr.Instance.Sent); +Check(sent["newData"].Split(',')[3]==sent["energy"],"canonical and alias energy must agree"); +Console.WriteLine($"LEGACY SOURCE PROBE failed={failed}; no network; extracted actual methods from reviewed snapshot"); +return failed==0?0:1; +public enum NetworkReachability { NotReachable,Reachable } +public static class Application { public static NetworkReachability internetReachability=NetworkReachability.Reachable; } +public sealed class FishingData { public bool IsRetry; public bool IsPackItemFish()=>false; } +public sealed class EnergyDef { public int Mag=3; public int RetryCost=2; } +public sealed class EnergyTable { public List DataList=new(){new()}; } +public sealed class Tables { public EnergyTable TbEnergyDef=new(); } +public sealed class PlayerData { public int Energy=403; public int magnification; public Dictionary PlayerDataDic=new(){{"newData","60,213645,400,403,445,4,30003"}}; public void ReduceEnergy(int cost,bool remote) { Energy-=cost; } } +public sealed class Container { readonly PlayerData p=new(); public T Resolve()=>(T)(object)p; } +public static class GContext { public static Container container=new(); public static void Publish(object e){} } +public enum ConditionType { ConsumeEnergy } +public sealed class ConditionTypeEvent { public ConditionTypeEvent(ConditionType t,int n){} } +public static class Debug { public static void Log(object x){} } +public sealed class PlayFabMgr { public static PlayFabMgr Instance=new(); public string Sent; public Task SyncDataImmidiate(string key,string data) { Sent=data; return new TaskCompletionSource().Task; } } diff --git a/CodexOutput/upserver/KVFishingConsumeTests/LegacyRed.csproj b/CodexOutput/upserver/KVFishingConsumeTests/LegacyRed.csproj new file mode 100644 index 000000000..667b59143 --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/LegacyRed.csproj @@ -0,0 +1,4 @@ + + Exenet9.0falseenablefalse$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config + ../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll + diff --git a/CodexOutput/upserver/KVFishingConsumeTests/LegacySlice.cs.txt b/CodexOutput/upserver/KVFishingConsumeTests/LegacySlice.cs.txt new file mode 100644 index 000000000..a5874d93f --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/LegacySlice.cs.txt @@ -0,0 +1,36 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; +public class LegacySlice { +public int Continues; public FishingData fishingData=new(); public Tables _tables=new(); public void Run()=>DoConsumeBait(); void NoInternet(){} void WaitForFishBiteED()=>Continues++; +private void DoConsumeBait() + { + if (Application.internetReachability == NetworkReachability.NotReachable) + { + NoInternet(); + } + else + { + ConsumeBaitOnPlayfab(); + WaitForFishBiteED(); + } + } +private async void ConsumeBaitOnPlayfab() + { + var playerData = GContext.container.Resolve(); + EnergyDef energyDef = _tables.TbEnergyDef.DataList[playerData.magnification]; + var energy = energyDef.Mag; + if (fishingData.IsRetry) + { + energy = energyDef.RetryCost; + } + var commonDatas = new Dictionary(playerData.PlayerDataDic); + commonDatas["energy"] = (playerData.Energy - energy).ToString(); + playerData.ReduceEnergy(energy, false); + if (!fishingData.IsPackItemFish()) + { + GContext.Publish(new ConditionTypeEvent(ConditionType.ConsumeEnergy, energy)); + } + var success = await PlayFabMgr.Instance.SyncDataImmidiate("NewPlayerDataDic", + Newtonsoft.Json.JsonConvert.SerializeObject(commonDatas)); + Debug.Log($"[FishBehaviourB::ConsumeBaitOnPlayfab] {success}"); + } +} \ No newline at end of file diff --git a/CodexOutput/upserver/KVFishingConsumeTests/PermitTests.cs.txt b/CodexOutput/upserver/KVFishingConsumeTests/PermitTests.cs.txt new file mode 100644 index 000000000..6e9b9b822 --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/PermitTests.cs.txt @@ -0,0 +1,15 @@ +using game.PlayerDataStorage; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +var root=Path.GetFullPath("CodexOutput/upserver/KVFishingConsumeTests/permit-runs/"+Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); +var backup=Path.Combine(root,"backup.json"); File.WriteAllText(backup,JsonConvert.SerializeObject(new{playerId="102",api="http://s101.local:18080/",data=new Dictionary{{"NewPlayerDataDic","{}"}}})); +int count=0,failed=0; +void Case(int version,string operation,bool accept) { + var o=JObject.FromObject(new{Version=version,Enabled=true,PlayerId="102",Api="http://s101.local:18080/",BackupPath=backup,BackupSha256=PlayerKVLoopPermit.Hash(File.ReadAllBytes(backup)),AllowedKeys=new[]{"NewPlayerDataDic"}}); + if(operation!=null)o["Operation"]=operation; + var file=Path.Combine(root,"permit.json"); File.WriteAllText(file,o.ToString()); bool accepted; + try { PlayerKVLoopPermit.Read(file); accepted=true; }catch(InvalidOperationException){accepted=false;} + count++; if(accepted!=accept)failed++; Console.WriteLine($"{(accepted==accept?"PASS":"FAIL")} version={version} operation={operation??"absent"} accepted={accepted}"); +} +Case(1,null,true); Case(1,"fishing-consume-once",false); Case(1,"equipment",false); Case(2,null,false); Case(2,"equipment",true); Case(2,"fishing-consume-once",true); Case(2,"Fishing",false); Case(3,"equipment",false); +Console.WriteLine($"RESULT cases={count} failed={failed}"); return failed==0?0:1; diff --git a/CodexOutput/upserver/KVFishingConsumeTests/PermitTests.csproj b/CodexOutput/upserver/KVFishingConsumeTests/PermitTests.csproj new file mode 100644 index 000000000..d04ecb90a --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/PermitTests.csproj @@ -0,0 +1,4 @@ + + Exenet9.0falseenablefalseUNITY_EDITOR;SERVICE_HOST$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config + ../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll + diff --git a/CodexOutput/upserver/KVFishingConsumeTests/unity-fixed.cs.txt b/CodexOutput/upserver/KVFishingConsumeTests/unity-fixed.cs.txt new file mode 100644 index 000000000..daca9df7d --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/unity-fixed.cs.txt @@ -0,0 +1 @@ +return await KVGameplayLoopSetup.RunValidationAsync("fishing-consume"); diff --git a/CodexOutput/upserver/KVFishingConsumeTests/unity-live-gates.cs.txt b/CodexOutput/upserver/KVFishingConsumeTests/unity-live-gates.cs.txt new file mode 100644 index 000000000..c9967151d --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/unity-live-gates.cs.txt @@ -0,0 +1,8 @@ +using asap.core; +using System.Linq; +var p=GContext.container.Resolve(); +var t=GContext.container.Resolve(); +var manager=GContext.container.Resolve(); +var scopes=GContext.container.Resolve(); +var events=t.TbFishingEvent.DataList.GroupBy(e=>new {e.Type,e.SubType}).Select(g=>g.First()).Select(e=>new {e.Type,e.SubType,current=manager.GetSentry(e)?.CurrentEventConfig?.ID,scope=scopes.HasActiveScope((e.Type,e.SubType))}).ToArray(); +return new {at=System.DateTime.UtcNow,serverNow=ZZTimeHelper.UtcNow(),energy=(int)p.Energy,maxEnergy=(int)p.MaxEnergy,energyRM=p.GetEnergyRM(),mag=(int)p.magnification,cost=t.TbEnergyDef.DataList[(int)p.magnification].Mag,equipRod=(int)p.equipRodID,map=(int)p.lastMapId,lv=(int)p.lv,gold=(ulong)p.gold,pearl=(int)p.pearl,diamond=(int)p.diamond,recoverySeconds=p.GetEnergyRecoveryTimeMAndS().TotalSeconds,consumeActivations=string.Join(",",t.TbFishingEvent.DataList.Where(e=>e.ConditionList.Any(c=>c.Condition==cfg.ConditionType.ConsumeEnergy)).Select(e=>e.ID)),consumeTriggerPacks=string.Join(",",t.TbTriggerPackManager.DataList.Where(e=>e.Condition==cfg.ConditionType.ConsumeEnergy).Select(e=>e.ID)),events=string.Join(";",events.Select(e=>e.Type+":"+e.SubType+":"+e.current+":"+e.scope)),state=KVGameplayLoopSetup.State()}; diff --git a/CodexOutput/upserver/KVFishingConsumeTests/unity-prepare-fishing.cs.txt b/CodexOutput/upserver/KVFishingConsumeTests/unity-prepare-fishing.cs.txt new file mode 100644 index 000000000..a2a693428 --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/unity-prepare-fishing.cs.txt @@ -0,0 +1 @@ +return KVGameplayLoopSetup.PrepareFishingPermit("102","C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260924T074727Z/102.json",new string[]{"AquariumEventDataNewKey","BattlePassRecordTask","BombGameCount","BuffDataList","BuyData","CampDataDic","ChainPackDataNew","DigCount","DigGridData","epbdk","epdk","Event1v1BattleSlapData","EventBoostPackData","EventCubeMeltData","EventLuckGameRecord","EventLuckMagicPlayfabData","EventPartnerStatueData","FishHuntGameCount","FixedTimeShopDic","GuideSaveDataNew","IsOpenClubRed","LastPaymentTime","Leadboard_LastContinent","Map_5_101","Map_5_102","MiniBPKeyNew","MMTest","MonthlySignEvent","NewPlayerDataDic","NewRodList","OfferDiscoPartyManager","OfferStoryChain","OfferSweepPackData","Pack1A1Data","PiggyBankPackData","RodData","RodLevelData","SeasonStampData","ShopPackBuyCount","SoloDataKey","TargetBuffDataList","TransitionData","TriggerPackDataList","VIPAdPackData","WelcomeBack"}); diff --git a/CodexOutput/upserver/KVFishingConsumeTests/unity-prepare-recovery.cs.txt b/CodexOutput/upserver/KVFishingConsumeTests/unity-prepare-recovery.cs.txt new file mode 100644 index 000000000..0d0c3c435 --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/unity-prepare-recovery.cs.txt @@ -0,0 +1 @@ +return KVGameplayLoopSetup.PreparePermit("102","C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260924T075711Z/102.json",new string[]{"AquariumEventDataNewKey","BattlePassRecordTask","BombGameCount","BuffDataList","BuyData","CampDataDic","ChainPackDataNew","DigCount","DigGridData","epbdk","epdk","Event1v1BattleSlapData","EventBoostPackData","EventCubeMeltData","EventLuckGameRecord","EventLuckMagicPlayfabData","EventPartnerStatueData","FishHuntGameCount","FixedTimeShopDic","GuideSaveDataNew","IsOpenClubRed","LastPaymentTime","Leadboard_LastContinent","Map_5_101","Map_5_102","MiniBPKeyNew","MMTest","MonthlySignEvent","NewPlayerDataDic","NewRodList","OfferDiscoPartyManager","OfferStoryChain","OfferSweepPackData","Pack1A1Data","PiggyBankPackData","RodData","RodLevelData","SeasonStampData","ShopPackBuyCount","SoloDataKey","TargetBuffDataList","TransitionData","TriggerPackDataList","VIPAdPackData","WelcomeBack"}); diff --git a/CodexOutput/upserver/KVFishingConsumeTests/unity-quit-request.cs.txt b/CodexOutput/upserver/KVFishingConsumeTests/unity-quit-request.cs.txt new file mode 100644 index 000000000..2d531b64a --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/unity-quit-request.cs.txt @@ -0,0 +1,3 @@ +using asap.core; +_ = GContext.container.Resolve().SwitchStageAsync("QuitStage"); +return "normal QuitStage requested; inspect state separately"; diff --git a/CodexOutput/upserver/KVFishingConsumeTests/unity-recovered-values.cs.txt b/CodexOutput/upserver/KVFishingConsumeTests/unity-recovered-values.cs.txt new file mode 100644 index 000000000..5df4fde97 --- /dev/null +++ b/CodexOutput/upserver/KVFishingConsumeTests/unity-recovered-values.cs.txt @@ -0,0 +1,6 @@ +using asap.core; +using System.Linq; +var p=GContext.container.Resolve(); +var a=GContext.container.Resolve(); +var b=GContext.container.Resolve(); +return new {energy=(int)p.Energy,energyRM=p.GetEnergyRM(),achievement=a.GetAchievementDatas(cfg.ConditionType.ConsumeEnergy),daily=string.Join(";",b.BattleTask.dic_dailyTasks.Select(x=>x.Key+":"+x.Value)),weekly=string.Join(";",b.BattleTask.dic_weeklyTasks.Select(x=>x.Key+":"+x.Value)),equipRod=(int)p.equipRodID,gold=(ulong)p.gold,diamond=(int)p.diamond,pearl=(int)p.pearl,state=KVGameplayLoopSetup.State()}; diff --git a/CodexOutput/upserver/KVFishingRewardTests/ConsumeControlSlice.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/ConsumeControlSlice.cs.txt new file mode 100644 index 000000000..5097b2b8a --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/ConsumeControlSlice.cs.txt @@ -0,0 +1,56 @@ +using System; using System.Linq; using System.Threading.Tasks; +public class ControlSlice { +public bool isActiveAndEnabled=true; public int Continues,LegacyCalls; +public FishingData fishingData=new(); public Tables _tables=new(); +public game.PlayerDataStorage.PlayerKVGameplayLoop.FishingAction kvFishingAction=new(); +public void Run()=>ConsumeBait(); public Task RunAwaited()=>ConsumeBaitOnKVAsync(); +void WaitForFishBiteED()=>Continues++; void DoConsumeBait()=>LegacyCalls++; +bool KVFishingCurrent() + { + if(!game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) return true; + return this!=null && isActiveAndEnabled && game.PlayerDataStorage.PlayerKVGameplayLoop.FishingCurrent(kvFishingAction); + } +async Task ConsumeBaitOnKVAsync() + { + try + { + if(!KVFishingCurrent()) return; + if(!game.PlayerDataStorage.PlayerKVGameplayLoop.ClaimFishingConsumption(kvFishingAction)) return; + if(fishingData.fishItem==null || !fishingData.IsGeneralFish() || fishingData.IsFree || fishingData.IsRetry || + fishingData.IsDuel || fishingData.IsPackItemFish() || fishingData.curFishData==null) + throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Only one ordinary paid fishing action is permitted. fishItem.ID="+(fishingData.fishItem==null?"null":fishingData.fishItem.ID.ToString())+", fishItem.SubType="+(fishingData.fishItem==null?"null":fishingData.fishItem.SubType.ToString())+", curFishData.ID="+(fishingData.curFishData==null?"null":fishingData.curFishData.ID.ToString())+", IsFree="+fishingData.IsFree+", IsRetry="+fishingData.IsRetry+", IsDuel="+fishingData.IsDuel); + if(Application.internetReachability==NetworkReachability.NotReachable) + throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Fishing stopped before consumption: offline."); + if(_tables.TbFishingEvent.DataList.Any(e=>e.ConditionList.Any(c=>c.Condition==ConditionType.ConsumeEnergy))) + throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("ConsumeEnergy activity activation requires separate review."); + var player=GContext.container.Resolve(); + int mag=player.magnification; + if(mag<0 || mag>=_tables.TbEnergyDef.DataList.Count) + throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Invalid fishing magnification."); + int fish=fishingData.curFishData.ID, rod=fishingData.RodId, map=player.lastMapId; + int cost=_tables.TbEnergyDef.DataList[mag].Mag; + game.PlayerDataStorage.PlayerKVGameplayLoop.PrepareFishingReward(kvFishingAction,fishingData); + await player.ConsumeEnergyForKVLoopAsync(cost,kvFishingAction); + if(!KVFishingCurrent()) return; + if(fishingData.curFishData==null || fishingData.curFishData.ID!=fish || fishingData.RodId!=rod || + player.lastMapId!=map || (int)player.magnification!=mag || fishingData.IsFree || fishingData.IsRetry || fishingData.IsDuel) + throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Fishing context changed during confirmation."); + game.PlayerDataStorage.PlayerKVGameplayLoop.ContinueFishing(kvFishingAction,WaitForFishBiteED); + } + catch(Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); } + } +private void ConsumeBait() + { +#if UNITY_EDITOR && !SERVICE_HOST + if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) { _=ConsumeBaitOnKVAsync(); return; } +#endif + if (fishingData.IsFree) + { + WaitForFishBiteED(); + } + else + { + DoConsumeBait(); + } + } +} diff --git a/CodexOutput/upserver/KVFishingRewardTests/ConsumeControlTests.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/ConsumeControlTests.cs.txt new file mode 100644 index 000000000..3e0362003 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/ConsumeControlTests.cs.txt @@ -0,0 +1,58 @@ +using Loop=game.PlayerDataStorage.PlayerKVGameplayLoop; +int checks=0; +void Check(bool ok,string text){checks++;if(!ok)throw new Exception(text);} +ControlSlice Fresh(){Loop.Enabled=true;Loop.Ready=true;Loop.Faults=0;GContext.container.Player=new();Application.internetReachability=NetworkReachability.Reachable;return new();} +var s=Fresh();var player=GContext.container.Player;var first=s.RunAwaited(); +Check(s.Continues==0 && player.Calls==1,"actual method waits for response"); +s.Run();await s.RunAwaited();Check(player.Calls==1 && s.Continues==0,"duplicate consume and callback cannot repeat"); +player.Response.SetResult();await first;Check(s.Continues==1,"actual success continuation exactly once"); +await s.RunAwaited();Check(s.Continues==1 && player.Calls==1,"post-success duplicate suppressed"); +foreach(var kind in new[]{"free","retry","duel","pack","special","missing-fish","missing-item","offline","activity","bad-mag"}){ + s=Fresh();player=GContext.container.Player; + switch(kind){ + case "free":s.fishingData.IsFree=true;break;case "retry":s.fishingData.IsRetry=true;break; + case "duel":s.fishingData.IsDuel=true;break;case "pack":s.fishingData.Pack=true;break; + case "special":s.fishingData.General=false;break;case "missing-fish":s.fishingData.curFishData=null;break; + case "missing-item":s.fishingData.fishItem=null;break;case "offline":Application.internetReachability=NetworkReachability.NotReachable;break; + case "activity":s._tables.TbFishingEvent.DataList.Add(new());break;case "bad-mag":player.magnification=-1;break;} + await s.RunAwaited();Check(player.Calls==0 && s.Continues==0 && Loop.Faults>0,"pre-consumption gate "+kind); +} +foreach(var kind in new[]{"fish","rod","map","mag","free","retry","duel","disabled","cancelled","fault"}){ + s=Fresh();player=GContext.container.Player;var task=s.RunAwaited(); + switch(kind){case "fish":s.fishingData.curFishData.ID++;break;case "rod":s.fishingData.RodId++;break; + case "map":player.lastMapId++;break;case "mag":player.magnification++;break;case "free":s.fishingData.IsFree=true;break; + case "retry":s.fishingData.IsRetry=true;break;case "duel":s.fishingData.IsDuel=true;break; + case "disabled":s.isActiveAndEnabled=false;break;case "cancelled":s.kvFishingAction.Cancelled=true;break; + case "fault":Loop.Ready=false;break;} + player.Response.SetResult();await task;Check(player.Calls==1 && s.Continues==0,"no stale continuation "+kind); +} +s=Fresh();player=GContext.container.Player;var failed=s.RunAwaited();player.Response.SetException(new InvalidOperationException("Unknown"));await failed; +Check(s.Continues==0 && player.Calls==1 && !Loop.Ready,"async exception observed, no retry"); +s=Fresh();Loop.Enabled=false;s.Run();Check(s.LegacyCalls==1 && GContext.container.Player.Calls==0,"default paid PF path unchanged"); +s.fishingData.IsFree=true;s.Run();Check(s.Continues==1,"default free PF path unchanged"); +Console.WriteLine($"PASS {checks} assertions; compiled actual source methods; visual continuation and storage boundaries are spies; no network"); +public enum NetworkReachability{NotReachable,Reachable} +public static class Application{public static NetworkReachability internetReachability;} +public enum ConditionType{ConsumeEnergy} +public sealed class Fish{public int ID=1;} +public sealed class Item{public int ID=1;public int SubType=1;} +public sealed class FishingData{public Item fishItem=new();public Fish curFishData=new();public int RodId=1;public bool IsFree,IsRetry,IsDuel,Pack;public bool General=true;public bool IsGeneralFish()=>General;public bool IsPackItemFish()=>Pack;} +public sealed class EnergyDef{public int Mag=3;} +public sealed class EnergyTable{public List DataList=new(){new()};} +public sealed class ConditionRule{public ConditionType Condition=ConditionType.ConsumeEnergy;} +public sealed class Activity{public List ConditionList=new(){new()};} +public sealed class ActivityTable{public List DataList=new();} +public sealed class Tables{public EnergyTable TbEnergyDef=new();public ActivityTable TbFishingEvent=new();} +public sealed class PlayerData{public int Calls,magnification,lastMapId;public TaskCompletionSource Response=new();public async Task ConsumeEnergyForKVLoopAsync(int n,Loop.FishingAction a){Calls++;await Response.Task;a.Confirmed=true;}} +public sealed class Container{public PlayerData Player=new();public T Resolve()=>(T)(object)Player;} +public static class GContext{public static Container container=new();} +namespace game.PlayerDataStorage{public static class PlayerKVGameplayLoop{ +public static void PrepareFishingReward(FishingAction a,FishingData d) {} + public static bool Enabled,Ready;public static int Faults; + public sealed class FishingAction{public bool Consuming,Cancelled,Confirmed,Continued;} + public static bool FishingCurrent(FishingAction a)=>Ready && !a.Cancelled; + public static bool ClaimFishingConsumption(FishingAction a){if(a.Consuming)return false;a.Consuming=true;return true;} + public static InvalidOperationException Stop(string m){Fault(m);return new(m);} + public static void Fault(string m){Ready=false;Faults++;} + public static void ContinueFishing(FishingAction a,Action next){if(!FishingCurrent(a)||!a.Confirmed)throw Stop("invalid");if(a.Continued)return;a.Continued=true;next();} +}} diff --git a/CodexOutput/upserver/KVFishingRewardTests/ConsumeControlTests.csproj b/CodexOutput/upserver/KVFishingRewardTests/ConsumeControlTests.csproj new file mode 100644 index 000000000..41f92589f --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/ConsumeControlTests.csproj @@ -0,0 +1,4 @@ + + Exenet9.0falseenableUNITY_EDITORfalse$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config + ../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll + diff --git a/CodexOutput/upserver/KVFishingRewardTests/ControllerTests.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/ControllerTests.cs.txt new file mode 100644 index 000000000..d0057e652 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/ControllerTests.cs.txt @@ -0,0 +1,99 @@ +using System;using System.Threading.Tasks;using game.PlayerDataStorage; +int checks=0;void Check(bool b,string m){checks++;if(!b)throw new Exception(m);} +Controller Fresh(){PlayerKVGameplayLoop.Reset();UIManager.Instance=new();return new();} +var c=Fresh();c.PiercingFish(3);c.PiercingFish(3);c.End();c.End(); +Check(c.Strikes==1&&c.Endings==1&&UIManager.Instance.Calls==1,"actual wrappers suppress repeated drawing/end"); +Check(PlayerKVGameplayLoop.Scope==null,"UI await never holds ending scope"); +c.Disable();UIManager.Instance.Response.SetResult(new object());await Task.Delay(20); +Check(c.Destroyed==1&&PlayerKVGameplayLoop.Faults==1,"actual Disable during UI await disposes stale returned panel"); +c=Fresh();c.PiercingFish(3);c.Dead();Check(PlayerKVGameplayLoop.Cancelled,"actual Destroy revokes controller"); +c=Fresh();c.PiercingFish(3);c.fishingData.IsCaughtFish=false;c.End();Check(c.Endings==0&&UIManager.Instance.Calls==0,"natural failure cannot invoke reward core"); +foreach(int version in new[]{1,2}){c=Fresh();PlayerKVGameplayLoop.Version=version;c.PiercingFish(3);Check(c.Strikes==0&&PlayerKVGameplayLoop.Faults==1,"old permits do not draw");} +c=Fresh();c.PiercingFish(3);PlayerKVGameplayLoop.Confirmed=true;c.Disable();Check(PlayerKVGameplayLoop.Faults==0,"confirmed controller normal disable no second fault"); +c=Fresh();PlayerKVGameplayLoop.Enabled=false;c.PiercingFish(3);c.End();Check(c.Strikes==1&&c.Endings==1,"ordinary PF wrapper takes original core"); +Console.WriteLine($"PASS actual controller source wrappers/lifecycle {checks} assertions; core/render/storage services are spies; no gameplay or network"); +public class Controller { +public int Strikes,Endings,Destroyed;public FishingData fishingData=new();bool isFail;IDisposable disposables,_comboDisposable;object _hpcoroutine; +public void End()=>FishEnd();public void Disable()=>OnDisable();public void Dead()=>OnDestroy(); +void PiercingFishCore(int zone){Strikes++;}void FishEndCore(){Endings++;}void Destroy(object p){Destroyed++;} +void StopSoundCombo(){}void StopRodAudio(){}void StopNotReeling(){}void StopDrawingSplash(){}void AudioRelease(){}void SpSkillFxRelease(){}void StopAllCoroutines(){} +public void PiercingFish(int waitingZone) + { +#if UNITY_EDITOR && !SERVICE_HOST + if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) + { + try + { + if(!game.PlayerDataStorage.PlayerKVGameplayLoop.BeginRewardDrawing(this)) return; + using(game.PlayerDataStorage.PlayerKVGameplayLoop.BeginRewardSegment("strike")) PiercingFishCore(waitingZone); + } + catch(System.Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); } + return; + } +#endif + PiercingFishCore(waitingZone); + } +void FishEnd() + { +#if UNITY_EDITOR && !SERVICE_HOST + if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) + { + try + { + if(!game.PlayerDataStorage.PlayerKVGameplayLoop.BeginRewardEnding(this,fishingData.IsCaughtFish && !isFail)) return; + using(game.PlayerDataStorage.PlayerKVGameplayLoop.BeginRewardSegment("ending")) FishEndCore(); + _=ShowKVRewardPanelAsync(); + } + catch(System.Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); } + return; + } +#endif + FishEndCore(); + } +async Task ShowKVRewardPanelAsync() + { + try + { + var panel=await UIManager.Instance.ShowUI(UITypes.FishingRewardPanel); + if(!game.PlayerDataStorage.PlayerKVGameplayLoop.RewardRunCurrent && panel!=null) Destroy(panel); + } + catch(System.Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); } + } +private void OnDisable() + { +#if UNITY_EDITOR && !SERVICE_HOST + game.PlayerDataStorage.PlayerKVGameplayLoop.CancelReward(this,"controller disabled"); +#endif + + StopSoundCombo(); + StopRodAudio(); + StopNotReeling(); + StopDrawingSplash(); + GContext.container.Resolve().StopActiveFishingLoopSounds(0.05f); + AudioRelease(); + SpSkillFxRelease(); + } +private void OnDestroy() + { +#if UNITY_EDITOR && !SERVICE_HOST + game.PlayerDataStorage.PlayerKVGameplayLoop.CancelReward(this,"controller destroyed"); +#endif + + disposables?.Dispose(); + _comboDisposable?.Dispose(); + _hpcoroutine = null; + StopAllCoroutines(); + } +} +public class FishingData{public bool IsCaughtFish=true;}public enum UITypes{FishingRewardPanel} +public class UIManager{public static UIManager Instance=new();public int Calls;public TaskCompletionSource Response=new();public Task ShowUI(UITypes type){Calls++;return Response.Task;}} +public static class GContext{public static Container container=new();}public class Container{public T Resolve() where T:new()=>new();}public class SoundMgr{public void StopActiveFishingLoopSounds(float x){}} +namespace game.PlayerDataStorage {public static class PlayerKVGameplayLoop{ +public static bool Enabled,Drawing,Ending,Cancelled,Confirmed;public static int Version,Faults;public static string Scope; +public static void Reset(){Enabled=true;Drawing=Ending=Cancelled=Confirmed=false;Version=3;Faults=0;Scope=null;} +public static bool RewardRunCurrent=>!Cancelled; +public static bool BeginRewardDrawing(object o){if(Version!=3||Cancelled)throw new Exception("no draw");if(Drawing)return false;Drawing=true;return true;} +public static bool BeginRewardEnding(object o,bool success){if(Ending)return false;if(!success)throw new Exception("failed catch");Ending=true;return true;} +public static IDisposable BeginRewardSegment(string n){Scope=n;return new Batch();}class Batch:IDisposable{public void Dispose(){Scope=null;}} +public static void Fault(string s){Faults++;}public static void CancelReward(object o,string r){if(Cancelled)return;Cancelled=true;if(!Confirmed)Fault(r);} +}} diff --git a/CodexOutput/upserver/KVFishingRewardTests/ControllerTests.csproj b/CodexOutput/upserver/KVFishingRewardTests/ControllerTests.csproj new file mode 100644 index 000000000..76e0d90e7 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/ControllerTests.csproj @@ -0,0 +1 @@ +Exenet9.0falseUNITY_EDITOR$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config \ No newline at end of file diff --git a/CodexOutput/upserver/KVFishingRewardTests/FormulaTests.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/FormulaTests.cs.txt new file mode 100644 index 000000000..45674f4af --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/FormulaTests.cs.txt @@ -0,0 +1,123 @@ +using System; +foreach(var c in new[]{(10f,0.25f,10,0.5f,0.5f,0.2f,0.1f,122),(0f,0.25f,10,0.5f,0.5f,0.2f,0.1f,0),(10f,0f,1,1f,0f,0f,0f,10),(1f,0f,1,0.5f,0f,0f,0f,0)}){ + var old=new OldFormula();var next=new NewFormula(); + foreach(var x in new FormulaBase[]{old,next}){x._tables.TbDrop[0].DropList.DropCountList[1]=c.Item1;x.pointBonus=c.Item2;x.Magnification=c.Item3;x.fishingData.random=c.Item4;x.fishingData.WeightIncEvent=c.Item5;x.playerData.benefitsCashMag=c.Item6;GContext.container.Buff.Gold=c.Item7;} + int a=old.Run(),b=next.Run();if(a!=c.Item8||b!=a)throw new Exception($"Formula {a}/{b} expected {c.Item8}");Console.WriteLine($"PASS original/current cash={b}"); +} +public class FormulaBase {public Tables _tables=new();public Fishing fishingData=new();public Player playerData=new();public float pointBonus,extraPointEvent;public int Magnification;protected float _fishScore;protected Text text_gold_num=new(),text_num_buff=new();} +public class Tables {public Drop[] TbDrop={new()};}public class Drop{public DropPackageList DropList=new();}public class DropPackageList {public float[] DropCountList={20,10};} +public class Fishing{public Fish curFishData=new(),fishItem=new();public float random,fishWeight,WeightIncEvent;public int MapId;public bool IsNewItemFish()=>false;} +public class Fish{public int DropID,ID,Quality=2;}public class Player{public float benefitsCashMag,benefitsEventFishCashMag;} +public class CollectingData{public float ExtraCashDrop()=>0;}public class BuffDataCenter{public float Gold;public float GetExtraGoldBuffValue(int m,int q)=>Gold;} +public class FishingChallengeManager{public void AddScoreByQuality(int a,float b,int c){}} +public class Container{public BuffDataCenter Buff=new();public T Resolve() where T:new()=>typeof(T)==typeof(BuffDataCenter)?(T)(object)Buff:new T();} +public static class GContext{public static Container container=new();}public class Text{public Color color;}public struct Color{public static Color black,white;} +public static class Mathf{public static int RoundToInt(float x)=>(int)MathF.Round(x,MidpointRounding.ToEven);} +public class OldFormula : FormulaBase { public int Run(){DropPackageList dropPackageList = _tables.TbDrop[fishingData.curFishData.DropID].DropList; + float proficiency = dropPackageList.DropCountList[0] * fishingData.random; + var proficiencyExtra = SetProficiency(proficiency, extraPointEvent, fishingData.curFishData.ID);//额外的Buff加成 + var endWeight = fishingData.fishWeight; + float extraCashDrop = GContext.container.Resolve().ExtraCashDrop(); + //Data + //金币各种加成 + float gold = (dropPackageList.DropCountList[1] + extraCashDrop) + * (1 + pointBonus) + * Magnification + * fishingData.random; + //杆加成 + //解锁地图Buff加成 + float glodBuff = GContext.container.Resolve().GetExtraGoldBuffValue(fishingData.MapId, fishingData.fishItem.Quality); + + if (glodBuff > 0.001f) + { + text_gold_num.color = Color.black; + text_num_buff.color = Color.white; + } + int Quality = fishingData.curFishData.Quality; + if (Quality > 6) + { + Quality = 6; + } + + //权益全局加成 + var ExtraPoint = playerData.benefitsCashMag + glodBuff; + int cash = Mathf.RoundToInt(gold * proficiencyExtra * (1 + ExtraPoint)); + +return cash;}float SetProficiency(float proficiency, float proficiencyExtra, int fishId) + { + int Quality = fishingData.curFishData.Quality; + if (Quality > 6) + { + Quality = 6; + } + if (!fishingData.IsNewItemFish()) + { + proficiencyExtra = (1 + fishingData.WeightIncEvent) * (1 + proficiencyExtra); + } + else + { + proficiencyExtra = (1 + proficiencyExtra) * (1 + playerData.benefitsEventFishCashMag); + } + + proficiency *= proficiencyExtra; + proficiency = Mathf.RoundToInt(proficiency); + + GContext.container.Resolve().AddScoreByQuality(Quality, proficiency, fishId); + + _fishScore = proficiency; + return proficiencyExtra; + }} +public class NewFormula : FormulaBase { public int Run(){DropPackageList dropPackageList = _tables.TbDrop[fishingData.curFishData.DropID].DropList; + float proficiency = dropPackageList.DropCountList[0] * fishingData.random; + var proficiencyExtra = SetProficiency(proficiency, extraPointEvent, fishingData.curFishData.ID);//额外的Buff加成 + var endWeight = fishingData.fishWeight; + float extraCashDrop = GContext.container.Resolve().ExtraCashDrop(); + //Data + //金币各种加成 + float gold = (dropPackageList.DropCountList[1] + extraCashDrop) + * (1 + pointBonus) + * Magnification + * fishingData.random; + //杆加成 + //解锁地图Buff加成 + float glodBuff = GContext.container.Resolve().GetExtraGoldBuffValue(fishingData.MapId, fishingData.fishItem.Quality); + + if (glodBuff > 0.001f) + { + text_gold_num.color = Color.black; + text_num_buff.color = Color.white; + } + int Quality = fishingData.curFishData.Quality; + if (Quality > 6) + { + Quality = 6; + } + + //权益全局加成 + var ExtraPoint = playerData.benefitsCashMag + glodBuff; + int cash = Mathf.RoundToInt(gold * proficiencyExtra * (1 + ExtraPoint)); + +return cash;}float SetProficiency(float proficiency, float proficiencyExtra, int fishId) + { + int Quality = fishingData.curFishData.Quality; + if (Quality > 6) + { + Quality = 6; + } + if (!fishingData.IsNewItemFish()) + { + proficiencyExtra = (1 + fishingData.WeightIncEvent) * (1 + proficiencyExtra); + } + else + { + proficiencyExtra = (1 + proficiencyExtra) * (1 + playerData.benefitsEventFishCashMag); + } + + proficiency *= proficiencyExtra; + proficiency = Mathf.RoundToInt(proficiency); + + GContext.container.Resolve().AddScoreByQuality(Quality, proficiency, fishId); + + _fishScore = proficiency; + return proficiencyExtra; + }} diff --git a/CodexOutput/upserver/KVFishingRewardTests/FormulaTests.csproj b/CodexOutput/upserver/KVFishingRewardTests/FormulaTests.csproj new file mode 100644 index 000000000..9e22d490a --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/FormulaTests.csproj @@ -0,0 +1 @@ +Exenet9.0false$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config \ No newline at end of file diff --git a/CodexOutput/upserver/KVFishingRewardTests/LegacyRed.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/LegacyRed.cs.txt new file mode 100644 index 000000000..bdc9a7219 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/LegacyRed.cs.txt @@ -0,0 +1,17 @@ +using System; using System.Collections.Generic; +var fish=new ActualFish(); fish.fishData[7]=new FishWeightData {TotalNum=20,MaxG=10}; +fish.SetGetFishCount(7); fish.SetWeight(7,5); +bool ok=fish.fishData[7].TotalNum==30 && PlayFabMgr.Instance.Writes==1; +Console.WriteLine($"{(ok?"PASS":"FAIL")} non-record catch: total={fish.fishData[7].TotalNum}, mapWrites={PlayFabMgr.Instance.Writes}; expected total=30 and one persisted map."); +return ok?0:1; +public class FishWeightData { public int TotalNum; public float MaxG; public bool NewFish; public string Serialize()=>TotalNum+"|"+MaxG; } +public enum ConditionType {GetFishWeight,GetFishID} +public record ConditionTypeEvent(ConditionType type,int count); +public class PlayerData {public int GetMagnification()=>10;} +public static class GContext {public static Container container=new(); public static void Publish(object x) {} } +public class Container {public T Resolve() where T:new()=>new();} +public class Tables {public FishTable TbFishData=new();} +public class FishTable {public FishConfig GetOrDefault(int id)=>new();} +public class FishConfig {public int AreaID=102;} +public static class JsonMapper {public static string ToJson(object x)=>System.Text.Json.JsonSerializer.Serialize(x);} +public class PlayFabMgr {public static PlayFabMgr Instance=new(); public int Writes; public void UpdateUserDataValue(string key,string value) {Writes++;} } diff --git a/CodexOutput/upserver/KVFishingRewardTests/LegacyRed.csproj b/CodexOutput/upserver/KVFishingRewardTests/LegacyRed.csproj new file mode 100644 index 000000000..f184fcd22 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/LegacyRed.csproj @@ -0,0 +1 @@ +Exenet9.0falseenablefalse$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config \ No newline at end of file diff --git a/CodexOutput/upserver/KVFishingRewardTests/LegacySlice.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/LegacySlice.cs.txt new file mode 100644 index 000000000..2e008ab54 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/LegacySlice.cs.txt @@ -0,0 +1,49 @@ +using System; using System.Collections.Generic; using System.Linq; +public class ActualFish { public Dictionary fishData=new(); public Dictionary> mapFishData=new(); public Tables _tables=new(); +public void SetWeight(int fishId, float weight) + { + //获取鱼重量记录为g + GContext.Publish(new ConditionTypeEvent(ConditionType.GetFishWeight, (int)(weight * 1000))); + CheckFishData(fishId); + if (fishData[fishId].MaxG < weight) + { + fishData[fishId].MaxG = weight; + SetFishData(fishId, fishData[fishId]); + } + GContext.Publish(new ConditionTypeEvent(ConditionType.GetFishID, fishId)); + } +public void SetGetFishCount(int fishId) + { + int count = GContext.container.Resolve().GetMagnification(); + CheckFishData(fishId); + fishData[fishId].TotalNum += count; + } +void CheckFishData(int fishId) + { + if (!fishData.ContainsKey(fishId)) + { + fishData.Add(fishId, new FishWeightData() { NewFish = true }); + } + } +void SetFishData(int id, FishWeightData data) + { + int mapID = _tables.TbFishData.GetOrDefault(id).AreaID; + if (mapFishData.ContainsKey(mapID)) + { + mapFishData[mapID][id] = data; + } + else + { + mapFishData[mapID] = new Dictionary + { + [id] = data + }; + } + MapNewUpdateUserDataValue(mapID, mapFishData[mapID]); + } +void MapNewUpdateUserDataValue(int mapID, Dictionary keyValuePairs) + { + Dictionary mapData = keyValuePairs.ToDictionary(_ => _.Key, _ => _.Value.Serialize()); + PlayFabMgr.Instance.UpdateUserDataValue($"Map_5_{mapID}", JsonMapper.ToJson(mapData)); + } +} diff --git a/CodexOutput/upserver/KVFishingRewardTests/OldPresentationTests.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/OldPresentationTests.cs.txt new file mode 100644 index 000000000..94a8a1e0a --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/OldPresentationTests.cs.txt @@ -0,0 +1,175 @@ +using System;using System.Threading.Tasks;using System.Runtime.CompilerServices; +var panel=new OriginalPanel();panel.Run(); +bool ok=panel.Animations==0&&!panel.go_info.active; +Console.WriteLine($"{(ok?"PASS":"FAIL")} original full Success method displays before pending storage response: writes={Storage.Writes}, confirmed={Storage.Confirmed}, animations={panel.Animations}, info={panel.go_info.active}"); +return ok?0:1; +public class OriginalPanel{ +public int Animations;public Node go_info=new(),go_weight=new(),fx_ui_blingbling_01=new(),bg_target=new(); +int addCollecting,curCollectingID,add;float extraPointEvent;bool isSocial,isEnable=true;int Magnification=10;float waitTime1,duration1,waitTime2,duration2; +Fishing fishingData=new();FishingEventData fishingEventData=new();Tables _tables=new();PlayerData playerData=new();string PanelName="reward"; +Text root_target_text_info=new(),root_1v1battle_text_info=new(),text_gold_num=new(),text_num_buff=new(),txt_magnification=new(); +Bonus bonusReward=new();UI uiService=new();Social fishingRewardSocial=new();Animator anim_panel; +public OriginalPanel(){anim_panel=new Animator(()=>Animations++);}public void Run()=>Success(0); +void MaterialChange(){}void PlayAudioRewardFish(){}void SetTextTitleAsync(float x,bool s){}void SetFishWeight(float w){}void VibrationData(){} +float SetProficiency(float a,float b,int c)=>1;object AddCash()=>new();object StartCoroutine(object o){Animations++;return o;} + async void Success(float pointBonus) + { + go_info.gameObject.SetActive(false); + GContext.container.Resolve().SaveFishingFrequency(); + + if (addCollecting > 0) + { + extraPointEvent = fishingEventData.OnFishReward(fishingData.MapId, fishingData.NewIndex, Magnification); + addCollecting *= Magnification; + GContext.container.Resolve().AddItemCount(curCollectingID, addCollecting, null); + MaterialChange(); + } + PlayAudioRewardFish(); + GContext.container.Resolve().EndCollectionTargetWeight(fishingData.fishItem.ID); + fishingEventData.AddProgress(fishingData.fishItem.Quality, Magnification); + + GContext.container.Resolve().AddIndex(true); + + if (fishingData.IsKeepsakeFish()) + { + root_target_text_info.text = LocalizationMgr.GetText("UI_FishingTargetPopupPanel_Advanced_9"); + } + else if (fishingData.IsPiggyBankFish()) + { + fishingEventData.curActivateItemId = fishingData.fishItem.ID; + root_target_text_info.text = LocalizationMgr.GetText("UI_GiftPiggyBankPopupPanel_10"); + PiggyBankPackData piggyBankPackData = GContext.container.Resolve(); + piggyBankPackData.TriggerPack(); + } + else if (fishingData.IsBarginPackFish()) + { + fishingEventData.curActivateItemId = fishingData.fishItem.ID; + root_target_text_info.text = LocalizationMgr.GetText("UI_GiftBargainPopupPanel_7"); + BargainPackData piggyBankPackData = GContext.container.Resolve(); + piggyBankPackData.TriggerPack(); + + } + else if (fishingData.IsGeneralFish()) + { + isSocial = false; + DropPackageList dropPackageList = _tables.TbDrop[fishingData.curFishData.DropID].DropList; + float proficiency = dropPackageList.DropCountList[0] * fishingData.random; + var proficiencyExtra = SetProficiency(proficiency, extraPointEvent, fishingData.curFishData.ID);//额外的Buff加成 + var endWeight = fishingData.fishWeight; + float extraCashDrop = GContext.container.Resolve().ExtraCashDrop(); + //Data + //金币各种加成 + float gold = (dropPackageList.DropCountList[1] + extraCashDrop) + * (1 + pointBonus) + * Magnification + * fishingData.random; + //杆加成 + //解锁地图Buff加成 + float glodBuff = GContext.container.Resolve().GetExtraGoldBuffValue(fishingData.MapId, fishingData.fishItem.Quality); + + if (glodBuff > 0.001f) + { + text_gold_num.color = Color.black; + text_num_buff.color = Color.white; + } + int Quality = fishingData.curFishData.Quality; + if (Quality > 6) + { + Quality = 6; + } + + //权益全局加成 + var ExtraPoint = playerData.benefitsCashMag + glodBuff; + int cash = Mathf.RoundToInt(gold * proficiencyExtra * (1 + ExtraPoint)); + + add = cash; + GContext.Publish(new ConditionTypeEvent(ConditionType.CashFromFishing, add)); + GContext.Publish(new ConditionTypeEvent(ConditionType.MaxCashFromFishing, add / Magnification)); + + //AddEventData + if (add > 0) + { + playerData.AddGold((ulong)add); + } + GContext.Publish(new TargetAddData(dropPackageList.DropIDList[1], 0, add)); + var isSpecialFish = fishingData.curFishData.GeneralType == FishGeneralType.SpMapFish || fishingData.curFishData.GeneralType == FishGeneralType.EventFish; + SetTextTitleAsync(proficiencyExtra, isSpecialFish); + SetFishWeight(fishingData.fishWeight); + MMTService mmService = GContext.container.Resolve(); + if (addCollecting > 0) + { + var collectingTargetInit = fishingEventData.collectingTargetInit; + string BgRewardAdd = collectingTargetInit.BgRewardAdd; + if (!string.IsNullOrEmpty(BgRewardAdd)) + { + bg_target.gameObject.SetActive(true); + uiService.SetImageSprite(bg_target, BgRewardAdd, PanelName); + } + } + bonusReward.Show(Magnification); + _ = StartCoroutine(AddCash()); + //Show Fish Info UI + go_info.gameObject.SetActive(true); + go_weight.SetActive(false); + + //Show Magnification Animation + if (Magnification > 1) + { + txt_magnification.text = "x" + Magnification; + + anim_panel.Play("info_show_2"); + + await new WaitForSeconds(waitTime1 + duration1 + waitTime2); + if (!isEnable) + { + return; + } + + bonusReward.DOTweenAdd(Magnification, duration2); + + var progress = 0f; + var newCash = add / Magnification; + var addCash = add - newCash; + DOTween.To(() => progress, x => progress = x, 1f, duration2).OnUpdate(() => + { + text_gold_num.text = ConvertTools.GetNumberString2((int)(newCash + addCash * progress)); + text_num_buff.text = text_gold_num.text; + }).SetId("AddCash"); + + } + else + { + anim_panel.Play("info_show_1"); + } + if (glodBuff > 0.001f) + { + fx_ui_blingbling_01.gameObject.SetActive(true); + } + VibrationData(); + fishingRewardSocial.ShowSocial(isSocial); + } + else if (fishingData.fishItem.IsSpinachItem()) + { + //todo 1v1battle + root_1v1battle_text_info.text=LocalizationMgr.GetText("UI_GiftBargainPopupPanel_8"); + } + }} +public static class Storage{public static int Writes;public static bool Confirmed=false;} +public class Node{public bool active;public Node gameObject=>this;public void SetActive(bool b){active=b;}} +public class Text{public string text;public Color color;}public struct Color{public static Color black,white;} +public class PlayerData{public float benefitsCashMag;public void SaveFishingFrequency(){Storage.Writes++;}public void AddGold(ulong v){Storage.Writes++;}} +public class PlayerItemData{public void AddItemCount(int a,int b,object c){}}public class PiggyBankPackData{public void TriggerPack(){}}public class BargainPackData{public void TriggerPack(){}} +public class BuffDataCenter{public void EndCollectionTargetWeight(int x){}public float GetExtraGoldBuffValue(int a,int b)=>0;}public class CollectingData{public float ExtraCashDrop()=>0;} +public class GuideDataCenter{public void AddIndex(bool b){}}public class MMTService{} +public class Tables{public Drop[] TbDrop={new()};}public class Drop{public DropPackageList DropList=new();}public class DropPackageList{public float[] DropCountList={1,10};public int[] DropIDList={0,1001};} +public class Fishing{public Fish curFishData=new(),fishItem=new();public int MapId,NewIndex;public float random=1,fishWeight=1;public bool IsKeepsakeFish()=>false;public bool IsPiggyBankFish()=>false;public bool IsBarginPackFish()=>false;public bool IsGeneralFish()=>true;} +public class Fish{public int ID,DropID,Quality=2;public FishGeneralType GeneralType;public bool IsSpinachItem()=>false;} +public enum FishGeneralType{Normal,SpMapFish,EventFish}public enum ConditionType{CashFromFishing,MaxCashFromFishing} +public record ConditionTypeEvent(ConditionType c,int count);public record TargetAddData(int a,int b,int c); +public class FishingEventData{public int curActivateItemId;public Target collectingTargetInit=new();public float OnFishReward(int a,int b,int c)=>0;public void AddProgress(int a,int b){}}public class Target{public string BgRewardAdd;} +public static class GContext{public static Container container=new();public static void Publish(object e){}}public class Container{public T Resolve() where T:new()=>new();} +public class Bonus{public void Show(int n){}public void DOTweenAdd(int n,float t){}}public class UI{public void SetImageSprite(object a,string b,string c){}}public class Social{public void ShowSocial(bool b){}} +public class Animator{Action play;public Animator(Action a){play=a;}public void Play(string s){play();}} +public static class LocalizationMgr{public static string GetText(string s)=>s;}public static class Mathf{public static int RoundToInt(float v)=>(int)MathF.Round(v);} +public static class ConvertTools{public static string GetNumberString2(int n)=>n.ToString();}public static class DOTween{public static Tween To(Funca,Actionb,float c,float d)=>new();}public class Tween{public Tween OnUpdate(Action a)=>this;public Tween SetId(string s)=>this;} +public class WaitForSeconds{static TaskCompletionSource wait=new();public WaitForSeconds(float t){}public TaskAwaiter GetAwaiter()=>wait.Task.GetAwaiter();} diff --git a/CodexOutput/upserver/KVFishingRewardTests/OldPresentationTests.csproj b/CodexOutput/upserver/KVFishingRewardTests/OldPresentationTests.csproj new file mode 100644 index 000000000..9098c9601 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/OldPresentationTests.csproj @@ -0,0 +1 @@ +Exenet9.0false$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config \ No newline at end of file diff --git a/CodexOutput/upserver/KVFishingRewardTests/PanelSlice.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/PanelSlice.cs.txt new file mode 100644 index 000000000..77647d785 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/PanelSlice.cs.txt @@ -0,0 +1,56 @@ +using System;using System.Threading.Tasks; +public class ActualPanel { + game.PlayerDataStorage.PlayerKVGameplayLoop.FishingRewardAction kvReward; Func kvPresentation; + public bool isActiveAndEnabled=true; public Canvas canvasGroup=new(); public Fish playerFishData=new();public Player playerData=new(); int fishID=7;public int Applied; + bool KVRewardViewCurrent()=>this!=null && isActiveAndEnabled && game.PlayerDataStorage.PlayerKVGameplayLoop.RewardPanelCurrent(kvReward,this); +public Button playButton=new();public bool NonKvOnly;public int HomeClicks; + public void StartTail() + { + playButton.onClick.AddListener(OnClickInHome); +#if UNITY_EDITOR && !SERVICE_HOST + if(!game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) +#endif + { + NonKvOnly=true; + } + OnShow(); + } + void OnClickInHome()=>HomeClicks++; + void OnShowCore(){Applied++;}public void Run()=>OnShow(); + void OnShow() + { +#if UNITY_EDITOR && !SERVICE_HOST + if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) + { + try + { + var bound=game.PlayerDataStorage.PlayerKVGameplayLoop.BindRewardPanel(this); + if(bound==null) return; + kvReward=bound; + game.PlayerDataStorage.PlayerKVEditResult primary; + using(game.PlayerDataStorage.PlayerKVGameplayLoop.BeginRewardSegment("reward")) + { + OnShowCore(); + playerFishData.SaveCaughtFishForKVLoop(fishID); + primary=playerData.SaveFishingRewardForKVLoop(); + } + _=ConfirmKVRewardAsync(primary); + } + catch(System.Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); } + return; + } +#endif + OnShowCore(); + } + async System.Threading.Tasks.Task ConfirmKVRewardAsync(game.PlayerDataStorage.PlayerKVEditResult primary) + { + try + { + await game.PlayerDataStorage.PlayerKVGameplayLoop.ConfirmRewardAsync(kvReward,primary); + if(!KVRewardViewCurrent()) return; + game.PlayerDataStorage.PlayerKVGameplayLoop.PresentReward(kvReward,()=>{canvasGroup.alpha=1;canvasGroup.blocksRaycasts=true;canvasGroup.interactable=true;}); + if(kvPresentation!=null) await kvPresentation(); + } + catch(System.Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); } + } +} diff --git a/CodexOutput/upserver/KVFishingRewardTests/PanelTests.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/PanelTests.cs.txt new file mode 100644 index 000000000..9a2eb04a0 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/PanelTests.cs.txt @@ -0,0 +1,31 @@ +using System;using System.Threading.Tasks;using game.PlayerDataStorage; +var panel=new ActualPanel();panel.Run();panel.Run(); +if(panel.Applied!=1)throw new Exception("duplicate applies rewards"); +if(panel.canvasGroup.alpha!=0)throw new Exception("pending reward already visible"); +bool pendingBlocked=panel.canvasGroup.blocksRaycasts==false && panel.canvasGroup.interactable==false; +PlayerKVGameplayLoop.Pending.SetResult(true);await Task.Delay(25); +bool presented=panel.canvasGroup.alpha==1 && PlayerKVGameplayLoop.Shows==1; +bool interactRestored=panel.canvasGroup.blocksRaycasts && panel.canvasGroup.interactable; +var s2=new ActualPanel();s2.StartTail();bool kvListener=s2.playButton.onClick.Count==1 && !s2.NonKvOnly; +s2.playButton.onClick.Invoke();bool kvClick=s2.HomeClicks==1; +PlayerKVGameplayLoop.Enabled=false;var s3=new ActualPanel();s3.StartTail();bool pfListener=s3.playButton.onClick.Count==1 && s3.NonKvOnly; +bool ok=presented && interactRestored && kvListener && kvClick && pfListener && pendingBlocked; +Console.WriteLine($"{(ok?"PASS":"FAIL")} presentation restores interaction; KV listener registered; raycasts={panel.canvasGroup.blocksRaycasts}, kvListener={kvListener}, pfListener={pfListener}");return ok?0:1; +public class Canvas {public float alpha;public bool blocksRaycasts,interactable;} +public class ButtonEvent{System.Action h;public int Count=>h==null?0:1;public void AddListener(System.Action a)=>h+=a;public void Invoke()=>h?.Invoke();} +public class Button{public ButtonEvent onClick=new();} +public class Fish {public void SaveCaughtFishForKVLoop(int id){}} +public class Player {public PlayerKVEditResult SaveFishingRewardForKVLoop()=>new();} +namespace game.PlayerDataStorage { +public class PlayerKVEditResult{} +public static class PlayerKVGameplayLoop { + public class FishingRewardAction{} + public static bool Enabled=true;static FishingRewardAction action;public static int Shows;public static TaskCompletionSource Pending=new(); + public static FishingRewardAction BindRewardPanel(object p) {if(action!=null)return null;return action=new();} + public static bool RewardPanelCurrent(FishingRewardAction a,object p)=>a!=null && ReferenceEquals(a,action); + public static IDisposable BeginRewardSegment(string name)=>new Scope(); + public static Task ConfirmRewardAsync(FishingRewardAction a,PlayerKVEditResult p)=>Pending.Task; + public static void PresentReward(FishingRewardAction a,Action show){Shows++;show();} + public static void Fault(string message)=>throw new Exception(message); + class Scope:IDisposable{public void Dispose(){}} +}} diff --git a/CodexOutput/upserver/KVFishingRewardTests/PanelTests.csproj b/CodexOutput/upserver/KVFishingRewardTests/PanelTests.csproj new file mode 100644 index 000000000..50c1893a6 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/PanelTests.csproj @@ -0,0 +1 @@ +Exenet9.0falseenablefalseUNITY_EDITOR$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config \ No newline at end of file diff --git a/CodexOutput/upserver/KVFishingRewardTests/PermitTests.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/PermitTests.cs.txt new file mode 100644 index 000000000..fa3993e42 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/PermitTests.cs.txt @@ -0,0 +1,16 @@ +using game.PlayerDataStorage; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +var root=Path.GetFullPath("CodexOutput/upserver/KVFishingRewardTests/permit-runs/"+Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); +var backup=Path.Combine(root,"backup.json"); File.WriteAllText(backup,JsonConvert.SerializeObject(new{playerId="102",api="http://s101.local:18080/",data=new Dictionary{{"NewPlayerDataDic","{}"}}})); +int count=0,failed=0; +void Case(int version,string operation,bool accept) { + var o=JObject.FromObject(new{Version=version,Enabled=true,PlayerId="102",Api="http://s101.local:18080/",BackupPath=backup,BackupSha256=PlayerKVLoopPermit.Hash(File.ReadAllBytes(backup)),AllowedKeys=new[]{"NewPlayerDataDic"}}); + if(operation!=null)o["Operation"]=operation; + var file=Path.Combine(root,"permit.json"); File.WriteAllText(file,o.ToString()); bool accepted; + try { PlayerKVLoopPermit.Read(file); accepted=true; }catch(InvalidOperationException){accepted=false;} + count++; if(accepted!=accept)failed++; Console.WriteLine($"{(accepted==accept?"PASS":"FAIL")} version={version} operation={operation??"absent"} accepted={accepted}"); +} +Case(1,null,true); Case(1,"fishing-consume-once",false); Case(1,"equipment",false); Case(2,null,false); Case(2,"equipment",true); Case(2,"fishing-consume-once",true); Case(2,"Fishing",false); Case(3,"equipment",false); +Case(3,"fishing-reward-once",true); Case(2,"fishing-reward-once",false); Case(3,"fishing-consume-once",false); Case(4,"fishing-reward-once",false); +Console.WriteLine($"RESULT cases={count} failed={failed}"); return failed==0?0:1; diff --git a/CodexOutput/upserver/KVFishingRewardTests/PermitTests.csproj b/CodexOutput/upserver/KVFishingRewardTests/PermitTests.csproj new file mode 100644 index 000000000..d04ecb90a --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/PermitTests.csproj @@ -0,0 +1,4 @@ + + Exenet9.0falseenablefalseUNITY_EDITOR;SERVICE_HOST$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config + ../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll + diff --git a/CodexOutput/upserver/KVFishingRewardTests/build-controller-tests.py b/CodexOutput/upserver/KVFishingRewardTests/build-controller-tests.py new file mode 100644 index 000000000..e25695fc6 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/build-controller-tests.py @@ -0,0 +1,44 @@ +from pathlib import Path +r=Path(__file__).parent +s=Path('Client/Assets/Scripts/GampPlay/FishingB/FishingUpdateSys.cs').read_text(encoding='utf-8-sig') +def method(sig): + a=s.index(sig);i=s.index('{',a)+1;n=1 + while n:n+=(s[i]=='{')-(s[i]=='}');i+=1 + return s[a:i] +code='''using System;using System.Threading.Tasks;using game.PlayerDataStorage; +int checks=0;void Check(bool b,string m){checks++;if(!b)throw new Exception(m);} +Controller Fresh(){PlayerKVGameplayLoop.Reset();UIManager.Instance=new();return new();} +var c=Fresh();c.PiercingFish(3);c.PiercingFish(3);c.End();c.End(); +Check(c.Strikes==1&&c.Endings==1&&UIManager.Instance.Calls==1,"actual wrappers suppress repeated drawing/end"); +Check(PlayerKVGameplayLoop.Scope==null,"UI await never holds ending scope"); +c.Disable();UIManager.Instance.Response.SetResult(new object());await Task.Delay(20); +Check(c.Destroyed==1&&PlayerKVGameplayLoop.Faults==1,"actual Disable during UI await disposes stale returned panel"); +c=Fresh();c.PiercingFish(3);c.Dead();Check(PlayerKVGameplayLoop.Cancelled,"actual Destroy revokes controller"); +c=Fresh();c.PiercingFish(3);c.fishingData.IsCaughtFish=false;c.End();Check(c.Endings==0&&UIManager.Instance.Calls==0,"natural failure cannot invoke reward core"); +foreach(int version in new[]{1,2}){c=Fresh();PlayerKVGameplayLoop.Version=version;c.PiercingFish(3);Check(c.Strikes==0&&PlayerKVGameplayLoop.Faults==1,"old permits do not draw");} +c=Fresh();c.PiercingFish(3);PlayerKVGameplayLoop.Confirmed=true;c.Disable();Check(PlayerKVGameplayLoop.Faults==0,"confirmed controller normal disable no second fault"); +c=Fresh();PlayerKVGameplayLoop.Enabled=false;c.PiercingFish(3);c.End();Check(c.Strikes==1&&c.Endings==1,"ordinary PF wrapper takes original core"); +Console.WriteLine($"PASS actual controller source wrappers/lifecycle {checks} assertions; core/render/storage services are spies; no gameplay or network"); +public class Controller { +public int Strikes,Endings,Destroyed;public FishingData fishingData=new();bool isFail;IDisposable disposables,_comboDisposable;object _hpcoroutine; +public void End()=>FishEnd();public void Disable()=>OnDisable();public void Dead()=>OnDestroy(); +void PiercingFishCore(int zone){Strikes++;}void FishEndCore(){Endings++;}void Destroy(object p){Destroyed++;} +void StopSoundCombo(){}void StopRodAudio(){}void StopNotReeling(){}void StopDrawingSplash(){}void AudioRelease(){}void SpSkillFxRelease(){}void StopAllCoroutines(){} +''' +for sig in ['public void PiercingFish(int waitingZone)','void FishEnd()','async Task ShowKVRewardPanelAsync()','private void OnDisable()','private void OnDestroy()']:code+=method(sig)+'\n' +code+='''} +public class FishingData{public bool IsCaughtFish=true;}public enum UITypes{FishingRewardPanel} +public class UIManager{public static UIManager Instance=new();public int Calls;public TaskCompletionSource Response=new();public Task ShowUI(UITypes type){Calls++;return Response.Task;}} +public static class GContext{public static Container container=new();}public class Container{public T Resolve() where T:new()=>new();}public class SoundMgr{public void StopActiveFishingLoopSounds(float x){}} +namespace game.PlayerDataStorage {public static class PlayerKVGameplayLoop{ +public static bool Enabled,Drawing,Ending,Cancelled,Confirmed;public static int Version,Faults;public static string Scope; +public static void Reset(){Enabled=true;Drawing=Ending=Cancelled=Confirmed=false;Version=3;Faults=0;Scope=null;} +public static bool RewardRunCurrent=>!Cancelled; +public static bool BeginRewardDrawing(object o){if(Version!=3||Cancelled)throw new Exception("no draw");if(Drawing)return false;Drawing=true;return true;} +public static bool BeginRewardEnding(object o,bool success){if(Ending)return false;if(!success)throw new Exception("failed catch");Ending=true;return true;} +public static IDisposable BeginRewardSegment(string n){Scope=n;return new Batch();}class Batch:IDisposable{public void Dispose(){Scope=null;}} +public static void Fault(string s){Faults++;}public static void CancelReward(object o,string r){if(Cancelled)return;Cancelled=true;if(!Confirmed)Fault(r);} +}} +''' +(r/'ControllerTests.cs.txt').write_text(code,encoding='utf-8') +(r/'ControllerTests.csproj').write_text('''Exenet9.0falseUNITY_EDITOR$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config''',encoding='utf-8') diff --git a/CodexOutput/upserver/KVFishingRewardTests/build-formula-tests.py b/CodexOutput/upserver/KVFishingRewardTests/build-formula-tests.py new file mode 100644 index 000000000..80b82fd52 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/build-formula-tests.py @@ -0,0 +1,34 @@ +from pathlib import Path +import json +r=Path(__file__).parent +before=(r/'before/FishingRewardPanel.cs').read_text(encoding='utf-8-sig') +current=Path('Client/Assets/Scripts/UI/FishingReward/FishingRewardPanel.cs').read_text(encoding='utf-8-sig') +def method(src,signature): + start=src.index(signature);a=src.index('{',start);depth=1;i=a+1 + while depth: + depth += (src[i]=='{')-(src[i]=='}');i+=1 + return src[start:i] +def block(src): + start=src.index('DropPackageList dropPackageList =');end=src.index(' add = cash;',start) + return src[start:end] +def formula(src,name): + return 'public class '+name+' : FormulaBase { public int Run(){'+block(src)+'return cash;}'+method(src,'float SetProficiency(')+'}\n' +text='''using System; +foreach(var c in new[]{(10f,0.25f,10,0.5f,0.5f,0.2f,0.1f,122),(0f,0.25f,10,0.5f,0.5f,0.2f,0.1f,0),(10f,0f,1,1f,0f,0f,0f,10),(1f,0f,1,0.5f,0f,0f,0f,0)}){ + var old=new OldFormula();var next=new NewFormula(); + foreach(var x in new FormulaBase[]{old,next}){x._tables.TbDrop[0].DropList.DropCountList[1]=c.Item1;x.pointBonus=c.Item2;x.Magnification=c.Item3;x.fishingData.random=c.Item4;x.fishingData.WeightIncEvent=c.Item5;x.playerData.benefitsCashMag=c.Item6;GContext.container.Buff.Gold=c.Item7;} + int a=old.Run(),b=next.Run();if(a!=c.Item8||b!=a)throw new Exception($"Formula {a}/{b} expected {c.Item8}");Console.WriteLine($"PASS original/current cash={b}"); +} +public class FormulaBase {public Tables _tables=new();public Fishing fishingData=new();public Player playerData=new();public float pointBonus,extraPointEvent;public int Magnification;protected float _fishScore;protected Text text_gold_num=new(),text_num_buff=new();} +public class Tables {public Drop[] TbDrop={new()};}public class Drop{public DropPackageList DropList=new();}public class DropPackageList {public float[] DropCountList={20,10};} +public class Fishing{public Fish curFishData=new(),fishItem=new();public float random,fishWeight,WeightIncEvent;public int MapId;public bool IsNewItemFish()=>false;} +public class Fish{public int DropID,ID,Quality=2;}public class Player{public float benefitsCashMag,benefitsEventFishCashMag;} +public class CollectingData{public float ExtraCashDrop()=>0;}public class BuffDataCenter{public float Gold;public float GetExtraGoldBuffValue(int m,int q)=>Gold;} +public class FishingChallengeManager{public void AddScoreByQuality(int a,float b,int c){}} +public class Container{public BuffDataCenter Buff=new();public T Resolve() where T:new()=>typeof(T)==typeof(BuffDataCenter)?(T)(object)Buff:new T();} +public static class GContext{public static Container container=new();}public class Text{public Color color;}public struct Color{public static Color black,white;} +public static class Mathf{public static int RoundToInt(float x)=>(int)MathF.Round(x,MidpointRounding.ToEven);} +''' +(r/'FormulaTests.cs.txt').write_text(text+formula(before,'OldFormula')+formula(current,'NewFormula'),encoding='utf-8') +(r/'FormulaTests.csproj').write_text('''Exenet9.0false$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config''',encoding='utf-8') +(r/'formula-source-check.json').write_text(json.dumps({'cashBlockUnchanged':block(before)==block(current),'proficiencyUnchanged':method(before,'float SetProficiency(')==method(current,'float SetProficiency('),'fishWeightUnchanged':method(before,'void SetFishWeight(')==method(current,'void SetFishWeight(')},indent=2)) diff --git a/CodexOutput/upserver/KVFishingRewardTests/build-old-presentation-test.py b/CodexOutput/upserver/KVFishingRewardTests/build-old-presentation-test.py new file mode 100644 index 000000000..4b3e7e0fb --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/build-old-presentation-test.py @@ -0,0 +1,42 @@ +from pathlib import Path +r=Path(__file__).parent +s=(r/'before/FishingRewardPanel.cs').read_text(encoding='utf-8-sig');a=s.index(' async void Success(float pointBonus)');i=s.index('{',a)+1;n=1 +while n:n+=(s[i]=='{')-(s[i]=='}');i+=1 +method=s[a:i] +text='''using System;using System.Threading.Tasks;using System.Runtime.CompilerServices; +var panel=new OriginalPanel();panel.Run(); +bool ok=panel.Animations==0&&!panel.go_info.active; +Console.WriteLine($"{(ok?"PASS":"FAIL")} original full Success method displays before pending storage response: writes={Storage.Writes}, confirmed={Storage.Confirmed}, animations={panel.Animations}, info={panel.go_info.active}"); +return ok?0:1; +public class OriginalPanel{ +public int Animations;public Node go_info=new(),go_weight=new(),fx_ui_blingbling_01=new(),bg_target=new(); +int addCollecting,curCollectingID,add;float extraPointEvent;bool isSocial,isEnable=true;int Magnification=10;float waitTime1,duration1,waitTime2,duration2; +Fishing fishingData=new();FishingEventData fishingEventData=new();Tables _tables=new();PlayerData playerData=new();string PanelName="reward"; +Text root_target_text_info=new(),root_1v1battle_text_info=new(),text_gold_num=new(),text_num_buff=new(),txt_magnification=new(); +Bonus bonusReward=new();UI uiService=new();Social fishingRewardSocial=new();Animator anim_panel; +public OriginalPanel(){anim_panel=new Animator(()=>Animations++);}public void Run()=>Success(0); +void MaterialChange(){}void PlayAudioRewardFish(){}void SetTextTitleAsync(float x,bool s){}void SetFishWeight(float w){}void VibrationData(){} +float SetProficiency(float a,float b,int c)=>1;object AddCash()=>new();object StartCoroutine(object o){Animations++;return o;} +'''+method+'''} +public static class Storage{public static int Writes;public static bool Confirmed=false;} +public class Node{public bool active;public Node gameObject=>this;public void SetActive(bool b){active=b;}} +public class Text{public string text;public Color color;}public struct Color{public static Color black,white;} +public class PlayerData{public float benefitsCashMag;public void SaveFishingFrequency(){Storage.Writes++;}public void AddGold(ulong v){Storage.Writes++;}} +public class PlayerItemData{public void AddItemCount(int a,int b,object c){}}public class PiggyBankPackData{public void TriggerPack(){}}public class BargainPackData{public void TriggerPack(){}} +public class BuffDataCenter{public void EndCollectionTargetWeight(int x){}public float GetExtraGoldBuffValue(int a,int b)=>0;}public class CollectingData{public float ExtraCashDrop()=>0;} +public class GuideDataCenter{public void AddIndex(bool b){}}public class MMTService{} +public class Tables{public Drop[] TbDrop={new()};}public class Drop{public DropPackageList DropList=new();}public class DropPackageList{public float[] DropCountList={1,10};public int[] DropIDList={0,1001};} +public class Fishing{public Fish curFishData=new(),fishItem=new();public int MapId,NewIndex;public float random=1,fishWeight=1;public bool IsKeepsakeFish()=>false;public bool IsPiggyBankFish()=>false;public bool IsBarginPackFish()=>false;public bool IsGeneralFish()=>true;} +public class Fish{public int ID,DropID,Quality=2;public FishGeneralType GeneralType;public bool IsSpinachItem()=>false;} +public enum FishGeneralType{Normal,SpMapFish,EventFish}public enum ConditionType{CashFromFishing,MaxCashFromFishing} +public record ConditionTypeEvent(ConditionType c,int count);public record TargetAddData(int a,int b,int c); +public class FishingEventData{public int curActivateItemId;public Target collectingTargetInit=new();public float OnFishReward(int a,int b,int c)=>0;public void AddProgress(int a,int b){}}public class Target{public string BgRewardAdd;} +public static class GContext{public static Container container=new();public static void Publish(object e){}}public class Container{public T Resolve() where T:new()=>new();} +public class Bonus{public void Show(int n){}public void DOTweenAdd(int n,float t){}}public class UI{public void SetImageSprite(object a,string b,string c){}}public class Social{public void ShowSocial(bool b){}} +public class Animator{Action play;public Animator(Action a){play=a;}public void Play(string s){play();}} +public static class LocalizationMgr{public static string GetText(string s)=>s;}public static class Mathf{public static int RoundToInt(float v)=>(int)MathF.Round(v);} +public static class ConvertTools{public static string GetNumberString2(int n)=>n.ToString();}public static class DOTween{public static Tween To(Funca,Actionb,float c,float d)=>new();}public class Tween{public Tween OnUpdate(Action a)=>this;public Tween SetId(string s)=>this;} +public class WaitForSeconds{static TaskCompletionSource wait=new();public WaitForSeconds(float t){}public TaskAwaiter GetAwaiter()=>wait.Task.GetAwaiter();} +''' +(r/'OldPresentationTests.cs.txt').write_text(text,encoding='utf-8') +(r/'OldPresentationTests.csproj').write_text('''Exenet9.0false$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config''',encoding='utf-8') diff --git a/CodexOutput/upserver/KVFishingRewardTests/freeze-guide-projection.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/freeze-guide-projection.cs.txt new file mode 100644 index 000000000..9e8a6ccf9 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/freeze-guide-projection.cs.txt @@ -0,0 +1,5 @@ +var s=UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/ABPackage/pkgPrepare/Export/GuidanceConfig.asset"); +var groups=s.guidanceGroupDefines.Where(g=>g.guidanceDefines.Any(n=>n.panelName=="FishingPanel"||n.panelName=="FishingPanel_Advanced"||n.panelName=="FishingRewardPanel")).OrderBy(g=>g.groupName.ToString(),System.StringComparer.Ordinal).ToArray(); +var projection=string.Join("\n",groups.Select(g=>UnityEngine.JsonUtility.ToJson(g))); +System.IO.File.WriteAllText("C:/projects/ft/CodexOutput/upserver/KVFishingRewardTests/guide-baseline-projection.jsonl",projection,new System.Text.UTF8Encoding(false)); +return new{count=groups.Length,nodeCounts=string.Join(",",groups.Select(g=>g.groupName+":"+g.guidanceDefines.Length))}; diff --git a/CodexOutput/upserver/KVFishingRewardTests/guide-details.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/guide-details.cs.txt new file mode 100644 index 000000000..96c6aac61 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/guide-details.cs.txt @@ -0,0 +1,2 @@ +var s=UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/ABPackage/pkgPrepare/Export/GuidanceConfig.asset"); +return string.Join("\n",System.Linq.Enumerable.Select(System.Linq.Enumerable.Where(s.guidanceGroupDefines,g=>new[]{"FishSkill01","Pollution01","FishSkill02","FishSkill03","FishSkill04","Egyptian01","EasterFish01"}.Contains(g.groupName.ToString())),g=>g.groupName+" | "+g.TriggerConditionsType+" | "+g.TriggerConditionsParameter+" | "+string.Join(",",System.Linq.Enumerable.Select(g.guidanceDefines,x=>x.panelName)))); diff --git a/CodexOutput/upserver/KVFishingRewardTests/offline-guide-gate.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/offline-guide-gate.cs.txt new file mode 100644 index 000000000..c3206be44 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/offline-guide-gate.cs.txt @@ -0,0 +1,7 @@ +var settings=UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/ABPackage/pkgPrepare/Export/GuidanceConfig.asset"); +var backup=SimpleJSON.JSON.Parse(System.IO.File.ReadAllText("C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260924T084836Z/102.json")); +var entries=backup["data"]["GuideSaveDataNew"].Value.Split(';'); +var completed=new System.Collections.Generic.HashSet(System.Linq.Enumerable.Select(System.Linq.Enumerable.Where(entries,x=>x.EndsWith("|1")),x=>x.Split('|')[0])); +var groups=System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Where(settings.guidanceGroupDefines,g=>System.Linq.Enumerable.Any(g.guidanceDefines,s=>s.panelName=="FishingPanel" || s.panelName=="FishingPanel_Advanced" || s.panelName=="FishingRewardPanel"))); +var unfinished=System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Where(groups,g=>!completed.Contains(g.groupName.ToString()))); +return new{count=groups.Length,required=string.Join(",",System.Linq.Enumerable.Select(groups,g=>g.groupName.ToString())),unfinished=string.Join(",",System.Linq.Enumerable.Select(unfinished,g=>g.groupName.ToString())),settings.SkipLevel,missingNotSkipped=string.Join(",",System.Linq.Enumerable.Select(System.Linq.Enumerable.Where(unfinished,g=>!settings.SkipGroupNames.Contains(g.groupName)),g=>g.groupName.ToString())),scope="read-only asset and backup; no runtime guide state assumed"}; diff --git a/CodexOutput/upserver/KVFishingRewardTests/read-guide-projection.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/read-guide-projection.cs.txt new file mode 100644 index 000000000..20e8741a1 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/read-guide-projection.cs.txt @@ -0,0 +1,3 @@ +var s=UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/ABPackage/pkgPrepare/Export/GuidanceConfig.asset"); +var groups=s.guidanceGroupDefines.Where(g=>g.guidanceDefines.Any(n=>n.panelName=="FishingPanel"||n.panelName=="FishingPanel_Advanced"||n.panelName=="FishingRewardPanel")).OrderBy(g=>g.groupName.ToString(),System.StringComparer.Ordinal).ToArray(); +return string.Join("\n",groups.Select(g=>UnityEngine.JsonUtility.ToJson(g))); diff --git a/CodexOutput/upserver/KVFishingRewardTests/reconcile-fourth.ps1 b/CodexOutput/upserver/KVFishingRewardTests/reconcile-fourth.ps1 new file mode 100644 index 000000000..1f265f7c2 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/reconcile-fourth.ps1 @@ -0,0 +1,75 @@ +$ErrorActionPreference='Stop' +$root='C:/projects/ft/CodexOutput/upserver' +function Read-Backup($stamp) { Get-Content -Raw -LiteralPath "$root/KVGameplayLoopTests/backups/$stamp/102.json" | ConvertFrom-Json } +function Changed-Keys($left,$right) { + @(@($left.PSObject.Properties.Name)+@($right.PSObject.Properties.Name) | Sort-Object -Unique | Where-Object { $left.$_ -cne $right.$_ }) +} +function Hash-Value([string]$value) { + $sha=[Security.Cryptography.SHA256]::Create() + try { [BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($value))).Replace('-','') } finally { $sha.Dispose() } +} +$before=Read-Backup '20260928T024346Z' +$reward=Read-Backup '20260928T024903Z' +$closed=Read-Backup '20260928T025001Z' +$final=Read-Backup '20260928T025514Z' +$events=@(Get-Content -LiteralPath "$root/KVGameplayLoopTests/runs/44848_639261601944579366/events.jsonl" | ForEach-Object { $_ | ConvertFrom-Json }) +$recoveryEvents=@(Get-Content -LiteralPath "$root/KVGameplayLoopTests/runs/103828_639261606481142886/events.jsonl" | ForEach-Object { $_ | ConvertFrom-Json }) +$recovered=(Get-Content -Raw -LiteralPath "$root/KVFishingRewardTests/fourth-recovered-values.json" | ConvertFrom-Json).Result | ConvertFrom-Json +$preMain=$before.data.NewPlayerDataDic | ConvertFrom-Json +$postMain=$final.data.NewPlayerDataDic | ConvertFrom-Json +$preMap=$before.data.Map_5_102 | ConvertFrom-Json +$postMap=$final.data.Map_5_102 | ConvertFrom-Json +$rewardConfirmation=@($events | Where-Object kind -EQ 'fishing-reward-confirmed')[0] +$presentation=@($events | Where-Object kind -EQ 'fishing-reward-presented')[0] +$ticketMatches=@($rewardConfirmation.detail.tickets | ForEach-Object { [pscustomobject]@{key=$_.key;sequence=$_.sequence;matchesFinalServer=((Hash-Value $final.data.($_.key)) -ceq $_.valueHash)} }) +$cacheResults=@('44848_639261601944579366','103828_639261606481142886' | ForEach-Object { + $cacheFile=Get-ChildItem -LiteralPath "$root/KVGameplayLoopTests/runs/$_/cache" -Recurse -Filter state.json + $cache=Get-Content -Raw -LiteralPath $cacheFile.FullName | ConvertFrom-Json + [pscustomobject]@{run=$_;pending=@($cache.pending).Count;unknown=($null -ne $cache.unknownBatch);active=($null -ne $cache.activeBatch);baselineChangedKeys=@(Changed-Keys $cache.baseline $final.data)} +}) +$report=[ordered]@{ + phase='4C2-fourth-real-run-and-fresh-process-recovery-awaiting-independent-review' + api=$recovered.api + backups=@('20260928T024346Z','20260928T024903Z','20260928T025001Z','20260928T025514Z') + countBefore=@($before.data.PSObject.Properties).Count;countAfter=@($final.data.PSObject.Properties).Count + changedKeys=@(Changed-Keys $before.data $final.data) + changedFishIds=@(Changed-Keys $preMap $postMap) + fishBefore=$preMap.'10223';fishAfter=$postMap.'10223' + mainBefore=$preMain;mainAfter=$postMain + rewardToQuitChangedKeys=@(Changed-Keys $reward.data $closed.data) + quitToRecoveryFinalChangedKeys=@(Changed-Keys $closed.data $final.data) + consumeCount=@($events | Where-Object kind -EQ 'fishing-energy-applied').Count + rewardConfirmedCount=@($events | Where-Object kind -EQ 'fishing-reward-confirmed').Count + rewardPresentedCount=@($events | Where-Object kind -EQ 'fishing-reward-presented').Count + confirmedBeforePresented=([DateTime]$rewardConfirmation.at -lt [DateTime]$presentation.at) + rewardTickets=$ticketMatches + faults=@($events | Where-Object kind -EQ 'fault').Count + recoveryFishingEvents=@($recoveryEvents | Where-Object kind -Like 'fishing-*').Count + recoveryWrites=@($recoveryEvents | Where-Object kind -EQ 'accepted').Count + recovered=$recovered + recoveredCoreEqualsFinalServer=($recovered.core -ceq $postMain.newData) + recoveredFishRawEqualsFinalServer=($recovered.fishRaw -ceq $postMap.'10223') + cacheResults=$cacheResults + signupGold=16200;fishingGold=2516;totalGold=18716 + independentFormulaRecomputed=$false + precastIndependentBackupCaptured=$false + boundary='用户在主会话尚未完成动作前快照时手动签到并抛竿;precast/live-gates实际采样晚于扣费,不能充当动作前基线。保存完整审计和服务器前后值,交独立复核裁决证据是否充分,不自动重跑。鱼TotalNum由新进程KV有效值读回验证;活对象直接验证重量/等级/NewFish,未通过私有反射读取TotalNum。' +} +$report | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath "$root/KVFishingRewardTests/fourth-reconciliation.json" -Encoding utf8 +$checks=@( + $report.api -ceq 'http://s101.local:18080/' + $report.consumeCount -eq 1 + $report.rewardConfirmedCount -eq 1 + $report.rewardPresentedCount -eq 1 + $report.confirmedBeforePresented + $report.faults -eq 0 + $report.recoveryFishingEvents -eq 0 + $report.rewardToQuitChangedKeys.Count -eq 0 + $report.quitToRecoveryFinalChangedKeys.Count -eq 0 + $report.recoveredCoreEqualsFinalServer + $report.recoveredFishRawEqualsFinalServer + @($ticketMatches | Where-Object { -not $_.matchesFinalServer }).Count -eq 0 + @($cacheResults | Where-Object { $_.pending -ne 0 -or $_.unknown -or $_.active -or $_.baselineChangedKeys.Count -ne 0 }).Count -eq 0 +) +if($checks -contains $false) { throw 'Reconciliation assertion failed; inspect fourth-reconciliation.json' } +[pscustomobject]@{checks=$checks.Count;passed=$true;changedKeys=$report.changedKeys;changedFishIds=$report.changedFishIds;recoveryUnchanged=$true} | ConvertTo-Json -Depth 4 diff --git a/CodexOutput/upserver/KVFishingRewardTests/unity-dismiss-signup.cs.txt b/CodexOutput/upserver/KVFishingRewardTests/unity-dismiss-signup.cs.txt new file mode 100644 index 000000000..b0237df03 --- /dev/null +++ b/CodexOutput/upserver/KVFishingRewardTests/unity-dismiss-signup.cs.txt @@ -0,0 +1,7 @@ +using UnityEngine; +using UnityEngine.UI; +var panel=GameObject.Find("UIRoot/EventSignupPopupPanel_2"); +if(panel==null)return "No signup popup"; +var button=panel.transform.Find("btn_close").GetComponent