保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
558 lines
52 KiB
Plaintext
558 lines
52 KiB
Plaintext
#if UNITY_EDITOR && !SERVICE_HOST
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using asap.core;
|
|
using GameCore;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using UniRx;
|
|
using UnityEngine;
|
|
|
|
namespace game.PlayerDataStorage
|
|
{
|
|
internal static partial class PlayerKVGameplayLoop
|
|
{
|
|
internal static async Task<string> VerifyFishingRewardForEditorAsync()
|
|
{
|
|
if(UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || GContext.Ready ||
|
|
Resources.FindObjectsOfTypeAll<GContext>().Length!=0 || PlayFabMgr.HasLiveInstanceForKVLoad || Enabled || handle!=null ||
|
|
File.Exists(Path.Combine(Root,"permit.json")) || validationCleanupFailure!=null)
|
|
throw new InvalidOperationException("Idle editor without a real permit required.");
|
|
if(Interlocked.CompareExchange(ref validationRunning,1,0)!=0) throw new InvalidOperationException("Validation running.");
|
|
var saved=(Enabled,Ready,Error,Permit,RunPath,handle,onFault,onClose,collecting,mainThread,failed,mainContext,fishingAction,rewardPlan,rewardAction);
|
|
var oldContainer=GContext.container;var oldEvents=FishingContextAccess.Events;
|
|
var folder=Path.Combine(Root,"reward-fixed-runs",DateTime.UtcNow.ToString("yyyyMMddTHHmmss")+"_"+Guid.NewGuid().ToString("N"));
|
|
EquipmentFixture fixture=null;Task pending=null;Container container=null;PlayerData player=null;
|
|
RewardSubscribers subscribers=null;
|
|
var oldLanguages=LocalizationMgr.enumLanguage;
|
|
var textTable=typeof(LocalizationMgr).GetField("table",BindingFlags.Static|BindingFlags.NonPublic);
|
|
var oldTextTable=textTable.GetValue(null);
|
|
GameObject pfShell=null;var pfField=typeof(PlayFabMgr).GetField("_instance",BindingFlags.Static|BindingFlags.NonPublic);var oldPF=pfField.GetValue(null);
|
|
int checks=0,groups=0;var passed=new List<string>();string result=null;
|
|
var ioDiagnostics=new System.Collections.Concurrent.ConcurrentQueue<string>();
|
|
EventHandler<System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs> observeIO=(_,e)=>
|
|
{if(e.Exception is IOException || e.Exception is UnauthorizedAccessException)ioDiagnostics.Enqueue(e.Exception.ToString());};
|
|
AppDomain.CurrentDomain.FirstChanceException+=observeIO;
|
|
void Check(bool yes,string reason) {checks++;if(!yes)throw new InvalidOperationException("Reward validation: "+reason);}
|
|
async Task Start(string name)
|
|
{
|
|
subscribers?.Dispose();subscribers=null;
|
|
player?.StopRecoveryEnergy();player=null;
|
|
if(fixture!=null)await fixture.Close();if(pending!=null){try{await pending;}catch{}pending=null;}
|
|
pfField.SetValue(null,oldPF);if(pfShell!=null)UnityEngine.Object.DestroyImmediate(pfShell);pfShell=null;
|
|
FishingContextAccess.Use(oldContainer);FishingContextAccess.Events=oldEvents;container?.Dispose();container=null;
|
|
fixture=new EquipmentFixture(Path.Combine(folder,(++groups).ToString()));
|
|
fixture.Remote["Map_5_102"]="{\"10211\":\"0|10|20|20|False\",\"10241\":\"2|20|30|40|False\"}";
|
|
var owner=await fixture.Open();
|
|
Enabled=true;Ready=false;Error=null;failed=0;handle=null;collecting=null;
|
|
Permit=new PlayerKVLoopPermit{Version=3,Operation="fishing-reward-once",PlayerId="101",
|
|
AllowedKeys=new[]{"NewPlayerDataDic","Map_5_102","BattlePassRecordTask","late"}};
|
|
RunPath=folder;mainThread=Thread.CurrentThread.ManagedThreadId;mainContext=SynchronizationContext.Current;
|
|
Attach(_=>fixture.Faults++,fixture.Close);Publish(owner);
|
|
fishingAction=new FishingAction(new object(),owner){Consuming=true,Confirmed=true,Continued=true};
|
|
rewardPlan=new FishingRewardPlan{Fish=10211,Area=102};
|
|
rewardAction=new FishingRewardAction(fishingAction,new object(),rewardPlan);
|
|
passed.Add(name);
|
|
}
|
|
PlayerKVEditResult Batch(bool localFinal=false)
|
|
{
|
|
using(BeginRewardSegment("strike")) Write("BattlePassRecordTask","strike",true);
|
|
rewardAction.Ending=true;using(BeginRewardSegment("ending")){}
|
|
rewardAction.Applying=true;
|
|
PlayerKVEditResult primary;
|
|
using(BeginRewardSegment("reward"))
|
|
{
|
|
Write("BattlePassRecordTask","reward-task",true);
|
|
Write("Map_5_102","fish",!localFinal);
|
|
Write("NewPlayerDataDic","intermediate",false);
|
|
primary=Write("NewPlayerDataDic","reward-main",true);
|
|
}
|
|
rewardAction.GuideNotifications=1;
|
|
return primary;
|
|
}
|
|
async Task Reject(Task task,string label)
|
|
{
|
|
bool rejected=false;try{await task;}catch(InvalidOperationException){rejected=true;}
|
|
Check(rejected && !Ready && fixture.Faults==1,label);
|
|
}
|
|
void RejectSync(Action body,string label)
|
|
{bool rejected=false;try{body();}catch(InvalidOperationException){rejected=true;}Check(rejected,label);}
|
|
try
|
|
{
|
|
Directory.CreateDirectory(folder);
|
|
// Exercise the real async Init path, using isolated services and the actual loaded guide asset.
|
|
var potionSource=JObject.Parse(File.ReadAllText(Path.GetFullPath(Path.Combine(Application.dataPath,
|
|
"../../CodexOutput/upserver/KVGameplayLoopTests/backups/20260928T082052Z/101.json"))))["data"].ToObject<Dictionary<string,string>>();
|
|
var guideAssetHandle=UnityEngine.AddressableAssets.Addressables.LoadAssetAsync<object>("GuidanceConfig");
|
|
var guideAsset=(GuidanceDefineSetting)guideAssetHandle.WaitForCompletion();
|
|
var savedGuideGroups=guideAsset.guidanceGroupDefines;
|
|
var awaitAssembly=typeof(IEnumeratorAwaitExtensions).Assembly;
|
|
var syncType=awaitAssembly.GetType("UnityAsyncAwaitUtil.SyncContextUtil",true);
|
|
var syncField=syncType.GetField("<UnitySynchronizationContext>k__BackingField",BindingFlags.Static|BindingFlags.NonPublic);
|
|
var threadField=syncType.GetField("<UnityThreadId>k__BackingField",BindingFlags.Static|BindingFlags.NonPublic);
|
|
var runnerField=awaitAssembly.GetType("UnityAsyncAwaitUtil.AsyncCoroutineRunner",true).GetField("_instance",BindingFlags.Static|BindingFlags.NonPublic);
|
|
var savedSync=syncField.GetValue(null);var savedThread=threadField.GetValue(null);var savedRunner=runnerField.GetValue(null);
|
|
syncField.SetValue(null,SynchronizationContext.Current);threadField.SetValue(null,Thread.CurrentThread.ManagedThreadId);
|
|
try
|
|
{
|
|
foreach(var scenario in new[]{"unavailable-real-101","available","query-failure","node-panel-mismatch",
|
|
"other-group","finished","index-zero","missing-save","out-of-range","preserve-other-pointer"})
|
|
{
|
|
await Start("potion-actual-init-"+scenario);
|
|
rewardPlan=null;rewardAction=null;
|
|
Permit.AllowedKeys=Permit.AllowedKeys.Concat(new[]{"GuideSaveDataNew"}).ToArray();
|
|
var tables=new cfg.Tables(n=>SimpleJSON.JSON.Parse(File.ReadAllText(Path.GetFullPath(Path.Combine(Application.dataPath,"../../cfgData",n+".json")))));
|
|
container=new Container();FishingContextAccess.Use(container);FishingContextAccess.Events=new EventAggregator();
|
|
player=new PlayerData(tables,null);player.lv=62;
|
|
container.RegisterInstance(tables);container.RegisterInstance(player);
|
|
var eventData=new FishingEventData(tables);
|
|
if(scenario!="query-failure")container.RegisterInstance(eventData);
|
|
if(scenario=="available")
|
|
{
|
|
var ev=tables.TbFishingEvent.DataList.First(e=>e.Type==4 && e.SubType==5);
|
|
typeof(cfg.FishingEvent).GetProperty("TimeDefinition").SetValue(ev,new cfg.LimitedTime(
|
|
ZZTimeHelper.UtcNow().AddHours(-1).ToString("yyyy/MM/dd HH:mm:ss",System.Globalization.CultureInfo.InvariantCulture),
|
|
ZZTimeHelper.UtcNow().AddHours(1).ToString("yyyy/MM/dd HH:mm:ss",System.Globalization.CultureInfo.InvariantCulture)));
|
|
typeof(cfg.FishingEvent).GetProperty("ConditionList").SetValue(ev,new List<cfg.EventCondition>{
|
|
new cfg.EventCondition(cfg.ConditionType.AccountLevel,new List<string>{"1"},"")});
|
|
Check(eventData.GetEvent(4,5)==ev.ID,"isolated actual query sees available potion event");
|
|
}
|
|
else if(scenario!="query-failure")Check(eventData.GetEvent(4,5)==-1,"isolated actual query sees no potion event");
|
|
var guideState=new GuideDataCenter(tables,player);container.RegisterInstance(guideState);
|
|
var diagnostics=new PlayerKVSchemaDiagnostics();guideState.ReplaceFromKVDecoded(GuideDataCenter.DecodeForKVLoad(potionSource,diagnostics));
|
|
Check(diagnostics.TotalErrors==0,"real 101 guide source decoded");
|
|
var potionGroup=JsonUtility.FromJson<GuidanceGroupDefine>(JsonUtility.ToJson(savedGuideGroups.First(g=>g.groupName==GroupName.EventPotion01)));
|
|
guideAsset.guidanceGroupDefines=savedGuideGroups.Select(g=>g.groupName==GroupName.EventPotion01?potionGroup:g).ToArray();
|
|
var key=GroupName.EventPotion01.ToString();var entry=guideState.GuideSaveDataDic[key];
|
|
Check(entry.guideSubIndex==4 && !entry.isFinish,"real historical potion progress is unfinished at four");
|
|
if(scenario=="node-panel-mismatch")potionGroup.guidanceDefines[entry.guideSubIndex].panelName="DifferentPanel";
|
|
if(scenario=="other-group")
|
|
{
|
|
guideState.GuideSaveDataDic.Remove(key);key=GroupName.EventPotion02.ToString();
|
|
entry.groupName=key;guideState.GuideSaveDataDic[key]=entry;
|
|
guideAsset.guidanceGroupDefines=guideAsset.guidanceGroupDefines.Where(g=>g.groupName!=GroupName.EventPotion02).ToArray();
|
|
potionGroup.groupName=GroupName.EventPotion02;
|
|
}
|
|
if(scenario=="finished")entry.isFinish=true;
|
|
if(scenario=="index-zero")entry.guideSubIndex=0;
|
|
if(scenario=="missing-save")guideState.GuideSaveDataDic.Remove(key);
|
|
if(scenario=="out-of-range")entry.guideSubIndex=potionGroup.guidanceDefines.Length;
|
|
var otherGroup=savedGuideGroups.First(g=>g.groupName==GroupName.Fish_01);
|
|
if(scenario=="preserve-other-pointer")
|
|
{
|
|
guideState.curGroupDefine=otherGroup;guideState.curDefine=otherGroup.guidanceDefines[0];
|
|
guideState.curSaveData=guideState.GuideSaveDataDic[otherGroup.groupName.ToString()];
|
|
}
|
|
var before=JsonConvert.SerializeObject(guideState.GuideSaveDataDic);
|
|
pfShell=new GameObject("KV inactive guide Init fixture");pfShell.SetActive(false);pfShell.hideFlags=HideFlags.HideAndDontSave;
|
|
var pf=pfShell.AddComponent<PlayFabMgr>();
|
|
Check(pf.IsUnstartedForKVLoad,"guide fixture never starts PF lifecycle");pfField.SetValue(null,pf);
|
|
var initErrors=new List<string>();var initWarnings=new List<string>();int completeEvents=0;
|
|
Application.LogCallback onLog=(message,stack,type)=>{if(type==LogType.Exception || type==LogType.Error)initErrors.Add(message);if(type==LogType.Warning)initWarnings.Add(message);};
|
|
Application.logMessageReceived+=onLog;
|
|
var completion=GContext.OnEvent<ConditionTypeEvent>().Subscribe(e=>{if(e.type==cfg.ConditionType.CompleteGuide)completeEvents++;});
|
|
bool initStarted=false;
|
|
try
|
|
{
|
|
// Asset is preloaded above: Init has no outstanding asset operation when teardown begins.
|
|
initStarted=true;guideState.Init();
|
|
var deadline=System.Diagnostics.Stopwatch.StartNew();
|
|
while(guideState.guidanceGroupDefines==null && deadline.ElapsedMilliseconds<10000)await Task.Delay(10);
|
|
await Task.Delay(1);
|
|
Check(guideState.guidanceGroupDefines!=null && initErrors.Count==0,"actual Init completed without asynchronous errors: "+scenario+"; "+string.Join(" | ",initErrors));
|
|
if(scenario=="unavailable-real-101")
|
|
{
|
|
Check(guideState.curDefine==null && guideState.curGroupDefine==null && guideState.curSaveData==null,
|
|
"unavailable historical potion guide is not activated by actual Init");
|
|
Check(before==JsonConvert.SerializeObject(guideState.GuideSaveDataDic),"deferred actual Init preserves every guide entry");
|
|
guideState.AddIndex(true);GContext.Publish(new GuideNextEvent());
|
|
Check(before==JsonConvert.SerializeObject(guideState.GuideSaveDataDic) && completeEvents==0,
|
|
"real AddIndex and GuideNext cannot finish deferred potion progress");
|
|
Check(!handle.ReadLocalSnapshot().Data.ContainsKey("GuideSaveDataNew") && fixture.Sets==0,
|
|
"deferral and callbacks introduce no guide write or remote request");
|
|
}
|
|
else if(scenario=="preserve-other-pointer")
|
|
Check(ReferenceEquals(guideState.curGroupDefine,otherGroup) && ReferenceEquals(guideState.curDefine,otherGroup.guidanceDefines[0]),"deferral does not clear another active guide");
|
|
else if(scenario=="available" || scenario=="query-failure" || scenario=="node-panel-mismatch" || scenario=="other-group")
|
|
Check(ReferenceEquals(guideState.curGroupDefine,potionGroup) && guideState.curDefine!=null,"non-deferred input retains original recovery: "+scenario);
|
|
else
|
|
Check(guideState.curGroupDefine==null && guideState.curDefine==null,"original non-resuming boundary retained: "+scenario);
|
|
if(scenario!="missing-save")Check(ReferenceEquals(guideState.GuideSaveDataDic[key],entry),"saved progress object retained: "+scenario);
|
|
if(scenario=="query-failure")Check(initWarnings.Any(w=>w.Contains("Potion guide availability")),"query failure is reported and not treated as unavailable");
|
|
}
|
|
finally
|
|
{
|
|
completion.Dispose();Application.logMessageReceived-=onLog;guideState.Dispose();
|
|
guideAsset.guidanceGroupDefines=savedGuideGroups;
|
|
if(initStarted)UnityEngine.AddressableAssets.Addressables.Release(guideAsset);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
guideAsset.guidanceGroupDefines=savedGuideGroups;
|
|
UnityEngine.AddressableAssets.Addressables.Release(guideAssetHandle);
|
|
var runner=runnerField.GetValue(null) as MonoBehaviour;
|
|
if(runner!=null && !ReferenceEquals(runner,savedRunner))UnityEngine.Object.DestroyImmediate(runner.gameObject);
|
|
runnerField.SetValue(null,savedRunner);syncField.SetValue(null,savedSync);threadField.SetValue(null,savedThread);
|
|
}
|
|
await Start("original-tickets-cross-segment-supersession");var primary=Batch();
|
|
Check(collecting==null && rewardAction.Segment==null && rewardAction.Records.Count==5,"three scopes disposed and all original writes captured");
|
|
await ConfirmRewardAsync(rewardAction,primary);int shows=0;PresentReward(rewardAction,()=>shows++);PresentReward(rewardAction,()=>shows++);
|
|
Check(shows==1 && fixture.Remote["Map_5_102"]=="fish" && fixture.Remote["BattlePassRecordTask"]=="reward-task","final values and one presentation");
|
|
|
|
await Start("pending-cannot-present");primary=Batch();fixture.HoldSet=true;
|
|
pending=ConfirmRewardAsync(rewardAction,primary);await fixture.SetEntered.Task;
|
|
Check(!rewardAction.Confirmed && !rewardAction.Presented,"pending is not success");
|
|
fixture.ReleaseSet.TrySetResult(true);await pending;pending=null;Check(rewardAction.Confirmed,"response confirms original action");
|
|
|
|
await Start("localOnly-map-final-rejected");await Reject(ConfirmRewardAsync(rewardAction,Batch(true)),"local map not remote confirmed");
|
|
await Start("outside-segment-before-final-save-rejected");
|
|
using(BeginRewardSegment("strike"))Write("BattlePassRecordTask","strike",true);
|
|
RejectSync(()=>Write("NewPlayerDataDic","external",true),"external changed write denied before later reward can hide it");
|
|
Check(fixture.Sets==0 && ReadRejected(),"stopped facade and no outgoing request");
|
|
|
|
await Start("same-value-external-supersession-still-rejected");primary=Batch();
|
|
Write("NewPlayerDataDic","reward-main",true);
|
|
await Reject(ConfirmRewardAsync(rewardAction,primary),"same-value external ticket is not the original final ticket");
|
|
await Start("unexpected-backed-up-key-before-admission");
|
|
using(BeginRewardSegment("strike"))RejectSync(()=>Write("late","unrelated",true),"backed-up but unrelated reward key rejected");
|
|
Check(rewardAction.Records.Count==0 && fixture.Sets==0,"no unrelated admission");
|
|
|
|
await Start("lost-response-no-reward-replay");primary=Batch();fixture.LoseResponse=true;
|
|
await Reject(ConfirmRewardAsync(rewardAction,primary),"unknown result stops reward");Check(fixture.Sets==1 && !rewardAction.Presented,"single request only");
|
|
await Start("unauthorized-no-reauthentication");primary=Batch();fixture.Unauthorized=true;
|
|
await Reject(ConfirmRewardAsync(rewardAction,primary),"401 stops reward");Check(fixture.Auth==1 && fixture.Sets==1,"no relogin or replay");
|
|
await Start("partial-commit-is-not-success");primary=Batch();fixture.PartialCommit=true;fixture.LoseResponse=true;
|
|
await Reject(ConfirmRewardAsync(rewardAction,primary),"partial result remains failure");Check(fixture.Sets==1 && !rewardAction.Confirmed,"no refund or second write");
|
|
await Start("cancel-after-consume-before-reward-confirmed");primary=Batch();fixture.HoldSet=true;
|
|
pending=ConfirmRewardAsync(rewardAction,primary);await fixture.SetEntered.Task;
|
|
CancelFishing(fishingAction,"actual consumed action cancellation");fixture.ReleaseSet.TrySetResult(true);
|
|
await Reject(pending,"C1 Continued cannot exempt pending reward");pending=null;Check(rewardAction.Cancelled && !rewardAction.Presented,"old callbacks revoked");
|
|
await Start("confirmed-normal-cancel-no-second-exit");primary=Batch();await ConfirmRewardAsync(rewardAction,primary);
|
|
CancelReward(rewardAction.Controller,"normal disable");CancelFishing(fishingAction,"normal actor destroy");
|
|
Check(rewardAction.Cancelled && fishingAction.Cancelled && fixture.Faults==0 && Ready,"confirmed normal disable never faults");
|
|
await Start("battle-controller-disable-still-cancels");
|
|
CancelReward(rewardAction.Controller,"controller disabled");
|
|
Check(rewardAction.Cancelled && !Ready && fixture.Faults==1,"battle controller disable still cancels");
|
|
await Start("ending-teardown-pre-confirm-not-cancel");primary=Batch();
|
|
CancelReward(rewardAction.Controller,"controller disabled");CancelReward(rewardAction.Controller,"controller destroyed");
|
|
Check(!rewardAction.Cancelled && Ready && fixture.Faults==0,"ending teardown window is not a cancellation");
|
|
await ConfirmRewardAsync(rewardAction,primary);shows=0;PresentReward(rewardAction,()=>shows++);
|
|
Check(shows==1 && rewardAction.Confirmed && rewardAction.Presented,"reward completes and presents after teardown window");
|
|
await Start("repeated-segment-and-sealed-action-rejected");
|
|
using(BeginRewardSegment("strike")){}
|
|
RejectSync(()=>BeginRewardSegment("strike"),"strike cannot repeat");
|
|
|
|
await Start("reward-storage-error-before-send");
|
|
var file=Directory.GetFiles(Path.Combine(folder,groups.ToString()),"state.json",SearchOption.AllDirectories).Single();
|
|
using(var locked=new FileStream(file,FileMode.Open,FileAccess.Read,FileShare.None))
|
|
using(BeginRewardSegment("strike"))RejectSync(()=>Write("NewPlayerDataDic","locked",true),"real state-file storage failure");
|
|
Check(fixture.Sets==0 && !Ready,"storage fault before network");
|
|
|
|
foreach(var scenario in new[]{"non-record","new-record","new-fish"})
|
|
{
|
|
await Start("real-fish-final-save-"+scenario);
|
|
var tables=new cfg.Tables(n=>SimpleJSON.JSON.Parse(File.ReadAllText(Path.GetFullPath(Path.Combine(Application.dataPath,"../../cfgData",n+".json")))));
|
|
container=new Container();FishingContextAccess.Use(container);FishingContextAccess.Events=new EventAggregator();container.RegisterInstance(tables);
|
|
player=new PlayerData(tables,null);var fish=new PlayerFishData(tables,null);container.RegisterInstance(player);container.RegisterInstance(fish);
|
|
var core=new PlayerKVCoreState(new object[]{60,100UL,0,393,5,4,30003,new DateTime(2026,1,1),0,112,7UL,2,102,102,13,102,0,0,0});
|
|
player.ReplaceFromKVDecoded(new PlayerKVPlayerState(core,new Dictionary<string,string>(),new Dictionary<int,int>{{-1,1}},new List<int>(),0,false,"0"));
|
|
var data=new Dictionary<int,FishWeightData>{{10241,FishWeightData.Deserialize("2|20|30|40|False")}};
|
|
if(scenario!="new-fish")data[10211]=FishWeightData.Deserialize("0|10|20|20|False");
|
|
var fields=BindingFlags.Instance|BindingFlags.NonPublic;
|
|
typeof(PlayerFishData).GetField("fishData",fields).SetValue(fish,data);
|
|
typeof(PlayerFishData).GetField("mapFishData",fields).SetValue(fish,new Dictionary<int,Dictionary<int,FishWeightData>>{{102,data}});
|
|
pfShell=new GameObject("KV reward unstarted PF fixture");pfShell.SetActive(false);pfShell.hideFlags=HideFlags.HideAndDontSave;
|
|
var pf=pfShell.AddComponent<PlayFabMgr>();Check(pf.IsUnstartedForKVLoad,"PF lifecycle never starts");pfField.SetValue(null,pf);
|
|
rewardAction.BeforeFishCount=scenario=="new-fish"?0:20;
|
|
using(BeginRewardSegment("strike")){}rewardAction.Ending=true;using(BeginRewardSegment("ending")){}
|
|
rewardAction.Applying=true;
|
|
var events=new List<cfg.ConditionType>();using(GContext.OnEvent<ConditionTypeEvent>().Subscribe(e=>events.Add(e.type)))
|
|
using(BeginRewardSegment("reward"))
|
|
{
|
|
fish.SetGetFishCount(10211);fish.SetWeight(10211,scenario=="new-record"?12:5);
|
|
fish.SaveCaughtFishForKVLoop(10211);
|
|
primary=Write("NewPlayerDataDic","fixture-main",true);
|
|
}
|
|
rewardAction.GuideNotifications=1;await ConfirmRewardAsync(rewardAction,primary);
|
|
var savedMap=JsonConvert.DeserializeObject<Dictionary<int,string>>(fixture.Remote["Map_5_102"]);
|
|
var value=FishWeightData.Deserialize(savedMap[10211]);
|
|
Check(value.TotalNum==(scenario=="new-fish"?10:30),"original count increment exactly once");
|
|
Check(value.MaxG==(scenario=="new-record"?12:scenario=="new-fish"?5:10),"original maximum weight rule");
|
|
Check(savedMap[10241]=="2|20|30|40|False" && value.TotalCard==(scenario=="new-fish"?0:20) && value.Level==0,"other fish and card data unchanged");
|
|
Check(events.Count==2 && events.Contains(cfg.ConditionType.GetFishWeight) && events.Contains(cfg.ConditionType.GetFishID),"final save never republishes fish events");
|
|
var fishSchema=new PlayerKVSchemaSnapshot("reward-fish",tables.TbRodData.DataMap.Keys,tables.TbMapData.DataMap.Keys,new Dictionary<string,int>(),System.Globalization.CultureInfo.CurrentCulture,new DateTime(2026,9,24),0,
|
|
fish:tables.TbFishData.DataMap.ToDictionary(p=>p.Key,p=>new PlayerKVFishConfig(p.Key,p.Value.AreaID,p.Value.Quality,p.Value.FishCardID,p.Value.BaseWeight,p.Value.PointExtraParam,p.Value.WeightRandom)),
|
|
cards:tables.TbFishCard.DataMap.ToDictionary(p=>p.Key,p=>new PlayerKVFishCardConfig(p.Key,p.Value.MaxLevel,p.Value.CardsRequiredList,p.Value.WeightMagList)));
|
|
var fishErrors=new PlayerKVSchemaDiagnostics();
|
|
var decodedFish=PlayerKVSaveSchema.DecodeFish(new Dictionary<string,string>{{"Map_5_102",fixture.Remote["Map_5_102"]}},new Dictionary<string,string>(),fishSchema,fishErrors);
|
|
Check(decodedFish!=null && fishErrors.TotalErrors==0 && decodedFish.Maps[102][10211].Number==value.TotalNum && decodedFish.RawMaps[102][10211].Weight==value.MaxG,"real Map schema reads the saved catch");
|
|
}
|
|
foreach(var cash in new[]{0,122})
|
|
{
|
|
await Start("actual-main-reward-and-subscribers-cash-"+cash);
|
|
var tables=new cfg.Tables(n=>SimpleJSON.JSON.Parse(File.ReadAllText(Path.GetFullPath(Path.Combine(Application.dataPath,"../../cfgData",n+".json")))));
|
|
container=new Container();FishingContextAccess.Use(container);FishingContextAccess.Events=new EventAggregator();container.RegisterInstance(tables);
|
|
LocalizationMgr.enumLanguage=tables.TbLanguageConfig.DataList.Select(x=>x.ID).ToList();textTable.SetValue(null,null);
|
|
player=new PlayerData(tables,null);var fish=new PlayerFishData(tables,null);var small=new SmallGameData(tables,null,null);
|
|
var achievements=new AchievementDataManager(tables);var box=new FishingBoxData();var events=new FishingEventData(tables);
|
|
container.RegisterInstance(player);container.RegisterInstance(fish);container.RegisterInstance(small);container.RegisterInstance(achievements);container.RegisterInstance(box);container.RegisterInstance(events);
|
|
fish.ResetForKVLoad();small.ResetForKVLoad();achievements.ResetForKVLoad();box.ResetForKVLoad();achievements.Init();
|
|
var core=new PlayerKVCoreState(new object[]{60,100UL,0,393,5,4,30003,new DateTime(2026,1,1),0,112,7UL,2,102,102,13,102,0,0,0});
|
|
player.ReplaceFromKVDecoded(new PlayerKVPlayerState(core,new Dictionary<string,string>(),new Dictionary<int,int>{{-1,1}},new List<int>(),0,false,"0"));
|
|
subscribers=new RewardSubscribers();subscribers.Bind(tables,events,container);
|
|
Permit.AllowedKeys=Permit.AllowedKeys.Concat(rewardTaskKeys).Distinct().ToArray();
|
|
int countEvents=0;var seen=new List<cfg.ConditionType>();
|
|
using(GContext.OnEvent<ConditionTypeEvent>().Subscribe(e=>{seen.Add(e.type);if(e.type==cfg.ConditionType.GetFishCount)countEvents++;}))
|
|
{
|
|
using(BeginRewardSegment("strike"))GContext.Publish(new ConditionTypeEvent(cfg.ConditionType.PerfectStrikeCount,1));
|
|
rewardAction.Ending=true;using(BeginRewardSegment("ending")){}
|
|
rewardAction.Applying=true;
|
|
using(BeginRewardSegment("reward"))
|
|
{
|
|
// Actual unchanged data methods; visual/formula extraction is tested separately.
|
|
player.SaveFishingFrequency();
|
|
GContext.Publish(new ConditionTypeEvent(cfg.ConditionType.CashFromFishing,cash));
|
|
GContext.Publish(new ConditionTypeEvent(cfg.ConditionType.MaxCashFromFishing,cash/10));
|
|
if(cash>0)player.AddGold((ulong)cash);
|
|
foreach(var condition in new[]{cfg.ConditionType.GetFishWeight,cfg.ConditionType.GetFishID,cfg.ConditionType.GetBoosFish,cfg.ConditionType.GetBiggerThanHugeFishCount})
|
|
GContext.Publish(new ConditionTypeEvent(condition,2));
|
|
Write("Map_5_102",Read("Map_5_102"),true);
|
|
primary=player.SaveFishingRewardForKVLoop();
|
|
}
|
|
}
|
|
rewardAction.GuideNotifications=1;await ConfirmRewardAsync(rewardAction,primary);
|
|
var modern=JsonConvert.DeserializeObject<Dictionary<string,string>>(fixture.Remote["NewPlayerDataDic"]);
|
|
var coreValues=modern["newData"].Split(',');
|
|
Check(coreValues[1]==(100+cash).ToString() && coreValues[14]=="14" && coreValues[15]=="112","cash and one catch with original x10 total frequency");
|
|
Check(coreValues[3]=="393" && coreValues[9]=="112" && countEvents==1,"reward never consumes again or repeats count event");
|
|
Check(modern["ad"]==achievements.AchievementDataToJson() && achievements.GetAchievementDatas(cfg.ConditionType.GetFishCount)==1,"last primary serializes real achievement subscriber");
|
|
Check(modern.ContainsKey("ld") && modern["ld"].Split('|').Length==3,"original luck serialization preserved");
|
|
Check(subscribers.Battle.Data.BattleTask.dic_dailyTasks.Any(k=>tables.TbBattlePassTask.GetOrDefault(k.Key).Type==cfg.ConditionType.PerfectStrikeCount && k.Value==1),"S1 real battle task writes");
|
|
Check(subscribers.Lucky.DicTasks.All(k=>k.Value==0) && subscribers.Magic.DicTasks.All(k=>k.Value==0) && subscribers.Mini.Data.Exp==0,"actual lucky/magic/mini ignore reward conditions absent from their real config");
|
|
foreach(var condition in rewardConditions.Where(x=>x!=cfg.ConditionType.ConsumeEnergy))
|
|
Check(seen.Count(x=>x==condition)==1,"reward condition once: "+condition);
|
|
Check(fixture.Remote.ContainsKey("BattlePassRecordTask") && !fixture.Remote.ContainsKey("MiniBPKeyNew") && !fixture.Remote.ContainsKey("EventLuckGameRecord") && !fixture.Remote.ContainsKey("EventLuckMagicPlayfabData"),"only applicable original task saves confirmed; no invented task updates");
|
|
var schema=new PlayerKVSchemaSnapshot("reward-main",tables.TbRodData.DataMap.Keys,tables.TbMapData.DataMap.Keys,new Dictionary<string,int>(),System.Globalization.CultureInfo.CurrentCulture,new DateTime(2026,9,24),0);
|
|
var errors=new PlayerKVSchemaDiagnostics();Check(PlayerKVSaveSchema.DecodeCore(new Dictionary<string,string>(),modern,schema,errors)!=null && errors.TotalErrors==0,"main schema reads final original serializer");
|
|
}
|
|
await Start("actual-reentry-and-old-permit-gates");
|
|
Check(!BeginRewardDrawing(rewardAction.Controller),"duplicate drawing never reaches original battle");
|
|
rewardAction.Ending=true;Check(!BeginRewardEnding(rewardAction.Controller,true),"duplicate fish end never opens another panel");
|
|
rewardAction.Applying=true;Check(BindRewardPanel(new object())==null,"second panel never reapplies rewards");
|
|
Permit.Version=2;Permit.Operation="fishing-consume-once";RejectSync(()=>BeginRewardDrawing(new object()),"v2 cannot draw");
|
|
await Start("v1-cannot-draw");Permit.Version=1;Permit.Operation=null;RejectSync(()=>BeginRewardDrawing(new object()),"v1 cannot draw");
|
|
await Start("natural-failure-stops-without-reward");
|
|
RejectSync(()=>BeginRewardEnding(rewardAction.Controller,false),"natural failure not converted to catch");
|
|
Check(fixture.Sets==0 && rewardAction.Records.Count==0 && !rewardAction.Applying,"failure has no reward or refund");
|
|
foreach(bool complete in new[]{false,true})
|
|
{
|
|
await Start(complete?"actual-panel-disable-confirmed":"actual-panel-disable-pending");
|
|
primary=Batch();if(complete)await ConfirmRewardAsync(rewardAction,primary);
|
|
var shell=new GameObject("KV inactive reward panel lifecycle fixture");shell.SetActive(false);shell.hideFlags=HideFlags.HideAndDontSave;
|
|
try
|
|
{
|
|
var panel=shell.AddComponent<FishingRewardPanel>();rewardAction.Panel=panel;
|
|
typeof(FishingRewardPanel).GetMethod("OnDisable",BindingFlags.Instance|BindingFlags.NonPublic).Invoke(panel,null);
|
|
Check(rewardAction.Cancelled && fixture.Faults==(complete?0:1),"actual panel OnDisable revokes pending only");
|
|
Check(!RewardPanelCurrent(rewardAction,panel),"old panel continuation is revoked");
|
|
}
|
|
finally{UnityEngine.Object.DestroyImmediate(shell);}
|
|
}
|
|
await Start("real-backup-no-special-fish-guide-preflight");
|
|
container=new Container();FishingContextAccess.Use(container);FishingContextAccess.Events=new EventAggregator();
|
|
var guide=new GuideDataCenter(null,null);container.RegisterInstance(guide);
|
|
var backup=JsonConvert.DeserializeObject<Dictionary<string,object>>(File.ReadAllText(Path.GetFullPath(Path.Combine(Application.dataPath,"../../CodexOutput/upserver/KVGameplayLoopTests/backups/20260924T084836Z/102.json"))));
|
|
var raw=JsonConvert.DeserializeObject<Dictionary<string,string>>(backup["data"].ToString());
|
|
var guideErrors=new PlayerKVSchemaDiagnostics();guide.ReplaceFromKVDecoded(GuideDataCenter.DecodeForKVLoad(raw,guideErrors));
|
|
guide.guidanceGroupDefines=UnityEditor.AssetDatabase.LoadAssetAtPath<GuidanceDefineSetting>("Assets/ABPackage/pkgPrepare/Export/GuidanceConfig.asset").guidanceGroupDefines;
|
|
Check(guideErrors.TotalErrors==0,"real guide backup decodes");
|
|
var gateTables=new cfg.Tables(n=>SimpleJSON.JSON.Parse(File.ReadAllText(Path.GetFullPath(Path.Combine(Application.dataPath,"../../cfgData",n+".json")))));
|
|
container.RegisterInstance(gateTables);
|
|
rewardPlan.Data=new FishingData{curFishData=gateTables.TbFishData.GetOrDefault(10211)};
|
|
var guideBefore=JsonConvert.SerializeObject(guide.GuideSaveDataDic);
|
|
CheckRewardGuide();
|
|
Check(Ready && fixture.Sets==0 && guideBefore==JsonConvert.SerializeObject(guide.GuideSaveDataDic),"ordinary fish admitted without guide writes or consumption");
|
|
var originalGroups=guide.guidanceGroupDefines;
|
|
foreach(var kind in new[]{"ordinary-incomplete","active-node","active-group","last-node","extra-node","missing-node","trigger","parameter","unknown-group","positive-skill","last-skill","escape-skill","active-skill","active-dash","active-dirty"})
|
|
{
|
|
// Read-only fault probes share an isolated fixture; no real run is reset.
|
|
var guideCopy=JsonUtility.FromJson<GuidanceGroupDefine>(JsonUtility.ToJson(originalGroups.First(g=>g.groupName.ToString()=="FishSkill01")));
|
|
var baseFish=gateTables.TbFishData.GetOrDefault(10211);
|
|
var fishJson=SimpleJSON.JSON.Parse(baseFish.json.ToString());
|
|
var testFish=new cfg.FishData(fishJson);
|
|
var testData=new FishingData{curFishData=testFish};
|
|
guide.guidanceGroupDefines=originalGroups.Select(g=>g.groupName==guideCopy.groupName?guideCopy:g).ToArray();
|
|
guide.curDefine=null;guide.curGroupDefine=null;
|
|
guide.GuideSaveDataDic["Fish_01"].isFinish=true;
|
|
switch(kind)
|
|
{
|
|
case "ordinary-incomplete":guide.GuideSaveDataDic["Fish_01"].isFinish=false;break;
|
|
case "active-node":guide.curDefine=guideCopy.guidanceDefines[0];break;
|
|
case "active-group":guide.curGroupDefine=guideCopy;break;
|
|
case "last-node":guideCopy.guidanceDefines.Last().panelName="FishingRewardPanel";break;
|
|
case "extra-node":guideCopy.guidanceDefines=guideCopy.guidanceDefines.Concat(new[]{new GuidanceDefine{panelName="FishingPanel_Advanced"}}).ToArray();break;
|
|
case "missing-node":guideCopy.guidanceDefines=Array.Empty<GuidanceDefine>();break;
|
|
case "trigger":guideCopy.TriggerConditionsType=GuideTriggerConditions.Next;break;
|
|
case "parameter":guideCopy.TriggerConditionsParameter="changed";break;
|
|
case "unknown-group":guideCopy.groupName=(GroupName)999999;break;
|
|
case "positive-skill":case "last-skill":case "escape-skill":
|
|
var skillJson=SimpleJSON.JSON.Parse(baseFish.FishEscape.json.ToString());skillJson["SpSkillID"]=123456789;
|
|
if(kind=="escape-skill")fishJson["FishEscape"]=skillJson;
|
|
else {var list=new SimpleJSON.JSONArray();if(kind=="last-skill")list.Add(SimpleJSON.JSON.Parse(baseFish.FishEscape.json.ToString()));list.Add(skillJson);fishJson["FishSKillList"]=list;}
|
|
testData.curFishData=new cfg.FishData(fishJson);break;
|
|
case "active-skill":testData.fishSpSkillData=new CorruptSpSkillData();break;
|
|
case "active-dash":testData.superDashSpSkillData=new SuperDashSpSkillData();break;
|
|
case "active-dirty":testData.dirtyStuckSpSkillData=new DirtyStuckSpSkillData();break;
|
|
}
|
|
// Stop() is idempotent; all cases are read-only gate probes, not fresh consumption attempts.
|
|
RejectSync(()=>CheckRewardGuide(testData),"guide/skill drift denied: "+kind);
|
|
Check(fixture.Sets==0,"gate drift never writes: "+kind);
|
|
}
|
|
guide.guidanceGroupDefines=originalGroups;guide.curDefine=null;guide.curGroupDefine=null;guide.GuideSaveDataDic["Fish_01"].isFinish=true;
|
|
|
|
foreach(var scenario in new[]{"ordinary","zero-cruise","cruise","invalid-cruise","free","retry","fixed","duel","special-item","missing-map","wrong-area","event-field","event-count","trigger-pack","activity-drift","guide-before-S1","guide-before-S2","guide-before-S3"})
|
|
{
|
|
await Start("actual-full-reward-preflight-"+scenario);
|
|
var tables=new cfg.Tables(n=>SimpleJSON.JSON.Parse(File.ReadAllText(Path.GetFullPath(Path.Combine(Application.dataPath,"../../cfgData",n+".json")))));
|
|
container=new Container();FishingContextAccess.Use(container);FishingContextAccess.Events=new EventAggregator();container.RegisterInstance(tables);
|
|
player=new PlayerData(tables,null);var fish=new PlayerFishData(tables,null);fish.ResetForKVLoad();
|
|
var events=new FishingEventData(tables);var guideState=new GuideDataCenter(tables,player);
|
|
var diag=new PlayerKVSchemaDiagnostics();guideState.ReplaceFromKVDecoded(GuideDataCenter.DecodeForKVLoad(raw,diag));guideState.guidanceGroupDefines=originalGroups;
|
|
guideState.guidanceDefineSetting=UnityEditor.AssetDatabase.LoadAssetAtPath<GuidanceDefineSetting>("Assets/ABPackage/pkgPrepare/Export/GuidanceConfig.asset");
|
|
var scopes=new Activity.ActivityScopeManager();var manager=new Activity.ActivityManager(tables,scopes);
|
|
container.RegisterInstance(player);container.RegisterInstance(fish);container.RegisterInstance(events);container.RegisterInstance(guideState);
|
|
container.RegisterInstance(new CollectingData());container.RegisterInstance(new PiggyBankPackData());container.RegisterInstance(new BargainPackData());container.RegisterInstance(new FishingChallengeManager());
|
|
container.RegisterInstance(scopes);container.RegisterInstance(manager);
|
|
player.ReplaceFromKVDecoded(new PlayerKVPlayerState(new PlayerKVCoreState(new object[]{60,100UL,0,393,5,4,30003,new DateTime(2026,1,1),0,112,7UL,2,102,102,13,102,0,0,0}),new Dictionary<string,string>(),new Dictionary<int,int>{{-1,1}},new List<int>(),0,false,"0"));
|
|
player.IsOpenMagnification=true;fish.IsOpenPiercing=true;
|
|
var data=new FishingData{curFishData=tables.TbFishData.GetOrDefault(10211),fishItem=tables.TbItem.DataList.First(x=>x.RedirectID==10211 && x.SubType==1),RodId=30003,MapId=102};
|
|
rewardPlan=null;rewardAction=null;
|
|
switch(scenario)
|
|
{
|
|
case "zero-cruise":case "cruise":case "invalid-cruise":
|
|
typeof(PlayerFishData).GetField("fishSwimToMap",BindingFlags.Instance|BindingFlags.NonPublic).SetValue(fish,new Dictionary<int,int>{{102,scenario=="zero-cruise"?0:scenario=="cruise"?10211:-2}});break;
|
|
case "free":data.IsFree=true;break;case "retry":data.IsRetry=true;break;case "duel":data.IsDuel=true;break;
|
|
case "fixed":data.fixedFishList=tables.TbFixedFishList.DataList.First();break;
|
|
case "special-item":data.fishItem=tables.TbItem.DataList.First(x=>x.SubType==11);break;
|
|
case "missing-map":Permit.AllowedKeys=new[]{"NewPlayerDataDic"};break;
|
|
case "wrong-area":var fj=SimpleJSON.JSON.Parse(data.curFishData.json.ToString());fj["AreaID"]=999999;data.curFishData=new cfg.FishData(fj);break;
|
|
case "event-field":var ev=tables.TbFishingEvent.DataList.First(x=>x.ID==10100000);typeof(cfg.FishingEvent).GetProperty("PopupPriority").SetValue(ev,ev.PopupPriority+1);break;
|
|
case "event-count":tables.TbFishingEvent.DataList.Remove(tables.TbFishingEvent.DataList.First(x=>x.ID==10100000));break;
|
|
case "trigger-pack":typeof(cfg.TriggerPackManager).GetProperty("Condition").SetValue(tables.TbTriggerPackManager.DataList.First(),cfg.ConditionType.GetFishCount);break;
|
|
}
|
|
if(scenario=="ordinary" || scenario=="zero-cruise" || scenario=="activity-drift" || scenario.StartsWith("guide-before-"))
|
|
{
|
|
PrepareFishingReward(fishingAction,data);
|
|
Check(Ready && rewardPlan.Data==data && fixture.Sets==0,"actual full preflight admitted before any consumption");
|
|
if(scenario=="activity-drift")
|
|
{scopes.CreateActivityScope((1,1));RejectSync(CheckRewardContext,"actual activity scope drift denied");}
|
|
else if(scenario.StartsWith("guide-before-"))
|
|
{
|
|
var actor=new object();
|
|
if(scenario!="guide-before-S1") {Check(BeginRewardDrawing(actor),"first drawing");using(BeginRewardSegment("strike")){} }
|
|
if(scenario=="guide-before-S3") {Check(BeginRewardEnding(actor,true),"first ending");using(BeginRewardSegment("ending")){} }
|
|
guideState.curDefine=originalGroups.First().guidanceDefines.First();
|
|
if(scenario=="guide-before-S1")RejectSync(()=>BeginRewardDrawing(actor),"active guide before drawing");
|
|
else if(scenario=="guide-before-S2")RejectSync(()=>BeginRewardEnding(actor,true),"active guide before ending");
|
|
else RejectSync(()=>BindRewardPanel(new object()),"active guide before reward data");
|
|
}
|
|
else
|
|
{
|
|
var actor=new object();Check(BeginRewardDrawing(actor),"actual prepare-draw");using(BeginRewardSegment("strike")){}
|
|
Check(BeginRewardEnding(actor,true),"actual ending");using(BeginRewardSegment("ending")){}
|
|
Check(BindRewardPanel(new object())!=null,"actual panel binding with before count");
|
|
using(GContext.OnEvent<OnEventTriggerGuide>().Subscribe(guideState.TriggerGuideNext))
|
|
using(BeginRewardSegment("reward")){GContext.Publish(new OnEventTriggerGuide("FishingRewardPanel"));RecordRewardGuideNotification();}
|
|
Check(rewardAction.GuideNotifications==1 && guideState.curDefine==null && guideState.curGroupDefine==null,"one original guide notification and no activated guide");
|
|
}
|
|
}
|
|
else RejectSync(()=>PrepareFishingReward(fishingAction,data),"full gate rejects "+scenario);
|
|
Check(fixture.Sets==0,"preflight case no network or consumed energy: "+scenario);
|
|
}
|
|
await Start("reward-confirmation-timeout-no-replay");primary=Batch();fixture.HoldSet=true;
|
|
pending=ConfirmRewardAsync(rewardAction,primary);await fixture.SetEntered.Task;
|
|
await Reject(pending,"actual 30-second timeout");pending=null;fixture.ReleaseSet.TrySetResult(true);
|
|
Check(fixture.Sets==1 && !rewardAction.Confirmed && !rewardAction.Presented,"timeout never repeats reward");
|
|
result=JsonConvert.SerializeObject(new{success=true,groups,assertions=checks,passed,formalRequests=0,synthetic=true,folder});
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
subscribers?.Dispose();subscribers=null;
|
|
LocalizationMgr.enumLanguage=oldLanguages;textTable.SetValue(null,oldTextTable);
|
|
player?.StopRecoveryEnergy();if(fixture!=null)await fixture.Close();if(pending!=null){try{await pending;}catch{}}
|
|
pfField.SetValue(null,oldPF);if(pfShell!=null)UnityEngine.Object.DestroyImmediate(pfShell);
|
|
FishingContextAccess.Use(oldContainer);FishingContextAccess.Events=oldEvents;container?.Dispose();
|
|
(Enabled,Ready,Error,Permit,RunPath,handle,onFault,onClose,collecting,mainThread,failed,mainContext,fishingAction,rewardPlan,rewardAction)=saved;
|
|
}
|
|
catch {validationCleanupFailure=fixture;throw;}
|
|
finally
|
|
{
|
|
AppDomain.CurrentDomain.FirstChanceException-=observeIO;
|
|
if(Directory.Exists(folder))File.WriteAllText(Path.Combine(folder,"storage-diagnostics.json"),JsonConvert.SerializeObject(ioDiagnostics.ToArray()));
|
|
Interlocked.Exchange(ref validationRunning,0);
|
|
}
|
|
}
|
|
File.WriteAllText(Path.Combine(folder,"result.json"),result);return result;
|
|
}
|
|
sealed class RewardSubscribers:IDisposable
|
|
{
|
|
const System.Reflection.BindingFlags Fields=System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic;
|
|
static readonly System.Reflection.FieldInfo PF=typeof(PlayFabMgr).GetField("_instance",System.Reflection.BindingFlags.Static|System.Reflection.BindingFlags.NonPublic);
|
|
static readonly System.Reflection.FieldInfo Red=typeof(RedPointManager).GetField("instance",System.Reflection.BindingFlags.Static|System.Reflection.BindingFlags.NonPublic);
|
|
readonly object oldRed=Red.GetValue(null),oldPF=PF.GetValue(null);
|
|
GameObject shell; LuckyTaskData magicTask; MiniBattlePassDataProvider provider;
|
|
internal BattlePassDataProvider Battle; internal EventLuckyGameModel Lucky;
|
|
internal EventLuckMagicModel Magic; internal MiniBattlePassDataModel Mini;
|
|
internal void Bind(cfg.Tables tables,FishingEventData events,Container container)
|
|
{
|
|
Red.SetValue(null,new RedPointManager());
|
|
shell=new GameObject("KV inactive subscriber storage fixture");shell.SetActive(false);shell.hideFlags=HideFlags.HideAndDontSave;
|
|
var pf=shell.AddComponent<PlayFabMgr>();
|
|
if(!pf.IsUnstartedForKVLoad)throw new InvalidOperationException("No PF lifecycle may start in this fixture.");
|
|
PF.SetValue(null,pf);
|
|
var items=new PlayerItemData(tables);container.RegisterInstance(items);
|
|
var data=new BattlePassData(tables);
|
|
data.BattlePassRecord.ID=tables.TbFishingEvent.DataList.First(e=>tables.TbBattlePassMain.GetOrDefault(e.RedirectID)!=null).ID;
|
|
data.BattlePassRecord.Level=1;
|
|
foreach(var t in tables.TbBattlePassTask.DataList.Where(t=>rewardConditions.Contains(t.Type)))data.BattleTask.dic_dailyTasks.Add(t.TaskID,0);
|
|
Battle=new BattlePassDataProvider(tables,data,items);container.RegisterInstance(Battle);
|
|
var common=tables.TbCommonTask.DataList.Where(t=>rewardConditions.Contains(t.TaskType)).Select(t=>t.Id).ToArray();
|
|
Lucky=new EventLuckyGameModel(events,tables);foreach(var id in common)Lucky.DicTasks.Add(id,0);
|
|
Magic=new EventLuckMagicModel{DicTasks=common.ToDictionary(x=>x,x=>0),HigherRingData=EventLuckMagicHigherRingData.CreateEmpty()};
|
|
var rings=(List<EventLuckMagicRing>)typeof(EventLuckMagicModel).GetField("_rings",Fields).GetValue(Magic);
|
|
for(int i=1;i<=3;i++)rings.Add(EventLuckMagicRing.CreateEmpty(1,0,(EEventLuckMagicRingType)i));
|
|
magicTask=new LuckyTaskData(Magic);typeof(EventLuckMagicModel).GetField("_taskData",Fields).SetValue(Magic,magicTask);
|
|
provider=new MiniBattlePassDataProvider(tables,items);
|
|
var table=tables.TbMiniBattlePass.DataList.First(t=>t.TaskType==cfg.ConditionType.ConsumeEnergy && t.Param==0);
|
|
var activity=tables.TbFishingEvent.DataList.First(t=>t.Type==9 && t.RedirectID==table.Id);
|
|
var type=(MiniBattlePassType)table.Type;
|
|
Mini=provider.GetModel(type);
|
|
provider.GetMiniBattlePassRecord("BP_"+type).Id=activity.ID;
|
|
typeof(MiniBattlePassDataModel).GetField("fishingEvent",Fields).SetValue(Mini,activity);
|
|
typeof(MiniBattlePassDataModel).GetMethod("InitData",Fields).Invoke(Mini,new object[]{table.Id});
|
|
// Synthetic fixture has an active window; real-run time is never modified.
|
|
Mini.startTime=ZZTimeHelper.UtcNow().AddDays(-1);
|
|
typeof(MiniBattlePassDataModel).GetProperty("endTime").SetValue(Mini,ZZTimeHelper.UtcNow().AddDays(1));
|
|
}
|
|
public void Dispose()
|
|
{
|
|
Battle?.Dispose();Lucky?.Dispose();magicTask?.Dispose();
|
|
if(Mini!=null)(typeof(MiniBattlePassDataModel).GetField("disposables",Fields).GetValue(Mini) as IDisposable)?.Dispose();
|
|
PF.SetValue(null,oldPF);Red.SetValue(null,oldRed);
|
|
if(shell!=null)UnityEngine.Object.DestroyImmediate(shell);
|
|
}
|
|
}
|
|
static bool ReadRejected() {try{Read("NewPlayerDataDic");return false;}catch(InvalidOperationException){return true;}}
|
|
}
|
|
}
|
|
#endif
|