610 lines
35 KiB
C#
610 lines
35 KiB
C#
#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<T>(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<string, string> modern)
|
|
{
|
|
typeof(PlayerData).GetField("NewPlayerDataDic", InstanceFlags).SetValue(player, new Dictionary<string, string>(modern));
|
|
Invoke(player, "SetPlayerData");
|
|
}
|
|
|
|
internal static void LoadCore(PlayerData player, object[] slots)
|
|
{
|
|
LoadCore(player, new Dictionary<string, string> { ["newData"] = string.Join(",", slots.Select(value => Convert.ToString(value, CultureInfo.CurrentCulture))), ["vipL"] = "-1,1" });
|
|
}
|
|
|
|
internal static void LoadGuide(GuideDataCenter guide, IDictionary<string, string> raw)
|
|
{
|
|
if (raw.TryGetValue("GuideSaveData", out var legacy))
|
|
{
|
|
guide.GuideSaveDataDic = JsonConvert.DeserializeObject<Dictionary<string, GuideSaveData>>(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<string, string> data = new Dictionary<string, string>();
|
|
internal readonly List<string> Writes = new List<string>();
|
|
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<bool> SyncDataImmidiate(string key, string value)
|
|
{
|
|
Write(key, value);
|
|
return Task.FromResult(true);
|
|
}
|
|
|
|
|
|
}
|
|
|
|
private sealed class FaceObserver : IFaceUIService
|
|
{
|
|
internal readonly List<string> Calls = new List<string>();
|
|
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<string, UIType> previousUITypes = new Dictionary<string, UIType>(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<IFaceUIService>(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<IFootPrintService>(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>(T instance)
|
|
where T : class
|
|
{
|
|
isolated.RegisterInstance<T>(instance);
|
|
return instance;
|
|
}
|
|
|
|
internal T Get<T>()
|
|
where T : class
|
|
{
|
|
return isolated.Resolve<T>();
|
|
}
|
|
|
|
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<string, string> 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<string> RunAsync(string caseId)
|
|
{
|
|
if (running || EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling || EditorApplication.isUpdating || GContext.Ready || UnityEngine.Resources.FindObjectsOfTypeAll<GContext>().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<object>();
|
|
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<cfg.GlobalConfig>(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<string, string>
|
|
{
|
|
["MMTest"] = "MMFailParam#0",
|
|
["EventPackData"] = JsonConvert.SerializeObject(new { currentEventID = selected.ID, purchaseCount = 2, redirectID = selected.RedirectID }),
|
|
["LimitedTimeEventDic"] = JsonConvert.SerializeObject(new Dictionary<int, object> { [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<EventPackData>().CurrentEventID;
|
|
var energyTime = Field<DateTime>(fixture.Player, "_lastEnergyRecoveryTime");
|
|
var limitedCount = ((System.Collections.IDictionary)Field<object>(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<cfg.GlobalConfig>(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<PlayerItemData>()))
|
|
{
|
|
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<string, string>());
|
|
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<BargainPackData>();
|
|
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<cfg.EventCondition> { new cfg.EventCondition(cfg.ConditionType.AccountLevel, new List<string> { "1" }, "") });
|
|
ZZTimeHelper.AdjustTime(boundary.AddSeconds(-1));
|
|
var pinata = fixture.Get<EventPinataPackDataManager>();
|
|
pinata.LoadData("{\"EventID\":0,\"PinataMainID\":0,\"PurchasedCount\":7}");
|
|
var target = fixture.Get<EventPinataPackData>();
|
|
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<SmallGameData>();
|
|
Check(small.HeistGameCount == 12 && small.BombGameCount == 13 && small.FishHuntGameCount == 14, "Original small game slots");
|
|
Check(Field<Dictionary<string, int>>(fixture.Get<CampDataMM>(), "CampDataDic")["CampId"] == 205, "Original Camp dictionary");
|
|
Check(Field<Dictionary<int, FishBuffTimeData>>(fixture.Get<BuffDataCenter>(), "mapBuffDataList")[7].buffID == 3, "Original buff deserialization");
|
|
Check(fixture.Get<FishingBoxData>().GetString() == "1,2", "Original modern box replaces legacy");
|
|
Check(fixture.Get<SigninData>().monthlySignEvent.index == 4 && fixture.Get<SigninData>().dailySignEvent.index == 2, "Original monthly sign history");
|
|
Check(fixture.Get<TriggerPackData>().triggerPackList[0].vipLevel == 4, "Original trigger pack history");
|
|
foreach (var sample in new[]
|
|
{
|
|
("FishingChallengeData", (object)fixture.Get<FishingChallengeManager>().fishingChallengeData),
|
|
("OfferChainsChestData", (object)fixture.Get<DataCenter.OfferChainsChestManager>().OfferChainsData),
|
|
("OfferDiscoPartyManager", (object)fixture.Get<DataCenter.OfferDiscoPartyManager>().OfferDiscoPartyData),
|
|
("EventPartnerGatherData", (object)fixture.Get<DataCenter.EventPartnerGatherManager>().GatherData),
|
|
("EventCubeMeltData", (object)fixture.Get<EventCubeMeltManager>().EventData),
|
|
("epdk", (object)fixture.Get<EventPotionDataManager>().PotionData),
|
|
("estdk", (object)fixture.Get<EventScratchTicketDataManager>().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<EventPinballDataManager>();
|
|
Check(pinball.PinballData.Token == 11 && pinball.RewardList.SequenceEqual(fixture.Tables.TbEventPinballReward.DataList.Select(r => (float)r.SpinPoint)), "Original Pinball initializes reward configuration");
|
|
Check(!Field<bool>(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<AchievementDataManager>();
|
|
achievement.LoadAchievementDataToObject("{\"0\":\"7|18446744073709551615|9\"}");
|
|
Check(Field<Dictionary<cfg.ConditionType, AchievementData>>(achievement, "_achievementDatas")[(cfg.ConditionType)0].Target == ulong.MaxValue, "Original achievement preserves ulong maximum");
|
|
Check(fixture.Get<ClubService>().IsOpenClubRed == false, "Club key existence rule");
|
|
Check(fixture.Get<EventPackData>().PurchaseCount == 2, "Unknown EventPack preserves parsed data");
|
|
Check(fixture.Get<BargainPackData>().PurchaseCount == 11, "Bargain original data");
|
|
Check(fixture.Get<PiggyBankPackData>().PurchaseCount == 11, "Piggy original data");
|
|
Check(fixture.Get<IFootPrintService>().FootPrint == (EFootPrint)ulong.Parse(raw["FootPrint"]), "Original FootPrint enum conversion");
|
|
var model = fixture.Get<Script.RuntimeScript.DiggingGameManager>().DataModel;
|
|
fixture.Player.LoadPlayerData(new Dictionary<string, string>());
|
|
Check(ReferenceEquals(model, fixture.Get<Script.RuntimeScript.DiggingGameManager>().DataModel), "Digging original existing model retained");
|
|
Check(fixture.Shop.shopPackDailyBuyCount["x"] == 1 && fixture.Get<BargainPackData>().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<string, string>
|
|
{
|
|
["Lv"] = "2",
|
|
["Energy"] = "10",
|
|
["EquipRodID"] = rod.ToString(),
|
|
["NewPlayerDataDic"] = JsonConvert.SerializeObject(new Dictionary<string, string> { ["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<string, string> { ["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<string, string> { ["Map_5_" + realFish.AreaID] = JsonConvert.SerializeObject(new Dictionary<int, string> { [realFish.ID] = "0|7|2|3|True" }), ["Map_" + realFish.AreaID] = "bad legacy JSON" });
|
|
var parsedFish = Field<Dictionary<int, Dictionary<int, FishWeightData>>>(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<string, string> { ["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<string, string> { ["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<string, string> { ["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
|
|
|