1227 lines
71 KiB
C#
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
|