保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
350 lines
24 KiB
Plaintext
350 lines
24 KiB
Plaintext
#if SERVICE_HOST
|
|
using game.PlayerDataStorage;
|
|
|
|
static partial class Probe
|
|
{
|
|
public sealed class M2EventFixture : Activity.Data.IEventData { public int Count { get; set; } }
|
|
sealed class DailyTicks : TimeManager.ITimeTickService
|
|
{
|
|
public IObservable<long> SecondTick { get; } = new UniRx.Stream<long>();
|
|
public IObservable<long> MinuteTick { get; } = new UniRx.Stream<long>();
|
|
public void Start() { }
|
|
public void Stop() { }
|
|
}
|
|
|
|
static void MergedDailyRefresh()
|
|
{
|
|
GContext.container.Values[typeof(cfg.Tables)] = new cfg.Tables();
|
|
var user = new game.KVUserService();
|
|
int refreshes = 0;
|
|
using var events = GContext.OnEvent<TimeManager.Event.ActivityCrossDayRefreshEvent>().Subscribe(_ => refreshes++);
|
|
using (var manager = new TimeManager.TimeRefreshManager(new DailyTicks(), user))
|
|
Check(manager.GetLastLoginTime() == null && manager.InitialRefreshSucceeded && refreshes == 1,
|
|
"actual daily manager consumes missing-login sentinel and records successful initial refresh");
|
|
using (var failure = GContext.OnEvent<TimeManager.Event.ActivityCrossDayRefreshEvent>().Subscribe(_ => throw new InvalidOperationException("synthetic refresh failure")))
|
|
using (var manager = new TimeManager.TimeRefreshManager(new DailyTicks(), user))
|
|
Check(!manager.InitialRefreshSucceeded && user.LastLoginTime == DateTime.MinValue,
|
|
"caught daily subscriber failure is visible to the timestamp commit gate");
|
|
typeof(game.KVUserService).GetProperty(nameof(user.LastLoginTime)).SetValue(user, ZZTimeHelper.UtcNow().UtcNowOffset());
|
|
var before = refreshes;
|
|
using (var manager = new TimeManager.TimeRefreshManager(new DailyTicks(), user))
|
|
Check(manager.InitialRefreshSucceeded && refreshes == before, "same configured game day needs no initial refresh");
|
|
user.Logout();
|
|
Pass("merged-real-daily-refresh-success-failure-and-login-sentinel");
|
|
}
|
|
static async Task MergedSaveContract(game.KVUserService user, Dictionary<string, string> remote, bool unknown, Action<Action> onNextSet)
|
|
{
|
|
var store = user.SaveStore;
|
|
var original = (PlayerKVGameSessionHandle)typeof(game.KVUserService).GetField("handle",
|
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(user);
|
|
try { ((KVPlayerSaveStore)store).Bind(original, user.LoginLifetime); throw new Exception("store rebound"); }
|
|
catch (InvalidOperationException) { Check(true, "bound store cannot be rebound even to the same handle"); }
|
|
Check(PlayerKVGameLoadGuard.TryEnter(out var guard), "save contract guard lease acquired");
|
|
using (guard)
|
|
{
|
|
Check(!store.Read("Language").Success && !store.Write("M2Guarded", "x").Accepted && guard.Violations == 2,
|
|
"common store preserves original read/write guard enforcement");
|
|
}
|
|
var absent = store.Read("M2Missing");
|
|
Check(absent.Success && absent.Found == false && absent.Value == null, "successful missing key is explicit");
|
|
var invalid = store.Read(null);
|
|
Check(!invalid.Success && invalid.Found == null, "invalid read is never a missing key");
|
|
try { invalid.ValueOrThrow(); throw new Exception("failed read swallowed"); }
|
|
catch (InvalidOperationException) { Check(true, "failed read cannot enter a default-value branch"); }
|
|
var local = store.Write("M2LocalOnly", "", false);
|
|
Check(local.Accepted && store.Read("M2LocalOnly").Found == true && store.Read("M2LocalOnly").Value == "",
|
|
"local-only empty string remains present: " + local.Error + "/" + original.Status);
|
|
Check((await store.ConfirmAsync(local.Receipt)).Status == PlayerSaveStatus.LocalOnly && !remote.ContainsKey("M2LocalOnly"),
|
|
"local-only acceptance never becomes remote confirmation");
|
|
if (unknown)
|
|
{
|
|
await MergedUnknownRewards(user);
|
|
var before = user.Language;
|
|
try { await user.UpdateLanguage("de"); throw new Exception("unknown profile published"); }
|
|
catch (InvalidOperationException) { Check(user.Language == before, "unknown profile save does not update public language"); }
|
|
Check((await store.FlushAsync()).Status == PlayerSaveStatus.Unknown, "unknown outcome remains explicit");
|
|
Check(!store.Write("M2BlockedAfterUnknown", "x").Accepted, "unknown outcome blocks further writes");
|
|
var read = store.Read("M2Missing");
|
|
Check(!read.Success && read.Found == null, "unreadable owner cannot impersonate an empty account");
|
|
}
|
|
else
|
|
{
|
|
await MergedActivityTokens(user, remote);
|
|
await MergedRewardActions(user, remote, onNextSet);
|
|
await MergedSurveyAndMail(user, remote, onNextSet);
|
|
var repository = new Activity.Data.EventDataRepository(store);
|
|
Check(!repository.Exists<M2EventFixture>() && repository.Load<M2EventFixture>().Count == 0,
|
|
"real activity repository defaults only for an absent key");
|
|
foreach (var malformed in new[] { "", "null", "{broken" })
|
|
{
|
|
store.Write(nameof(M2EventFixture), malformed, false).RequireAccepted();
|
|
try { repository.Load<M2EventFixture>(); throw new Exception("corrupt activity became empty"); }
|
|
catch (Newtonsoft.Json.JsonException) { Check(true, "bad activity JSON never becomes a new save"); }
|
|
}
|
|
repository.Save(new M2EventFixture { Count = 19 });
|
|
Check(repository.Load<M2EventFixture>().Count == 19, "real activity repository preserves generic key and JSON");
|
|
var first = store.Write("M2Replace", "first");
|
|
var last = store.Write("M2Replace", "last");
|
|
Check(first.Accepted && last.Accepted && !remote.ContainsKey("M2Replace"), "buffered writes are not sent immediately");
|
|
Check((await store.ConfirmAsync(first.Receipt)).Status == PlayerSaveStatus.Superseded,
|
|
"overwritten standalone receipt is not confirmed");
|
|
Check((await store.ConfirmAsync(last.Receipt)).Confirmed && remote["M2Replace"] == "last",
|
|
"final original receipt confirms its remote value");
|
|
Check(!remote.ContainsKey("M2LocalOnly"), "flush never sends local-only keys");
|
|
Check(Newtonsoft.Json.JsonConvert.DeserializeObject<M2EventFixture>(remote[nameof(M2EventFixture)]).Count == 19,
|
|
"real repository data reaches the original KV transport");
|
|
var other = new KVPlayerSaveStore();
|
|
Check((await store.ConfirmAsync(new PlayerSaveReceipt(other, null))).Status == PlayerSaveStatus.Rejected,
|
|
"a receipt from another store cannot cross accounts");
|
|
await user.UpdateLanguage("de");
|
|
Check(user.Language == "de" && remote["Language"] == "de", "real profile consumer publishes only after confirmation");
|
|
for (int i = 0; i < 11; i++) store.Write("M2Watermark" + i, "initial").RequireAccepted();
|
|
onNextSet(() => store.Write("M2Watermark10", "later").RequireAccepted());
|
|
Check((await store.FlushAsync()).Status == PlayerSaveStatus.Superseded && !remote.ContainsKey("M2Watermark10"),
|
|
"aggregate flush cannot confirm an unsent receipt replaced beyond its captured watermark");
|
|
Check((await store.FlushAsync()).Confirmed && remote["M2Watermark10"] == "later",
|
|
"later replacement requires its own flush and confirmation");
|
|
using var cancelled = new CancellationTokenSource(); cancelled.Cancel();
|
|
Check((await store.WriteAndConfirmAsync("M2Cancelled", "x", cancelled.Token)).Status == PlayerSaveStatus.Cancelled &&
|
|
!store.Read("M2Cancelled").Found.Value, "pre-cancelled consumption does not enqueue a debit");
|
|
}
|
|
Pass(unknown ? "merged-save-unknown-is-not-missing-or-profile-success" : "merged-save-local-buffered-superseded-confirmed-and-foreign-receipt");
|
|
}
|
|
|
|
sealed class PFSurveyUser : game.PlayFabUserService { public PFSurveyUser() { UserId = "101"; } }
|
|
static async Task MergedUnknownRewards(game.KVUserService user)
|
|
{
|
|
var store = user.SaveStore;
|
|
int drops = 0, shown = 0, requests = 0;
|
|
GContext.container.Values[typeof(game.PlayerData)] = new game.PlayerData { Save = () => store.Write("M2UnknownPrimary", "saved").RequireAccepted() };
|
|
GContext.container.Values[typeof(game.PlayerItemData)] = new game.PlayerItemData { Drop = (id, inflated) =>
|
|
{
|
|
drops++;
|
|
store.Write("M2UnknownItem", "credit").RequireAccepted();
|
|
PlayerSaveAction.Present(() => GContext.Publish(new game.ShowData()));
|
|
}};
|
|
using var subscription = GContext.OnEvent<game.ShowData>().Subscribe(_ => shown++);
|
|
var tables = new cfg.Tables();
|
|
var definition = new cfg.SurveyData { ID = 99, Reward = 99 };
|
|
var survey = new SurveyService(user, tables, store);
|
|
try { await survey.GetSurveyCheckAnswer(definition); throw new Exception("unknown reward reported success"); }
|
|
catch (InvalidOperationException) { Check(drops == 1 && shown == 0, "unknown actual survey credit never presents success"); }
|
|
try { await survey.GetSurveyCheckAnswer(definition); throw new Exception("unknown survey was replayed"); }
|
|
catch (InvalidOperationException) { Check(drops == 1, "actual survey rejects replay after unknown confirmation"); }
|
|
try { await PlayerSaveAction.RunAsync(store, "another-reward", () => drops++, () => { }); throw new Exception("unknown owner mutated another reward"); }
|
|
catch (InvalidOperationException) { Check(drops == 1, "readiness check rejects another reward before inventory mutation"); }
|
|
var cache = new MailCache(tables.TbAutoDeliveryMail); cache.Load(user.UserId);
|
|
var server = System.Reflection.DispatchProxy.Create<ICustomServerMgr, MailServerProbe>();
|
|
((MailServerProbe)(object)server).Post = (route, json) => { requests++; return Task.FromResult("{\"success\":true}"); };
|
|
var handler = new BaseMailHandler(cache, server, user, store);
|
|
Check(!await handler.TakeReward(new GameMail { Id="unknown", DropIds=new[]{1} }) && requests==0,
|
|
"unknown KV owner blocks mail state request before consuming server reward");
|
|
handler.Close(); cache.Unload();
|
|
Pass("merged-actual-survey-unknown-no-replay-and-mail-before-state-readiness");
|
|
}
|
|
|
|
public class MailServerProbe : System.Reflection.DispatchProxy
|
|
{
|
|
public Func<string,string,Task<string>> Post;
|
|
protected override object Invoke(System.Reflection.MethodInfo method, object[] args)
|
|
{
|
|
if (method.Name != "CustomServerPost") throw new Exception("Unexpected mail transport method: " + method.Name);
|
|
return Post((string)args[0], (string)args[1]);
|
|
}
|
|
}
|
|
static async Task MergedSurveyAndMail(game.KVUserService user, Dictionary<string,string> remote, Action<Action> onNextSet)
|
|
{
|
|
var store = user.SaveStore;
|
|
GContext.container.Values[typeof(IPlayerSaveStore)] = store;
|
|
var player = new game.PlayerData { lv = 10, Save = () => store.Write("M2RewardFixturePrimary", "saved").RequireAccepted() };
|
|
var drops = new List<(int id, bool inflated)>();
|
|
GContext.container.Values[typeof(game.PlayerData)] = player;
|
|
GContext.container.Values[typeof(game.PlayerItemData)] = new game.PlayerItemData { Drop = (id, inflated) =>
|
|
{
|
|
drops.Add((id, inflated));
|
|
store.Write("M2RewardFixtureDrop" + id, drops.Count.ToString()).RequireAccepted();
|
|
PlayerSaveAction.Present(() => GContext.Publish(new game.ShowData()));
|
|
}};
|
|
var footprint = new FootPrintService { SaveStore = store };
|
|
footprint.LoadData((1UL << 40).ToString());
|
|
footprint.UpdateFootPrint(EFootPrint.IsFaceBookRewardLink);
|
|
Check(store.Read("FootPrint").ValueOrThrow() == ((1UL << 40) | (uint)EFootPrint.IsFaceBookRewardLink).ToString(),
|
|
"actual footprint write retains high KV bits while updating its legacy int view");
|
|
var statue = new EventPartnerStatuePlayfabData { EventId=42, TicketCount=7, DisplayScoreMap=new(), RobotScoreMap=new() };
|
|
statue.Save();
|
|
var reloaded = EventPartnerStatuePlayfabData.Load();
|
|
Check(reloaded.EventId==42 && reloaded.TicketCount==7, "actual dynamic archive constructor and static loader retain common store wire data");
|
|
var tables = new cfg.Tables();
|
|
var decimalSurvey = new cfg.SurveyData { ID = 11, Hash = "test", Reward = 501, TailNumberList = new List<int>{1} };
|
|
tables.TbSurveyData.DataList.Add(decimalSurvey);
|
|
tables.TbSurveyData.DataList.Add(new cfg.SurveyData { ID = 22, Hash = "test", Reward = 502, TailNumberList = new List<int>{7} });
|
|
int presentations = 0;
|
|
using var subscription = GContext.OnEvent<game.ShowData>().Subscribe(_ => presentations++);
|
|
var survey = new SurveyService(user, tables, store);
|
|
survey.GetSurveyMatchlist();
|
|
Check(survey.tableData == decimalSurvey, "real survey uses decimal new user ID rather than PF hexadecimal tail");
|
|
Check(survey.GetSurveyAnswer() == "https://wesurvey.com/s2/11/test/?&uid=101", "survey retains original external URL and custom ID semantics");
|
|
Task duplicate = null;
|
|
onNextSet(() => duplicate = survey.GetSurveyCheckAnswer(decimalSurvey));
|
|
await survey.GetSurveyCheckAnswer(decimalSurvey);
|
|
Check(duplicate != null && duplicate.IsFaulted && drops.Count == 1 && presentations == 2 && remote["SurveyData"] == "[\"11\"]",
|
|
"actual survey rejects pending reentry and presents only after inventory plus marker confirmation");
|
|
await survey.GetSurveyCheckAnswer(decimalSurvey);
|
|
Check(drops.Count == 1, "already claimed survey never credits twice");
|
|
store.Write("SurveyReadSidData", "null", false).RequireAccepted();
|
|
try { new SurveyService(user, tables, store); throw new Exception("null survey JSON defaulted"); }
|
|
catch (Newtonsoft.Json.JsonException) { Check(true, "actual survey rejects present null JSON"); }
|
|
store.Write("SurveyReadSidData", "[\"11\"]").RequireAccepted();
|
|
|
|
var cache = new MailCache(tables.TbAutoDeliveryMail); cache.Load(user.UserId);
|
|
var first = new GameMail { Id = "m1", Type = EMailType.Personal, DropIds = new[]{601}, ExpireTime = ZZTimeHelper.UtcNow().AddDays(1) };
|
|
var second = new GameMail { Id = "m2", Type = EMailType.Global, DropIds = new[]{602}, ExpireTime = first.ExpireTime };
|
|
cache.Cache(new[]{first,second});
|
|
var server = System.Reflection.DispatchProxy.Create<ICustomServerMgr, MailServerProbe>();
|
|
var probe = (MailServerProbe)(object)server;
|
|
var response = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
Newtonsoft.Json.Linq.JObject sent = null;
|
|
probe.Post = (route,json) => { Check(route == MailUrls.UpdateState, "mail keeps original updateState route"); sent = Newtonsoft.Json.Linq.JObject.Parse(json); return response.Task; };
|
|
var handler = new BaseMailHandler(cache, server, user, store);
|
|
var claim = handler.TakeReward(new[]{first,second,first});
|
|
Check(!await handler.TakeReward(first) && sent["mailStates"].Count() == 2 && sent["mailStates"].All(m => ((int)m["state"] & (int)EMailState.RewardTaken) != 0),
|
|
"batch deduplicates frozen eligible mails, requests RewardTaken and blocks a concurrent single claim");
|
|
first.DropIds = new[]{999};
|
|
response.SetResult("{\"success\":true}");
|
|
Check(await claim && drops.Contains((601,true)) && drops.Contains((602,false)) && !drops.Any(d => d.id==999),
|
|
"real mail batch preserves pre-request rewards and personal inflation after state update");
|
|
Check(first.HasReward && second.HasReward && !await handler.TakeReward(first), "real mail cannot repeat a completed claim");
|
|
var third = new GameMail { Id = "m3", DropIds = new[]{603}, ExpireTime = first.ExpireTime };
|
|
cache.Cache(third);
|
|
response = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var delayed = handler.TakeReward(third);
|
|
int before = drops.Count;
|
|
handler.Close(); cache.Unload();
|
|
response.SetResult("{\"success\":true}");
|
|
Check(!await delayed && drops.Count == before && !third.HasReward, "closed mail handler discards old response before cache or reward mutation");
|
|
Pass("merged-real-survey-decimal-id-reentry-and-mail-batch-personal-stale-response");
|
|
}
|
|
|
|
static async Task MergedRewardActions(game.KVUserService user, Dictionary<string, string> remote, Action<Action> onNextSet)
|
|
{
|
|
var store = user.SaveStore;
|
|
int shown = 0, applied = 0;
|
|
await PlayerSaveAction.RunAsync(store, "test-reward", () =>
|
|
{
|
|
applied++;
|
|
store.Write("M2Reward", "intermediate").RequireAccepted();
|
|
PlayerSaveAction.Present(() => { Check(remote["M2Reward"] == "final", "presentation follows remote confirmation"); shown++; });
|
|
store.Write("M2Reward", "final").RequireAccepted();
|
|
Check(shown == 0, "nested presentation remains deferred during synchronous reward mutation");
|
|
}, () => store.Write("M2RewardPrimary", "aggregate").RequireAccepted(), user.LoginLifetime);
|
|
Check(applied == 1 && shown == 1 && remote["M2RewardPrimary"] == "aggregate", "same-action final receipt covers intermediate writes and primary save");
|
|
bool outsidePresented = false;
|
|
onNextSet(() =>
|
|
{
|
|
PlayerSaveAction.Present(() => outsidePresented = true);
|
|
store.Write("M2RewardRace", "unrelated").RequireAccepted();
|
|
});
|
|
// The last key remains unsent when a different action supersedes its receipt.
|
|
try
|
|
{
|
|
await PlayerSaveAction.RunAsync(store, "test-race", () =>
|
|
{
|
|
for (int i = 0; i < 11; i++) store.Write("M2RewardBatch" + i, "reward").RequireAccepted();
|
|
store.Write("M2RewardRace", "reward").RequireAccepted();
|
|
PlayerSaveAction.Present(() => shown++);
|
|
}, () => store.Write("M2RewardBatch0", "primary").RequireAccepted());
|
|
throw new Exception("unrelated superseding receipt confirmed a reward");
|
|
}
|
|
catch (InvalidOperationException) { Check(shown == 1 && outsidePresented, "collector ends before await; unrelated replacement cannot show reward success"); }
|
|
try
|
|
{
|
|
await PlayerSaveAction.RunAsync(store, "test-race", () => applied++, () => { });
|
|
throw new Exception("failed reward replayed");
|
|
}
|
|
catch (InvalidOperationException) { Check(applied == 1, "failed reward action stays reserved before mutation"); }
|
|
Check((await store.FlushAsync()).Confirmed, "unrelated later writes can be flushed independently");
|
|
try
|
|
{
|
|
await PlayerSaveAction.RunAsync(store, "test-local", () => store.Write("M2RewardLocal", "x", false), () => { });
|
|
throw new Exception("local-only reward confirmed");
|
|
}
|
|
catch (InvalidOperationException) { Check(!remote.ContainsKey("M2RewardLocal"), "local-only final action receipt is never cloud success"); }
|
|
Check(PlayerKVGameLoadGuard.TryEnter(out var guard), "reward rejected-write guard acquired");
|
|
using (guard)
|
|
{
|
|
try
|
|
{
|
|
await PlayerSaveAction.RunAsync(store, "test-rejected", () => store.Write("M2RewardRejected", "x"), () => { });
|
|
throw new Exception("ignored rejection reported reward success");
|
|
}
|
|
catch (InvalidOperationException) { Check(!remote.ContainsKey("M2RewardRejected"), "collector remembers rejection even if caller ignores return value"); }
|
|
}
|
|
Pass("merged-reward-final-receipts-presentation-race-rejection-and-no-replay");
|
|
}
|
|
|
|
static async Task MergedActivityTokens(game.KVUserService user, Dictionary<string, string> remote)
|
|
{
|
|
var store = user.SaveStore;
|
|
foreach (var malformed in new[] { "", "null", "{bad", "[]" })
|
|
{
|
|
store.Write("TransitionData", malformed, false).RequireAccepted();
|
|
var bad = new Activity.Services.EventTokenService(store);
|
|
try { bad.GetTokenCount(1); throw new Exception("invalid token save became zero balance"); }
|
|
catch (Newtonsoft.Json.JsonException) { Check(true, "invalid token JSON is not a zero balance"); }
|
|
}
|
|
store.Write("TransitionData", "{\"1\":10,\"2\":7}", false).RequireAccepted();
|
|
var tokens = new Activity.Services.EventTokenService(store);
|
|
int notifications = 0;
|
|
using var subscription = GContext.OnEvent<Activity.ActivityEventTokenBalanceChanged>().Subscribe(_ => notifications++);
|
|
Check(tokens.GetTokenCount(1) == 10 && tokens.GetTokenCount(3) == 0, "real token service preserves existing balances");
|
|
Check(PlayerKVGameLoadGuard.TryEnter(out var guard), "token mutation rejection guard acquired");
|
|
using (guard)
|
|
{
|
|
try { tokens.AddToken(1, 5); throw new Exception("guarded token mutation accepted"); }
|
|
catch (InvalidOperationException) { Check(tokens.GetTokenCount(1) == 10 && notifications == 0, "rejected token write changes neither balance nor notification"); }
|
|
}
|
|
tokens.AddToken(1, 5);
|
|
Check(tokens.GetTokenCount(1) == 15 && notifications == 1, "accepted token credit publishes its original notification");
|
|
Check(tokens.RemoveToken(1, 20) == 15 && tokens.GetTokenCount(1) == 0 && notifications == 2, "token debit preserves original clamp and removes empty entry");
|
|
tokens.SetTokens(new Dictionary<int, int> { [3] = 8, [4] = 9, [-1] = 10 });
|
|
tokens.ClearTokens(new[] { 3, 4 });
|
|
Check(tokens.GetAllTokens().Count == 0, "batch clear changes the complete candidate, not a per-key copy");
|
|
tokens.SetToken(5, 17);
|
|
tokens.ClearToken(999);
|
|
Check((await store.FlushAsync()).Confirmed, "token data confirms through original KV store");
|
|
var saved = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<int, int>>(remote["TransitionData"]);
|
|
Check(saved.Count == 1 && saved[5] == 17, "token wire JSON retains original key and dictionary format");
|
|
Pass("merged-real-activity-tokens-malformed-rejected-credit-debit-and-batch-save");
|
|
}
|
|
|
|
static async Task MergedPFStore(PlayFabMgr manager)
|
|
{
|
|
using var lifetime = new CancellationTokenSource();
|
|
var store = new PlayFabPlayerSaveStore(manager, lifetime.Token);
|
|
var pfTables = new cfg.Tables();
|
|
var pfDefinition = new cfg.SurveyData { ID=22, TailNumberList=new List<int>{7} };
|
|
pfTables.TbSurveyData.DataList.Add(pfDefinition);
|
|
GContext.container.Values[typeof(game.PlayerData)] = new game.PlayerData { lv=10 };
|
|
var pfSurvey = new SurveyService(new PFSurveyUser(), pfTables, store);
|
|
pfSurvey.GetSurveyMatchlist();
|
|
Check(pfSurvey.tableData == pfDefinition, "actual PF survey preserves hexadecimal identity tail");
|
|
var tokens = new Activity.Services.EventTokenService(store);
|
|
Check(tokens.GetTokenCount(91) == 0, "missing PF token key remains a valid empty balance");
|
|
tokens.AddToken(91, 4);
|
|
Check(Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<int, int>>(manager.GetLocalData("TransitionData"))[91] == 4,
|
|
"same real token consumer preserves the PF storage route");
|
|
var write = store.Write("M2PF", "unchanged");
|
|
Check(write.Accepted && store.Read("M2PF").Value == "unchanged", "PF adapter uses original local buffer");
|
|
Check((await store.ConfirmAsync(write.Receipt)).Status == PlayerSaveStatus.Buffered, "PF buffered receipt is not a cloud confirmation");
|
|
Check((await store.WriteAndConfirmAsync("M2PFImmediate", "value")).Confirmed, "PF immediate operation keeps original acknowledgement");
|
|
lifetime.Cancel();
|
|
try { tokens.GetTokenCount(91); throw new Exception("closed token service returned cached balance"); }
|
|
catch (InvalidOperationException) { Check(true, "closed token consumer rejects cached data before use"); }
|
|
Check(!store.IsCurrent && !store.Write("M2PF", "stale").Accepted && !store.Read("M2PF").Success,
|
|
"captured PF lifetime does not follow replacement login");
|
|
Pass("merged-PF-original-provider-and-frozen-lifetime");
|
|
}
|
|
}
|
|
#endif
|
|
|