Files
ft/Client/Assets/Scripts/Services/UserService.KVFishingRewardValidation.cs

1227 lines
71 KiB
C#

#if UNITY_EDITOR
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
{
// Fixed original data-method fragments; does not execute the reward panel or formulas.
internal static async Task<string> VerifyOriginalFishingRewardForEditorAsync()
{
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 oldContainer = GContext.container;
var oldEvents = FishingContextAccess.Events;
var oldLanguages = LocalizationMgr.enumLanguage;
var textTable = typeof(LocalizationMgr).GetField("table", BindingFlags.Static | BindingFlags.NonPublic);
var oldTextTable = textTable.GetValue(null);
var folder = Path.Combine(Root, "reward-original-fixed-runs", DateTime.UtcNow.ToString("yyyyMMddTHHmmss") + "_" + Guid.NewGuid().ToString("N"));
var passed = new List<string>();
int checks = 0;
void Check(bool value, string message)
{
checks++;
if (!value)
{
throw new InvalidOperationException("Original reward data: " + message);
}
}
try
{
foreach (var scenario in new[] { "fish-non-record", "fish-new-record", "fish-new-fish", "main-cash-0", "main-cash-122" })
{
var errors = new List<Exception>();
var fixture = new EquipmentFixture(Path.Combine(folder, scenario));
KVPlayerSaveStore store = null;
Container container = null;
PlayerData player = null;
AchievementDataManager achievements = null;
RewardSubscribers subscribers = null;
IDisposable subscription = null;
var unexpectedLogs = new List<string>();
void Clean(Action action)
{
try
{
action();
}
catch (Exception error)
{
errors.Add(error);
validationCleanupFailure = fixture;
}
}
void Log(string message, string stack, LogType type)
{
if (type == LogType.Error || type == LogType.Exception || type == LogType.Assert)
{
unexpectedLogs.Add(message + "\n" + stack);
}
}
Application.logMessageReceived += Log;
try
{
var initialMap = new Dictionary<int, string> { { 10241, "2|20|30|40|False" } };
if (scenario != "fish-new-fish")
{
initialMap[10211] = "0|10|20|20|False";
}
string initialMapJson = JsonConvert.SerializeObject(initialMap);
fixture.Remote["Map_5_102"] = initialMapJson;
var owner = await fixture.Open();
store = new KVPlayerSaveStore();
store.Bind(owner, CancellationToken.None);
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);
container.RegisterInstance<IPlayerSaveStore>(store);
player = new PlayerData(tables, null);
var fish = new PlayerFishData(tables, null);
var small = new SmallGameData(tables, null, null);
achievements = new AchievementDataManager(tables);
var events = new FishingEventData(tables);
container.RegisterInstance(player);
container.RegisterInstance(fish);
container.RegisterInstance(small);
container.RegisterInstance(achievements);
container.RegisterInstance(new FishingBoxData());
container.RegisterInstance(events);
player.playerSaveStore = store;
fish.playerSaveStore = store;
small.playerSaveStore = store;
events.playerSaveStore = store;
achievements.Init();
LocalizationMgr.enumLanguage = tables.TbLanguageConfig.DataList.Select(x => x.ID).ToList();
textTable.SetValue(null, null);
Module02OriginalLoadTests.LoadCore(player, 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 });
var seen = new List<cfg.ConditionType>();
subscription = GContext.OnEvent<ConditionTypeEvent>().Subscribe(e => seen.Add(e.type));
if (scenario.StartsWith("fish-", StringComparison.Ordinal))
{
var data = initialMap.ToDictionary(pair => pair.Key, pair => FishWeightData.Deserialize(pair.Value));
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 } });
fish.SetGetFishCount(10211);
fish.SetWeight(10211, scenario == "fish-new-record" ? 12 : 5);
var actual = data[10211];
Check(actual.TotalNum == (scenario == "fish-new-fish" ? 10 : 30), "original in-memory catch count");
Check(actual.MaxG == (scenario == "fish-new-record" ? 12 : scenario == "fish-new-fish" ? 5 : 10), "original maximum weight rule");
Check(actual.TotalCard == (scenario == "fish-new-fish" ? 0 : 20) && actual.Level == 0 && data[10241].Serialize() == "2|20|30|40|False", "original other fish and card data");
Check(seen.SequenceEqual(new[] { cfg.ConditionType.GetFishWeight, cfg.ConditionType.GetFishID }), "two original fish events without final-save replay");
string expectedMap = store.Read("Map_5_102").ValueOrThrow();
Check(fixture.Sets == 0 && fixture.Remote["Map_5_102"] == initialMapJson, "fish data waits for ordinary save");
Check(scenario == "fish-non-record" ? expectedMap == initialMapJson : expectedMap != initialMapJson, "original non-record does not force a map write");
var flushed = await KVMgr.SyncKVAsync(owner);
Check(flushed.TransportError == PlayerKVError.None && flushed.Blocked == 0 && flushed.Unknown == 0 && fixture.Sets == (scenario == "fish-non-record" ? 0 : 1), "original map ordinary save result");
Check(fixture.Remote["Map_5_102"] == expectedMap && fixture.Remote["NewPlayerDataDic"] == "original-save", "exact original map payload without invented main write");
var savedMap = JsonConvert.DeserializeObject<Dictionary<int, string>>(expectedMap);
var savedFish = FishWeightData.Deserialize(savedMap[10211]);
Check(savedFish.TotalNum == (scenario == "fish-non-record" ? 20 : scenario == "fish-new-fish" ? 10 : 30), "persisted count follows original save timing");
Check(savedMap[10241] == "2|20|30|40|False" && savedFish.TotalCard == actual.TotalCard && savedFish.Level == actual.Level && savedFish.NewFish == actual.NewFish, "saved fish fields and neighbor preserved");
var reloaded = new PlayerFishData(tables, null);
reloaded.InitData(new Dictionary<string, string> { { "Map_5_102", expectedMap } });
var reloadedMap = Module02OriginalLoadTests.Field<Dictionary<int, Dictionary<int, FishWeightData>>>(reloaded, "mapFishData");
var config = tables.TbFishData[10211];
float cap = Mathf.Round(config.BaseWeight * config.PointExtraParam * (1 + config.WeightRandom) * 100) / 100;
float expectedWeight = config.Quality <= 5 ? Mathf.Min(savedFish.MaxG, cap) : savedFish.MaxG;
Check(reloadedMap[102][10211].TotalNum == savedFish.TotalNum && reloadedMap[102][10211].MaxG == expectedWeight && reloadedMap[102][10211].TotalCard == savedFish.TotalCard && reloadedMap[102][10211].NewFish == savedFish.NewFish, "original fish parser and original maximum-weight correction");
}
else
{
int cash = scenario == "main-cash-122" ? 122 : 0;
subscribers = new RewardSubscribers();
subscribers.Bind(tables, events, container, store);
GContext.Publish(new ConditionTypeEvent(cfg.ConditionType.PerfectStrikeCount, 1));
player.SaveFishingFrequency();
string expectedMain = store.Read("NewPlayerDataDic").ValueOrThrow();
string expectedAchievements = achievements.AchievementDataToJson();
GContext.Publish(new ConditionTypeEvent(cfg.ConditionType.CashFromFishing, cash));
GContext.Publish(new ConditionTypeEvent(cfg.ConditionType.MaxCashFromFishing, cash / 10));
if (cash > 0)
{
player.AddGold((ulong)cash);
expectedMain = store.Read("NewPlayerDataDic").ValueOrThrow();
expectedAchievements = achievements.AchievementDataToJson();
}
foreach (var condition in new[] { cfg.ConditionType.GetFishWeight, cfg.ConditionType.GetFishID, cfg.ConditionType.GetBoosFish, cfg.ConditionType.GetBiggerThanHugeFishCount })
{
GContext.Publish(new ConditionTypeEvent(condition, 2));
}
Check(store.Read("NewPlayerDataDic").ValueOrThrow() == expectedMain, "later original events do not invent a final main save");
var modern = JsonConvert.DeserializeObject<Dictionary<string, string>>(expectedMain);
var core = modern["newData"].Split(',');
Check(core[1] == (100 + cash).ToString() && core[14] == "14" && core[15] == "112", "original cash and frequency snapshot");
Check(core[3] == "393" && core[9] == "112" && seen.Count(x => x == cfg.ConditionType.GetFishCount) == 1, "reward fragment never consumes energy twice");
Check(modern["ad"] == expectedAchievements && achievements.GetAchievementDatas(cfg.ConditionType.GetFishCount) == 1, "achievement snapshot follows last original save");
Check(modern["ld"].Split('|').Length == 3, "original luck serialization preserved");
Check(subscribers.Battle.Data.BattleTask.dic_dailyTasks.Any(pair => tables.TbBattlePassTask.GetOrDefault(pair.Key).Type == cfg.ConditionType.PerfectStrikeCount && pair.Value == 1), "actual battle task subscription");
Check(subscribers.Lucky.DicTasks.All(pair => pair.Value == 0) && subscribers.Magic.DicTasks.All(pair => pair.Value == 0) && subscribers.Mini.Data.Exp == 0, "unrelated actual task configuration stays unchanged");
foreach (var condition in rewardConditions.Where(x => x != cfg.ConditionType.ConsumeEnergy))
{
Check(seen.Count(x => x == condition) == 1, "original fragment event once: " + condition);
}
string expectedTask = store.Read("BattlePassRecordTask").ValueOrThrow();
Check(!string.IsNullOrEmpty(expectedTask) && new[] { "MiniBPKeyNew", "EventLuckGameRecord", "EventLuckMagicPlayfabData" }.All(key => store.Read(key).Success && store.Read(key).Found == false), "only applicable original task writes exist");
Check(fixture.Sets == 0 && fixture.Remote["NewPlayerDataDic"] == "original-save", "original reward data uses ordinary save timing");
var flushed = await KVMgr.SyncKVAsync(owner);
Check(flushed.TransportError == PlayerKVError.None && flushed.Blocked == 0 && flushed.Unknown == 0 && fixture.Sets == 1, "ordinary reward data save succeeds once");
Check(fixture.Remote["NewPlayerDataDic"] == expectedMain && fixture.Remote["BattlePassRecordTask"] == expectedTask && fixture.Remote["Map_5_102"] == initialMapJson, "exact actual main and task payloads without forced fish write");
var reloaded = new PlayerData(tables, null);
Module02OriginalLoadTests.LoadCore(reloaded, JsonConvert.DeserializeObject<Dictionary<string, string>>(fixture.Remote["NewPlayerDataDic"]));
Check(reloaded.Energy == player.Energy && (ulong)reloaded.gold == (ulong)player.gold && reloaded.equipRodID == player.equipRodID, "original main parser reads last original save");
}
Check(fixture.Auth == 1 && !Enabled && !Ready && handle == null, "isolated data methods never enable temporary gameplay");
}
catch (Exception error)
{
errors.Add(error);
}
finally
{
Clean(() => subscription?.Dispose());
Clean(() => subscribers?.Dispose());
Clean(() => achievements?.Dispose());
Clean(() => player?.StopRecoveryEnergy());
Clean(() => store?.Dispose());
try
{
await fixture.Close();
}
catch (Exception error)
{
errors.Add(error);
validationCleanupFailure = fixture;
}
Clean(() => container?.Dispose());
Clean(() => FishingContextAccess.Use(oldContainer));
Clean(() => FishingContextAccess.Events = oldEvents);
Clean(() => LocalizationMgr.enumLanguage = oldLanguages);
Clean(() => textTable.SetValue(null, oldTextTable));
Clean(() => Check(ReferenceEquals(GContext.container, oldContainer) && ReferenceEquals(FishingContextAccess.Events, oldEvents) && ReferenceEquals(LocalizationMgr.enumLanguage, oldLanguages) && ReferenceEquals(textTable.GetValue(null), oldTextTable), "original fixture globals restored"));
Application.logMessageReceived -= Log;
if (unexpectedLogs.Count != 0)
{
errors.Add(new InvalidOperationException(string.Join("\n", unexpectedLogs)));
}
}
if (errors.Count != 0)
{
Directory.CreateDirectory(folder);
File.WriteAllText(Path.Combine(folder, "failure.json"), JsonConvert.SerializeObject(new { scenario, errors = errors.Select(x => x.ToString()), formalRequests = 0 }));
throw new AggregateException(errors);
}
passed.Add(scenario);
}
}
finally
{
Interlocked.Exchange(ref validationRunning, 0);
}
string result = JsonConvert.SerializeObject(new { success = true, groups = passed.Count, assertions = checks, passed, coverage = "original-data-fragments-only", formalRequests = 0, synthetic = true, restored = true, folder });
Directory.CreateDirectory(folder);
File.WriteAllText(Path.Combine(folder, "result.json"), result);
return result;
}
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, 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;
var oldLanguages = LocalizationMgr.enumLanguage;
var textTable = typeof(LocalizationMgr).GetField("table", BindingFlags.Static | BindingFlags.NonPublic);
var oldTextTable = textTable.GetValue(null);
KVPlayerSaveStore guideStore = 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)
{
player?.StopRecoveryEnergy();
player = null;
Exception guideDisposeError = null;
try
{
guideStore?.Dispose();
}
catch (Exception error)
{
guideDisposeError = error;
}
finally
{
guideStore = null;
}
if (fixture != null)
{
await fixture.Close();
}
if (pending != null)
{
try
{
await pending;
}
catch
{
}
pending = null;
}
FishingContextAccess.Use(oldContainer);
FishingContextAccess.Events = oldEvents;
container?.Dispose();
container = null;
if (guideDisposeError != null)
{
throw guideDisposeError;
}
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++);
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);
Module02OriginalLoadTests.LoadGuide(guideState, potionSource);
Check(guideState.GuideSaveDataDic.Count > 0, "real 101 guide source loaded through original parser");
var potionGroup = JsonUtility.FromJson<GuidanceGroupDefine>(JsonUtility.ToJson(savedGuideGroups.First(g => g.groupName == GroupName.EventPotion01)));
Check(potionGroup.guidanceDefines[4].isKey && potionGroup.guidanceDefines[4].JumpIndex == 4, "original recovery asset boundary: " + scenario);
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()];
}
guideStore = new KVPlayerSaveStore();
guideStore.Bind(handle, CancellationToken.None);
var store = guideStore;
container.RegisterInstance<IPlayerSaveStore>(store);
player.playerSaveStore = store;
eventData.playerSaveStore = store;
guideState.playerSaveStore = store;
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" || scenario == "available" || scenario == "query-failure" ||
scenario == "node-panel-mismatch" || scenario == "other-group" || scenario == "preserve-other-pointer")
{
Check(ReferenceEquals(guideState.curGroupDefine, potionGroup) &&
ReferenceEquals(guideState.curDefine, potionGroup.guidanceDefines[4]) &&
ReferenceEquals(guideState.curSaveData, entry) && entry.guideSubIndex == 4 && !entry.isFinish,
"original Init resumes saved key independently of availability or old pointer: " + scenario);
Check(handle.ReadLocalSnapshot().Data.TryGetValue("GuideSaveDataNew", out var savedGuide) &&
savedGuide.Split(';').Contains(key + "|4|0"), "original Init accepts resumed guide ordinary write: " + scenario);
Check(fixture.Sets == 0 && completeEvents == 0, "original recovery neither forces HTTP nor completes guide: " + 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")), "original Init does not query potion availability");
}
}
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");
_ = KVMgr.SyncKVAsync(rewardAction.Consumption.Owner); // One simulated ordinary timer tick.
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;
_ = KVMgr.SyncKVAsync(rewardAction.Consumption.Owner); // One simulated ordinary timer tick.
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);
_ = KVMgr.SyncKVAsync(rewardAction.Consumption.Owner); // One simulated ordinary timer tick.
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;
_ = KVMgr.SyncKVAsync(rewardAction.Consumption.Owner); // One simulated ordinary timer tick.
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;
_ = KVMgr.SyncKVAsync(rewardAction.Consumption.Owner); // One simulated ordinary timer tick.
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;
_ = KVMgr.SyncKVAsync(rewardAction.Consumption.Owner); // One simulated ordinary timer tick.
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;
_ = KVMgr.SyncKVAsync(rewardAction.Consumption.Owner); // One simulated ordinary timer tick.
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();
_ = KVMgr.SyncKVAsync(rewardAction.Consumption.Owner); // One simulated ordinary timer tick.
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");
_ = KVMgr.SyncKVAsync(rewardAction.Consumption.Owner); // One simulated ordinary timer tick.
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");
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");
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());
Module02OriginalLoadTests.LoadGuide(guide, raw);
guide.guidanceGroupDefines = UnityEditor.AssetDatabase.LoadAssetAtPath<GuidanceDefineSetting>("Assets/ABPackage/pkgPrepare/Export/GuidanceConfig.asset").guidanceGroupDefines;
Check(guide.GuideSaveDataDic.Count > 0, "real guide backup loads through original parser");
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);
var events = new FishingEventData(tables);
var guideState = new GuideDataCenter(tables, player);
Module02OriginalLoadTests.LoadGuide(guideState, raw);
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(new SmallGameData(tables, null, null));
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);
Module02OriginalLoadTests.LoadCore(player, 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.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;
_ = KVMgr.SyncKVAsync(rewardAction.Consumption.Owner); // One simulated ordinary timer tick.
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
{
LocalizationMgr.enumLanguage = oldLanguages;
textTable.SetValue(null, oldTextTable);
player?.StopRecoveryEnergy();
Exception guideDisposeError = null;
try
{
guideStore?.Dispose();
}
catch (Exception error)
{
guideDisposeError = error;
}
finally
{
guideStore = null;
}
if (fixture != null)
{
await fixture.Close();
}
if (pending != null)
{
try
{
await pending;
}
catch
{
}
}
FishingContextAccess.Use(oldContainer);
FishingContextAccess.Events = oldEvents;
container?.Dispose();
(Enabled, Ready, Error, Permit, RunPath, handle, onFault, collecting, mainThread, failed, mainContext, fishingAction, rewardPlan, rewardAction) = saved;
if (guideDisposeError != null)
{
throw guideDisposeError;
}
}
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 Red=typeof(RedPointManager).GetField("instance",System.Reflection.BindingFlags.Static|System.Reflection.BindingFlags.NonPublic);
private readonly object oldRed = Red.GetValue(null);
private LuckyTaskData magicTask;
private MiniBattlePassDataProvider provider;
internal BattlePassDataProvider Battle; internal EventLuckyGameModel Lucky;
internal EventLuckMagicModel Magic; internal MiniBattlePassDataModel Mini;
internal void Bind(cfg.Tables tables, FishingEventData events, Container container, IPlayerSaveStore store)
{
Red.SetValue(null,new RedPointManager());
container.RegisterInstance<IPlayerSaveStore>(store);
container.Resolve<PlayerData>().playerSaveStore=store;
container.Resolve<PlayerFishData>().playerSaveStore=store;
container.Resolve<SmallGameData>().playerSaveStore=store;
events.playerSaveStore=store;
var items=new PlayerItemData(tables);container.RegisterInstance(items);
var data=new BattlePassData(tables){playerSaveStore=store};
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()
{
var errors = new List<Exception>();
void Release(Action action)
{
try
{
action();
}
catch (Exception error)
{
errors.Add(error);
}
}
Release(() => Battle?.Dispose());
Release(() => Lucky?.Dispose());
Release(() => magicTask?.Dispose());
Release(() =>
{
if (Mini != null)
{
(typeof(MiniBattlePassDataModel).GetField("disposables", Fields).GetValue(Mini) as IDisposable)?.Dispose();
}
});
Release(() => Red.SetValue(null, oldRed));
Release(() =>
{
if (!ReferenceEquals(Red.GetValue(null), oldRed))
{
throw new InvalidOperationException("Subscriber fixture statics did not restore.");
}
});
if (errors.Count != 0)
{
throw new AggregateException(errors);
}
}
}
static bool ReadRejected() {try{Read("NewPlayerDataDic");return false;}catch(InvalidOperationException){return true;}}
}
}
#endif