test(upserver): 归档存档迁移与玩法闭环回归工具

This commit is contained in:
Liubing\LB
2026-09-28 18:16:14 +08:00
parent c4ae02def9
commit a6f5e107f8
232 changed files with 12903 additions and 0 deletions

View File

@@ -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<PlayerData>();
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();
}
}
}

View File

@@ -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<EnergyDef> DataList=new(){new()};}
public sealed class ConditionRule{public ConditionType Condition=ConditionType.ConsumeEnergy;}
public sealed class Activity{public List<ConditionRule> ConditionList=new(){new()};}
public sealed class ActivityTable{public List<Activity> 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>()=>(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();}
}}

View File

@@ -0,0 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><EnableDefaultCompileItems>false</EnableDefaultCompileItems><ImplicitUsings>enable</ImplicitUsings><DefineConstants>UNITY_EDITOR</DefineConstants><UseAppHost>false</UseAppHost><RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile></PropertyGroup>
<ItemGroup><Compile Include="ConsumeControlSlice.cs.txt"/><Compile Include="ConsumeControlTests.cs.txt"/><Reference Include="Newtonsoft.Json"><HintPath>../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll</HintPath></Reference></ItemGroup>
</Project>

View File

@@ -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<SoundMgr>().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<object> Response=new();public Task<object> ShowUI(UITypes type){Calls++;return Response.Task;}}
public static class GContext{public static Container container=new();}public class Container{public T Resolve<T>() 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);}
}}

View File

@@ -0,0 +1 @@
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><EnableDefaultCompileItems>false</EnableDefaultCompileItems><DefineConstants>UNITY_EDITOR</DefineConstants><RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile></PropertyGroup><ItemGroup><Compile Include="ControllerTests.cs.txt"/></ItemGroup></Project>

View File

@@ -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<T>() 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<CollectingData>().ExtraCashDrop();
//Data
//金币各种加成
float gold = (dropPackageList.DropCountList[1] + extraCashDrop)
* (1 + pointBonus)
* Magnification
* fishingData.random;
//杆加成
//解锁地图Buff加成
float glodBuff = GContext.container.Resolve<BuffDataCenter>().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<FishingChallengeManager>().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<CollectingData>().ExtraCashDrop();
//Data
//金币各种加成
float gold = (dropPackageList.DropCountList[1] + extraCashDrop)
* (1 + pointBonus)
* Magnification
* fishingData.random;
//杆加成
//解锁地图Buff加成
float glodBuff = GContext.container.Resolve<BuffDataCenter>().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<FishingChallengeManager>().AddScoreByQuality(Quality, proficiency, fishId);
_fishScore = proficiency;
return proficiencyExtra;
}}

View File

@@ -0,0 +1 @@
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><EnableDefaultCompileItems>false</EnableDefaultCompileItems><RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile></PropertyGroup><ItemGroup><Compile Include="FormulaTests.cs.txt"/></ItemGroup></Project>

View File

@@ -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<T>() 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++;} }

View File

@@ -0,0 +1 @@
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><EnableDefaultCompileItems>false</EnableDefaultCompileItems><ImplicitUsings>enable</ImplicitUsings><UseAppHost>false</UseAppHost><RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile></PropertyGroup><ItemGroup><Compile Include="LegacySlice.cs.txt"/><Compile Include="LegacyRed.cs.txt"/></ItemGroup></Project>

View File

@@ -0,0 +1,49 @@
using System; using System.Collections.Generic; using System.Linq;
public class ActualFish { public Dictionary<int,FishWeightData> fishData=new(); public Dictionary<int,Dictionary<int,FishWeightData>> 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<PlayerData>().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<int, FishWeightData>
{
[id] = data
};
}
MapNewUpdateUserDataValue(mapID, mapFishData[mapID]);
}
void MapNewUpdateUserDataValue(int mapID, Dictionary<int, FishWeightData> keyValuePairs)
{
Dictionary<int, string> mapData = keyValuePairs.ToDictionary(_ => _.Key, _ => _.Value.Serialize());
PlayFabMgr.Instance.UpdateUserDataValue($"Map_5_{mapID}", JsonMapper.ToJson(mapData));
}
}

View File

@@ -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<PlayerData>().SaveFishingFrequency();
if (addCollecting > 0)
{
extraPointEvent = fishingEventData.OnFishReward(fishingData.MapId, fishingData.NewIndex, Magnification);
addCollecting *= Magnification;
GContext.container.Resolve<PlayerItemData>().AddItemCount(curCollectingID, addCollecting, null);
MaterialChange();
}
PlayAudioRewardFish();
GContext.container.Resolve<BuffDataCenter>().EndCollectionTargetWeight(fishingData.fishItem.ID);
fishingEventData.AddProgress(fishingData.fishItem.Quality, Magnification);
GContext.container.Resolve<GuideDataCenter>().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>();
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<BargainPackData>();
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<CollectingData>().ExtraCashDrop();
//Data
//金币各种加成
float gold = (dropPackageList.DropCountList[1] + extraCashDrop)
* (1 + pointBonus)
* Magnification
* fishingData.random;
//杆加成
//解锁地图Buff加成
float glodBuff = GContext.container.Resolve<BuffDataCenter>().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<MMTService>();
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<T>() 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(Func<float>a,Action<float>b,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<bool> wait=new();public WaitForSeconds(float t){}public TaskAwaiter<bool> GetAwaiter()=>wait.Task.GetAwaiter();}

View File

@@ -0,0 +1 @@
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><EnableDefaultCompileItems>false</EnableDefaultCompileItems><RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile></PropertyGroup><ItemGroup><Compile Include="OldPresentationTests.cs.txt"/></ItemGroup></Project>

View File

@@ -0,0 +1,56 @@
using System;using System.Threading.Tasks;
public class ActualPanel {
game.PlayerDataStorage.PlayerKVGameplayLoop.FishingRewardAction kvReward; Func<Task> 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); }
}
}

View File

@@ -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<bool> 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(){}}
}}

View File

@@ -0,0 +1 @@
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><EnableDefaultCompileItems>false</EnableDefaultCompileItems><ImplicitUsings>enable</ImplicitUsings><UseAppHost>false</UseAppHost><DefineConstants>UNITY_EDITOR</DefineConstants><RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile></PropertyGroup><ItemGroup><Compile Include="PanelSlice.cs.txt"/><Compile Include="PanelTests.cs.txt"/></ItemGroup></Project>

View File

@@ -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<string,string>{{"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;

View File

@@ -0,0 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><EnableDefaultCompileItems>false</EnableDefaultCompileItems><ImplicitUsings>enable</ImplicitUsings><UseAppHost>false</UseAppHost><DefineConstants>UNITY_EDITOR;SERVICE_HOST</DefineConstants><RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile></PropertyGroup>
<ItemGroup><Compile Include="PermitTests.cs.txt"/><Compile Include="../../../Client/Assets/Scripts/Services/UserService.KVGameplayLoop.cs"/><Reference Include="Newtonsoft.Json"><HintPath>../../../Client/Library/PackageCache/com.unity.nuget.newtonsoft-json@3.2.1/Runtime/Newtonsoft.Json.dll</HintPath></Reference></ItemGroup>
</Project>

View File

@@ -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<object> Response=new();public Task<object> ShowUI(UITypes type){Calls++;return Response.Task;}}
public static class GContext{public static Container container=new();}public class Container{public T Resolve<T>() 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('''<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><EnableDefaultCompileItems>false</EnableDefaultCompileItems><DefineConstants>UNITY_EDITOR</DefineConstants><RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile></PropertyGroup><ItemGroup><Compile Include="ControllerTests.cs.txt"/></ItemGroup></Project>''',encoding='utf-8')

View File

@@ -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<T>() 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('''<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><EnableDefaultCompileItems>false</EnableDefaultCompileItems><RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile></PropertyGroup><ItemGroup><Compile Include="FormulaTests.cs.txt"/></ItemGroup></Project>''',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))

View File

@@ -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<T>() 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(Func<float>a,Action<float>b,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<bool> wait=new();public WaitForSeconds(float t){}public TaskAwaiter<bool> GetAwaiter()=>wait.Task.GetAwaiter();}
'''
(r/'OldPresentationTests.cs.txt').write_text(text,encoding='utf-8')
(r/'OldPresentationTests.csproj').write_text('''<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><EnableDefaultCompileItems>false</EnableDefaultCompileItems><RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile></PropertyGroup><ItemGroup><Compile Include="OldPresentationTests.cs.txt"/></ItemGroup></Project>''',encoding='utf-8')

View File

@@ -0,0 +1,5 @@
var s=UnityEditor.AssetDatabase.LoadAssetAtPath<GuidanceDefineSetting>("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))};

View File

@@ -0,0 +1,2 @@
var s=UnityEditor.AssetDatabase.LoadAssetAtPath<GuidanceDefineSetting>("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))));

View File

@@ -0,0 +1,7 @@
var settings=UnityEditor.AssetDatabase.LoadAssetAtPath<GuidanceDefineSetting>("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<string>(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"};

View File

@@ -0,0 +1,3 @@
var s=UnityEditor.AssetDatabase.LoadAssetAtPath<GuidanceDefineSetting>("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)));

View File

@@ -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

View File

@@ -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<Button>();
button.onClick.Invoke();
return "Invoked existing signup close callback only; no claim callback";

View File

@@ -0,0 +1 @@
return KVGameplayLoopSetup.State();

View File

@@ -0,0 +1,17 @@
using asap.core;
using System.Linq;
using LitJson;
var p=GContext.container.Resolve<GameCore.PlayerData>();
var f=GContext.container.Resolve<GameCore.PlayerFishData>();
var b=GContext.container.Resolve<BattlePassData>();
var map=JsonMapper.ToObject<System.Collections.Generic.Dictionary<string,string>>(PlayFabMgr.Instance.GetLocalData("Map_5_102"));
return JsonMapper.ToJson(new {
api=GContext.container.Resolve<ICustomServerMgr>().ApiBaseUri.ToString(),
core=p.PlayerDataDic["newData"],
gold=(ulong)p.gold,energy=(int)p.Energy,energyRM=p.GetEnergyRM(),frequency=p._fishingFrequency,
fishId=10223,fishRaw=map["10223"],
liveMaxWeight=f.GetDataMaxWeight(10223),liveLevel=f.GetDataLevel(10223),liveNewFish=f.GetDataNewFish(10223),
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)),
state=KVGameplayLoopSetup.State()
});

View File

@@ -0,0 +1,6 @@
using asap.core;
if(GContext.container==null)return "Game container unavailable after stop";
try {
var d=GContext.container.Resolve<FishingData>();
return new{postStopObservationOnly=true,ready=GContext.Ready,item=d.fishItem?.ID,subtype=d.fishItem?.SubType,fish=d.curFishData?.ID,free=d.IsFree,retry=d.IsRetry,duel=d.IsDuel,map=d.MapId};
} catch(System.Exception e){return "Post-stop game data unavailable: "+e.Message;}

View File

@@ -0,0 +1,2 @@
return KVGameplayLoopSetup.PrepareFishingRewardPermit("102","C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260928T024346Z/102.json",new string[]{"AquariumEventDataNewKey","BattlePassRecordTask","BombGameCount","BuffDataList","BuyData","CampDataDic","ChainPackDataNew","DigCount","DigGridData","Event1v1BattleSlapData","EventBoostPackData","EventCubeMeltData","EventLuckGameRecord","EventLuckMagicPlayfabData","EventPartnerStatueData","FishHuntGameCount","FixedTimeShopDic","GuideSaveDataNew","IsOpenClubRed","LastPaymentTime","Leadboard_LastContinent","MMTest","Map_5_101","Map_5_102","MiniBPKeyNew","MonthlySignEvent","NewPlayerDataDic","NewRodList","OfferDiscoPartyManager","OfferStoryChain","OfferSweepPackData","Pack1A1Data","PiggyBankPackData","RodData","RodLevelData","SeasonStampData","ShopPackBuyCount","SoloDataKey","TargetBuffDataList","TransitionData","TriggerPackDataList","VIPAdPackData","WelcomeBack","epbdk","epdk"});

View File

@@ -0,0 +1,2 @@
return KVGameplayLoopSetup.PreparePermit("102","C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260928T025001Z/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"});

View File

@@ -0,0 +1 @@
return KVGameplayLoopSetup.PrepareFishingRewardPermit("102","C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260928T023420Z/102.json",new string[]{"AquariumEventDataNewKey","BattlePassRecordTask","BombGameCount","BuffDataList","BuyData","CampDataDic","ChainPackDataNew","DigCount","DigGridData","Event1v1BattleSlapData","EventBoostPackData","EventCubeMeltData","EventLuckGameRecord","EventLuckMagicPlayfabData","EventPartnerStatueData","FishHuntGameCount","FixedTimeShopDic","GuideSaveDataNew","IsOpenClubRed","LastPaymentTime","Leadboard_LastContinent","MMTest","Map_5_101","Map_5_102","MiniBPKeyNew","MonthlySignEvent","NewPlayerDataDic","NewRodList","OfferDiscoPartyManager","OfferStoryChain","OfferSweepPackData","Pack1A1Data","PiggyBankPackData","RodData","RodLevelData","SeasonStampData","ShopPackBuyCount","SoloDataKey","TargetBuffDataList","TransitionData","TriggerPackDataList","VIPAdPackData","WelcomeBack","epbdk","epdk"});

View File

@@ -0,0 +1 @@
return KVGameplayLoopSetup.PrepareFishingRewardPermit("102","C:/projects/ft/CodexOutput/upserver/KVGameplayLoopTests/backups/20260924T093341Z/102.json",new string[]{"AquariumEventDataNewKey","BattlePassRecordTask","BombGameCount","BuffDataList","BuyData","CampDataDic","ChainPackDataNew","DigCount","DigGridData","Event1v1BattleSlapData","EventBoostPackData","EventCubeMeltData","EventLuckGameRecord","EventLuckMagicPlayfabData","EventPartnerStatueData","FishHuntGameCount","FixedTimeShopDic","GuideSaveDataNew","IsOpenClubRed","LastPaymentTime","Leadboard_LastContinent","MMTest","Map_5_101","Map_5_102","MiniBPKeyNew","MonthlySignEvent","NewPlayerDataDic","NewRodList","OfferDiscoPartyManager","OfferStoryChain","OfferSweepPackData","Pack1A1Data","PiggyBankPackData","RodData","RodLevelData","SeasonStampData","ShopPackBuyCount","SoloDataKey","TargetBuffDataList","TransitionData","TriggerPackDataList","VIPAdPackData","WelcomeBack","epbdk","epdk"});

View File

@@ -0,0 +1,8 @@
using asap.core;
using System.Linq;
var p=GContext.container.Resolve<GameCore.PlayerData>();
var g=GContext.container.Resolve<GameCore.GuideDataCenter>();
var f=GContext.container.Resolve<GameCore.PlayerFishData>();
var c=GContext.container.Resolve<FishingChallengeManager>();
return new{time=ZZTimeHelper.UtcNow(),offset=ZZTimeHelper.LocalTimeOffsetInSec,timeDiff=ZZTimeHelper.timeDiff.TotalSeconds,unlockedMag=p.IsOpenMagnification,unlockedStrike=f.IsOpenPiercing,frequency=p._fishingFrequency,cruise=f.GetFishSwimToMap(p.lastMapId),collect=GContext.container.Resolve<CollectingData>().GetAddCollecting(),collectingTarget=GContext.container.Resolve<GameCore.FishingEventData>().collectingTargetInit!=null,piggy=GContext.container.Resolve<PiggyBankPackData>().IsPackActivated,bargain=GContext.container.Resolve<BargainPackData>().IsPackActivated,challenge=c.fishingChallengeData!=null,guideNode=g.curDefine!=null,guideGroup=g.curGroupDefine!=null,groups=string.Join(";",g.guidanceGroupDefines.Where(x=>x.guidanceDefines.Any(n=>n.panelName=="FishingPanel"||n.panelName=="FishingPanel_Advanced"||n.panelName=="FishingRewardPanel")).Select(x=>x.groupName+":"+g.GetIsFinish(x.groupName.ToString()))),state=KVGameplayLoopSetup.State()};

View File

@@ -0,0 +1,6 @@
var consume=await KVGameplayLoopSetup.RunValidationAsync("fishing-consume");
var core=await KVGameLoadValidation.RunAsync("M01-M02-M21-M22-M24");
var full=await KVGameLoadValidation.RunAsync("B2-20");
var route=await KVSaveRouteValidation.RunAsync("fixed-route");
var equipment=await KVGameplayLoopSetup.RunValidationAsync("equipment-chain");
return new {consume,core,full,route,equipment};

View File

@@ -0,0 +1 @@
return await KVGameplayLoopSetup.RunValidationAsync("fishing-reward");

View File

@@ -0,0 +1,12 @@
using asap.core;
using System.Linq;
using System.Reflection;
var p=GContext.container.Resolve<GameCore.PlayerData>();
var t=GContext.container.Resolve<cfg.Tables>();
var guide=GContext.container.Resolve<GameCore.GuideDataCenter>();
var sys=UnityEngine.Object.FindObjectOfType<FishingUpdateSys>();
var d=sys==null?null:(FishingData)typeof(FishingUpdateSys).GetField("fishingData",BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic).GetValue(sys);
var loop=typeof(GameCore.PlayerData).Assembly.GetType("game.PlayerDataStorage.PlayerKVGameplayLoop");
var a=loop.GetField("rewardAction",BindingFlags.Static|BindingFlags.NonPublic).GetValue(null);
object Field(object o,string n)=>o?.GetType().GetField(n,BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic)?.GetValue(o);
return new{utc=System.DateTime.UtcNow,server=ZZTimeHelper.UtcNow(),energy=(int)p.Energy,energyRM=p.GetEnergyRM(),gold=(ulong)p.gold,frequency=p._fishingFrequency,core=p.PlayerDataDic.GetValueOrDefault("newData"),map=(int)p.lastMapId,mag=p.GetMagnification(),guideNode=guide.curDefine?.panelName,guideGroup=guide.curGroupDefine?.groupName.ToString(),controller=sys!=null,fish=d?.curFishData?.ID,area=d?.curFishData?.AreaID,item=d?.fishItem?.ID,subtype=d?.fishItem?.SubType,draw=d?.IsDrawing,caught=d?.IsCaughtFish,result=d?.fishingResult.ToString(),random=d?.random,weight=d?.fishWeight,weightBonus=d?.WeightIncEvent,tension=Field(sys,"_linePower"),rewardConfirmed=Field(a,"Confirmed"),rewardPresented=Field(a,"Presented"),rewardCancelled=Field(a,"Cancelled"),state=KVGameplayLoopSetup.State()};

View File

@@ -0,0 +1,5 @@
using asap.core;
using System.Linq;
var p=GContext.container.Resolve<GameCore.PlayerData>();
var guide=GContext.container.Resolve<GameCore.GuideDataCenter>();
return new{utc=System.DateTime.UtcNow,server=ZZTimeHelper.UtcNow(),energy=(int)p.Energy,energyRM=p.GetEnergyRM(),gold=(ulong)p.gold,frequency=p._fishingFrequency,core=p.PlayerDataDic["newData"],map=(int)p.lastMapId,mag=p.GetMagnification(),guideNode=guide.curDefine?.panelName,guideGroup=guide.curGroupDefine?.groupName.ToString(),controller=UnityEngine.Object.FindObjectOfType<FishingUpdateSys>()!=null,state=KVGameplayLoopSetup.State()};

View File

@@ -0,0 +1,3 @@
var reward=await KVGameplayLoopSetup.RunValidationAsync("fishing-reward");
var consume=await KVGameplayLoopSetup.RunValidationAsync("fishing-consume");
return new {reward,consume,state=KVGameplayLoopSetup.State()};