#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using asap.core; using GameCore; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; namespace game.PlayerDataStorage { // Editor-only probes of the existing business methods; no production decoding or reset graph. internal static class Module02OriginalLoadTests { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public; private static bool running; private static bool ContainsPayload(JToken actual, JToken expected) { if (expected is JObject obj) { return actual is JObject && obj.Properties().All(p => ContainsPayload(actual[p.Name], p.Value)); } if (expected is JArray array) { return actual is JArray other && array.Count == other.Count && Enumerable.Range(0, array.Count).All(i => ContainsPayload(other[i], array[i])); } return JToken.DeepEquals(actual, expected); } internal static T Field(object target, string name) { return (T)target.GetType().GetField(name, InstanceFlags).GetValue(target); } private static void Invoke(object target, string name, params object[] arguments) { try { target.GetType().GetMethod(name, InstanceFlags).Invoke(target, arguments); } catch (TargetInvocationException ex)when (ex.InnerException != null) { ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); throw; } } internal static void LoadCore(PlayerData player, IDictionary modern) { typeof(PlayerData).GetField("NewPlayerDataDic", InstanceFlags).SetValue(player, new Dictionary(modern)); Invoke(player, "SetPlayerData"); } internal static void LoadCore(PlayerData player, object[] slots) { LoadCore(player, new Dictionary { ["newData"] = string.Join(",", slots.Select(value => Convert.ToString(value, CultureInfo.CurrentCulture))), ["vipL"] = "-1,1" }); } internal static void LoadGuide(GuideDataCenter guide, IDictionary raw) { if (raw.TryGetValue("GuideSaveData", out var legacy)) { guide.GuideSaveDataDic = JsonConvert.DeserializeObject>(legacy); } if (raw.TryGetValue("GuideSaveDataNew", out var modern)) { guide.SetData(modern); } } private sealed class ContextAccess : GContext { protected override Task SetupAsync() { throw new InvalidOperationException("Validation context cannot start gameplay."); } internal static void Use(Container value) { container = value; } internal static IEventAggregator Events { get => eventAggregator; set => eventAggregator = value; } } private sealed class MemoryStore : IPlayerSaveStore { private readonly Dictionary data = new Dictionary(); internal readonly List Writes = new List(); public bool IsCurrent => true; public PlayerSaveReadResult Read(string key) { return new PlayerSaveReadResult(data.TryGetValue(key, out var value), value); } public PlayerSaveWriteResult Write(string key, string value, bool remoteStore = true) { data[key] = value; Writes.Add(key); return new PlayerSaveWriteResult(new PlayerSaveReceipt(this, null)); } public Task SyncDataImmidiate(string key, string value) { Write(key, value); return Task.FromResult(true); } } private sealed class FaceObserver : IFaceUIService { internal readonly List Calls = new List(); public void AddFaceCustomUIData(UIType type, bool first = false) { } public void ShowFaceUI(string value = "") { } public void AddGiftFaceUIForce(int id, UIType type) { Calls.Add("force:" + id + ":" + type); } public void AddGiftFaceUIOnLogin(int id, UIType type, int priority) { } public void AddGiftFaceUI(int id, UIType type, int priority = 0) { Calls.Add("gift:" + id + ":" + type + ":" + priority); } public int GetFaceUICount() => Calls.Count; public void StopFaceUI() { } public void AddPostFaceUI(UIType type) { } public void ShowPostFaceUI() { } public int GetPostFaceUICount() => 0; public void StopPostFaceUI() { } } private sealed class Fixture : IDisposable { private readonly Container previous = GContext.container; private readonly IEventAggregator previousEvents = ContextAccess.Events; private readonly Container isolated = new Container(); private readonly string previousPinataPath = UITypes.PinataPack_PonyPanel.Path; private readonly string previousPinataName = UITypes.PinataPack_PonyPanel.Name; private readonly Dictionary previousUITypes = new Dictionary(UITypes.UITypesDic); private readonly object previousRedPoints = typeof(RedPointManager).GetField("instance", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); internal readonly cfg.Tables Tables; internal readonly PlayerData Player; internal readonly PlayerFishData Fish; internal readonly FishingEventData Events; internal readonly PlayerShopData Shop; internal readonly GuideDataCenter Guide; internal readonly BattlePassData Battle; internal readonly MemoryStore Store = new MemoryStore(); internal readonly FaceObserver Face = new FaceObserver(); internal Fixture() { var directory = System.IO.Path.GetFullPath(System.IO.Path.Combine(UnityEngine.Application.dataPath, "../../cfgData")); Tables = new cfg.Tables(name => SimpleJSON.JSON.Parse(System.IO.File.ReadAllText(System.IO.Path.Combine(directory, name + ".json")))); ContextAccess.Use(isolated); ContextAccess.Events = new EventAggregator(); typeof(RedPointManager).GetField("instance", BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, new RedPointManager()); Register(Tables); isolated.RegisterInstance(Face); Player = Register(new PlayerData(Tables, null) { playerSaveStore = Store }); Register(new SmallGameData(Tables, null, null)); Fish = Register(new PlayerFishData(Tables, null)); Events = Register(new FishingEventData(Tables)); Shop = Register(new PlayerShopData { _tables = Tables, SaveStore = Store }); Register(new BuffDataCenter(Tables)); Register(new CampDataMM(Tables, null)); Register(new AchievementDataManager(Tables)); Register(new FishingBoxData()); Guide = Register(new GuideDataCenter(Tables, Player)); Battle = Register(new BattlePassData(Tables) { playerSaveStore = Store }); Register(new SigninData(Tables)); Register(new TriggerPackData(Tables)); Register(new ClubService(false)); Register(new FishingChallengeManager()); Register(new DataCenter.OfferChainsChestManager()); Register(new DataCenter.OfferDiscoPartyManager()); Register(new DataCenter.EventPartnerGatherManager()); Register(new EventCubeMeltManager()); Register(new EventPackData()); Register(new PiggyBankPackData()); Register(new BargainPackData()); isolated.RegisterInstance(new FootPrintService()); Register(new EventShootingRangeData()); Register(new EventPotionDataManager()); Register(new EventPinballDataManager()); Register(new EventScratchTicketDataManager()); Register(new Script.RuntimeScript.DiggingGameManager()); Register(new PlayerItemData(Tables)); // Original constructor, after its existing dependencies are available. Register(new EventPinataPackDataManager()); } private T Register(T instance) where T : class { isolated.RegisterInstance(instance); return instance; } internal T Get() where T : class { return isolated.Resolve(); } public void Dispose() { ContextAccess.Use(previous); ContextAccess.Events = previousEvents; typeof(RedPointManager).GetField("instance", BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, previousRedPoints); UITypes.PinataPack_PonyPanel.SetType(previousPinataPath); UITypes.PinataPack_PonyPanel.SetName(previousPinataName); UITypes.UITypesDic.Clear(); foreach (var pair in previousUITypes) { UITypes.UITypesDic[pair.Key] = pair.Value; } isolated.Dispose(); } } internal static string InspectRaw(Dictionary raw) { using (var fixture = new Fixture()) { fixture.Player.LoadPlayerData(raw); return JsonConvert.SerializeObject(new { originalLoad = true, keys = raw.Count, level = (int)fixture.Player.lv, writes = fixture.Store.Writes, faceCalls = fixture.Face.Calls, formalRequests = 0 }); } } internal static Task RunAsync(string caseId) { if (running || EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling || EditorApplication.isUpdating || GContext.Ready || UnityEngine.Resources.FindObjectsOfTypeAll().Length != 0 || PlayerKVGameplayLoop.Enabled) { throw new InvalidOperationException("Original load validation requires an idle isolated Editor."); } running = true; var previousContainer = GContext.container; var previousEvents = ContextAccess.Events; var previousDiff = ZZTimeHelper.timeDiff; var previousOffset = ZZTimeHelper.LocalTimeOffsetInSec; var traces = new List(); int assertions = 0; string failure = null; void Check(bool condition, string name) { assertions++; if (!condition) { throw new InvalidOperationException(name); } } try { // Baseline executes the unchanged original method; target executes the KV private common entry. // The third path deliberately reproduces the former pre-adjust load for a visible counterexample. foreach (var seconds in new[] { -1, 1 } ) { foreach (var offset in new[] { -3600, 3600 } ) { var outputs = new List<(int eventId, DateTime energyTime, int limitedCount)>(); foreach (var path in new[] { "original-after-adjust", "kv-after-adjust", "previous-before-adjust" } ) { using (var fixture = new Fixture()) { var boundary = new DateTime(2026, 9, 23, 0, 0, 0, DateTimeKind.Utc); var selected = fixture.Tables.TbFishingEvent.DataList.First(e => e.TimeDefinition is cfg.LimitedTime && fixture.Tables.TbEventPackManager.GetOrDefault(e.RedirectID)?.PackType == 3); var time = (cfg.LimitedTime)selected.TimeDefinition; var globalOffset = offset < 0 ? -9 : 9; typeof(cfg.GlobalConfig).GetProperty("GlobalTimeOffset").SetValue(Field(fixture.Tables.TbGlobalConfig, "_data"), globalOffset); typeof(cfg.LimitedTime).GetProperty("StartTime").SetValue(time, boundary.AddDays(-1).ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture)); typeof(cfg.LimitedTime).GetProperty("EndTime").SetValue(time, boundary.ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture)); var raw = new Dictionary { ["MMTest"] = "MMFailParam#0", ["EventPackData"] = JsonConvert.SerializeObject(new { currentEventID = selected.ID, purchaseCount = 2, redirectID = selected.RedirectID }), ["LimitedTimeEventDic"] = JsonConvert.SerializeObject(new Dictionary { [900001] = new { id = 900001, duration = 2, startTime = boundary.AddHours(-globalOffset).AddMinutes(-2).ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture) } }) }; ZZTimeHelper.LocalTimeOffsetInSec = offset; var desired = boundary.AddSeconds(seconds); ZZTimeHelper.AdjustTime((path == "previous-before-adjust" ? boundary.AddDays(2) : desired).AddSeconds(-offset)); if (path == "kv-after-adjust") { var user = new KVUserService { SaveStore = fixture.Store }; Invoke(user, "InitializePlayerData", raw); } else { fixture.Player.LoadPlayerData(raw); } var eventId = fixture.Get().CurrentEventID; var energyTime = Field(fixture.Player, "_lastEnergyRecoveryTime"); var limitedCount = ((System.Collections.IDictionary)Field(fixture.Events, "limitedTimeEventDic")).Count; outputs.Add((eventId, energyTime, limitedCount)); traces.Add(new { path, seconds, offset, globalOffset, desired, eventId, energyTime, limitedCount, faceCalls = fixture.Face.Calls.ToArray(), writes = fixture.Store.Writes.ToArray() }); Check(fixture.Face.Calls.Count == 0, "M20 original commented UI remains inactive"); } } Check(outputs[0].eventId == outputs[1].eventId, "Original and KV activity boundary match"); Check(outputs[0].limitedCount == outputs[1].limitedCount && outputs[0].limitedCount == (seconds < 0 ? 1 : 0), "LimitedTime expiry uses original global offset on both paths"); Check(Math.Abs((outputs[0].energyTime - outputs[1].energyTime).TotalSeconds) < 1, "Original and KV energy clock match"); Check(outputs[0].eventId == (seconds < 0 ? 0 : outputs[2].eventId), "M20 original expiry/reset semantics"); Check(Math.Abs((outputs[2].energyTime - outputs[1].energyTime).TotalHours) > 24, "Old load-before-adjust clock counterexample is real"); } } // Existing refresh lifecycle is tested separately; login loading must not invoke it early. foreach (var globalOffset in new[] { -9, 9 } ) { foreach (var seconds in new[] { -1, 1 } ) { using (var fixture = new Fixture()) { typeof(cfg.GlobalConfig).GetProperty("GlobalTimeOffset").SetValue(Field(fixture.Tables.TbGlobalConfig, "_data"), globalOffset); var eventData = fixture.Tables.TbFishingEvent.DataList.First(e => fixture.Tables.TbBattlePassMain.GetOrDefault(e.RedirectID) != null); fixture.Battle.BattlePassRecord.ID = eventData.ID; using (var provider = new BattlePassDataProvider(fixture.Tables, fixture.Battle, fixture.Get())) { GContext.container.RegisterInstance(provider); ZZTimeHelper.LocalTimeOffsetInSec = 0; var desired = new DateTime(2026, 9, 27, 0, 0, 0, DateTimeKind.Utc).AddHours(globalOffset).AddSeconds(seconds); ZZTimeHelper.AdjustTime(desired); fixture.Battle.RefreshDaiyTask(); fixture.Battle.RefreshWeeklyTask(); var offsetNow = ZZTimeHelper.UtcNow().UtcNowOffset(); Check(fixture.Battle.BattleTask.DailyTaskEndTime == ConvertTools.GetDateTimeYMD(offsetNow).AddDays(1), "BP original daily lifecycle boundary"); Check(fixture.Battle.BattleTask.WeeklyTaskEndTime == ConvertTools.GetDateTimeSunDay(offsetNow).AddDays(1), "BP original weekly lifecycle boundary"); Check(fixture.Store.Writes.Contains("BattlePassRecordTask"), "BP original lifecycle saves through existing store"); traces.Add(new { path = "original-BP-refresh-lifecycle", globalOffset, seconds, desired, dailyEnd = fixture.Battle.BattleTask.DailyTaskEndTime, weeklyEnd = fixture.Battle.BattleTask.WeeklyTaskEndTime, writes = fixture.Store.Writes.ToArray() }); } } } } using (var fixture = new Fixture()) { var boundary = new DateTime(2026, 9, 23, 0, 0, 0, DateTimeKind.Utc); fixture.Player.LoadPlayerData(new Dictionary()); ZZTimeHelper.LocalTimeOffsetInSec = 0; ZZTimeHelper.AdjustTime(boundary.AddSeconds(-1)); var selected = fixture.Tables.TbFishingEvent.DataList.First(e => e.TimeDefinition is cfg.LimitedTime && fixture.Tables.TbEventPackManager.GetOrDefault(e.RedirectID)?.PackType == 3); var limit = (cfg.LimitedTime)selected.TimeDefinition; typeof(cfg.LimitedTime).GetProperty("StartTime").SetValue(limit, boundary.AddDays(-1).ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture)); typeof(cfg.LimitedTime).GetProperty("EndTime").SetValue(limit, boundary.ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture)); var bargain = fixture.Get(); var pack = fixture.Tables.TbEventPackManager[selected.RedirectID]; var packId = pack.VIPPackList[0][0]; var saved = new GeneralEventPackData(eventID: selected.ID, redirectID: selected.RedirectID, vip: 0, purchaseCount: 0, isTriggered: true, savingProgress: int.MaxValue, discountLvl: fixture.Tables.TbSpecialPack[packId].IAPID.Count - 1, inflation: 1); bargain.LoadData(saved); Check(fixture.Face.Calls.Count == 1 && fixture.Face.Calls[0].StartsWith("force:" + selected.ID + ":"), "M22 original full active bargain registers original UI once"); saved.isTriggered = false; bargain.LoadData(saved); Check(fixture.Face.Calls.Count == 1, "M22 untriggered bargain does not register UI"); saved.isTriggered = true; ZZTimeHelper.AdjustTime(boundary.AddSeconds(1)); bargain.LoadData(saved); Check(fixture.Face.Calls.Count == 1, "M22 expired bargain does not register UI"); traces.Add(new { path = "original-bargain-side-effects", calls = fixture.Face.Calls.ToArray() }); fixture.Face.Calls.Clear(); var pinataEvent = fixture.Tables.TbFishingEvent.DataList.First(e => e.Type == 9 && e.SubType == 6 && e.TimeDefinition is cfg.LimitedTime && fixture.Tables.TbEventPinataPackMain.GetOrDefault(e.RedirectID) != null); foreach (var ev in fixture.Tables.TbFishingEvent.DataList.Where(e => e.Type == 9 && e.SubType == 6)) { if (ev.TimeDefinition is cfg.LimitedTime other) { typeof(cfg.LimitedTime).GetProperty("StartTime").SetValue(other, boundary.AddDays(-2).ToString("s")); typeof(cfg.LimitedTime).GetProperty("EndTime").SetValue(other, boundary.AddDays(-1).ToString("s")); } } var pinataTime = (cfg.LimitedTime)pinataEvent.TimeDefinition; typeof(cfg.LimitedTime).GetProperty("StartTime").SetValue(pinataTime, boundary.AddDays(-1).ToString("s")); typeof(cfg.LimitedTime).GetProperty("EndTime").SetValue(pinataTime, boundary.ToString("s")); typeof(cfg.FishingEvent).GetProperty("ConditionList").SetValue(pinataEvent, new List { new cfg.EventCondition(cfg.ConditionType.AccountLevel, new List { "1" }, "") }); ZZTimeHelper.AdjustTime(boundary.AddSeconds(-1)); var pinata = fixture.Get(); pinata.LoadData("{\"EventID\":0,\"PinataMainID\":0,\"PurchasedCount\":7}"); var target = fixture.Get(); Check(target.EventID == pinataEvent.ID && target.PurchasedCount == 0 && fixture.Face.Calls.Count == 1, "M29 original changed event refreshes purchase state and registers UI"); pinata.LoadData(JsonConvert.SerializeObject(new { target.EventID, target.PinataMainID, PurchasedCount = 1 })); Check(target.PurchasedCount == 1 && fixture.Face.Calls.Count == 1, "M29 same event preserves purchase count without repeated UI"); ZZTimeHelper.AdjustTime(boundary.AddSeconds(1)); pinata.GetFreshPinataPackData(); Check(fixture.Face.Calls.Count == 1, "M29 expired event does not add UI"); traces.Add(new { path = "original-pinata-side-effects", calls = fixture.Face.Calls.ToArray(), target.EventID, target.PinataMainID, target.PurchasedCount }); } using (var fixture = new Fixture()) { var raw = PlayerKVEditorValidationHost.FullRaw(fixture.Tables, true); fixture.Player.LoadPlayerData(raw); Check((int)fixture.Player.lv == 3 && (ulong)fixture.Player.gold == 999 && (int)fixture.Player.Energy == 5, "Original newData wins over defaults"); Check(fixture.Shop.Voucher == 6 && fixture.Shop.shopPackDailyBuyCount["x"] == 1, "Shop numeric dictionaries"); Check(fixture.Events.TurntableCoupon == 7 && fixture.Events.WheelLevelCount[5] == 6, "Event original defaults and saved counts"); Check(fixture.Guide.GuideSaveDataDic["A"].guideSubIndex == 3 && fixture.Guide.GuideSaveDataDic["A"].isFinish, "Guide original modern format"); Check(fixture.Battle.BattlePassRecord.Level == 2 && fixture.Battle.BattleTask.dic_dailyTasks[9] == 10, "BP legacy values"); var small = fixture.Get(); Check(small.HeistGameCount == 12 && small.BombGameCount == 13 && small.FishHuntGameCount == 14, "Original small game slots"); Check(Field>(fixture.Get(), "CampDataDic")["CampId"] == 205, "Original Camp dictionary"); Check(Field>(fixture.Get(), "mapBuffDataList")[7].buffID == 3, "Original buff deserialization"); Check(fixture.Get().GetString() == "1,2", "Original modern box replaces legacy"); Check(fixture.Get().monthlySignEvent.index == 4 && fixture.Get().dailySignEvent.index == 2, "Original monthly sign history"); Check(fixture.Get().triggerPackList[0].vipLevel == 4, "Original trigger pack history"); foreach (var sample in new[] { ("FishingChallengeData", (object)fixture.Get().fishingChallengeData), ("OfferChainsChestData", (object)fixture.Get().OfferChainsData), ("OfferDiscoPartyManager", (object)fixture.Get().OfferDiscoPartyData), ("EventPartnerGatherData", (object)fixture.Get().GatherData), ("EventCubeMeltData", (object)fixture.Get().EventData), ("epdk", (object)fixture.Get().PotionData), ("estdk", (object)fixture.Get().ScratchTicketData) } ) { var expected = JToken.Parse(raw[sample.Item1]); if (sample.Item1 == "OfferDiscoPartyManager") { ((JObject)expected).Remove("ChestReward"); ((JObject)expected).Remove("Finished"); } Check(ContainsPayload(JToken.FromObject(sample.Item2), expected), "Original numeric/history payload: " + sample.Item1); } var pinball = fixture.Get(); Check(pinball.PinballData.Token == 11 && pinball.RewardList.SequenceEqual(fixture.Tables.TbEventPinballReward.DataList.Select(r => (float)r.SpinPoint)), "Original Pinball initializes reward configuration"); Check(!Field(pinball, "m_IsSimulatorMode"), "Original Pinball exits simulator mode"); Check(fixture.Events.ThanksGivingPackData.EventId == 1 && fixture.Events.ThanksGivingPackData.ChainProgress == 4 && fixture.Events.ChainPackWithProgressMigrationData.EventId == 5 && fixture.Events.ChainPackWithProgressMigrationData.ChainProgress == 8, "Two original progress keys remain independent"); var achievement = fixture.Get(); achievement.LoadAchievementDataToObject("{\"0\":\"7|18446744073709551615|9\"}"); Check(Field>(achievement, "_achievementDatas")[(cfg.ConditionType)0].Target == ulong.MaxValue, "Original achievement preserves ulong maximum"); Check(fixture.Get().IsOpenClubRed == false, "Club key existence rule"); Check(fixture.Get().PurchaseCount == 2, "Unknown EventPack preserves parsed data"); Check(fixture.Get().PurchaseCount == 11, "Bargain original data"); Check(fixture.Get().PurchaseCount == 11, "Piggy original data"); Check(fixture.Get().FootPrint == (EFootPrint)ulong.Parse(raw["FootPrint"]), "Original FootPrint enum conversion"); var model = fixture.Get().DataModel; fixture.Player.LoadPlayerData(new Dictionary()); Check(ReferenceEquals(model, fixture.Get().DataModel), "Digging original existing model retained"); Check(fixture.Shop.shopPackDailyBuyCount["x"] == 1 && fixture.Get().PurchaseCount == 11, "Retry missing keys retains original existing objects"); traces.Add(new { path = "full-original-and-missing-key-retry", keys = raw.Keys.ToArray(), writes = fixture.Store.Writes.ToArray(), faceCalls = fixture.Face.Calls.ToArray() }); } using (var fixture = new Fixture()) { var rod = fixture.Tables.TbGlobalConfig.InitRodID; foreach (var count in new[] { 19, 20 } ) { var slots = new object[] { 9, 23UL, 0, -4, 5, 6, -999, new DateTime(2026, 1, 1), 0, 8, 7UL, 0, 0, 0, 0, 0, 2, 3, 4, 99 }.Take(count).ToArray(); var raw = new Dictionary { ["Lv"] = "2", ["Energy"] = "10", ["EquipRodID"] = rod.ToString(), ["NewPlayerDataDic"] = JsonConvert.SerializeObject(new Dictionary { ["LV"] = "4", ["newData"] = string.Join(",", slots), ["vipL"] = "-1,1" }) }; fixture.Player.LoadPlayerData(raw); Check((int)fixture.Player.lv == 9 && fixture.Player.Energy == 0 && fixture.Player.equipRodID == rod, "19 and 20 slots original override/clamp/rod fallback"); } fixture.Shop.CouponData.LoadData("2026,9,23,1,2,3,14"); Check(fixture.Shop.CouponData.RedirectId == 0 && !fixture.Shop.CouponData.IsBought, "Seven-field coupon history"); fixture.Shop.CouponData.LoadData("2026,9,23,1,2,3,14,15,1"); Check(fixture.Shop.CouponData.RedirectId == 15 && fixture.Shop.CouponData.IsBought, "Nine-field coupon history"); var compressed = fixture.Battle.BattlePassRecord.SerializeObject() + "#" + fixture.Battle.BattleTask.SerializeObject(); fixture.Battle.InitData(new Dictionary { ["BattlePassRecordTask"] = compressed, ["BattlePassRecord"] = "bad JSON", ["BattleTask"] = "bad JSON" }); Check(fixture.Battle.BattlePassRecord != null && fixture.Battle.BattleTask != null, "Compressed BP takes precedence without parsing conflicting legacy"); var realFish = fixture.Tables.TbFishData.DataList.First(f => fixture.Tables.TbMapData.DataMap.ContainsKey(f.AreaID)); fixture.Fish.InitData(new Dictionary { ["Map_5_" + realFish.AreaID] = JsonConvert.SerializeObject(new Dictionary { [realFish.ID] = "0|7|2|3|True" }), ["Map_" + realFish.AreaID] = "bad legacy JSON" }); var parsedFish = Field>>(fixture.Fish, "mapFishData")[realFish.AreaID][realFish.ID]; Check(parsedFish.TotalNum == 3 && parsedFish.NewFish, "Map_5 takes precedence over broken legacy map with original weight correction"); fixture.Player.LoadPlayerData(new Dictionary { ["GuideSaveData"] = "bad JSON", ["GuideSaveDataNew"] = "K|bad|1", ["LimitedTimeEventDic"] = "bad JSON", ["epdk"] = "", ["epbdk"] = "bad JSON", ["estdk"] = "bad JSON", ["Lv"] = "17" }); Check((int)fixture.Player.lv == 17 && fixture.Guide.GuideSaveDataDic["K"].guideSubIndex == 0, "Original local catches continue and Guide TryParse default"); Exception thrown = null; try { fixture.Player.LoadPlayerData(new Dictionary { ["IsFix"] = "invalid-bool" }); } catch (FormatException ex) { thrown = ex; } Check(thrown != null && thrown.StackTrace.Contains("InitData"), "Original external parse exception is not Schema rejection"); fixture.Player.LoadPlayerData(new Dictionary { ["Lv"] = "19" }); Check((int)fixture.Player.lv == 19, "Original load can retry after failure without uniform reset"); } } catch (Exception ex) { failure = ex.ToString(); } finally { ZZTimeHelper.timeDiff = previousDiff; ZZTimeHelper.LocalTimeOffsetInSec = previousOffset; ContextAccess.Use(previousContainer); ContextAccess.Events = previousEvents; running = false; } var restored = ReferenceEquals(GContext.container, previousContainer) && ReferenceEquals(ContextAccess.Events, previousEvents) && ZZTimeHelper.timeDiff == previousDiff && ZZTimeHelper.LocalTimeOffsetInSec == previousOffset; return Task.FromResult(JsonConvert.SerializeObject(new { success = failure == null && restored, caseId, assertions, traces, failure, restored, formalRequests = 0, baseline = "f40a816cc414fc4f30d67626d060d90fdc5eaca3 original business methods (source comparison required)", playerFabNetworkLoginExecuted = false })); } } } #endif