feat(upserver): 保存M2客户端KV迁移至本人统计写检查点

保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
This commit is contained in:
Liubing\LB
2026-09-30 19:19:05 +08:00
parent 1c0cdf7a99
commit 6813da149a
511 changed files with 151858 additions and 2376 deletions

9
AGENTS.md Normal file
View File

@@ -0,0 +1,9 @@
# 项目通用开发规则
- KV模式的迁移目标是由KV管理器及其接线承接原PlayFab相关能力,不只迁个人存档;等级榜、公开资料、好友和匹配是原PlayFab调用的既有功能,不得当作新增产品需求、默认延期或直接转交服务端。先核客户端已有KV能力及等价调用映射;只有明确证实缺少必要能力时,才把具体接口证据集中写入沟通文档。不得反复要求用户确认“是否存KV”、新ID或原邮件流程。已审同合同替换连续实施,确有新合同或架构差异时一次合并评审,避免逐功能送审。
- 整个迁移流程、所有阶段与批次均只做平移,不限于 M2。全部迁移完成前,仅修改完成迁移所必需的存储、身份和接口接线,保留原业务规则、调用顺序、同步/异步方式、返回值、异常处理和界面流程。不得夹带额外业务修正、优化或代码重构;不得以“更安全”“更合理”或“顺手修复”为由改变原逻辑。
- 发现原有业务缺陷、代码结构问题或可优化点,统一记录到迁移完成后的待办,不在任何迁移阶段穿插处理。必须等整个迁移流程全部完成后,再由用户另行授权修正;单个阶段完成不构成提前修正的许可。此前方案或评审包含此类扩展时,以此规则及用户最新要求为准。
- 最终闲置实现清理仅限本次迁移造成且经核实已无消费者的实现,不得借清理重构代码或删除原有有效功能。确有无法保持原流程的迁移障碍,先说明具体差异,不自行改写业务规则或冻结功能。
- 在满足已确认的功能、正确性和必要安全边界的前提下,优先选择最简单、最直观、最容易维护的实现。抽象应服务于实际调用者;不要为假设中的扩展增加接口、适配器、管理器、状态机或重复保护层。
- 修改代码前完成相关方案的完整评审,记录裁决和开发边界。评审新增抽象时,说明它解决的具体问题、实际调用点,以及为何复用或小幅修改现有实现不足;无法说明必要性的层次不要引入。
- 同一事实保持单一来源,同一业务流程避免多重开关和重复状态。迁移中的临时路径须登记,最终通关前恢复或清理;最终代码删除闲置实现,但保留仍在使用的原有功能。

View File

@@ -503,7 +503,7 @@ public class Boostrap : MonoBehaviour
private async Task AfterAssetUpdateV2() private async Task AfterAssetUpdateV2()
{ {
bsLoading.SetProgress(0.15f); bsLoading.SetProgress(0.15f);
await SubBoostrap.RegisterServices(ctx.gameObject); await SubBoostrap.RegisterServices(ctx.gameObject, config);
bsLoading.SetProgress(0.16f); bsLoading.SetProgress(0.16f);
await SubBoostrap.AfterAssetUpdate(config, bsLoading.SetProgress); await SubBoostrap.AfterAssetUpdate(config, bsLoading.SetProgress);
bsLoading.SetProgress(0.18f); bsLoading.SetProgress(0.18f);

View File

@@ -9,6 +9,10 @@ namespace Activity.Data
/// </summary> /// </summary>
public class EventDataRepository : IEventDataRepository public class EventDataRepository : IEventDataRepository
{ {
private readonly game.PlayerDataStorage.IPlayerSaveStore store;
public EventDataRepository(game.PlayerDataStorage.IPlayerSaveStore store)
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
public void Save<T>(T data) where T : IEventData public void Save<T>(T data) where T : IEventData
{ {
if (data == null) if (data == null)
@@ -23,7 +27,7 @@ namespace Activity.Data
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"[EventDataRepository] Saving {typeof(T).Name}: {json}"); Debug.Log($"[EventDataRepository] Saving {typeof(T).Name}: {json}");
#endif #endif
PlayFabMgr.Instance?.UpdateUserDataValue(typeof(T).Name, json); store.Write(typeof(T).Name, json).RequireAccepted();
} }
catch (Exception e) catch (Exception e)
{ {
@@ -35,7 +39,7 @@ namespace Activity.Data
{ {
try try
{ {
var dataStr = PlayFabMgr.Instance?.GetLocalData(typeof(T).Name); var dataStr = store.Read(typeof(T).Name).ValueOrThrow();
if (string.IsNullOrEmpty(dataStr)) return new T(); if (string.IsNullOrEmpty(dataStr)) return new T();
// #if UNITY_EDITOR // #if UNITY_EDITOR
// Debug.Log($"[EventDataRepository] Load {typeof(T).Name}: {dataStr}"); // Debug.Log($"[EventDataRepository] Load {typeof(T).Name}: {dataStr}");
@@ -52,7 +56,7 @@ namespace Activity.Data
public bool Exists<T>() where T : IEventData public bool Exists<T>() where T : IEventData
{ {
var dataStr = PlayFabMgr.Instance?.GetLocalData(typeof(T).Name); var dataStr = store.Read(typeof(T).Name).ValueOrThrow();
return !string.IsNullOrEmpty(dataStr); return !string.IsNullOrEmpty(dataStr);
} }
} }

View File

@@ -28,6 +28,10 @@ namespace Activity.Services
/// </summary> /// </summary>
public class EventTokenService : IEventTokenService public class EventTokenService : IEventTokenService
{ {
private readonly game.PlayerDataStorage.IPlayerSaveStore store;
public EventTokenService(game.PlayerDataStorage.IPlayerSaveStore store)
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
#region 常量与字段 #region 常量与字段
private const string TransitionDataKey = "TransitionData"; private const string TransitionDataKey = "TransitionData";
@@ -75,7 +79,7 @@ namespace Activity.Services
{ {
try try
{ {
var dataStr = PlayFabMgr.Instance?.GetLocalData(TransitionDataKey); var dataStr = store.Read(TransitionDataKey).ValueOrThrow();
if (!string.IsNullOrEmpty(dataStr)) if (!string.IsNullOrEmpty(dataStr))
{ {
_tokenData.Clear(); _tokenData.Clear();
@@ -106,7 +110,7 @@ namespace Activity.Services
try try
{ {
var json = Newtonsoft.Json.JsonConvert.SerializeObject(_tokenData); var json = Newtonsoft.Json.JsonConvert.SerializeObject(_tokenData);
PlayFabMgr.Instance?.UpdateUserDataValue(TransitionDataKey, json); store.Write(TransitionDataKey, json).RequireAccepted();
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"[EventTokenService] Saved {_tokenData.Count} token entries to PlayFab"); Debug.Log($"[EventTokenService] Saved {_tokenData.Count} token entries to PlayFab");
#endif #endif

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -12,6 +12,8 @@ using UnityEngine;
public class AquariumManager public class AquariumManager
{ {
public const string AquariumEventDataNewKey = "AquariumEventDataNewKey"; public const string AquariumEventDataNewKey = "AquariumEventDataNewKey";
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore;
readonly IUserService loginUser;
private cfg.Tables _tables; private cfg.Tables _tables;
private ICustomServerMgr customServerMgr; private ICustomServerMgr customServerMgr;
public AquariumManagerData data { get; private set; } public AquariumManagerData data { get; private set; }
@@ -21,12 +23,15 @@ public class AquariumManager
public List<AquariumSlotData> sellList; public List<AquariumSlotData> sellList;
public AquariumManager() public AquariumManager()
{ {
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
loginUser = GContext.container.Resolve<IUserService>();
_tables = GContext.container.Resolve<Tables>(); _tables = GContext.container.Resolve<Tables>();
customServerMgr = GContext.container.Resolve<ICustomServerMgr>(); customServerMgr = GContext.container.Resolve<ICustomServerMgr>();
} }
public static AquariumManagerData GetAquariumManagerData() public static AquariumManagerData GetAquariumManagerData(game.PlayerDataStorage.IPlayerSaveStore store = null)
{ {
string dataStr = PlayFabMgr.Instance.GetLocalData(AquariumEventDataNewKey); var source = store ?? GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
string dataStr = source.Read(AquariumEventDataNewKey).ValueOrThrow();
if (!string.IsNullOrEmpty(dataStr)) if (!string.IsNullOrEmpty(dataStr))
{ {
try try
@@ -43,7 +48,8 @@ public class AquariumManager
public async Task Synchrodata() public async Task Synchrodata()
{ {
//从服务器拿数据 //从服务器拿数据
var dataSever = GetAquariumManagerData(); if (!saveStore.IsCurrent) throw new OperationCanceledException();
var dataSever = GetAquariumManagerData(saveStore);
if (dataSever == null) if (dataSever == null)
{ {
//新数据 //新数据
@@ -63,8 +69,9 @@ public class AquariumManager
return; return;
} }
GetAquariumDataRequest request = new GetAquariumDataRequest(); GetAquariumDataRequest request = new GetAquariumDataRequest();
request.playFabId = GContext.container.Resolve<IUserService>().UserId; request.playFabId = loginUser.UserId;
string result = await customServerMgr.CustomServerPost("aquarium/GetAquariumData", Newtonsoft.Json.JsonConvert.SerializeObject(request)); string result = await customServerMgr.CustomServerPost("aquarium/GetAquariumData", Newtonsoft.Json.JsonConvert.SerializeObject(request));
if (!saveStore.IsCurrent) throw new OperationCanceledException();
AquariumServerData serverData = null; AquariumServerData serverData = null;
if (result != null) if (result != null)
{ {
@@ -611,7 +618,7 @@ public class AquariumManager
} }
void UpdateUserData() void UpdateUserData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(AquariumEventDataNewKey, data.Serialize()); saveStore.Write(AquariumEventDataNewKey, data.Serialize()).RequireAccepted();
} }
void SaveData(bool all = false) void SaveData(bool all = false)
{ {
@@ -637,7 +644,7 @@ public class AquariumManager
if (all) if (all)
{ {
UpdateAquariumAllDataRequest request = new UpdateAquariumAllDataRequest(); UpdateAquariumAllDataRequest request = new UpdateAquariumAllDataRequest();
request.playFabId = GContext.container.Resolve<IUserService>().UserId; request.playFabId = loginUser.UserId;
request.data = new AquariumServerData() request.data = new AquariumServerData()
{ {
startTime = data.startTime, startTime = data.startTime,
@@ -653,7 +660,7 @@ public class AquariumManager
else else
{ {
UpdateSlotDatasRequest request = new UpdateSlotDatasRequest(); UpdateSlotDatasRequest request = new UpdateSlotDatasRequest();
request.playFabId = GContext.container.Resolve<IUserService>().UserId; request.playFabId = loginUser.UserId;
request.slotDatas = data.slotDatas; request.slotDatas = data.slotDatas;
request.cardLv = string.Join(',', cardLv); request.cardLv = string.Join(',', cardLv);
customServerMgr.CustomServerPost("aquarium/UpdateSlotData", Newtonsoft.Json.JsonConvert.SerializeObject(request)); customServerMgr.CustomServerPost("aquarium/UpdateSlotData", Newtonsoft.Json.JsonConvert.SerializeObject(request));

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -10,6 +10,8 @@ using UnityEngine;
public class CampDataMM public class CampDataMM
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
private cfg.Tables _tables; private cfg.Tables _tables;
MMTService _MMService; MMTService _MMService;
public CampDataMM(cfg.Tables tables, MMTService mmService) public CampDataMM(cfg.Tables tables, MMTService mmService)
@@ -142,7 +144,7 @@ public class CampDataMM
_isFix = isFix; _isFix = isFix;
if (isSave) if (isSave)
{ {
PlayFabMgr.Instance.UpdateUserDataValue("IsFix", _isFix.ToString()); SaveStore.Write("IsFix", _isFix.ToString()).RequireAccepted();
} }
} }
@@ -248,7 +250,7 @@ public class CampDataMM
UpdateCurrentData(); UpdateCurrentData();
} }
CampDataDic["CampId"] = Id; CampDataDic["CampId"] = Id;
PlayFabMgr.Instance.UpdateUserDataValue("CampDataDic", JsonMapper.ToJson(CampDataDic)); ; SaveStore.Write("CampDataDic", JsonMapper.ToJson(CampDataDic)).RequireAccepted(); ;
} }
else else
{ {
@@ -276,7 +278,7 @@ public class CampDataMM
if (NextMapID == -1 || !_tables.TbConstruction.DataMap.ContainsKey(NextMapID)) if (NextMapID == -1 || !_tables.TbConstruction.DataMap.ContainsKey(NextMapID))
{ {
_isOpenSkyscraper = "1"; _isOpenSkyscraper = "1";
PlayFabMgr.Instance.UpdateUserDataValue("IsOpenSkyscraper", _isOpenSkyscraper); SaveStore.Write("IsOpenSkyscraper", _isOpenSkyscraper).RequireAccepted();
return; return;
} }
Debug.LogError( Debug.LogError(
@@ -353,7 +355,7 @@ public class CampDataMM
if (NextMapID == -1 || !_tables.TbConstruction.DataMap.ContainsKey(NextMapID)) if (NextMapID == -1 || !_tables.TbConstruction.DataMap.ContainsKey(NextMapID))
{ {
_isOpenSkyscraper = "1"; _isOpenSkyscraper = "1";
PlayFabMgr.Instance.UpdateUserDataValue("IsOpenSkyscraper", _isOpenSkyscraper); SaveStore.Write("IsOpenSkyscraper", _isOpenSkyscraper).RequireAccepted();
} }
} }
} }

View File

@@ -4,6 +4,7 @@ using System;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
using game.Account; using game.Account;
@@ -39,23 +40,50 @@ public class CustomServerMgr : ICustomServerMgr
private CookieContainer _cookie; private CookieContainer _cookie;
private readonly Func<CookieContainer, HttpMessageHandler> handlerFactory; private readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
private readonly SynchronizationContext ownerContext;
public int SessionVersion { get; private set; } public int SessionVersion { get; private set; }
public event Action SessionChanged; public event Action SessionChanged;
PlayerKVAccountLease kvLease; PlayerKVAccountLease kvLease;
bool kvOnly; bool kvOnly;
bool kvBusinessReady;
bool kvBusinessPublished;
int pfRequests; int pfRequests;
internal event Action<int> KVUnauthorized; internal event Action<int> KVUnauthorized;
internal bool IsFreshForKVLoad => pfRequests==0 && kvLease==null; internal bool IsFreshForKVLoad => pfRequests==0 && kvLease==null;
internal void BindKVForValidation(PlayerKVAccountLease lease) internal void BindKVForValidation(PlayerKVAccountLease lease)
{ {
if(lease==null || !lease.IsCurrent || lease.ApiBaseUri!=ApiBaseUri || !IsFreshForKVLoad) if(lease==null || lease.IsFormal || !lease.IsCurrent || lease.ApiBaseUri!=ApiBaseUri || !IsFreshForKVLoad)
throw new InvalidOperationException("Fresh matching KV lease required."); throw new InvalidOperationException("Fresh matching KV lease required.");
kvOnly=true; SessionVersion++; kvLease=lease; _cookie=null; kvOnly=true; kvBusinessReady=false; kvBusinessPublished=false; SessionVersion++; kvLease=lease; _cookie=null;
var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke(); var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke();
} }
internal void BindKVForGame(PlayerKVAccountLease lease)
{
if(lease==null || !lease.IsFormal || !lease.IsCurrent || lease.ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl ||
ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl || !IsFreshForKVLoad)
throw new InvalidOperationException("Fresh formal KV lease required.");
kvOnly=true; kvBusinessReady=false; kvBusinessPublished=false; SessionVersion++; kvLease=lease; _cookie=null;
var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke();
}
internal void EnableKVBusiness(long epoch)
{
if(!kvOnly || kvBusinessReady || kvBusinessPublished || kvLease==null || !kvLease.IsFormal || !kvLease.IsCurrent ||
kvLease.Epoch!=epoch || kvLease.ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl ||
ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl)
throw new InvalidOperationException("Current formal KV lease required for business transport.");
kvBusinessPublished=true; kvBusinessReady=true;
}
internal void DisableKVBusiness()
{
if(!kvOnly || !kvBusinessReady) return;
kvBusinessReady=false; SessionVersion++;
try { SessionChanged?.Invoke(); }
catch(Exception ex) { Debug.LogError($"KV session change failed: {ex.GetType().Name}"); }
}
internal Task ClearKVForGame() => ClearKVForValidation();
internal Task ClearKVForValidation() internal Task ClearKVForValidation()
{ {
SessionVersion++; var previous=kvLease; kvLease=null; kvBusinessReady=false; SessionVersion++; var previous=kvLease; kvLease=null; _cookie=null;
var closing=previous?.CloseAsync() ?? Task.CompletedTask; var closing=previous?.CloseAsync() ?? Task.CompletedTask;
SessionChanged?.Invoke(); return closing; SessionChanged?.Invoke(); return closing;
} }
@@ -74,13 +102,16 @@ public class CustomServerMgr : ICustomServerMgr
} }
public CookieContainer Cookie => _cookie ?? throw new System.NullReferenceException(); public CookieContainer Cookie => kvOnly
? (kvBusinessReady && kvLease?.IsFormal==true ? kvLease.FormalCookies : null) ?? throw new NullReferenceException()
: _cookie ?? throw new NullReferenceException();
public Uri ApiBaseUri { get; } public Uri ApiBaseUri { get; }
public CustomServerMgr(IConfig config) : this(config, null) { } public CustomServerMgr(IConfig config) : this(config, null) { }
public CustomServerMgr(IConfig config, Func<CookieContainer, HttpMessageHandler> handlerFactory) public CustomServerMgr(IConfig config, Func<CookieContainer, HttpMessageHandler> handlerFactory)
{ {
ownerContext=SynchronizationContext.Current;
this.handlerFactory = handlerFactory ?? (cookies => new HttpClientHandler this.handlerFactory = handlerFactory ?? (cookies => new HttpClientHandler
{ {
CookieContainer = cookies, UseCookies = true, AllowAutoRedirect = false CookieContainer = cookies, UseCookies = true, AllowAutoRedirect = false
@@ -125,7 +156,42 @@ public class CustomServerMgr : ICustomServerMgr
// Unity callers use the main thread. Capture a fixed transport before the first await. // Unity callers use the main thread. Capture a fixed transport before the first await.
private async Task<string> SendAsync(HttpMethod method, string url, string body, string contentType) private async Task<string> SendAsync(HttpMethod method, string url, string body, string contentType)
{ {
if(kvOnly) throw new InvalidOperationException("Only controlled validation routes are enabled."); if(kvOnly)
{
if(ownerContext!=null && !ReferenceEquals(SynchronizationContext.Current,ownerContext))
{
var completion=new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
ownerContext.Post(async _=>
{
try { completion.TrySetResult(await SendAsync(method,url,body,contentType)); }
catch(Exception ex) { completion.TrySetException(ex); }
},null);
return await completion.Task.ConfigureAwait(false);
}
if(!kvBusinessReady) throw new InvalidOperationException("Formal KV business transport is not ready.");
var lease=kvLease; var kvVersion=SessionVersion;
if(lease==null || !lease.IsFormal || !lease.IsCurrent) return null;
try
{
var response=await lease.SendAsync(method,url,body,contentType);
if(!kvBusinessReady || kvVersion!=SessionVersion || !ReferenceEquals(lease,kvLease) || !response.IsCurrent)
return null;
if(response.StatusCode==(int)HttpStatusCode.OK) return response.Body;
if(response.StatusCode==(int)HttpStatusCode.Unauthorized || response.StatusCode==(int)HttpStatusCode.Forbidden)
{
DisableKVBusiness();
KVUnauthorized?.Invoke(SessionVersion);
}
else if(response.StatusCode.HasValue)
Debug.LogError($"API request failed: HTTP {response.StatusCode.Value}");
return null;
}
catch(Exception e)
{
if(kvVersion==SessionVersion) Debug.LogError($"API request failed: {e.GetType().Name}");
return null;
}
}
pfRequests++; pfRequests++;
var transport = client; var transport = client;
var version = SessionVersion; var version = SessionVersion;

View File

@@ -0,0 +1,52 @@
using System.Threading;
using System.Threading.Tasks;
using game.PlayerDataStorage;
internal static class KVMgr
{
internal static PlayerKVKeyReadResult ReadKV(PlayerKVGameSessionHandle handle, string key)
{
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.ReadLocal))
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.LoadGuardActive, handle);
if (handle == null || !KVSnapshot.ValidKey(key))
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.InvalidInput, handle);
var read = handle.ReadLocalSnapshot();
if (!read.Success)
return new PlayerKVKeyReadResult(read.Error == PlayerKVLocalReadError.StaleOrClosing
? PlayerKVKeyReadError.StaleOrClosing : PlayerKVKeyReadError.NotReady, handle);
var found = read.Data.TryGetValue(key, out var value);
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.None, handle, found, value);
}
internal static PlayerKVEditResult WriteKV(PlayerKVGameSessionHandle handle,
string key, string value, bool remoteStore = true)
{
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Update))
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Blocked));
if (handle == null)
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Validation));
return handle.Update(key, value, remoteStore);
}
internal static Task<PlayerKVFlushResult> FlushKVAsync(PlayerKVGameSessionHandle handle,
CancellationToken waitToken = default)
{
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Cancelled,
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
if (handle == null)
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
return handle.FlushAsync(waitToken);
}
internal static Task<PlayerKVTicketResult> WaitForKVTicketAsync(PlayerKVGameSessionHandle handle,
PlayerKVSyncTicket ticket, CancellationToken waitToken = default)
{
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Cancelled,
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
if (handle == null)
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
return handle.WaitForTicketAsync(ticket, waitToken);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e96f727fbe08473c89bb88150927d2be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -320,6 +320,8 @@ public class PlayFabMgr : MonoBehaviour
GameDebug.LogError("[PlayFabMgr]UpdateUserData Error: {0}", error.ToString()); GameDebug.LogError("[PlayFabMgr]UpdateUserData Error: {0}", error.ToString());
} }
internal void FlushBufferedData() => SyncData();
public async Task<bool> SyncDataImmidiate(string key, string data) public async Task<bool> SyncDataImmidiate(string key, string data)
{ {
#if UNITY_EDITOR && !SERVICE_HOST #if UNITY_EDITOR && !SERVICE_HOST
@@ -353,51 +355,4 @@ public class PlayFabMgr : MonoBehaviour
return await taskSource.Task; return await taskSource.Task;
} }
// BEGIN KV explicit fixed-handle access
internal static PlayerKVKeyReadResult ReadKV(PlayerKVGameSessionHandle handle, string key)
{
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.ReadLocal))
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.LoadGuardActive, handle);
if (handle == null || !KVSnapshot.ValidKey(key))
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.InvalidInput, handle);
var read = handle.ReadLocalSnapshot();
if (!read.Success)
return new PlayerKVKeyReadResult(read.Error == PlayerKVLocalReadError.StaleOrClosing
? PlayerKVKeyReadError.StaleOrClosing : PlayerKVKeyReadError.NotReady, handle);
var found = read.Data.TryGetValue(key, out var value);
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.None, handle, found, value);
}
internal static PlayerKVEditResult WriteKV(PlayerKVGameSessionHandle handle,
string key, string value, bool remoteStore = true)
{
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Update))
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Blocked));
if (handle == null)
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Validation));
return handle.Update(key, value, remoteStore);
}
internal static Task<PlayerKVFlushResult> FlushKVAsync(PlayerKVGameSessionHandle handle,
CancellationToken waitToken = default)
{
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Cancelled,
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
if (handle == null)
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
return handle.FlushAsync(waitToken);
}
internal static Task<PlayerKVTicketResult> WaitForKVTicketAsync(PlayerKVGameSessionHandle handle,
PlayerKVSyncTicket ticket, CancellationToken waitToken = default)
{
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Cancelled,
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
if (handle == null)
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
return handle.WaitForTicketAsync(ticket, waitToken);
}
// END KV explicit fixed-handle access
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using UnityEngine; using UnityEngine;
@@ -8,6 +8,8 @@ using System.Collections.Generic;
public class BargainPackData public class BargainPackData
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
private readonly Tables _tables = GContext.container.Resolve<Tables>(); private readonly Tables _tables = GContext.container.Resolve<Tables>();
private readonly TbEventPackManager _epm = GContext.container.Resolve<Tables>().TbEventPackManager; private readonly TbEventPackManager _epm = GContext.container.Resolve<Tables>().TbEventPackManager;
private readonly TbSpecialPack _spp = GContext.container.Resolve<Tables>().TbSpecialPack; private readonly TbSpecialPack _spp = GContext.container.Resolve<Tables>().TbSpecialPack;
@@ -178,8 +180,8 @@ public class BargainPackData
public void SaveData() public void SaveData()
{ {
//Debug.LogError("Testing, data not saved for debug purpose."); //Debug.LogError("Testing, data not saved for debug purpose.");
PlayFabMgr.Instance.UpdateUserDataValue("GeneralEventPackData", SaveStore.Write("GeneralEventPackData",
Newtonsoft.Json.JsonConvert.SerializeObject(_data)); Newtonsoft.Json.JsonConvert.SerializeObject(_data)).RequireAccepted();
} }
public void LoadData(GeneralEventPackData data) public void LoadData(GeneralEventPackData data)

View File

@@ -33,6 +33,7 @@ public struct BpVipUnlock
public class BattlePassData public class BattlePassData
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
public BattlePassRecord BattlePassRecord; public BattlePassRecord BattlePassRecord;
public BattleTask BattleTask; public BattleTask BattleTask;
@@ -114,7 +115,7 @@ public class BattlePassData
{ {
string battlePassRecord = BattlePassRecord.SerializeObject(); string battlePassRecord = BattlePassRecord.SerializeObject();
string battleTask = BattleTask.SerializeObject(); string battleTask = BattleTask.SerializeObject();
PlayFabMgr.Instance.UpdateUserDataValue("BattlePassRecordTask", battlePassRecord + "#" + battleTask); playerSaveStore.Write("BattlePassRecordTask", battlePassRecord + "#" + battleTask).RequireAccepted();
} }
public void SaveBattlePassRecord() public void SaveBattlePassRecord()
{ {

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using System; using System;
@@ -6,6 +6,8 @@ using System.Collections.Generic;
public class BuffDataCenter public class BuffDataCenter
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
private cfg.Tables _tables; private cfg.Tables _tables;
public BuffDataCenter(cfg.Tables tables) public BuffDataCenter(cfg.Tables tables)
{ {
@@ -202,7 +204,7 @@ public class BuffDataCenter
DateTime now = ZZTimeHelper.UtcNow().UtcNowOffset(); DateTime now = ZZTimeHelper.UtcNow().UtcNowOffset();
mapBuffData.buffEndTime = now.AddSeconds(buffData.CountDown); mapBuffData.buffEndTime = now.AddSeconds(buffData.CountDown);
mapBuffDataList.Add(mapID, mapBuffData); mapBuffDataList.Add(mapID, mapBuffData);
PlayFabMgr.Instance.UpdateUserDataValue("BuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(mapBuffDataList)); SaveStore.Write("BuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(mapBuffDataList)).RequireAccepted();
return true; return true;
} }
} }
@@ -291,12 +293,12 @@ public class BuffDataCenter
if (targetBuffDataList[i].buffID == fishBuffTimeData.buffID && targetBuffDataList[i].sourceID == fishBuffTimeData.sourceID) if (targetBuffDataList[i].buffID == fishBuffTimeData.buffID && targetBuffDataList[i].sourceID == fishBuffTimeData.sourceID)
{ {
targetBuffDataList[i] = fishBuffTimeData; targetBuffDataList[i] = fishBuffTimeData;
PlayFabMgr.Instance.UpdateUserDataValue("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)); SaveStore.Write("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)).RequireAccepted();
return; return;
} }
} }
targetBuffDataList.Add(fishBuffTimeData); targetBuffDataList.Add(fishBuffTimeData);
PlayFabMgr.Instance.UpdateUserDataValue("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)); SaveStore.Write("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)).RequireAccepted();
} }
} }
else else
@@ -307,7 +309,7 @@ public class BuffDataCenter
public void ClearTargetBuff() public void ClearTargetBuff()
{ {
targetBuffDataList.Clear(); targetBuffDataList.Clear();
PlayFabMgr.Instance.UpdateUserDataValue("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)); SaveStore.Write("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)).RequireAccepted();
} }
/// <summary> /// <summary>
/// CollectionTargetWeight的特殊处理 /// CollectionTargetWeight的特殊处理
@@ -345,7 +347,7 @@ public class BuffDataCenter
else if (fishItemId == eTKeepsake.FishIDList[collectionTargetWeight.TargetFishIndex - 1]) else if (fishItemId == eTKeepsake.FishIDList[collectionTargetWeight.TargetFishIndex - 1])
{ {
item.isEnd = true; item.isEnd = true;
PlayFabMgr.Instance.UpdateUserDataValue("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)); SaveStore.Write("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)).RequireAccepted();
break; break;
} }
} }
@@ -455,7 +457,7 @@ public class BuffDataCenter
fishBuffTimeData.addDown = buff.CountDown; fishBuffTimeData.addDown = buff.CountDown;
showFishBuffIds.Add(fishBuffTimeData); showFishBuffIds.Add(fishBuffTimeData);
} }
PlayFabMgr.Instance.UpdateUserDataValue("GlobalBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(globalBuffDataList)); SaveStore.Write("GlobalBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(globalBuffDataList)).RequireAccepted();
GContext.Publish(new AddGlobalBuffEvent(buffID)); GContext.Publish(new AddGlobalBuffEvent(buffID));
} }
} }
@@ -559,7 +561,7 @@ public class BuffDataCenter
} }
AddGlobalBuff(buff.BuffID); AddGlobalBuff(buff.BuffID);
fishDailyBuff.isEnd = false; fishDailyBuff.isEnd = false;
PlayFabMgr.Instance.UpdateUserDataValue("FishDailyBuff", Newtonsoft.Json.JsonConvert.SerializeObject(fishDailyBuff)); SaveStore.Write("FishDailyBuff", Newtonsoft.Json.JsonConvert.SerializeObject(fishDailyBuff)).RequireAccepted();
return fishDailyBuff; return fishDailyBuff;
} }
} }

View File

@@ -1,4 +1,4 @@
using Activity.Services; using Activity.Services;
using asap.core; using asap.core;
using Assets.Scripts.UI.SocialRush; using Assets.Scripts.UI.SocialRush;
using cfg; using cfg;
@@ -56,6 +56,17 @@ namespace GameCore
{ {
public partial class FishingEventData : IDisposable public partial class FishingEventData : IDisposable
{ {
game.PlayerDataStorage.IPlayerSaveStore boundSaveStore;
[Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore
{
get => boundSaveStore;
set
{
ThanksGivingPackData.BindSaveStore(value);
ChainPackWithProgressMigrationData.BindSaveStore(value);
boundSaveStore = value;
}
}
private cfg.Tables _tables; private cfg.Tables _tables;
private MMTService _mmtService; private MMTService _mmtService;
private IEventTokenService _eventTokenService; private IEventTokenService _eventTokenService;
@@ -578,7 +589,7 @@ namespace GameCore
id = t.ID, id = t.ID,
endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(), endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(),
}; };
PlayFabMgr.Instance.UpdateUserDataValue("FixedTimeEventDic", JsonMapper.ToJson(fixedTimeEventDic)); playerSaveStore.Write("FixedTimeEventDic", JsonMapper.ToJson(fixedTimeEventDic)).RequireAccepted();
} }
} }
switch (t.Type) switch (t.Type)
@@ -846,7 +857,7 @@ namespace GameCore
private bool Condition_NoRechargeSeconds(int timeSeconds) private bool Condition_NoRechargeSeconds(int timeSeconds)
{ {
var lastPaymentStr = PlayFabMgr.Instance.GetLocalData("LastPaymentTime"); var lastPaymentStr = playerSaveStore.Read("LastPaymentTime").ValueOrThrow();
DateTime curTime = ZZTimeHelper.UtcNow().UtcNowOffset(); DateTime curTime = ZZTimeHelper.UtcNow().UtcNowOffset();
DateTime endTime = curTime; DateTime endTime = curTime;
if (string.IsNullOrEmpty(lastPaymentStr) || !lastPaymentStr.Contains("__")) if (string.IsNullOrEmpty(lastPaymentStr) || !lastPaymentStr.Contains("__"))
@@ -866,7 +877,8 @@ namespace GameCore
} }
public static bool Condition_NoRechargeDay(int timeDay = 7) public static bool Condition_NoRechargeDay(int timeDay = 7)
{ {
var lastPaymentStr = PlayFabMgr.Instance.GetLocalData("LastPaymentTime"); var store = GContext.container.Resolve<FishingEventData>().playerSaveStore;
var lastPaymentStr = store.Read("LastPaymentTime").ValueOrThrow();
if (!string.IsNullOrEmpty(lastPaymentStr) && lastPaymentStr.Contains("__")) if (!string.IsNullOrEmpty(lastPaymentStr) && lastPaymentStr.Contains("__"))
return false; return false;
DateTime curTime = ZZTimeHelper.UtcNow(); DateTime curTime = ZZTimeHelper.UtcNow();
@@ -1052,7 +1064,7 @@ namespace GameCore
case 2: case 2:
// Debug.Log($"[EventBreak] Id {e.ID} activated."); // Debug.Log($"[EventBreak] Id {e.ID} activated.");
EventBreakAct.Ctx = EventBreakTableContext.ReadTables(e.ID, GContext.container.Resolve<Tables>()); EventBreakAct.Ctx = EventBreakTableContext.ReadTables(e.ID, GContext.container.Resolve<Tables>());
var playfabData = EventBreakPfData.Deserialize(PlayFabMgr.Instance.GetLocalData(EventBreakPfData.PfKey)); var playfabData = EventBreakPfData.Deserialize(playerSaveStore.Read(EventBreakPfData.PfKey).ValueOrThrow());
var eventBreakModel = GContext.container.Resolve<DrillModel>(); var eventBreakModel = GContext.container.Resolve<DrillModel>();
if (eventBreakModel == null) if (eventBreakModel == null)
{ {
@@ -1074,7 +1086,7 @@ namespace GameCore
break; break;
case 4: case 4:
var canModel = GContext.container.Resolve<EventCanModel>(); var canModel = GContext.container.Resolve<EventCanModel>();
var ecPlayfabString = PlayFabMgr.Instance.GetLocalData(EventCanPlayfabData.PlayFabKey); var ecPlayfabString = playerSaveStore.Read(EventCanPlayfabData.PlayFabKey).ValueOrThrow();
var ecPpString = PlayerPrefs.GetString(EventCanPlayerPreferenceData.PlayerPrefsKey); var ecPpString = PlayerPrefs.GetString(EventCanPlayerPreferenceData.PlayerPrefsKey);
var ecPlayfabData = EventCanPlayfabData.Deserialize(ecPlayfabString); var ecPlayfabData = EventCanPlayfabData.Deserialize(ecPlayfabString);
var ecPlayerPrefsData = EventCanPlayerPreferenceData.Deserialize(ecPpString); var ecPlayerPrefsData = EventCanPlayerPreferenceData.Deserialize(ecPpString);
@@ -1150,7 +1162,7 @@ namespace GameCore
SetTurntable(t); SetTurntable(t);
break; break;
case 3: case 3:
var pfString = PlayFabMgr.Instance.GetLocalData(EventLuckMagicPlayfabData.Key); var pfString = playerSaveStore.Read(EventLuckMagicPlayfabData.Key).ValueOrThrow();
var pfData = EventLuckMagicPlayfabData.Deserialize(pfString); var pfData = EventLuckMagicPlayfabData.Deserialize(pfString);
var ppString = PlayerPrefs.GetString(EventLuckMagicPlayerPreferenceData.Key); var ppString = PlayerPrefs.GetString(EventLuckMagicPlayerPreferenceData.Key);
var ppData = EventLuckMagicPlayerPreferenceData.Deserialize(ppString); var ppData = EventLuckMagicPlayerPreferenceData.Deserialize(ppString);
@@ -1339,7 +1351,7 @@ namespace GameCore
SaveTransitionData(turntableEvent.ID, TurntableCoupon); SaveTransitionData(turntableEvent.ID, TurntableCoupon);
} }
TurntableRed(); TurntableRed();
PlayFabMgr.Instance.UpdateUserDataValue("TurntableRoll", TurntableCoupon.ToString()); playerSaveStore.Write("TurntableRoll", TurntableCoupon.ToString()).RequireAccepted();
} }
public void RewardTurntableRoll(int value, bool remote = true) public void RewardTurntableRoll(int value, bool remote = true)
{ {
@@ -1358,7 +1370,7 @@ namespace GameCore
.AddContent("change_num", value); .AddContent("change_num", value);
} }
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue("TurntableRoll", TurntableCoupon.ToString(), remote); playerSaveStore.Write("TurntableRoll", TurntableCoupon.ToString(), remote).RequireAccepted();
} }
public void TurntableRed() public void TurntableRed()
@@ -1443,7 +1455,7 @@ namespace GameCore
void SetLimitedTimeEventDic() void SetLimitedTimeEventDic()
{ {
PlayFabMgr.Instance.UpdateUserDataValue("LimitedTimeEventDic", Newtonsoft.Json.JsonConvert.SerializeObject(limitedTimeEventDic)); playerSaveStore.Write("LimitedTimeEventDic", Newtonsoft.Json.JsonConvert.SerializeObject(limitedTimeEventDic)).RequireAccepted();
} }
#region 目标奖励活动 #region 目标奖励活动
@@ -1615,10 +1627,14 @@ namespace GameCore
public async Task<bool> CheckResourceLoadQueue(string key, List<string> label) public async Task<bool> CheckResourceLoadQueue(string key, List<string> label)
{ {
var store = playerSaveStore;
if (!store.IsCurrent) return false;
if (!collectingTargetResoure) if (!collectingTargetResoure)
{ {
ILoadResourceService loadResourceService = GContext.container.Resolve<ILoadResourceService>(); ILoadResourceService loadResourceService = GContext.container.Resolve<ILoadResourceService>();
collectingTargetResoure = await loadResourceService.CheckResourceLoadQueue(key, label); var loaded = await loadResourceService.CheckResourceLoadQueue(key, label);
if (!store.IsCurrent) return false;
collectingTargetResoure = loaded;
} }
return collectingTargetResoure; return collectingTargetResoure;
} }
@@ -2241,8 +2257,8 @@ namespace GameCore
public void UpdateTurnTableData() public void UpdateTurnTableData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)); playerSaveStore.Write("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)).RequireAccepted();
PlayFabMgr.Instance.UpdateUserDataValue("WheelLevelFreeCount", freecount.ToString()); playerSaveStore.Write("WheelLevelFreeCount", freecount.ToString()).RequireAccepted();
} }
/// <summary> /// <summary>
/// Add item by dropId and multiplier, or just return that item without giving to player, if doAdd is false. /// Add item by dropId and multiplier, or just return that item without giving to player, if doAdd is false.
@@ -2263,10 +2279,10 @@ namespace GameCore
{ {
IncreaseScore(score * multiplier); IncreaseScore(score * multiplier);
WheelOperateCount++; WheelOperateCount++;
PlayFabMgr.Instance.UpdateUserDataValue("WheelOperateCount", WheelOperateCount.ToString()); playerSaveStore.Write("WheelOperateCount", WheelOperateCount.ToString()).RequireAccepted();
AllWheelOperateCount += multiplier; AllWheelOperateCount += multiplier;
PlayFabMgr.Instance.UpdateUserDataValue("AllWheelOperateCount", AllWheelOperateCount.ToString()); playerSaveStore.Write("AllWheelOperateCount", AllWheelOperateCount.ToString()).RequireAccepted();
} }
public void IncreaseScore(int score) public void IncreaseScore(int score)
{ {
@@ -2283,7 +2299,7 @@ namespace GameCore
} }
else else
{ {
PlayFabMgr.Instance.UpdateUserDataValue("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)); playerSaveStore.Write("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)).RequireAccepted();
} }
} }
@@ -2305,7 +2321,7 @@ namespace GameCore
{ {
freecount = 0; freecount = 0;
} }
PlayFabMgr.Instance.UpdateUserDataValue("WheelLevelFreeCount", freecount.ToString()); playerSaveStore.Write("WheelLevelFreeCount", freecount.ToString()).RequireAccepted();
} }
//转盘等级增加函数 //转盘等级增加函数
public void IncreaseWheelLevel() public void IncreaseWheelLevel()
@@ -2325,7 +2341,7 @@ namespace GameCore
// 更新积分进度 // 更新积分进度
WheelLevelCount[wheelLevel.Level] = progress; WheelLevelCount[wheelLevel.Level] = progress;
TurntableRed(); TurntableRed();
PlayFabMgr.Instance.UpdateUserDataValue("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)); playerSaveStore.Write("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)).RequireAccepted();
} }
} }

View File

@@ -41,6 +41,7 @@ namespace GameCore
} }
public class GuideDataCenter : IDisposable public class GuideDataCenter : IDisposable
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
Tables _tables; Tables _tables;
PlayerData playerData; PlayerData playerData;
public GuideDataCenter(cfg.Tables tables, PlayerData playerData) public GuideDataCenter(cfg.Tables tables, PlayerData playerData)
@@ -94,7 +95,10 @@ namespace GameCore
} }
public async void Init() public async void Init()
{ {
guidanceDefineSetting = (GuidanceDefineSetting)await Addressables.LoadAssetAsync<object>("GuidanceConfig"); var store = playerSaveStore;
var setting = (GuidanceDefineSetting)await Addressables.LoadAssetAsync<object>("GuidanceConfig");
if (disposed || !store.IsCurrent) return;
guidanceDefineSetting = setting;
if (guidanceDefineSetting != null) if (guidanceDefineSetting != null)
{ {
bool isUpdate = false; bool isUpdate = false;
@@ -495,7 +499,9 @@ namespace GameCore
} }
async void TriggerGuide(string groupName, string curPanelName, GuidanceGroupDefine guidanceGroupDefine) async void TriggerGuide(string groupName, string curPanelName, GuidanceGroupDefine guidanceGroupDefine)
{ {
var store = playerSaveStore;
await System.Threading.Tasks.Task.Delay(200); await System.Threading.Tasks.Task.Delay(200);
if (disposed || !store.IsCurrent) return;
curSaveData = new GuideSaveData(); curSaveData = new GuideSaveData();
curSaveData.groupName = groupName; curSaveData.groupName = groupName;
GuideSaveDataDic[groupName] = curSaveData; GuideSaveDataDic[groupName] = curSaveData;
@@ -642,6 +648,7 @@ namespace GameCore
} }
async void ShowPanel(string PanelName) async void ShowPanel(string PanelName)
{ {
if (disposed || !playerSaveStore.IsCurrent) return;
if (curDefine.fingerType != FingerType.None) if (curDefine.fingerType != FingerType.None)
{ {
disposable3 = GContext.OnEvent<GuideNextEvent>().Subscribe(GuideNextEvent); disposable3 = GContext.OnEvent<GuideNextEvent>().Subscribe(GuideNextEvent);
@@ -652,10 +659,13 @@ namespace GameCore
public async void ShowGuidancePopupPanel(string nodeName) public async void ShowGuidancePopupPanel(string nodeName)
{ {
var store = playerSaveStore;
if (disposed || !store.IsCurrent) return;
popupPanelNode = nodeName; popupPanelNode = nodeName;
if (!string.IsNullOrEmpty(popupPanelNode)) if (!string.IsNullOrEmpty(popupPanelNode))
{ {
await UIManager.Instance.ShowUI(UITypes.GuidancePopupPanel); await UIManager.Instance.ShowUI(UITypes.GuidancePopupPanel);
if (disposed || !store.IsCurrent) return;
UIManager.Instance.HideUI(UITypes.GuidancePanel); UIManager.Instance.HideUI(UITypes.GuidancePanel);
} }
} }
@@ -712,11 +722,13 @@ namespace GameCore
{ {
List<string> strings = GuideSaveDataDic.Select(kv => kv.Key + "|" + kv.Value.guideSubIndex + "|" + (kv.Value.isFinish ? "1" : "0")).ToList(); List<string> strings = GuideSaveDataDic.Select(kv => kv.Key + "|" + kv.Value.guideSubIndex + "|" + (kv.Value.isFinish ? "1" : "0")).ToList();
string value = string.Join(";", strings); string value = string.Join(";", strings);
PlayFabMgr.Instance.UpdateUserDataValue("GuideSaveDataNew", value); playerSaveStore.Write("GuideSaveDataNew", value).RequireAccepted();
} }
bool disposed;
public void Dispose() public void Dispose()
{ {
disposed = true;
disposable?.Dispose(); disposable?.Dispose();
disposable = null; disposable = null;
disposable2?.Dispose(); disposable2?.Dispose();

View File

@@ -2,9 +2,11 @@ using asap.core;
using asap.playfab.async; using asap.playfab.async;
using cfg; using cfg;
using game; using game;
using game.PlayerDataStorage;
using PlayFab.ClientModels; using PlayFab.ClientModels;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using tysdk; using tysdk;
@@ -162,6 +164,27 @@ namespace GameCore
{ {
Statistics.Add(new StatisticUpdate() { StatisticName = names[i], Value = value[i] }); Statistics.Add(new StatisticUpdate() { StatisticName = names[i], Value = value[i] });
} }
if (userService is KVUserService kvUser)
{
var store = kvUser.SaveStore;
var writtenNames = new HashSet<string>(StringComparer.Ordinal);
bool accepted = false;
foreach (var statistic in Statistics)
{
if (!writtenNames.Add(statistic.StatisticName)) continue;
var write = store.Write(statistic.StatisticName, statistic.Value.ToString(CultureInfo.InvariantCulture));
if (write.Accepted) accepted = true;
else Debug.LogError("KV statistic write failed: " + statistic.StatisticName + ": " + write.Error);
}
if (accepted)
{
var confirmation = await store.FlushAsync();
if (confirmation.Status != PlayerSaveStatus.Confirmed)
Debug.LogError("KV statistic save not confirmed: " + string.Join(",", writtenNames) +
": " + confirmation.Status + ": " + confirmation.Error);
}
return;
}
UpdatePlayerStatisticsRequest request = new UpdatePlayerStatisticsRequest() UpdatePlayerStatisticsRequest request = new UpdatePlayerStatisticsRequest()
{ {
Statistics = Statistics, Statistics = Statistics,

View File

@@ -17,6 +17,7 @@ namespace GameCore
{ {
public class PlayerData public class PlayerData
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
private cfg.Tables _tables; private cfg.Tables _tables;
MMTService _MMService; MMTService _MMService;
public PlayerData(cfg.Tables tables, MMTService mmService) public PlayerData(cfg.Tables tables, MMTService mmService)
@@ -336,7 +337,7 @@ namespace GameCore
if (!newRodList.Contains(id)) if (!newRodList.Contains(id))
{ {
newRodList.Add(id); newRodList.Add(id);
PlayFabMgr.Instance.UpdateUserDataValue("NewRodList", JsonMapper.ToJson(newRodList)); playerSaveStore.Write("NewRodList", JsonMapper.ToJson(newRodList)).RequireAccepted();
if (addShowNewRod) if (addShowNewRod)
{ {
showNewRodList.Add(id); showNewRodList.Add(id);
@@ -367,7 +368,7 @@ namespace GameCore
public void RewardNewRod(int id) public void RewardNewRod(int id)
{ {
newRodList.Remove(id); newRodList.Remove(id);
PlayFabMgr.Instance.UpdateUserDataValue("NewRodList", JsonMapper.ToJson(newRodList)); playerSaveStore.Write("NewRodList", JsonMapper.ToJson(newRodList)).RequireAccepted();
GContext.container.Resolve<PlayerFishData>().SetRedPoint(); GContext.container.Resolve<PlayerFishData>().SetRedPoint();
} }
@@ -1233,7 +1234,7 @@ namespace GameCore
#if UNITY_EDITOR && !SERVICE_HOST #if UNITY_EDITOR && !SERVICE_HOST
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) return game.PlayerDataStorage.PlayerKVGameplayLoop.Write("NewPlayerDataDic",Newtonsoft.Json.JsonConvert.SerializeObject(NewPlayerDataDic),remote); if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) return game.PlayerDataStorage.PlayerKVGameplayLoop.Write("NewPlayerDataDic",Newtonsoft.Json.JsonConvert.SerializeObject(NewPlayerDataDic),remote);
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue("NewPlayerDataDic", Newtonsoft.Json.JsonConvert.SerializeObject(NewPlayerDataDic), remote); playerSaveStore.Write("NewPlayerDataDic", Newtonsoft.Json.JsonConvert.SerializeObject(NewPlayerDataDic), remote).RequireAccepted();
return null; return null;
} }
public void Init() public void Init()
@@ -1392,13 +1393,14 @@ namespace GameCore
} }
public async void CoRecoveryEnergy(int energyRecoveryTime) public async void CoRecoveryEnergy(int energyRecoveryTime)
{ {
var store = playerSaveStore;
tokenSource?.Cancel(); tokenSource?.Cancel();
tokenSource = new System.Threading.CancellationTokenSource(); tokenSource = new System.Threading.CancellationTokenSource();
var token = tokenSource.Token; var token = tokenSource.Token;
_isRecoveryEnergy = true; _isRecoveryEnergy = true;
int initEnergyRestorationSpeed = _tables.TbGlobalConfig.InitEnergyRestorationSpeed * 60; int initEnergyRestorationSpeed = _tables.TbGlobalConfig.InitEnergyRestorationSpeed * 60;
while (Energy < MaxEnergy && !token.IsCancellationRequested) while (Energy < MaxEnergy && !token.IsCancellationRequested && store.IsCurrent)
{ {
if (energyRecoveryTime >= initEnergyRestorationSpeed) if (energyRecoveryTime >= initEnergyRestorationSpeed)
{ {
@@ -1431,7 +1433,7 @@ namespace GameCore
SavePlayerData(); SavePlayerData();
} }
await Awaiters.Seconds(1); await Awaiters.Seconds(1);
if (token.IsCancellationRequested) if (token.IsCancellationRequested || !store.IsCurrent)
{ {
return; return;
} }
@@ -1502,14 +1504,17 @@ namespace GameCore
public async void OpenAppraisePopupPanel(int fishId) public async void OpenAppraisePopupPanel(int fishId)
{ {
var store = playerSaveStore;
if (!store.IsCurrent) return;
if (lastMapId != _tables.TbMapData.DataList[0].ID && !IsOpenAppraise) if (lastMapId != _tables.TbMapData.DataList[0].ID && !IsOpenAppraise)
{ {
var fishData = _tables.TbFishData.GetOrDefault(fishId); var fishData = _tables.TbFishData.GetOrDefault(fishId);
if (fishData != null && fishData.Quality >= 5) if (fishData != null && fishData.Quality >= 5)
{ {
await UIManager.Instance.ShowUI(UITypes.AppraisePopupPanel); await UIManager.Instance.ShowUI(UITypes.AppraisePopupPanel);
if (!store.IsCurrent) return;
IsOpenAppraise = true; IsOpenAppraise = true;
PlayFabMgr.Instance.UpdateUserDataValue("IsOpenAppraise", "true"); playerSaveStore.Write("IsOpenAppraise", "true").RequireAccepted();
} }
} }
} }

View File

@@ -52,6 +52,7 @@ namespace GameCore
} }
public partial class PlayerFishData public partial class PlayerFishData
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
private cfg.Tables _tables; private cfg.Tables _tables;
private ICustomServerMgr customServerMgr; private ICustomServerMgr customServerMgr;
public PlayerFishData(cfg.Tables tables, ICustomServerMgr customServerMgr) public PlayerFishData(cfg.Tables tables, ICustomServerMgr customServerMgr)
@@ -426,7 +427,7 @@ namespace GameCore
{ {
GContext.container.Resolve<PlayerData>().AddNewRod(id); GContext.container.Resolve<PlayerData>().AddNewRod(id);
rodData.Add(id, 0); rodData.Add(id, 0);
PlayFabMgr.Instance.UpdateUserDataValue("RodData", JsonMapper.ToJson(rodData)); playerSaveStore.Write("RodData", JsonMapper.ToJson(rodData)).RequireAccepted();
//鱼竿红点数据修改 //鱼竿红点数据修改
RedPointManager.Instance.SetRedPointState(RedPointName.Home_Rod, true); RedPointManager.Instance.SetRedPointState(RedPointName.Home_Rod, true);
} }
@@ -527,7 +528,7 @@ namespace GameCore
.AddContent("rod_chips_consume", 0); .AddContent("rod_chips_consume", 0);
} }
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue("RodLevelData", JsonMapper.ToJson(rodLevelData)); playerSaveStore.Write("RodLevelData", JsonMapper.ToJson(rodLevelData)).RequireAccepted();
GContext.container.Resolve<PlayerData>().SetPearl(GContext.container.Resolve<PlayerData>().pearl - count); GContext.container.Resolve<PlayerData>().SetPearl(GContext.container.Resolve<PlayerData>().pearl - count);
} }
@@ -586,7 +587,7 @@ namespace GameCore
} }
//鱼竿红点数据修改 //鱼竿红点数据修改
CheckRodRedPointUp(); CheckRodRedPointUp();
PlayFabMgr.Instance.UpdateUserDataValue("RodData", JsonMapper.ToJson(rodData)); playerSaveStore.Write("RodData", JsonMapper.ToJson(rodData)).RequireAccepted();
} }
public int GetRodPiece(int id) public int GetRodPiece(int id)
@@ -616,7 +617,7 @@ namespace GameCore
} }
} }
PlayFabMgr.Instance.UpdateUserDataValue("RodData", JsonMapper.ToJson(rodData)); playerSaveStore.Write("RodData", JsonMapper.ToJson(rodData)).RequireAccepted();
} }
public void GMSetStarAll(int lv) public void GMSetStarAll(int lv)
@@ -642,7 +643,7 @@ namespace GameCore
} }
} }
PlayFabMgr.Instance.UpdateUserDataValue("RodData", JsonMapper.ToJson(rodData)); playerSaveStore.Write("RodData", JsonMapper.ToJson(rodData)).RequireAccepted();
} }
public void GMAddAllFishMastery(bool all = false) public void GMAddAllFishMastery(bool all = false)
@@ -1221,7 +1222,7 @@ namespace GameCore
void MapNewUpdateUserDataValue(int mapID, Dictionary<int, FishWeightData> keyValuePairs) void MapNewUpdateUserDataValue(int mapID, Dictionary<int, FishWeightData> keyValuePairs)
{ {
Dictionary<int, string> mapData = keyValuePairs.ToDictionary(_ => _.Key, _ => _.Value.Serialize()); Dictionary<int, string> mapData = keyValuePairs.ToDictionary(_ => _.Key, _ => _.Value.Serialize());
PlayFabMgr.Instance.UpdateUserDataValue($"Map_5_{mapID}", JsonMapper.ToJson(mapData)); playerSaveStore.Write($"Map_5_{mapID}", JsonMapper.ToJson(mapData)).RequireAccepted();
} }
public int GetFishLevelByMap(int mapId) public int GetFishLevelByMap(int mapId)
{ {
@@ -1354,10 +1355,10 @@ namespace GameCore
if (fishId != 0) if (fishId != 0)
{ {
fishSwimToMapCount[id] = 0; fishSwimToMapCount[id] = 0;
PlayFabMgr.Instance.UpdateUserDataValue("FishSwimToMapCount", JsonMapper.ToJson(fishSwimToMapCount)); playerSaveStore.Write("FishSwimToMapCount", JsonMapper.ToJson(fishSwimToMapCount)).RequireAccepted();
} }
fishSwimToMap[id] = fishId; fishSwimToMap[id] = fishId;
PlayFabMgr.Instance.UpdateUserDataValue("FishSwimToMap", JsonMapper.ToJson(fishSwimToMap)); playerSaveStore.Write("FishSwimToMap", JsonMapper.ToJson(fishSwimToMap)).RequireAccepted();
} }
public void SetFishSwimToMapCount(int id, int count) public void SetFishSwimToMapCount(int id, int count)
{ {
@@ -1368,7 +1369,7 @@ namespace GameCore
{ {
SetFishSwimToMap(id, 0); SetFishSwimToMap(id, 0);
} }
PlayFabMgr.Instance.UpdateUserDataValue("FishSwimToMapCount", JsonMapper.ToJson(fishSwimToMapCount)); playerSaveStore.Write("FishSwimToMapCount", JsonMapper.ToJson(fishSwimToMapCount)).RequireAccepted();
} }
} }
//===========================鱼图鉴=========================== //===========================鱼图鉴===========================
@@ -1438,7 +1439,7 @@ namespace GameCore
} }
GContext.container.Resolve<PlayerItemData>().AddItemByDropList(dorpList, null); GContext.container.Resolve<PlayerItemData>().AddItemByDropList(dorpList, null);
PlayFabMgr.Instance.UpdateUserDataValue("MapProgress", Newtonsoft.Json.JsonConvert.SerializeObject(mapProgress)); playerSaveStore.Write("MapProgress", Newtonsoft.Json.JsonConvert.SerializeObject(mapProgress)).RequireAccepted();
} }
public int GetAllPTSByMap(int mapID) public int GetAllPTSByMap(int mapID)

View File

@@ -1,4 +1,4 @@
using cfg; using cfg;
using asap.core; using asap.core;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using CodeStage.AntiCheat.ObscuredTypes; using CodeStage.AntiCheat.ObscuredTypes;
using game; using game;
@@ -73,6 +73,13 @@ namespace GameCore
} }
public partial class PlayerShopData : IDisposable public partial class PlayerShopData : IDisposable
{ {
game.PlayerDataStorage.IPlayerSaveStore boundSaveStore;
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore
{
get => boundSaveStore;
set { CouponData.BindSaveStore(value); boundSaveStore = value; }
}
[Inject] [Inject]
public cfg.Tables _tables { get; set; } public cfg.Tables _tables { get; set; }
[Inject] [Inject]
@@ -250,7 +257,7 @@ namespace GameCore
public void AddVoucher(int value) public void AddVoucher(int value)
{ {
Voucher = Voucher + value; Voucher = Voucher + value;
PlayFabMgr.Instance.UpdateUserDataValue("Voucher", Voucher.ToString()); SaveStore.Write("Voucher", Voucher.ToString()).RequireAccepted();
} }
public void Init() public void Init()
{ {
@@ -369,10 +376,10 @@ namespace GameCore
if (Voucher >= iAPItemList.CostToken) if (Voucher >= iAPItemList.CostToken)
{ {
Voucher = Voucher - iAPItemList.CostToken; Voucher = Voucher - iAPItemList.CostToken;
PlayFabMgr.Instance.UpdateUserDataValue("Voucher", Voucher.ToString()); SaveStore.Write("Voucher", Voucher.ToString()).RequireAccepted();
//二次弹窗 //二次弹窗
//回调 //回调
PlayFabMgr.Instance.UpdateUserDataValue("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{0}"); SaveStore.Write("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{0}").RequireAccepted();
BuySuccess(buyType, iAPItemList, itemDataGift, rewardShowType, true); BuySuccess(buyType, iAPItemList, itemDataGift, rewardShowType, true);
OrderManager.OrderManager.OnOrderSuccess(buyType, iAPItemList, itemDataGift, rewardShowType, true); OrderManager.OrderManager.OnOrderSuccess(buyType, iAPItemList, itemDataGift, rewardShowType, true);
if (!buyType.NoShow) if (!buyType.NoShow)
@@ -429,7 +436,7 @@ namespace GameCore
if (result.isSuccess) if (result.isSuccess)
{ {
PlayFabMgr.Instance.UpdateUserDataValue("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{sku.price}"); SaveStore.Write("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{sku.price}").RequireAccepted();
BuySuccess(buyType, iAPItemList, itemDataGift, rewardShowType); BuySuccess(buyType, iAPItemList, itemDataGift, rewardShowType);
OrderManager.OrderManager.OnOrderSuccess(buyType, iAPItemList, itemDataGift, rewardShowType); OrderManager.OrderManager.OnOrderSuccess(buyType, iAPItemList, itemDataGift, rewardShowType);
if (!buyType.NoShow) if (!buyType.NoShow)
@@ -604,7 +611,7 @@ namespace GameCore
} }
} }
} }
PlayFabMgr.Instance.UpdateUserDataValue("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{data.ProdPrice}"); SaveStore.Write("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{data.ProdPrice}").RequireAccepted();
BuySuccess(data.BuyType, iAPItemList, itemDataGift, data.ShowType); BuySuccess(data.BuyType, iAPItemList, itemDataGift, data.ShowType);
if (data.BuyType.extraPurcheData != null) if (data.BuyType.extraPurcheData != null)
{ {
@@ -628,7 +635,7 @@ namespace GameCore
buyData[0] += iAPItemList.PaymentAmount; buyData[0] += iAPItemList.PaymentAmount;
} }
buyData.Add(iAPItemList.ID); buyData.Add(iAPItemList.ID);
PlayFabMgr.Instance.UpdateUserDataValue("BuyData", JsonMapper.ToJson(buyData)); SaveStore.Write("BuyData", JsonMapper.ToJson(buyData)).RequireAccepted();
#if AGG #if AGG
GEvent.SetUserProp("Total_consumption", buyData[0]); GEvent.SetUserProp("Total_consumption", buyData[0]);
#endif #endif
@@ -699,7 +706,7 @@ namespace GameCore
} }
TurntableAdData = null; TurntableAdData = null;
shopPackDailyBuyCount["0"] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; shopPackDailyBuyCount["0"] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
PlayFabMgr.Instance.UpdateUserDataValue("ShopPackBuyCount", JsonMapper.ToJson(shopPackDailyBuyCount)); SaveStore.Write("ShopPackBuyCount", JsonMapper.ToJson(shopPackDailyBuyCount)).RequireAccepted();
} }
//} //}
} }
@@ -729,7 +736,7 @@ namespace GameCore
{ {
shopPackDailyBuyCount["0"] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; shopPackDailyBuyCount["0"] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
} }
PlayFabMgr.Instance.UpdateUserDataValue("ShopPackBuyCount", JsonMapper.ToJson(shopPackDailyBuyCount)); SaveStore.Write("ShopPackBuyCount", JsonMapper.ToJson(shopPackDailyBuyCount)).RequireAccepted();
} }
public int GetShopPackBuyCount(int id) public int GetShopPackBuyCount(int id)
{ {
@@ -786,7 +793,7 @@ namespace GameCore
{ {
shopGiftBuyCount.Add(id, count); shopGiftBuyCount.Add(id, count);
} }
PlayFabMgr.Instance.UpdateUserDataValue("ShopGiftBuyCount", JsonMapper.ToJson(shopGiftBuyCount)); SaveStore.Write("ShopGiftBuyCount", JsonMapper.ToJson(shopGiftBuyCount)).RequireAccepted();
} }
/// <summary> /// <summary>
@@ -855,7 +862,7 @@ namespace GameCore
index = 0, index = 0,
time = time.ToString() time = time.ToString()
}; };
PlayFabMgr.Instance.UpdateUserDataValue("ChainPackDataNew", JsonMapper.ToJson(ChainPackData)); SaveStore.Write("ChainPackDataNew", JsonMapper.ToJson(ChainPackData)).RequireAccepted();
chainPackM = null; chainPackM = null;
} }
if (chainPackM == null) if (chainPackM == null)
@@ -897,7 +904,7 @@ namespace GameCore
{ {
AddAllPackCount(chainPackM.ID, 1); AddAllPackCount(chainPackM.ID, 1);
} }
PlayFabMgr.Instance.UpdateUserDataValue("ChainPackDataNew", JsonMapper.ToJson(ChainPackData)); SaveStore.Write("ChainPackDataNew", JsonMapper.ToJson(ChainPackData)).RequireAccepted();
//链式礼包 特殊处理 //链式礼包 特殊处理
GContext.Publish(new TargetEvent(chainPackM.ID, 3, 10)); GContext.Publish(new TargetEvent(chainPackM.ID, 3, 10));
} }
@@ -927,7 +934,7 @@ namespace GameCore
shopAllPackBuyCount.Add(id, count); shopAllPackBuyCount.Add(id, count);
} }
GContext.Publish(new ConditionTypeEvent(ConditionType.BuyPack, count)); GContext.Publish(new ConditionTypeEvent(ConditionType.BuyPack, count));
PlayFabMgr.Instance.UpdateUserDataValue("ShopAllPackBuyCount", JsonMapper.ToJson(shopAllPackBuyCount)); SaveStore.Write("ShopAllPackBuyCount", JsonMapper.ToJson(shopAllPackBuyCount)).RequireAccepted();
} }
#endregion #endregion
//========================================礼包======================================== //========================================礼包========================================
@@ -965,7 +972,7 @@ namespace GameCore
} }
} }
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue("AdvertAllCount", JsonMapper.ToJson(advertCount)); SaveStore.Write("AdvertAllCount", JsonMapper.ToJson(advertCount)).RequireAccepted();
GContext.Publish(new ResAddEvent(1006)); GContext.Publish(new ResAddEvent(1006));
} }
public int GetAdticketCount() public int GetAdticketCount()
@@ -991,7 +998,7 @@ namespace GameCore
{ {
advertCount.Add(id, 1); advertCount.Add(id, 1);
} }
PlayFabMgr.Instance.UpdateUserDataValue("AdvertAllCount", JsonMapper.ToJson(advertCount)); SaveStore.Write("AdvertAllCount", JsonMapper.ToJson(advertCount)).RequireAccepted();
return advertCount[id]; return advertCount[id];
} }
public void ClearAdvertCount() public void ClearAdvertCount()
@@ -1006,7 +1013,7 @@ namespace GameCore
advertCount[-1] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; advertCount[-1] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
advertCount[0] = allCount; advertCount[0] = allCount;
advertCount[-2] = ticket; advertCount[-2] = ticket;
PlayFabMgr.Instance.UpdateUserDataValue("AdvertAllCount", JsonMapper.ToJson(advertCount)); SaveStore.Write("AdvertAllCount", JsonMapper.ToJson(advertCount)).RequireAccepted();
} }
} }
else else
@@ -1050,7 +1057,7 @@ namespace GameCore
AdConfig adConfig1 = adConfigs[VIPAdPackData.index - 1]; AdConfig adConfig1 = adConfigs[VIPAdPackData.index - 1];
VIPAdPackData.cdTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(adConfig1.Interval).ToString(); VIPAdPackData.cdTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(adConfig1.Interval).ToString();
} }
PlayFabMgr.Instance.UpdateUserDataValue("VIPAdPackData", JsonMapper.ToJson(VIPAdPackData)); SaveStore.Write("VIPAdPackData", JsonMapper.ToJson(VIPAdPackData)).RequireAccepted();
//GContext.Publish(new VIPAdPackDataEvent()); //GContext.Publish(new VIPAdPackDataEvent());
} }
public DateTime GetVIPAdPackTime() public DateTime GetVIPAdPackTime()
@@ -1082,7 +1089,7 @@ namespace GameCore
VIPAdPackData = new AdvertData(); VIPAdPackData = new AdvertData();
VIPAdPackData.day = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; VIPAdPackData.day = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
VIPAdPackData.isOpen = true; VIPAdPackData.isOpen = true;
PlayFabMgr.Instance.UpdateUserDataValue("VIPAdPackData", JsonMapper.ToJson(VIPAdPackData)); SaveStore.Write("VIPAdPackData", JsonMapper.ToJson(VIPAdPackData)).RequireAccepted();
} }
public bool IsOpenAdPack() public bool IsOpenAdPack()
{ {
@@ -1139,7 +1146,7 @@ namespace GameCore
// { // {
// isOverForToday = true; // isOverForToday = true;
} }
PlayFabMgr.Instance.UpdateUserDataValue("TurntableAdData", Newtonsoft.Json.JsonConvert.SerializeObject(TurntableAdData)); SaveStore.Write("TurntableAdData", Newtonsoft.Json.JsonConvert.SerializeObject(TurntableAdData)).RequireAccepted();
// GContext.Publish(new VIPAdPackDataEvent()); // GContext.Publish(new VIPAdPackDataEvent());
} }
public bool IsTurntableAdMaxOut() public bool IsTurntableAdMaxOut()
@@ -1233,7 +1240,7 @@ namespace GameCore
{ {
_diamondShopItemRecords[itemShopId].hasBought = 1; _diamondShopItemRecords[itemShopId].hasBought = 1;
SetItemShopRedPoint(); SetItemShopRedPoint();
PlayFabMgr.Instance.UpdateUserDataValue("DiamondShopItemRecords", JsonConvert.SerializeObject(_diamondShopItemRecords)); SaveStore.Write("DiamondShopItemRecords", JsonConvert.SerializeObject(_diamondShopItemRecords)).RequireAccepted();
} }
@@ -1248,7 +1255,7 @@ namespace GameCore
{ {
RefreshItemShop(); RefreshItemShop();
_diamondShopRefreshCount++; _diamondShopRefreshCount++;
PlayFabMgr.Instance.UpdateUserDataValue("DiamondShopRefreshCount", _diamondShopRefreshCount.ToString()); SaveStore.Write("DiamondShopRefreshCount", _diamondShopRefreshCount.ToString()).RequireAccepted();
} }
} }
@@ -1288,8 +1295,8 @@ namespace GameCore
_diamondShopRefreshTime = new DateTime(now.Year, now.Month, now.Day).AddDays(1) + timeSpan; _diamondShopRefreshTime = new DateTime(now.Year, now.Month, now.Day).AddDays(1) + timeSpan;
_diamondShopRefreshCount = 0; _diamondShopRefreshCount = 0;
PlayFabMgr.Instance.UpdateUserDataValue("DiamondShopRefreshCount", _diamondShopRefreshCount.ToString()); SaveStore.Write("DiamondShopRefreshCount", _diamondShopRefreshCount.ToString()).RequireAccepted();
PlayFabMgr.Instance.UpdateUserDataValue("DiamondShopRefreshTime", _diamondShopRefreshTime.ToString()); SaveStore.Write("DiamondShopRefreshTime", _diamondShopRefreshTime.ToString()).RequireAccepted();
RefreshItemShop(); RefreshItemShop();
} }
private void RefreshItemShop() private void RefreshItemShop()
@@ -1364,7 +1371,7 @@ namespace GameCore
} }
} }
PlayFabMgr.Instance.UpdateUserDataValue("DiamondShopItemRecords", JsonConvert.SerializeObject(_diamondShopItemRecords)); SaveStore.Write("DiamondShopItemRecords", JsonConvert.SerializeObject(_diamondShopItemRecords)).RequireAccepted();
SetItemShopRedPoint(); SetItemShopRedPoint();
} }
#endregion #endregion

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using LitJson; using LitJson;
@@ -29,6 +29,8 @@ namespace GameCore
} }
public class SigninData public class SigninData
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
private cfg.Tables _tables; private cfg.Tables _tables;
public SigninData(cfg.Tables tables) public SigninData(cfg.Tables tables)
{ {
@@ -102,7 +104,7 @@ namespace GameCore
} }
if (isSave) if (isSave)
{ {
PlayFabMgr.Instance.UpdateUserDataValue("MonthlySignEvent", JsonMapper.ToJson(_monthlySignEvent)); SaveStore.Write("MonthlySignEvent", JsonMapper.ToJson(_monthlySignEvent)).RequireAccepted();
} }
bool isReward = CanDailySign(); bool isReward = CanDailySign();
if (isReward) if (isReward)
@@ -154,7 +156,7 @@ namespace GameCore
} }
} }
//GContext.Publish(new ShowData()); //GContext.Publish(new ShowData());
PlayFabMgr.Instance.UpdateUserDataValue("MonthlySignEvent", JsonMapper.ToJson(_monthlySignEvent)); SaveStore.Write("MonthlySignEvent", JsonMapper.ToJson(_monthlySignEvent)).RequireAccepted();
GContext.Publish(new OnSignEvent() { itemDataList = monthlySignReward }); GContext.Publish(new OnSignEvent() { itemDataList = monthlySignReward });
} }
return isSign; return isSign;

View File

@@ -20,6 +20,7 @@ namespace GameCore
} }
public class SmallGameData public class SmallGameData
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
private cfg.Tables _tables; private cfg.Tables _tables;
IUserService userService; IUserService userService;
ICustomServerMgr customServerMgr; ICustomServerMgr customServerMgr;
@@ -76,7 +77,7 @@ namespace GameCore
public void SaveHeistGameCount() public void SaveHeistGameCount()
{ {
_heistGameCount++; _heistGameCount++;
PlayFabMgr.Instance.UpdateUserDataValue("HeistGameCount", _heistGameCount.ToString()); playerSaveStore.Write("HeistGameCount", _heistGameCount.ToString()).RequireAccepted();
} }
public void SetBombGameCount(int value) public void SetBombGameCount(int value)
{ {
@@ -85,7 +86,7 @@ namespace GameCore
public void SaveBombGameCount() public void SaveBombGameCount()
{ {
_bombGameCount++; _bombGameCount++;
PlayFabMgr.Instance.UpdateUserDataValue("BombGameCount", _bombGameCount.ToString()); playerSaveStore.Write("BombGameCount", _bombGameCount.ToString()).RequireAccepted();
} }
public void SetFishHuntGameCount(int value) public void SetFishHuntGameCount(int value)
{ {
@@ -94,7 +95,7 @@ namespace GameCore
public void SaveFishHuntGameCount() public void SaveFishHuntGameCount()
{ {
_fishHuntGameCount++; _fishHuntGameCount++;
PlayFabMgr.Instance.UpdateUserDataValue("FishHuntGameCount", _fishHuntGameCount.ToString()); playerSaveStore.Write("FishHuntGameCount", _fishHuntGameCount.ToString()).RequireAccepted();
} }
#region 小游戏仇人 #region 小游戏仇人
public List<OpponentItemData> opponentItemDatas = new List<OpponentItemData>(); public List<OpponentItemData> opponentItemDatas = new List<OpponentItemData>();

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using LitJson; using LitJson;
@@ -38,6 +38,8 @@ namespace GameCore
} }
public class TriggerPackData public class TriggerPackData
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
Tables _tables; Tables _tables;
PlayerShopData playerShopData; PlayerShopData playerShopData;
public TriggerPackData(Tables tables) public TriggerPackData(Tables tables)
@@ -220,7 +222,7 @@ namespace GameCore
id = t.ID, id = t.ID,
endTime = dateTime.AddSeconds(fixedTime.EndTime).ToString(), endTime = dateTime.AddSeconds(fixedTime.EndTime).ToString(),
}; };
PlayFabMgr.Instance.UpdateUserDataValue("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)); SaveStore.Write("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)).RequireAccepted();
TriggerPackBuyData _newSignEvent = null; TriggerPackBuyData _newSignEvent = null;
for (int i = 0; i < triggerPackList.Count; i++) for (int i = 0; i < triggerPackList.Count; i++)
@@ -251,7 +253,7 @@ namespace GameCore
_newSignEvent.vipLevel = GetVIPLevel(t.PackType); _newSignEvent.vipLevel = GetVIPLevel(t.PackType);
} }
GContext.container.Resolve<IFaceUIService>().AddGiftFaceUIOnLogin(_newSignEvent.ID, UITypes.AdsPackPanel, _newSignEvent.lastID); GContext.container.Resolve<IFaceUIService>().AddGiftFaceUIOnLogin(_newSignEvent.ID, UITypes.AdsPackPanel, _newSignEvent.lastID);
PlayFabMgr.Instance.UpdateUserDataValue("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)); SaveStore.Write("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)).RequireAccepted();
} }
} }
@@ -287,7 +289,7 @@ namespace GameCore
endTime = time, endTime = time,
}; };
fixedTimeShopDic[t.ID] = fixedTimeEvent; fixedTimeShopDic[t.ID] = fixedTimeEvent;
PlayFabMgr.Instance.UpdateUserDataValue("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)); SaveStore.Write("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)).RequireAccepted();
} }
else if (t.TimeDefinition is LimitedTime) else if (t.TimeDefinition is LimitedTime)
{ {
@@ -306,7 +308,7 @@ namespace GameCore
}; };
triggerPackList.Insert(0, _newSignEvent); triggerPackList.Insert(0, _newSignEvent);
playerShopData.AddShopPackDailyBuyCount(t.PackType, 1, false); playerShopData.AddShopPackDailyBuyCount(t.PackType, 1, false);
PlayFabMgr.Instance.UpdateUserDataValue("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)); SaveStore.Write("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)).RequireAccepted();
} }
} }
/// <summary> /// <summary>
@@ -337,7 +339,7 @@ namespace GameCore
endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(), endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(),
}; };
PlayFabMgr.Instance.UpdateUserDataValue("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)); SaveStore.Write("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)).RequireAccepted();
} }
if (FixedTimeEventExpired(t.ID)) if (FixedTimeEventExpired(t.ID))
{ {
@@ -370,7 +372,7 @@ namespace GameCore
endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(), endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(),
}; };
PlayFabMgr.Instance.UpdateUserDataValue("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)); SaveStore.Write("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)).RequireAccepted();
} }
if (FixedTimeEventExpired(t.ID)) if (FixedTimeEventExpired(t.ID))
{ {
@@ -472,7 +474,7 @@ namespace GameCore
} }
if (isUpdate) if (isUpdate)
{ {
PlayFabMgr.Instance.UpdateUserDataValue("GiftVIPDData", JsonMapper.ToJson(giftVIPDData)); SaveStore.Write("GiftVIPDData", JsonMapper.ToJson(giftVIPDData)).RequireAccepted();
} }
} }
} }
@@ -516,7 +518,7 @@ namespace GameCore
}; };
} }
giftVIPDData[PackType] = giftVIPD; giftVIPDData[PackType] = giftVIPD;
PlayFabMgr.Instance.UpdateUserDataValue("GiftVIPDData", JsonMapper.ToJson(giftVIPDData)); SaveStore.Write("GiftVIPDData", JsonMapper.ToJson(giftVIPDData)).RequireAccepted();
} }
} }
#region 触发礼包 #region 触发礼包
@@ -573,7 +575,7 @@ namespace GameCore
}; };
triggerPackList.Insert(0, _newSignEvent); triggerPackList.Insert(0, _newSignEvent);
playerShopData.AddShopPackDailyBuyCount(t.PackType, 1, false); playerShopData.AddShopPackDailyBuyCount(t.PackType, 1, false);
PlayFabMgr.Instance.UpdateUserDataValue("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)); SaveStore.Write("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)).RequireAccepted();
} }
} }
public void AddTriggerPackCount(int id) public void AddTriggerPackCount(int id)
@@ -586,7 +588,7 @@ namespace GameCore
{ {
triggerPackList.Remove(triggerPackList[i]); triggerPackList.Remove(triggerPackList[i]);
} }
PlayFabMgr.Instance.UpdateUserDataValue("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)); SaveStore.Write("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)).RequireAccepted();
break; break;
} }
} }
@@ -641,7 +643,7 @@ namespace GameCore
{ {
triggerPackList.Remove(dailySignEvent); triggerPackList.Remove(dailySignEvent);
GContext.Publish(new TriggerPackEvent()); GContext.Publish(new TriggerPackEvent());
PlayFabMgr.Instance.UpdateUserDataValue("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)); SaveStore.Write("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)).RequireAccepted();
} }
return endTime - curTime; return endTime - curTime;
} }

View File

@@ -3,6 +3,7 @@ using GameCore;
using cfg; using cfg;
public class EventPackData public class EventPackData
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
private Tables _tables = GContext.container.Resolve<Tables>(); private Tables _tables = GContext.container.Resolve<Tables>();
private struct Data private struct Data
{ {
@@ -100,8 +101,8 @@ public class EventPackData
} }
public void SaveEventPackData() public void SaveEventPackData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue("EventPackData", playerSaveStore.Write("EventPackData",
Newtonsoft.Json.JsonConvert.SerializeObject(_data)); Newtonsoft.Json.JsonConvert.SerializeObject(_data)).RequireAccepted();
} }
private void TriggerEventPack() private void TriggerEventPack()
{ {

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -100,6 +100,8 @@ namespace DataCenter
// EventPartnerGather 管理器 // EventPartnerGather 管理器
public class EventPartnerGatherManager : IAutoMatchManagerContext public class EventPartnerGatherManager : IAutoMatchManagerContext
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
private const bool EnableTraceLogs = false; private const bool EnableTraceLogs = false;
public const string SaveKey = "EventPartnerGatherData"; public const string SaveKey = "EventPartnerGatherData";
internal static game.PlayerDataStorage.PlayerKVPartnerState DecodeForKVLoad( internal static game.PlayerDataStorage.PlayerKVPartnerState DecodeForKVLoad(
@@ -864,7 +866,7 @@ namespace DataCenter
Trace($"SaveData eventId={EventId}"); Trace($"SaveData eventId={EventId}");
#endif #endif
{ {
PlayFabMgr.Instance.UpdateUserDataValue(SaveKey, JsonConvert.SerializeObject(GatherData)); SaveStore.Write(SaveKey, JsonConvert.SerializeObject(GatherData)).RequireAccepted();
} }
} }
public void LoadData(string dataValue) public void LoadData(string dataValue)

View File

@@ -11,6 +11,7 @@ namespace game
{ {
public class EventPinballDataManager public class EventPinballDataManager
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
#if UNITY_EDITOR #if UNITY_EDITOR
private bool m_IsCheckCondition = true; private bool m_IsCheckCondition = true;
@@ -375,7 +376,7 @@ namespace game
public void SyncData() public void SyncData()
{ {
if (!m_IsSyncData) return; if (!m_IsSyncData) return;
PlayFabMgr.Instance.UpdateUserDataValue(EventPinballConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_PinballData)); playerSaveStore.Write(EventPinballConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_PinballData)).RequireAccepted();
} }
#endregion #endregion

View File

@@ -13,6 +13,7 @@ namespace game
{ {
public class EventPotionDataManager public class EventPotionDataManager
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
#if UNITY_EDITOR #if UNITY_EDITOR
private bool m_IsCheckCondition = true; private bool m_IsCheckCondition = true;
private bool m_IsSyncData = true; private bool m_IsSyncData = true;
@@ -572,7 +573,7 @@ namespace game
public void SyncData() public void SyncData()
{ {
if (!m_IsSyncData) return; if (!m_IsSyncData) return;
PlayFabMgr.Instance.UpdateUserDataValue(EventPotionConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_PotionData)); playerSaveStore.Write(EventPotionConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_PotionData)).RequireAccepted();
} }
public PotionItemData GetOperationItemData() public PotionItemData GetOperationItemData()
{ {

View File

@@ -14,6 +14,7 @@ namespace game
{ {
public class EventScratchTicketDataManager public class EventScratchTicketDataManager
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
#if UNITY_EDITOR #if UNITY_EDITOR
private bool m_IsCheckCondition = true; private bool m_IsCheckCondition = true;
private bool m_IsSyncData = true; private bool m_IsSyncData = true;
@@ -594,7 +595,7 @@ namespace game
public void SyncData() public void SyncData()
{ {
if (!m_IsSyncData) return; if (!m_IsSyncData) return;
PlayFabMgr.Instance.UpdateUserDataValue(EventScratchTicketConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_ScratchTicketData)); playerSaveStore.Write(EventScratchTicketConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_ScratchTicketData)).RequireAccepted();
} }
List<int> magList = new List<int>() { 1, 5, 10, 20 }; List<int> magList = new List<int>() { 1, 5, 10, 20 };

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -40,6 +40,8 @@ public class FishingChallengeData
public class FishingChallengeManager public class FishingChallengeManager
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
// //
private const string SaveKey = "FishingChallengeData"; private const string SaveKey = "FishingChallengeData";
@@ -69,7 +71,11 @@ public class FishingChallengeManager
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeChallenge(raw,snapshot,errors); game.PlayerDataStorage.PlayerKVSaveSchema.DecodeChallenge(raw,snapshot,errors);
internal void ResetForKVLoad() internal void ResetForKVLoad()
{ {
fishingChallengeData=null; IsInit=false; PrefabName=null; TopRankTier=Rank=FullStep=PlayerFullNumber=PreScore=_cheatingScore=0; fishingChallengeData=null; IsInit=false; PrefabName=null;
#if UNITY_EDITOR
_cheatingScore=0;
#endif
TopRankTier=Rank=FullStep=PlayerFullNumber=PreScore=0;
CurFishingEventData=null; StartTime=EndTime=default; eventChallengeMain=null; eventChallengeConfig=null; challengeMapData=null; itemDataList=null; CurFishingEventData=null; StartTime=EndTime=default; eventChallengeMain=null; eventChallengeConfig=null; challengeMapData=null; itemDataList=null;
_stageDropList.Clear(); _eliminateListScore.Clear(); _eventTrackStageList=new List<int>(); _eventTrackNormalRewardList=new List<int>(); InVisibleReason=string.Empty; _stageDropList.Clear(); _eliminateListScore.Clear(); _eventTrackStageList=new List<int>(); _eventTrackNormalRewardList=new List<int>(); InVisibleReason=string.Empty;
} }
@@ -1081,7 +1087,7 @@ public class FishingChallengeManager
private void DoSave() private void DoSave()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(SaveKey, JsonConvert.SerializeObject(fishingChallengeData)); SaveStore.Write(SaveKey, JsonConvert.SerializeObject(fishingChallengeData)).RequireAccepted();
} }
public string GetOpenEventKey() public string GetOpenEventKey()

View File

@@ -19,7 +19,7 @@ namespace GameCore
{ {
if (Pack1A1Data == null) if (Pack1A1Data == null)
{ {
string json_duelData = PlayFabMgr.Instance.GetLocalData(Pack1A1DataKey); string json_duelData = playerSaveStore.Read(Pack1A1DataKey).ValueOrThrow();
if (json_duelData != null) if (json_duelData != null)
{ {
Pack1A1Data = Newtonsoft.Json.JsonConvert.DeserializeObject<TriggerPackBuyData>(json_duelData); Pack1A1Data = Newtonsoft.Json.JsonConvert.DeserializeObject<TriggerPackBuyData>(json_duelData);
@@ -42,7 +42,7 @@ namespace GameCore
}; };
RedPointManager.Instance.SetRedPointState(HomeBtnOneOnePack.redKey, false); RedPointManager.Instance.SetRedPointState(HomeBtnOneOnePack.redKey, false);
PlayFabMgr.Instance.UpdateUserDataValue(Pack1A1DataKey, JsonMapper.ToJson(Pack1A1Data)); playerSaveStore.Write(Pack1A1DataKey, JsonMapper.ToJson(Pack1A1Data)).RequireAccepted();
} }
public void SetPack1A1Index(int index) public void SetPack1A1Index(int index)
{ {
@@ -58,7 +58,7 @@ namespace GameCore
GContext.Publish(new TargetEvent(Pack1A1Data.lastID, 3, 11)); GContext.Publish(new TargetEvent(Pack1A1Data.lastID, 3, 11));
} }
RedPointManager.Instance.SetRedPointState(HomeBtnOneOnePack.redKey, Pack1A1Data.index == 1); RedPointManager.Instance.SetRedPointState(HomeBtnOneOnePack.redKey, Pack1A1Data.index == 1);
PlayFabMgr.Instance.UpdateUserDataValue(Pack1A1DataKey, JsonMapper.ToJson(Pack1A1Data)); playerSaveStore.Write(Pack1A1DataKey, JsonMapper.ToJson(Pack1A1Data)).RequireAccepted();
} }
public int GetPack1A1Index() public int GetPack1A1Index()
{ {

View File

@@ -50,7 +50,7 @@ namespace GameCore
{ {
if (Pack1A2Data == null) if (Pack1A2Data == null)
{ {
string json_duelData = PlayFabMgr.Instance.GetLocalData(Pack1A2DataKey); string json_duelData = playerSaveStore.Read(Pack1A2DataKey).ValueOrThrow();
if (json_duelData != null) if (json_duelData != null)
{ {
Pack1A2Data = Newtonsoft.Json.JsonConvert.DeserializeObject<Pack1A2Data>(json_duelData); Pack1A2Data = Newtonsoft.Json.JsonConvert.DeserializeObject<Pack1A2Data>(json_duelData);
@@ -74,7 +74,7 @@ namespace GameCore
SetType(); SetType();
SetRodDstId(); SetRodDstId();
SetChainPackRed(); SetChainPackRed();
PlayFabMgr.Instance.UpdateUserDataValue(Pack1A2DataKey, JsonMapper.ToJson(Pack1A2Data)); playerSaveStore.Write(Pack1A2DataKey, JsonMapper.ToJson(Pack1A2Data)).RequireAccepted();
} }
void SetType() void SetType()
{ {
@@ -109,7 +109,7 @@ namespace GameCore
return; return;
} }
Pack1A2Data.index++; Pack1A2Data.index++;
PlayFabMgr.Instance.UpdateUserDataValue(Pack1A2DataKey, JsonMapper.ToJson(Pack1A2Data)); playerSaveStore.Write(Pack1A2DataKey, JsonMapper.ToJson(Pack1A2Data)).RequireAccepted();
SetChainPackRed(); SetChainPackRed();
} }
void SetChainPackRed() void SetChainPackRed()

View File

@@ -87,9 +87,12 @@ namespace GameCore
} }
public async void ShowRankTierChange() public async void ShowRankTierChange()
{ {
var store = playerSaveStore;
if (!store.IsCurrent) return;
if (rankTierChangeState > 0 && curSolomain != null) if (rankTierChangeState > 0 && curSolomain != null)
{ {
GameObject go = await UIManager.Instance.ShowUI(UITypes.EventFishingDuelRankChangePopupPanel); GameObject go = await UIManager.Instance.ShowUI(UITypes.EventFishingDuelRankChangePopupPanel);
if (!store.IsCurrent) return;
EventFishingDuelRankChangePopupPanel changePopupPanel = go.GetComponent<EventFishingDuelRankChangePopupPanel>(); EventFishingDuelRankChangePopupPanel changePopupPanel = go.GetComponent<EventFishingDuelRankChangePopupPanel>();
changePopupPanel.SetRank(rankTierChangeState == 1, curSolomain); changePopupPanel.SetRank(rankTierChangeState == 1, curSolomain);
rankTierChangeState = 0; rankTierChangeState = 0;
@@ -98,7 +101,7 @@ namespace GameCore
} }
public void SoloCheckClaimReward() public void SoloCheckClaimReward()
{ {
string json_duelData = PlayFabMgr.Instance.GetLocalData(FishingDuelManager.SoloDataKey); string json_duelData = playerSaveStore.Read(FishingDuelManager.SoloDataKey).ValueOrThrow();
if (json_duelData != null) if (json_duelData != null)
{ {
duelData = Newtonsoft.Json.JsonConvert.DeserializeObject<DuelData>(json_duelData); duelData = Newtonsoft.Json.JsonConvert.DeserializeObject<DuelData>(json_duelData);
@@ -120,7 +123,7 @@ namespace GameCore
duelData.rewardEventID = 0; duelData.rewardEventID = 0;
duelData.preProgress = 0; duelData.preProgress = 0;
string json_duelData = Newtonsoft.Json.JsonConvert.SerializeObject(duelData); string json_duelData = Newtonsoft.Json.JsonConvert.SerializeObject(duelData);
PlayFabMgr.Instance.UpdateUserDataValue(FishingDuelManager.SoloDataKey, json_duelData); playerSaveStore.Write(FishingDuelManager.SoloDataKey, json_duelData).RequireAccepted();
} }
} }
public void GMAddSoloProgress(int count) public void GMAddSoloProgress(int count)
@@ -133,7 +136,7 @@ namespace GameCore
} }
public void InitSoloData(FishingEvent t) public void InitSoloData(FishingEvent t)
{ {
string json_duelData = PlayFabMgr.Instance.GetLocalData(FishingDuelManager.SoloDataKey); string json_duelData = playerSaveStore.Read(FishingDuelManager.SoloDataKey).ValueOrThrow();
if (json_duelData != null) if (json_duelData != null)
{ {
duelData = Newtonsoft.Json.JsonConvert.DeserializeObject<DuelData>(json_duelData); duelData = Newtonsoft.Json.JsonConvert.DeserializeObject<DuelData>(json_duelData);
@@ -262,7 +265,7 @@ namespace GameCore
public void AddPVPToken(int value) public void AddPVPToken(int value)
{ {
PVPToken += value; PVPToken += value;
PlayFabMgr.Instance.UpdateUserDataValue("PVPToken", PVPToken.ToString()); playerSaveStore.Write("PVPToken", PVPToken.ToString()).RequireAccepted();
if (value < 0 && duelData != null) if (value < 0 && duelData != null)
{ {
duelData.rePVPToken -= value; duelData.rePVPToken -= value;
@@ -272,7 +275,7 @@ namespace GameCore
public void SaveDuelData() public void SaveDuelData()
{ {
string json_duelData = Newtonsoft.Json.JsonConvert.SerializeObject(duelData); string json_duelData = Newtonsoft.Json.JsonConvert.SerializeObject(duelData);
PlayFabMgr.Instance.UpdateUserDataValue(FishingDuelManager.SoloDataKey, json_duelData); playerSaveStore.Write(FishingDuelManager.SoloDataKey, json_duelData).RequireAccepted();
} }
#endregion SoloData #endregion SoloData
} }

View File

@@ -41,6 +41,7 @@ namespace DataCenter
public class OfferChainsChestManager public class OfferChainsChestManager
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
public const string SaveKey = "OfferChainsChestData"; public const string SaveKey = "OfferChainsChestData";
[Inject] public Tables _tables { get; set; } [Inject] public Tables _tables { get; set; }
@@ -277,7 +278,7 @@ namespace DataCenter
private void SaveData() private void SaveData()
{ {
ELog($"SaveData-> {JsonConvert.SerializeObject(OfferChainsData)}"); ELog($"SaveData-> {JsonConvert.SerializeObject(OfferChainsData)}");
PlayFabMgr.Instance.UpdateUserDataValue(SaveKey, JsonConvert.SerializeObject(OfferChainsData)); playerSaveStore.Write(SaveKey, JsonConvert.SerializeObject(OfferChainsData)).RequireAccepted();
} }
public void LoadData(string dataValue) public void LoadData(string dataValue)
@@ -289,6 +290,7 @@ namespace DataCenter
// 进入游戏舞台 // 进入游戏舞台
public async void OnEnterGameAct() public async void OnEnterGameAct()
{ {
if (!playerSaveStore.IsCurrent) return;
Log("OnEnterGameAct()"); Log("OnEnterGameAct()");
IsFirstOpen = false; IsFirstOpen = false;
await UIManager.Instance.ShowUI(new UIType(OfferChainsChestMain.UIPanel)); await UIManager.Instance.ShowUI(new UIType(OfferChainsChestMain.UIPanel));

View File

@@ -44,6 +44,7 @@ namespace DataCenter
// Disco Manager // Disco Manager
public class OfferDiscoPartyManager public class OfferDiscoPartyManager
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
public const string SaveKey = "OfferDiscoPartyManager"; public const string SaveKey = "OfferDiscoPartyManager";
// 免费抽奖次数 // 免费抽奖次数
@@ -291,7 +292,7 @@ namespace DataCenter
private void SaveData() private void SaveData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(SaveKey, JsonConvert.SerializeObject(OfferDiscoPartyData)); playerSaveStore.Write(SaveKey, JsonConvert.SerializeObject(OfferDiscoPartyData)).RequireAccepted();
} }
public void LoadData(string dataValue) public void LoadData(string dataValue)
@@ -309,6 +310,7 @@ namespace DataCenter
public async void OnEnterGameAct() public async void OnEnterGameAct()
{ {
if (!playerSaveStore.IsCurrent) return;
ELog("OnEnterGameAct()"); ELog("OnEnterGameAct()");
IsFirstOpen = false; IsFirstOpen = false;
await UIManager.Instance.ShowUI(new UIType(OfferDiscoParty.UIPanel)); await UIManager.Instance.ShowUI(new UIType(OfferDiscoParty.UIPanel));

View File

@@ -8,6 +8,7 @@ using game;
using System.Linq; using System.Linq;
public class PiggyBankPackData public class PiggyBankPackData
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
private readonly Tables _tables = GContext.container.Resolve<Tables>(); private readonly Tables _tables = GContext.container.Resolve<Tables>();
private readonly TbEventPackManager _epm private readonly TbEventPackManager _epm
= GContext.container.Resolve<Tables>().TbEventPackManager; = GContext.container.Resolve<Tables>().TbEventPackManager;
@@ -193,8 +194,8 @@ public class PiggyBankPackData
} }
public void SaveData() public void SaveData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue("PiggyBankPackData", playerSaveStore.Write("PiggyBankPackData",
Newtonsoft.Json.JsonConvert.SerializeObject(_data)); Newtonsoft.Json.JsonConvert.SerializeObject(_data)).RequireAccepted();
} }
public bool AddProgress(int fishTier, int magnification) public bool AddProgress(int fishTier, int magnification)
{ {

View File

@@ -74,7 +74,7 @@ namespace GameCore
public void SaveInitRodAccessoriesData() public void SaveInitRodAccessoriesData()
{ {
string value = Newtonsoft.Json.JsonConvert.SerializeObject(rodAccessoriesData); string value = Newtonsoft.Json.JsonConvert.SerializeObject(rodAccessoriesData);
PlayFabMgr.Instance.UpdateUserDataValue(InitRodAccessoriesDataKey, value); playerSaveStore.Write(InitRodAccessoriesDataKey, value).RequireAccepted();
} }
/// <summary> /// <summary>
/// 手是否解锁 /// 手是否解锁
@@ -272,7 +272,7 @@ namespace GameCore
.AddContent("rod_chips_consume", rquiredFragments); .AddContent("rod_chips_consume", rquiredFragments);
} }
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue("RodData", JsonMapper.ToJson(rodData)); playerSaveStore.Write("RodData", JsonMapper.ToJson(rodData)).RequireAccepted();
//鱼竿红点数据修改 //鱼竿红点数据修改
CheckRodRedPointUp(); CheckRodRedPointUp();
//合成鱼竿 //合成鱼竿
@@ -354,7 +354,7 @@ namespace GameCore
public void SaveHonorLevelData() public void SaveHonorLevelData()
{ {
string value = Newtonsoft.Json.JsonConvert.SerializeObject(_honorlevel); string value = Newtonsoft.Json.JsonConvert.SerializeObject(_honorlevel);
PlayFabMgr.Instance.UpdateUserDataValue(HonorLevelDataKey, value); playerSaveStore.Write(HonorLevelDataKey, value).RequireAccepted();
} }
#endregion 荣耀等级 #endregion 荣耀等级

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using System; using System;

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using LitJson; using LitJson;
@@ -17,12 +17,14 @@ namespace GameCore
} }
public class TotalRechargeManager public class TotalRechargeManager
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public const string TotalRechargeDataKey = "EventTotalRecharge"; public const string TotalRechargeDataKey = "EventTotalRecharge";
public TotalRechargeData GetEventData() public TotalRechargeData GetEventData()
{ {
var totalRechargeData = new TotalRechargeData(); var totalRechargeData = new TotalRechargeData();
string data = PlayFabMgr.Instance.GetLocalData(TotalRechargeDataKey); string data = saveStore.Read(TotalRechargeDataKey).ValueOrThrow();
if (!string.IsNullOrEmpty(data)) if (!string.IsNullOrEmpty(data))
{ {
try try
@@ -175,7 +177,7 @@ namespace GameCore
void SaveTotalRechargeData(TotalRechargeData totalRechargeData) void SaveTotalRechargeData(TotalRechargeData totalRechargeData)
{ {
PlayFabMgr.Instance.UpdateUserDataValue(TotalRechargeDataKey, JsonMapper.ToJson(totalRechargeData)); saveStore.Write(TotalRechargeDataKey, JsonMapper.ToJson(totalRechargeData)).RequireAccepted();
} }
} }
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using game; using game;
using GameCore; using GameCore;
using IngameDebugConsole; using IngameDebugConsole;
@@ -720,7 +720,7 @@ public class DebugFunc
} }
static void ClearLastPaymentTime() static void ClearLastPaymentTime()
{ {
PlayFabMgr.Instance.UpdateUserDataValue("LastPaymentTime", ""); GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Write("LastPaymentTime", "").RequireAccepted();
} }
static void HookWheelForecActive() static void HookWheelForecActive()
{ {

View File

@@ -607,6 +607,12 @@ public class EventFishingDuelPanel : BasePanel
} }
void CreateMatchmakingTicket() void CreateMatchmakingTicket()
{ {
if (GContext.container.Resolve<IUserService>() is not PlayFabUserService)
{
Debug.LogWarning("KV matchmaking service is not connected.");
FDM.MachingEnd(null);
return;
}
int MaxMatchTime = FDM.eventSoloConfig.MaxMatchTime; int MaxMatchTime = FDM.eventSoloConfig.MaxMatchTime;
//可匹配段位列表,杯数,选择倍率,地图列表,随机种子seed,鱼卡等级 //可匹配段位列表,杯数,选择倍率,地图列表,随机种子seed,鱼卡等级
List<int> matchTierList = FDM.eventSolomain.MatchTierList; List<int> matchTierList = FDM.eventSolomain.MatchTierList;
@@ -631,8 +637,8 @@ public class EventFishingDuelPanel : BasePanel
{ {
Entity = new EntityKey Entity = new EntityKey
{ {
Id = userService.entityKey.Id, Id = ((PlayFabUserService)userService).entityKey.Id,
Type = userService.entityKey.Type Type = ((PlayFabUserService)userService).entityKey.Type
}, },
Attributes = new MatchmakingPlayerAttributes Attributes = new MatchmakingPlayerAttributes
{ {
@@ -717,6 +723,12 @@ public class EventFishingDuelPanel : BasePanel
} }
void GetMatchDetails(string matchId) void GetMatchDetails(string matchId)
{ {
if (GContext.container.Resolve<IUserService>() is not PlayFabUserService)
{
Debug.LogWarning("KV matchmaking service is not connected.");
FDM.MachingEnd(null);
return;
}
var request = new PlayFab.MultiplayerModels.GetMatchRequest var request = new PlayFab.MultiplayerModels.GetMatchRequest
{ {
MatchId = matchId, MatchId = matchId,
@@ -745,7 +757,7 @@ public class EventFishingDuelPanel : BasePanel
for (int i = 0; i < result.Members.Count; i++) for (int i = 0; i < result.Members.Count; i++)
{ {
MatchmakingPlayerWithTeamAssignment member = result.Members[i]; MatchmakingPlayerWithTeamAssignment member = result.Members[i];
if (member.Entity.Id != userService.entityKey.Id) if (member.Entity.Id != ((PlayFabUserService)userService).entityKey.Id)
{ {
JObject dataObject = JObject.FromObject(result.Members[i].Attributes.DataObject); JObject dataObject = JObject.FromObject(result.Members[i].Attributes.DataObject);
string[] tierArray = dataObject["Tier"].ToObject<string[]>(); string[] tierArray = dataObject["Tier"].ToObject<string[]>();

View File

@@ -16,6 +16,8 @@ namespace game
GContext.OnEvent<TargetEvent>().Subscribe(OnEvent_TargetEvent).AddTo(Disposables); GContext.OnEvent<TargetEvent>().Subscribe(OnEvent_TargetEvent).AddTo(Disposables);
} }
private Tables _tables; private Tables _tables;
[Inject] public PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
bool disposed;
[Inject] [Inject]
public Tables Tables public Tables Tables
@@ -28,6 +30,7 @@ namespace game
public virtual void Dispose() public virtual void Dispose()
{ {
disposed = true;
Disposables?.Dispose(); Disposables?.Dispose();
} }
@@ -38,6 +41,7 @@ namespace game
protected virtual void OnEvent_TargetEvent(TargetEvent targetEvent) protected virtual void OnEvent_TargetEvent(TargetEvent targetEvent)
{ {
if (disposed || SaveStore == null || !SaveStore.IsCurrent) return;
if (targetEvent.type == EventType && targetEvent.subType == EventSubType) RefreshData(targetEvent.id); if (targetEvent.type == EventType && targetEvent.subType == EventSubType) RefreshData(targetEvent.id);
} }
@@ -54,7 +58,8 @@ namespace game
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"jsd {typeof(T).Name} SaveData {JsonConvert.SerializeObject(Data)}"); Debug.Log($"jsd {typeof(T).Name} SaveData {JsonConvert.SerializeObject(Data)}");
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue(typeof(T).Name, JsonConvert.SerializeObject(Data)); if (disposed) throw new ObjectDisposedException(GetType().Name);
SaveStore.Write(typeof(T).Name, JsonConvert.SerializeObject(Data)).RequireAccepted();
} }
protected virtual void SaveData(T data) protected virtual void SaveData(T data)
@@ -62,7 +67,8 @@ namespace game
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"jsd {typeof(T).Name} SaveData {JsonConvert.SerializeObject(data)}"); Debug.Log($"jsd {typeof(T).Name} SaveData {JsonConvert.SerializeObject(data)}");
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue(typeof(T).Name, JsonConvert.SerializeObject(data)); if (disposed) throw new ObjectDisposedException(GetType().Name);
SaveStore.Write(typeof(T).Name, JsonConvert.SerializeObject(data)).RequireAccepted();
} }
protected virtual void RefreshData(int eventId) protected virtual void RefreshData(int eventId)
@@ -82,7 +88,8 @@ namespace game
protected T GetCacheData() protected T GetCacheData()
{ {
var dataStr = PlayFabMgr.Instance.GetLocalData(typeof(T).Name); if (disposed) throw new ObjectDisposedException(GetType().Name);
var dataStr = SaveStore.Read(typeof(T).Name).ValueOrThrow();
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"jsd {typeof(T).Name} LoadData sData {dataStr}"); Debug.Log($"jsd {typeof(T).Name} LoadData sData {dataStr}");
#endif #endif
@@ -133,4 +140,4 @@ namespace game
} }
#endregion #endregion
} }
} }

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using GameCore; using GameCore;
using System; using System;
@@ -410,6 +410,8 @@ public class EventBingoStepState
public class EventBingoPlayfabData public class EventBingoPlayfabData
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public int EventId, TicketCount, RoundCount, StepInRound, Seed, ChainProgress, ChainTaskTokenProgress; public int EventId, TicketCount, RoundCount, StepInRound, Seed, ChainProgress, ChainTaskTokenProgress;
public float InflationRate; public float InflationRate;
private const int TokenCount = 8; private const int TokenCount = 8;
@@ -473,12 +475,12 @@ public class EventBingoPlayfabData
public void Save() public void Save()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(Key, Serialize()); saveStore.Write(Key, Serialize()).RequireAccepted();
} }
public static EventBingoPlayfabData Load() public static EventBingoPlayfabData Load()
{ {
var s = PlayFabMgr.Instance.GetLocalData(Key); var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
return Deserialize(s); return Deserialize(s);
} }
} }

View File

@@ -1,4 +1,4 @@
using System.Linq; using System.Linq;
using UnityEngine; using UnityEngine;
using SystemRandom = System.Random; using SystemRandom = System.Random;
using System.Text; using System.Text;
@@ -152,6 +152,8 @@ public class DrillModel : IHasChainPack
public class EventBreakPfData public class EventBreakPfData
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public int EventId { get; set; } public int EventId { get; set; }
public int TicketCount { get; set; } public int TicketCount { get; set; }
public int ChainPackProgress { get; set; } public int ChainPackProgress { get; set; }
@@ -238,7 +240,7 @@ public class EventBreakPfData
public void Save() public void Save()
{ {
var s = Serialize(); var s = Serialize();
PlayFabMgr.Instance.UpdateUserDataValue(PfKey, s); saveStore.Write(PfKey, s).RequireAccepted();
} }
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using GameCore; using GameCore;
using UnityEngine; using UnityEngine;
using System.Collections.Generic; using System.Collections.Generic;
@@ -350,6 +350,8 @@ public class EventCanProgressTaskInfo
public class EventCanPlayfabData public class EventCanPlayfabData
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public const string PlayFabKey = "EventCan"; public const string PlayFabKey = "EventCan";
private const char Splitter = '|', Comma = ','; private const char Splitter = '|', Comma = ',';
private const int SerializedTokenCount = 11; private const int SerializedTokenCount = 11;
@@ -433,7 +435,7 @@ public class EventCanPlayfabData
public void Save() public void Save()
{ {
var s = Serialize(); var s = Serialize();
PlayFabMgr.Instance.UpdateUserDataValue(PlayFabKey, s); saveStore.Write(PlayFabKey, s).RequireAccepted();
} }
public void Log() public void Log()

View File

@@ -1,4 +1,4 @@
// EventCubeMeltManager.cs // EventCubeMeltManager.cs
// 锻刀大赛 // 锻刀大赛
using System; using System;
@@ -22,6 +22,8 @@ using Random = UnityEngine.Random;
public class EventCubeMeltManager public class EventCubeMeltManager
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
// //
public const string SaveKey = "EventCubeMeltData"; public const string SaveKey = "EventCubeMeltData";
@@ -137,7 +139,7 @@ public class EventCubeMeltManager
{ {
ELog($"SaveData()-> {eventDataJson}"); ELog($"SaveData()-> {eventDataJson}");
} }
PlayFabMgr.Instance.UpdateUserDataValue(SaveKey, eventDataJson); SaveStore.Write(SaveKey, eventDataJson).RequireAccepted();
} }
public void AddTicket(int costValue) public void AddTicket(int costValue)

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using GameCore; using GameCore;
using asap.core; using asap.core;
@@ -265,6 +265,8 @@ public class EventGatherCoreRewardInfo
public class EventGatherCorePlayfabData public class EventGatherCorePlayfabData
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public int EventId { get; set; } public int EventId { get; set; }
public int TicketCount { get; set; } public int TicketCount { get; set; }
public int ChainPackID { get; set; } public int ChainPackID { get; set; }
@@ -312,12 +314,12 @@ public class EventGatherCorePlayfabData
public void Save() public void Save()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(Key, Serialize()); saveStore.Write(Key, Serialize()).RequireAccepted();
} }
public static EventGatherCorePlayfabData Load() public static EventGatherCorePlayfabData Load()
{ {
var s = PlayFabMgr.Instance.GetLocalData(Key); var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
var res = new EventGatherCorePlayfabData(); var res = new EventGatherCorePlayfabData();
try try
{ {

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using System; using System;
@@ -11,6 +11,8 @@ using UnityEngine;
/// </summary> /// </summary>
public class EventLuckyGameModel : ILuckyTaskGetData public class EventLuckyGameModel : ILuckyTaskGetData
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public static EventLuckyGameModel Get() public static EventLuckyGameModel Get()
{ {
var model = GContext.container.Resolve<EventLuckyGameModel>(); var model = GContext.container.Resolve<EventLuckyGameModel>();
@@ -57,7 +59,7 @@ public class EventLuckyGameModel : ILuckyTaskGetData
string s = Newtonsoft.Json.JsonConvert.SerializeObject(eventLuckGameRecord); string s = Newtonsoft.Json.JsonConvert.SerializeObject(eventLuckGameRecord);
//Debug.Log($"[EventLuckGameModel] Save EventLuckGameRecord: {s}"); //Debug.Log($"[EventLuckGameModel] Save EventLuckGameRecord: {s}");
PlayFabMgr.Instance.UpdateUserDataValue(Key, s); saveStore.Write(Key, s).RequireAccepted();
} }
//=========================以下是活动数据========================= //=========================以下是活动数据=========================
@@ -163,7 +165,7 @@ public class EventLuckyGameModel : ILuckyTaskGetData
{ {
return; return;
} }
string s = PlayFabMgr.Instance.GetLocalData(Key); string s = saveStore.Read(Key).ValueOrThrow();
if (string.IsNullOrEmpty(s)) if (string.IsNullOrEmpty(s))
{ {
eventLuckGameRecord = new EventLuckGameRecord(); eventLuckGameRecord = new EventLuckGameRecord();

View File

@@ -1,4 +1,4 @@
using System.Text; using System.Text;
using asap.core; using asap.core;
using GameCore; using GameCore;
using UnityEngine; using UnityEngine;
@@ -261,6 +261,8 @@ public class EventOfferJourneyBlockData
public class EventOfferJourneyPlayfabData public class EventOfferJourneyPlayfabData
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public int EventId, Progress, TicketCount, VipLvlOnTrigger, RoundCount; public int EventId, Progress, TicketCount, VipLvlOnTrigger, RoundCount;
public float InflationRate; public float InflationRate;
private const string Key = "EventOfferJourneyData"; private const string Key = "EventOfferJourneyData";
@@ -319,14 +321,14 @@ public class EventOfferJourneyPlayfabData
public static EventOfferJourneyPlayfabData Load() public static EventOfferJourneyPlayfabData Load()
{ {
var s = PlayFabMgr.Instance.GetLocalData(Key); var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
return Deserialize(s); return Deserialize(s);
} }
public void Save() public void Save()
{ {
var s = Serialize(); var s = Serialize();
PlayFabMgr.Instance.UpdateUserDataValue(Key, s); saveStore.Write(Key, s).RequireAccepted();
} }
} }

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Text; using System.Text;
using GameCore; using GameCore;
using UnityEngine; using UnityEngine;
@@ -156,6 +156,8 @@ public class EventOfferJourneyLvlTriggerData : AEventOfferJourneyData
public class EventOfferJourneyLvlTriggerPlayfabData public class EventOfferJourneyLvlTriggerPlayfabData
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public int Progress, TicketCount, RoundCount; public int Progress, TicketCount, RoundCount;
private const string Key = "EventOfferJourneyLvlTriggerData"; private const string Key = "EventOfferJourneyLvlTriggerData";
private const int TokenCount = 3; private const int TokenCount = 3;
@@ -195,13 +197,13 @@ public class EventOfferJourneyLvlTriggerPlayfabData
public static EventOfferJourneyLvlTriggerPlayfabData Load() public static EventOfferJourneyLvlTriggerPlayfabData Load()
{ {
var s = PlayFabMgr.Instance.GetLocalData(Key); var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
return Deserialize(s); return Deserialize(s);
} }
public void Save() public void Save()
{ {
var s = Serialize(); var s = Serialize();
PlayFabMgr.Instance.UpdateUserDataValue(Key, s); saveStore.Write(Key, s).RequireAccepted();
} }
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -564,6 +564,8 @@ public class RewardItemNewFlyToStashRequest
public class EventLuckMagicPlayfabData public class EventLuckMagicPlayfabData
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public int EventId; public int EventId;
public int TicketCount; public int TicketCount;
public int ChainProgress; public int ChainProgress;
@@ -668,7 +670,7 @@ public class EventLuckMagicPlayfabData
{ {
var s = Serialize(); var s = Serialize();
// Debug.Log($"[EventLuckMagic] Save playfab data : \n" + s); // Debug.Log($"[EventLuckMagic] Save playfab data : \n" + s);
PlayFabMgr.Instance.UpdateUserDataValue(Key, s); saveStore.Write(Key, s).RequireAccepted();
} }
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using System; using System;
@@ -15,6 +15,8 @@ public class WelcomeBackData
} }
public class EventWelcomeBackManager public class EventWelcomeBackManager
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
string DataKey_WelcomeBack = "WelcomeBack"; string DataKey_WelcomeBack = "WelcomeBack";
Tables tables; Tables tables;
public TbEventWelcomeBackConfig eventWelcomeBackConfig; public TbEventWelcomeBackConfig eventWelcomeBackConfig;
@@ -28,7 +30,7 @@ public class EventWelcomeBackManager
{ {
try try
{ {
string data = PlayFabMgr.Instance.GetLocalData(DataKey_WelcomeBack); string data = saveStore.Read(DataKey_WelcomeBack).ValueOrThrow();
if (!string.IsNullOrEmpty(data)) if (!string.IsNullOrEmpty(data))
{ {
welcomeBackData = Newtonsoft.Json.JsonConvert.DeserializeObject<WelcomeBackData>(data) ?? new WelcomeBackData(); welcomeBackData = Newtonsoft.Json.JsonConvert.DeserializeObject<WelcomeBackData>(data) ?? new WelcomeBackData();
@@ -228,7 +230,7 @@ public class EventWelcomeBackManager
void SaveData() void SaveData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(DataKey_WelcomeBack, Newtonsoft.Json.JsonConvert.SerializeObject(welcomeBackData)); saveStore.Write(DataKey_WelcomeBack, Newtonsoft.Json.JsonConvert.SerializeObject(welcomeBackData)).RequireAccepted();
} }
} }
public struct ClaimComeBack public struct ClaimComeBack

View File

@@ -1,3 +1,4 @@
using game.PlayerDataStorage;
using asap.core; using asap.core;
using cfg; using cfg;
using DG.Tweening; using DG.Tweening;
@@ -70,6 +71,7 @@ public partial class FishingBehaviorB : MonoBehaviour
catch(Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); } catch(Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); }
} }
#endif #endif
IPlayerSaveStore saveStore;
Tables _tables; Tables _tables;
[Header("RELATED OBJECT SETTINGS")] [Header("RELATED OBJECT SETTINGS")]
[SerializeField] [SerializeField]
@@ -213,6 +215,7 @@ public partial class FishingBehaviorB : MonoBehaviour
} }
public void Init() public void Init()
{ {
saveStore = GContext.container.Resolve<IPlayerSaveStore>();
fishingData = GContext.container.Resolve<FishingData>(); fishingData = GContext.container.Resolve<FishingData>();
fishingBehaviorConf = fishingData.fishingBehaviorConf; fishingBehaviorConf = fishingData.fishingBehaviorConf;
collectingData = GContext.container.Resolve<CollectingData>(); collectingData = GContext.container.Resolve<CollectingData>();
@@ -852,8 +855,8 @@ public partial class FishingBehaviorB : MonoBehaviour
{ {
GContext.Publish(new ConditionTypeEvent(ConditionType.ConsumeEnergy, energy)); GContext.Publish(new ConditionTypeEvent(ConditionType.ConsumeEnergy, energy));
} }
var success = await PlayFabMgr.Instance.SyncDataImmidiate("NewPlayerDataDic", var success = (await saveStore.WriteAndConfirmAsync("NewPlayerDataDic",
Newtonsoft.Json.JsonConvert.SerializeObject(commonDatas)); Newtonsoft.Json.JsonConvert.SerializeObject(commonDatas))).Confirmed;
Debug.Log($"[FishBehaviourB::ConsumeBaitOnPlayfab] {success}"); Debug.Log($"[FishBehaviourB::ConsumeBaitOnPlayfab] {success}");
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using System.Collections.Generic; using System.Collections.Generic;
@@ -6,6 +6,8 @@ using UnityEngine;
public class InfiniteBuildingData public class InfiniteBuildingData
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public int Lvl, Step, StepLvl = 1, LastHangingBlock = -1; public int Lvl, Step, StepLvl = 1, LastHangingBlock = -1;
public List<BlockRecord> Record = new List<BlockRecord>(); public List<BlockRecord> Record = new List<BlockRecord>();
// private readonly TbConstructionUnits BlocksTable = GContext.container.Resolve<Tables>().TbConstructionUnits; // private readonly TbConstructionUnits BlocksTable = GContext.container.Resolve<Tables>().TbConstructionUnits;
@@ -64,7 +66,7 @@ public class InfiniteBuildingData
//Debug.Log("Upload!!!!!!!!!!!!!!"); //Debug.Log("Upload!!!!!!!!!!!!!!");
//Debug.Log(s); //Debug.Log(s);
GContext.container.Resolve<PlayerData>().InfiniteBuildingData = s; GContext.container.Resolve<PlayerData>().InfiniteBuildingData = s;
PlayFabMgr.Instance.UpdateUserDataValue("InfiniteBuildingData", s); saveStore.Write("InfiniteBuildingData", s).RequireAccepted();
} }
public void DownloadData(string s) public void DownloadData(string s)

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -9,6 +9,8 @@ using UnityEngine;
public class LuckyADTaskPopupPanel : LuckyTaskPanelBase public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
{ {
game.PlayerDataStorage.IPlayerSaveStore saveStore;
List<LuckyTaskItemData> taskList; List<LuckyTaskItemData> taskList;
List<LuckyADTaskItem> taskItems = new List<LuckyADTaskItem>(); List<LuckyADTaskItem> taskItems = new List<LuckyADTaskItem>();
List<LuckyADTaskItemData> taskADList = new List<LuckyADTaskItemData>(); List<LuckyADTaskItemData> taskADList = new List<LuckyADTaskItemData>();
@@ -18,6 +20,7 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
System.DateTime refreshTime; System.DateTime refreshTime;
protected override void Awake() protected override void Awake()
{ {
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
base.Awake(); base.Awake();
luckyTaskItem = transform.Find("root/hook_missions_object").GetComponent<LuckyADTask>(); luckyTaskItem = transform.Find("root/hook_missions_object").GetComponent<LuckyADTask>();
text_time = transform.Find("root/text_time").GetComponent<TMP_Text>(); text_time = transform.Find("root/text_time").GetComponent<TMP_Text>();
@@ -120,7 +123,7 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
int round = taskData.GetValueOrDefault(-1, 0); int round = taskData.GetValueOrDefault(-1, 0);
float inflationRate = taskData.GetValueOrDefault(-3, 0) / 100f; float inflationRate = taskData.GetValueOrDefault(-3, 0) / 100f;
int group = taskData.GetValueOrDefault(-4, 0); int group = taskData.GetValueOrDefault(-4, 0);
string adData = PlayFabMgr.Instance.GetLocalData(LuckyTaskData.ADTaskADKey); string adData = saveStore.Read(LuckyTaskData.ADTaskADKey).ValueOrThrow();
int index = 0; int index = 0;
int curDay = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; int curDay = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
if (!string.IsNullOrEmpty(adData)) if (!string.IsNullOrEmpty(adData))
@@ -134,13 +137,13 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
else else
{ {
adData = $"{index}|{curDay}"; adData = $"{index}|{curDay}";
PlayFabMgr.Instance.UpdateUserDataValue(LuckyTaskData.ADTaskADKey, adData); saveStore.Write(LuckyTaskData.ADTaskADKey, adData).RequireAccepted();
} }
} }
else else
{ {
adData = $"{index}|{curDay}"; adData = $"{index}|{curDay}";
PlayFabMgr.Instance.UpdateUserDataValue(LuckyTaskData.ADTaskADKey, adData); saveStore.Write(LuckyTaskData.ADTaskADKey, adData).RequireAccepted();
} }
List<LuckyADTaskItemData> taskADList = new List<LuckyADTaskItemData>(); List<LuckyADTaskItemData> taskADList = new List<LuckyADTaskItemData>();
TbEventLuckyTask eventLuckyTask = LuckyTaskData.GetTbEventLuckyTask(luckyModel.EventId, round, group); TbEventLuckyTask eventLuckyTask = LuckyTaskData.GetTbEventLuckyTask(luckyModel.EventId, round, group);
@@ -180,7 +183,7 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
luckyADTaskItem.Play(); luckyADTaskItem.Play();
string adData = PlayFabMgr.Instance.GetLocalData(LuckyTaskData.ADTaskADKey); string adData = saveStore.Read(LuckyTaskData.ADTaskADKey).ValueOrThrow();
int index = 0; int index = 0;
int day = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; int day = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
if (!string.IsNullOrEmpty(adData)) if (!string.IsNullOrEmpty(adData))
@@ -195,7 +198,7 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
//保存进度 //保存进度
index++; index++;
adData = $"{index}|{day}"; adData = $"{index}|{day}";
PlayFabMgr.Instance.UpdateUserDataValue(LuckyTaskData.ADTaskADKey, adData); saveStore.Write(LuckyTaskData.ADTaskADKey, adData).RequireAccepted();
RefreshADItemState(index); RefreshADItemState(index);
canvasGroup.blocksRaycasts = true; canvasGroup.blocksRaycasts = true;
GContext.Publish(new ShowData()); GContext.Publish(new ShowData());

View File

@@ -1,3 +1,4 @@
using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using TMPro; using TMPro;
@@ -6,6 +7,8 @@ using UnityEngine.UI;
public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
{ {
game.PlayerDataStorage.IPlayerSaveStore saveStore;
Button btn; Button btn;
ILuckyTaskGetData model; ILuckyTaskGetData model;
@@ -15,6 +18,7 @@ public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
public TMP_Text text_redpoint_big; public TMP_Text text_redpoint_big;
void Awake() void Awake()
{ {
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
btn = GetComponent<Button>(); btn = GetComponent<Button>();
btn.onClick.AddListener(OnClick); btn.onClick.AddListener(OnClick);
} }
@@ -29,6 +33,7 @@ public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
if (model != null) if (model != null)
{ {
GameObject taskPanelName = await UIManager.Instance.ShowUI(new UIType(model.TaskPanelName)); GameObject taskPanelName = await UIManager.Instance.ShowUI(new UIType(model.TaskPanelName));
if (this == null || !saveStore.IsCurrent) return;
if (taskPanelName != null) if (taskPanelName != null)
{ {
var panel = taskPanelName.GetComponent<LuckyTaskPanelBase>(); var panel = taskPanelName.GetComponent<LuckyTaskPanelBase>();
@@ -61,7 +66,7 @@ public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
int count = RedPointManager.Instance.GetRedPointCount(key); int count = RedPointManager.Instance.GetRedPointCount(key);
if (!state) if (!state)
{ {
string adData = PlayFabMgr.Instance.GetLocalData(LuckyTaskData.ADTaskADKey); string adData = saveStore.Read(LuckyTaskData.ADTaskADKey).ValueOrThrow();
if (!string.IsNullOrEmpty(adData)) if (!string.IsNullOrEmpty(adData))
{ {
string[] strings = adData.Split("|"); string[] strings = adData.Split("|");

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -7,13 +7,21 @@ using UnityEngine;
public class LuckyTaskPanel : LuckyTaskPanelBase public class LuckyTaskPanel : LuckyTaskPanelBase
{ {
protected override void Awake()
{
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
base.Awake();
}
game.PlayerDataStorage.IPlayerSaveStore saveStore;
/// <summary> /// <summary>
/// luckyModel 也作为是否自动全部领奖的标志 /// luckyModel 也作为是否自动全部领奖的标志
/// </summary> /// </summary>
List<LuckyTaskItem> taskItems = new List<LuckyTaskItem>(); List<LuckyTaskItem> taskItems = new List<LuckyTaskItem>();
protected override void SetItem() protected override void SetItem()
{ {
string adData = PlayFabMgr.Instance.GetLocalData(LuckyTaskData.ADTaskADKey); string adData = saveStore.Read(LuckyTaskData.ADTaskADKey).ValueOrThrow();
int curDay = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; int curDay = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
if (!string.IsNullOrEmpty(adData)) if (!string.IsNullOrEmpty(adData))
{ {
@@ -22,13 +30,13 @@ public class LuckyTaskPanel : LuckyTaskPanelBase
if (strings.Length != 2 || !int.TryParse(strings[1], out day) || day != curDay) if (strings.Length != 2 || !int.TryParse(strings[1], out day) || day != curDay)
{ {
adData = $"{0}|{curDay}"; adData = $"{0}|{curDay}";
PlayFabMgr.Instance.UpdateUserDataValue(LuckyTaskData.ADTaskADKey, adData); saveStore.Write(LuckyTaskData.ADTaskADKey, adData).RequireAccepted();
} }
} }
else else
{ {
adData = $"{0}|{curDay}"; adData = $"{0}|{curDay}";
PlayFabMgr.Instance.UpdateUserDataValue(LuckyTaskData.ADTaskADKey, adData); saveStore.Write(LuckyTaskData.ADTaskADKey, adData).RequireAccepted();
} }
List<LuckyTaskItemData> taskList = new List<LuckyTaskItemData>(); List<LuckyTaskItemData> taskList = new List<LuckyTaskItemData>();
int level = taskData.GetValueOrDefault(-2, 0); int level = taskData.GetValueOrDefault(-2, 0);

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using LitJson; using LitJson;
@@ -36,6 +36,8 @@ public enum FlipResult
public class EventMatchGameManager public class EventMatchGameManager
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public string EventMatchGameDataKey => "EventMatchGameDataKey"; public string EventMatchGameDataKey => "EventMatchGameDataKey";
public int ItemSubType => 3; public int ItemSubType => 3;
public int ItemType => 6; public int ItemType => 6;
@@ -59,7 +61,7 @@ public class EventMatchGameManager
{ {
var matchGameData = new MatchGameData(); var matchGameData = new MatchGameData();
string data = PlayFabMgr.Instance.GetLocalData(EventMatchGameDataKey); string data = saveStore.Read(EventMatchGameDataKey).ValueOrThrow();
if (!string.IsNullOrEmpty(data)) if (!string.IsNullOrEmpty(data))
{ {
try try
@@ -491,7 +493,7 @@ public class EventMatchGameManager
public void SaveData(MatchGameData matchGameData) public void SaveData(MatchGameData matchGameData)
{ {
PlayFabMgr.Instance.UpdateUserDataValue(EventMatchGameDataKey, JsonMapper.ToJson(matchGameData)); saveStore.Write(EventMatchGameDataKey, JsonMapper.ToJson(matchGameData)).RequireAccepted();
} }
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -53,6 +53,8 @@ public interface IMiniBattlePassDataProvider
public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
private Tables _tables; private Tables _tables;
PlayerItemData _playerItemData; PlayerItemData _playerItemData;
string newDataKey => "MiniBPKeyNew"; string newDataKey => "MiniBPKeyNew";
@@ -138,10 +140,11 @@ public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider
public MiniBattlePassRecord GetMiniBattlePassRecord(string key) public MiniBattlePassRecord GetMiniBattlePassRecord(string key)
{ {
if (!saveStore.IsCurrent) throw new System.InvalidOperationException("Expired battle pass session.");
MiniBattlePassRecord Data; MiniBattlePassRecord Data;
if (datas == null) if (datas == null)
{ {
string dataStr = PlayFabMgr.Instance.GetLocalData(newDataKey); string dataStr = saveStore.Read(newDataKey).ValueOrThrow();
//Debug.Log($"GetMiniBattlePassRecord: dataStr = {dataStr}"); //Debug.Log($"GetMiniBattlePassRecord: dataStr = {dataStr}");
if (string.IsNullOrEmpty(dataStr)) if (string.IsNullOrEmpty(dataStr))
{ {
@@ -168,7 +171,7 @@ public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider
public void SetMiniBattlePassRecord(string key, MiniBattlePassRecord record) public void SetMiniBattlePassRecord(string key, MiniBattlePassRecord record)
{ {
datas[key] = record; datas[key] = record;
PlayFabMgr.Instance.UpdateUserDataValue(newDataKey, JsonConvert.SerializeObject(datas)); saveStore.Write(newDataKey, JsonConvert.SerializeObject(datas)).RequireAccepted();
} }
} }
public class MiniBattlePassProgressInfo public class MiniBattlePassProgressInfo

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -15,6 +15,8 @@ using UnityEngine.ResourceManagement.AsyncOperations;
public class DiggingGameModel public class DiggingGameModel
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
#region 协议Const区域 #region 协议Const区域
//工具个数 //工具个数
@@ -78,8 +80,8 @@ public class DiggingGameModel
redDot = cycle.RedDot; redDot = cycle.RedDot;
} }
DiggingGameManager.LogError("沙滩寻宝开启"); DiggingGameManager.LogError("沙滩寻宝开启");
string countJson = PlayFabMgr.Instance.GetLocalData(DigCountKey); string countJson = saveStore.Read(DigCountKey).ValueOrThrow();
string digGridDataJson = PlayFabMgr.Instance.GetLocalData(DigGridDataKey); string digGridDataJson = saveStore.Read(DigGridDataKey).ValueOrThrow();
if (string.IsNullOrEmpty(countJson)) if (string.IsNullOrEmpty(countJson))
{ {
SaveNull(); SaveNull();
@@ -105,8 +107,8 @@ public class DiggingGameModel
MaxCount = digActivityConfig.StageCount; MaxCount = digActivityConfig.StageCount;
} }
string countJson = PlayFabMgr.Instance.GetLocalData(DigCountKey); string countJson = saveStore.Read(DigCountKey).ValueOrThrow();
string digGridDataJson = PlayFabMgr.Instance.GetLocalData(DigGridDataKey); string digGridDataJson = saveStore.Read(DigGridDataKey).ValueOrThrow();
DoServerData(countJson, digGridDataJson); DoServerData(countJson, digGridDataJson);
await InitClientData(ClientDataHandleEnum.Init); await InitClientData(ClientDataHandleEnum.Init);
} }
@@ -460,7 +462,7 @@ public class DiggingGameModel
dataJson.ActivityId = _curActivityId; dataJson.ActivityId = _curActivityId;
dataJson.DigCount = _serverDigCount; dataJson.DigCount = _serverDigCount;
string json = JsonConvert.SerializeObject(dataJson); string json = JsonConvert.SerializeObject(dataJson);
PlayFabMgr.Instance.UpdateUserDataValue(DigCountKey, json); saveStore.Write(DigCountKey, json).RequireAccepted();
GContext.container.Resolve<FishingEventData>().SaveTransitionData(dataJson.ActivityId, dataJson.DigCount); GContext.container.Resolve<FishingEventData>().SaveTransitionData(dataJson.ActivityId, dataJson.DigCount);
} }
void SetRedPoint() void SetRedPoint()
@@ -493,7 +495,7 @@ public class DiggingGameModel
string json = JsonConvert.SerializeObject(dataJson); string json = JsonConvert.SerializeObject(dataJson);
//DiggingGameManager.LogError(json); //DiggingGameManager.LogError(json);
PlayFabMgr.Instance.UpdateUserDataValue(DigGridDataKey, json); saveStore.Write(DigGridDataKey, json).RequireAccepted();
} }
public void SaveAll() public void SaveAll()
@@ -509,7 +511,7 @@ public class DiggingGameModel
{ {
_serverDigCount = GContext.container.Resolve<FishingEventData>().GetInitWelcomeGift(_curActivityId); _serverDigCount = GContext.container.Resolve<FishingEventData>().GetInitWelcomeGift(_curActivityId);
SaveDigCount(); SaveDigCount();
PlayFabMgr.Instance.UpdateUserDataValue(DigGridDataKey, string.Empty); saveStore.Write(DigGridDataKey, string.Empty).RequireAccepted();
} }
#endregion #endregion

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using com.fpnn; using com.fpnn;
using com.fpnn.rtm; using com.fpnn.rtm;
using GameCore; using GameCore;

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using System; using System;
@@ -32,6 +32,57 @@ namespace game
} }
public class ClubService //: IClubService public class ClubService //: IClubService
{ {
[Inject] public PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
// BEGIN B01 fixed prompt owner
readonly bool requiresFormalKV;
PlayerDataStorage.PlayerKVGameSessionHandle promptOwner;
internal PlayerDataStorage.PlayerKVEditResult LastPromptEdit { get; private set; }
public ClubService() : this(RequireFrozenSelection()) { }
static bool RequireFrozenSelection()
{
var selection = GContext.container.Resolve<PlayerDataStorage.PlayerKVRuntimeSelection>();
if (selection == null) throw DefaultSettingService.PromptFailure("IsOpenClubRed: startup selection missing.");
return selection.IsKV;
}
internal ClubService(bool formalKV) { requiresFormalKV = formalKV; }
internal ClubService(PlayerDataStorage.PlayerKVGameSessionHandle owner) : this(true)
{
promptOwner = owner ?? throw new ArgumentNullException(nameof(owner));
RequirePromptOwner();
}
// The constructor captures the registered startup selection, never a later mutable config value.
internal void PreflightFormalPromptOwner(PlayerDataStorage.PlayerKVGameSessionHandle owner)
{
if (!requiresFormalKV || owner == null || (promptOwner != null && !ReferenceEquals(promptOwner, owner)))
throw DefaultSettingService.PromptFailure("IsOpenClubRed: formal owner mismatch.");
var read = KVMgr.ReadKV(owner, "IsOpenClubRed");
if (!read.Success)
throw DefaultSettingService.PromptFailure($"IsOpenClubRed owner={owner.OwnerInstanceId}: {read.Error}");
}
internal void BindFormalPromptOwner(PlayerDataStorage.PlayerKVGameSessionHandle owner)
{
PreflightFormalPromptOwner(owner);
if (promptOwner == null) promptOwner = owner;
RequirePromptOwner();
}
internal bool HasFormalPromptOwner(PlayerDataStorage.PlayerKVGameSessionHandle owner) =>
requiresFormalKV && ReferenceEquals(promptOwner, owner);
internal void RequirePromptOwner()
{
if (promptOwner != null) ReadPrompt();
else if (requiresFormalKV)
throw DefaultSettingService.PromptFailure("IsOpenClubRed: formal owner not handed off.");
}
PlayerDataStorage.PlayerKVKeyReadResult ReadPrompt()
{
if (LastPromptEdit != null && !LastPromptEdit.Accepted)
throw DefaultSettingService.PromptFailure($"IsOpenClubRed owner={promptOwner.OwnerInstanceId}: {LastPromptEdit.StateResult.Error}; uncertain={LastPromptEdit.StateResult.LocalCommitMayHaveSucceeded}");
var read = KVMgr.ReadKV(promptOwner, "IsOpenClubRed");
if (!read.Success)
throw DefaultSettingService.PromptFailure($"IsOpenClubRed owner={promptOwner.OwnerInstanceId}: {read.Error}");
return read;
}
// END B01 fixed prompt owner
[Inject] [Inject]
public IUserService userService { get; set; } public IUserService userService { get; set; }
[Inject] [Inject]
@@ -137,7 +188,7 @@ namespace game
} }
void SaveLureDonateData() void SaveLureDonateData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue("LureDonateData", Newtonsoft.Json.JsonConvert.SerializeObject(lureDonateData)); SaveStore.Write("LureDonateData", Newtonsoft.Json.JsonConvert.SerializeObject(lureDonateData)).RequireAccepted();
} }
//刷新体力求助次数 //刷新体力求助次数
public void RefreshLureDonate() public void RefreshLureDonate()
@@ -152,6 +203,22 @@ namespace game
public void SetOpenClubRed() public void SetOpenClubRed()
{ {
if (requiresFormalKV && promptOwner == null) RequirePromptOwner();
if (promptOwner != null)
{
var read = ReadPrompt();
// M14: any present string means read, including "0" and the empty string.
if (read.Found != true)
{
LastPromptEdit = KVMgr.WriteKV(promptOwner, "IsOpenClubRed", "1");
if (!LastPromptEdit.Accepted)
throw DefaultSettingService.PromptFailure($"IsOpenClubRed owner={promptOwner.OwnerInstanceId}: {LastPromptEdit.StateResult.Error}; uncertain={LastPromptEdit.StateResult.LocalCommitMayHaveSucceeded}");
}
RequirePromptOwner();
IsOpenClubRed = false;
SetClubRed();
return;
}
if (IsOpenClubRed) if (IsOpenClubRed)
{ {
IsOpenClubRed = false; IsOpenClubRed = false;
@@ -623,7 +690,7 @@ namespace game
{ {
if (!TryReadClubResponse<ClubSinginResp>(json, out var clubSinginResp, 0)) return; if (!TryReadClubResponse<ClubSinginResp>(json, out var clubSinginResp, 0)) return;
signTime = ZZTimeHelper.UtcNow().UtcNowOffset(); signTime = ZZTimeHelper.UtcNow().UtcNowOffset();
PlayFabMgr.Instance.UpdateUserDataValue("ClubSignin", signTime.Value.ToString("yyyy-MM-dd HH:mm:ss")); SaveStore.Write("ClubSignin", signTime.Value.ToString("yyyy-MM-dd HH:mm:ss")).RequireAccepted();
if (clubSinginResp.code == 0) if (clubSinginResp.code == 0)
{ {
chatService.SendSystemMessage($"{MessageChatType.SI}_{apiPlayerId}"); chatService.SendSystemMessage($"{MessageChatType.SI}_{apiPlayerId}");

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
@@ -20,6 +20,8 @@ public interface IDeferredRewardStashService
public class DeferredRewardStashService : IDeferredRewardStashService public class DeferredRewardStashService : IDeferredRewardStashService
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
private readonly TbItem _itemTable = GContext.container.Resolve<Tables>().TbItem; private readonly TbItem _itemTable = GContext.container.Resolve<Tables>().TbItem;
private readonly PlayerItemData _playerItemData = GContext.container.Resolve<PlayerItemData>(); private readonly PlayerItemData _playerItemData = GContext.container.Resolve<PlayerItemData>();
private CompositeDisposable _disposables; private CompositeDisposable _disposables;
@@ -190,13 +192,13 @@ public class DeferredRewardStashService : IDeferredRewardStashService
public void UploadData() public void UploadData()
{ {
var s = Serialize(); var s = Serialize();
PlayFabMgr.Instance.UpdateUserDataValue(Key, s); saveStore.Write(Key, s).RequireAccepted();
} }
public void LoadData() public void LoadData()
{ {
var s = saveStore.Read(Key).ValueOrThrow();
Reset(); Reset();
var s = PlayFabMgr.Instance.GetLocalData(Key);
if (string.IsNullOrEmpty(s)) if (string.IsNullOrEmpty(s))
return; return;
_items = Deserialize(s); _items = Deserialize(s);

View File

@@ -1,4 +1,4 @@
using System; using System;
public interface IFootPrintService public interface IFootPrintService
{ {
public EFootPrint FootPrint { get; } public EFootPrint FootPrint { get; }
@@ -11,6 +11,8 @@ public interface IFootPrintService
// ReSharper disable once ClassNeverInstantiated.Global // ReSharper disable once ClassNeverInstantiated.Global
public class FootPrintService : IFootPrintService public class FootPrintService : IFootPrintService
{ {
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
private EFootPrint _footPrint; private EFootPrint _footPrint;
private ulong _kvCompleteFootPrint; private ulong _kvCompleteFootPrint;
internal ulong CompleteFootPrintForKVValidation => _kvCompleteFootPrint; internal ulong CompleteFootPrintForKVValidation => _kvCompleteFootPrint;
@@ -39,13 +41,15 @@ public class FootPrintService : IFootPrintService
public void UploadData() public void UploadData()
{ {
var data = (ulong)_footPrint; _kvCompleteFootPrint |= unchecked((uint)(int)_footPrint);
PlayFabMgr.Instance.UpdateUserDataValue("FootPrint", data.ToString()); var data = _kvCompleteFootPrint;
SaveStore.Write("FootPrint", data.ToString()).RequireAccepted();
} }
public void LoadData(string data) public void LoadData(string data)
{ {
_footPrint = (EFootPrint) ulong.Parse(data); _kvCompleteFootPrint = ulong.Parse(data);
_footPrint = unchecked((EFootPrint)(int)(uint)_kvCompleteFootPrint);
} }
public bool HasFootPrint(EFootPrint state) public bool HasFootPrint(EFootPrint state)

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using asap.playfab.async; using asap.playfab.async;
using GameCore; using GameCore;
using PlayFab.ClientModels; using PlayFab.ClientModels;
@@ -14,6 +14,7 @@ namespace game
{ {
public class FriendService public class FriendService
{ {
[Inject] public PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
[Inject] [Inject]
public IUserService userService { get; set; } public IUserService userService { get; set; }
@@ -45,7 +46,7 @@ namespace game
{ {
List<cfg.Invite> invites = tables.TbInvite.DataList; List<cfg.Invite> invites = tables.TbInvite.DataList;
//已经领取奖励的数量 //已经领取奖励的数量
string receiveRewardCountStr = PlayFabMgr.Instance.GetLocalData(InviteLinkRewardCountKey); string receiveRewardCountStr = SaveStore.Read(InviteLinkRewardCountKey).ValueOrThrow();
int.TryParse(receiveRewardCountStr, out InviteLinkRewardCount); int.TryParse(receiveRewardCountStr, out InviteLinkRewardCount);
int canRewardCount = InviteLinkRewardCount; int canRewardCount = InviteLinkRewardCount;
for (int i = InviteLinkRewardCount; i < invites.Count; i++) for (int i = InviteLinkRewardCount; i < invites.Count; i++)
@@ -93,7 +94,7 @@ namespace game
GContext.container.Resolve<PlayerItemData>().AddItemByDropList(DropIDs, null); GContext.container.Resolve<PlayerItemData>().AddItemByDropList(DropIDs, null);
GContext.Publish(new ShowData()); GContext.Publish(new ShowData());
InviteLinkRewardCount = canRewardCount; InviteLinkRewardCount = canRewardCount;
PlayFabMgr.Instance.UpdateUserDataValue(InviteLinkRewardCountKey, InviteLinkRewardCount.ToString()); SaveStore.Write(InviteLinkRewardCountKey, InviteLinkRewardCount.ToString()).RequireAccepted();
RedPointManager.Instance.SetRedPointState(RedPointName.Home_Invite, false); RedPointManager.Instance.SetRedPointState(RedPointName.Home_Invite, false);
#if AGG #if AGG
using (var e = GEvent.GameEvent("invite_reward")) using (var e = GEvent.GameEvent("invite_reward"))

View File

@@ -37,14 +37,68 @@ public class LocalNotificationService : ILocalNotificationService
#endif #endif
bool isInit = false; bool isInit = false;
bool initing = false; bool initing = false;
DefaultSettingService promptSettings;
Task initialization;
Task opening;
internal bool HasKVPromptOwner => promptSettings != null;
public LocalNotificationService() public LocalNotificationService()
{ {
_notificationList = GContext.container.Resolve<cfg.Tables>().TbNotification.DataList; _notificationList = GContext.container.Resolve<cfg.Tables>().TbNotification.DataList;
} }
public async System.Threading.Tasks.Task Init() internal LocalNotificationService(cfg.Tables tables)
{ {
if (isInit) return; _notificationList = (tables ?? throw new ArgumentNullException(nameof(tables))).TbNotification.DataList;
}
internal LocalNotificationService(DefaultSettingService settings, cfg.Tables tables)
{
if (settings == null || !settings.HasKVPromptOwner)
throw new ArgumentException("Fixed KV settings required.", nameof(settings));
promptSettings = settings;
_notificationList = (tables ?? throw new ArgumentNullException(nameof(tables))).TbNotification.DataList;
RequirePromptOwner();
}
internal void RequirePromptOwner(ISettingService expected = null)
{
if (expected != null && (promptSettings != null || expected is DefaultSettingService setting && setting.HasKVPromptOwner)
&& !ReferenceEquals(expected, promptSettings))
throw DefaultSettingService.PromptFailure("ShowNotice: notification/settings owner mismatch.");
promptSettings?.RequirePromptOwner();
}
internal void PreflightFormalPromptSettings(DefaultSettingService settings)
{
if (settings == null || !isInit || initing ||
(initialization != null && !initialization.IsCompleted) ||
(opening != null && !opening.IsCompleted) ||
(promptSettings != null && !ReferenceEquals(promptSettings, settings)))
throw DefaultSettingService.PromptFailure("ShowNotice: notification handoff is not idle.");
}
internal void BindFormalPromptSettings(DefaultSettingService settings)
{
PreflightFormalPromptSettings(settings);
if (!settings.HasKVPromptOwner)
throw DefaultSettingService.PromptFailure("ShowNotice: notification owner not handed off.");
settings.RequirePromptOwner();
if (promptSettings == null) promptSettings = settings;
RequirePromptOwner(settings);
}
internal bool HasFormalPromptSettings(DefaultSettingService settings) =>
ReferenceEquals(promptSettings, settings);
public Task Init()
{
RequirePromptOwner();
if (isInit) return Task.CompletedTask;
if (initialization != null && !initialization.IsCompleted) return initialization;
return initialization = InitCoreAsync();
}
async Task InitCoreAsync()
{
RequirePromptOwner();
#if UNITY_EDITOR #if UNITY_EDITOR
await System.Threading.Tasks.Task.Yield(); await System.Threading.Tasks.Task.Yield();
#else #else
@@ -59,9 +113,11 @@ public class LocalNotificationService : ILocalNotificationService
Debug.Log("[LocalNotificationService::Init] Channel Inited"); Debug.Log("[LocalNotificationService::Init] Channel Inited");
await NotificationCenter.RequestPermission(); await NotificationCenter.RequestPermission();
RequirePromptOwner();
ScheduleDefaultNotification(); ScheduleDefaultNotification();
GContext.OnEvent<EventRankData>().Subscribe(ScheduleRankNotification); GContext.OnEvent<EventRankData>().Subscribe(ScheduleRankNotification);
#endif #endif
RequirePromptOwner();
isInit = true; isInit = true;
} }
@@ -86,14 +142,36 @@ public class LocalNotificationService : ILocalNotificationService
public async void OpenSettings() public async void OpenSettings()
{ {
if (initing) return; try { await OpenSettingsAsync(); }
catch (Exception ex) { game.DefaultSettingService.LogPromptFailure(ex); }
}
internal Task OpenSettingsAsync()
{
if (promptSettings == null && GContext.container.Resolve<ISettingService>() is DefaultSettingService pending)
pending.RequirePromptOwner();
RequirePromptOwner();
if (initing && opening == null)
throw DefaultSettingService.PromptFailure("ShowNotice: reentrant permission request.");
if (opening != null && !opening.IsCompleted) return opening;
return opening = OpenSettingsCoreAsync();
}
async Task OpenSettingsCoreAsync()
{
ISettingService settings = promptSettings ?? GContext.container.Resolve<ISettingService>();
RequirePromptOwner(settings);
(settings as DefaultSettingService)?.RequirePromptOwner();
initing = true; initing = true;
if (!isInit) try
await Init(); {
else if (!isInit) await Init();
NotificationCenter.OpenNotificationSettings(); else NotificationCenter.OpenNotificationSettings();
GContext.container.Resolve<ISettingService>().ShowNotice = true; RequirePromptOwner(settings);
initing = false; (settings as DefaultSettingService)?.RequirePromptOwner();
settings.ShowNotice = true;
}
finally { initing = false; }
} }
public void ScheduleDailyNotification(int notificationID, DateTime fireTime, string title, string text) public void ScheduleDailyNotification(int notificationID, DateTime fireTime, string title, string text)
@@ -150,6 +228,8 @@ public class LocalNotificationService : ILocalNotificationService
private void ScheduleRankNotification(EventRankData evt) private void ScheduleRankNotification(EventRankData evt)
{ {
try { RequirePromptOwner(); }
catch (InvalidOperationException ex) { Debug.LogError(ex.Message); return; }
if (rankNotifyScheduled || !IsNoticeOpen || !isInit) return; if (rankNotifyScheduled || !IsNoticeOpen || !isInit) return;
Debug.Log("[LocalNotificationService::SendNotification] RankEvent"); Debug.Log("[LocalNotificationService::SendNotification] RankEvent");
@@ -170,4 +250,4 @@ public class LocalNotificationService : ILocalNotificationService
rankNotifyScheduled = true; rankNotifyScheduled = true;
Debug.Log("[LocalNotificationService::SendRankEventNotification] "); Debug.Log("[LocalNotificationService::SendRankEventNotification] ");
} }
} }

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;

View File

@@ -0,0 +1,341 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using asap.core;
using game.PlayerDataStorage;
using GameCore;
using tysdk;
using UnityEngine;
namespace game
{
// One service belongs to one login lifetime. A revoked instance is never rebound.
public sealed class KVUserService : IUserService, IKVUserLoadReceiver, IKVUserLifecycle
{
[Inject] public ICustomServerMgr customServerMgr { get; set; }
[Inject] public IPlayerSaveStore SaveStore { get; set; }
readonly PlayerKVRuntimeSelection selection;
readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
readonly CancellationTokenSource lifetime = new CancellationTokenSource();
readonly Dictionary<string, PlayInfo> profiles = new Dictionary<string, PlayInfo>();
readonly Dictionary<string, int> levels = new Dictionary<string, int>();
PlayerKVGameLoadCoordinator coordinator;
PlayerKVGameSessionHandle handle;
PlayerKVInitializationIntents intents;
bool attempted, loading, closing, published, promptsBound;
Task closeTask;
long epoch;
string playerId;
uint internalId;
public string CustomId { get; set; }
public string UserId => IsAuthorized ? playerId : null;
public string ApiPlayerId => UserId;
public uint InternalId => IsAuthorized ? internalId : 0;
public bool IsLogin => IsAuthorized;
public bool IsAuthorized => published && KVHandoffReady;
public CancellationToken LoginLifetime => lifetime.Token;
public DateTime LastLoginTime { get; private set; }
public DateTime LoginTime { get; private set; }
public DateTime CreateTime { get; private set; }
public string DisplayName { get; private set; }
public string AvatarUrl { get; private set; }
public string Language { get; private set; }
public string ContinentCode { get; private set; }
public Dictionary<string, string> EAccoutUrl { get; private set; } = new Dictionary<string, string>();
internal int KVTapJoyBalance { get; private set; }
internal PlayerKVGameLoadError KVLastError { get; private set; }
internal bool KVHandoffReady => !closing && handle != null && coordinator != null &&
coordinator.TryGetCurrentGameHandle(epoch, out var current) && ReferenceEquals(current, handle);
bool IKVUserLifecycle.KVHandoffReady => KVHandoffReady;
PlayerKVGameLoadError IKVUserLifecycle.KVLastError => KVLastError;
public KVUserService() : this(SubBoostrap.RuntimeSelection, jar => new HttpClientHandler
{ CookieContainer = jar, UseCookies = true, AllowAutoRedirect = false }) { }
internal KVUserService(PlayerKVRuntimeSelection selection, Func<CookieContainer, HttpMessageHandler> handlerFactory)
{
this.selection = selection;
this.handlerFactory = handlerFactory ?? throw new ArgumentNullException(nameof(handlerFactory));
Application.quitting += Logout;
}
public async Task<ELoginStep> Login(Action<int> progress, bool tyrLoginByToken = true)
{
if (attempted || closing || selection == null || !selection.IsKV ||
!selection.TryAdmitPlayer(CustomId, out var canonicalId))
{ KVLastError = PlayerKVGameLoadError.Input; return ELoginStep.Api; }
// Set before the first await: duplicate calls cannot authenticate or subscribe twice.
attempted = true;
var requestedId = CustomId;
try
{
if (customServerMgr is not CustomServerMgr server ||
server.ApiBaseUri.AbsoluteUri != PlayerKVRuntimeSelection.FormalApiUrl)
throw new InvalidOperationException("Formal KV transport required.");
loading = true;
var transaction = PlayerKVGameplayBindings.Bind(this);
coordinator = new PlayerKVGameLoadCoordinator(transaction,
() => loading && !closing && !published && handle == null,
() => { }, () => intents?.Count ?? 0, server.BindKVForGame, server.ClearKVForGame);
server.KVUnauthorized += OnUnauthorized;
var identity = new PlayerKVIdentitySnapshot(null, false, PlayerKVIdentityChannel.none,
Array.Empty<PlayerKVIdentityChannel>());
var snapshot = PlayerKVGameplayBindings.Snapshot(DateTime.UtcNow, "formal-v1");
var capability = PlayerKVGameLoadCapability.CreateForGame(identity, snapshot, selection.Api,
Application.persistentDataPath, handlerFactory, handlerFactory, handlerFactory);
var result = await coordinator.LoadForGameAsync(canonicalId, capability, LoginLifetime,
() => !closing && CustomId == requestedId);
KVLastError = result.Error;
if (result.State != PlayerKVGameLoadState.GameLoaded || closing || CustomId != requestedId ||
result.Account == null || result.Account.PlayerId != canonicalId || result.Account.Epoch != result.Epoch ||
!coordinator.TryGetCurrentGameHandle(result.Epoch, out var original))
{ Logout(); return ELoginStep.Api; }
epoch = result.Epoch;
handle = original;
if (SaveStore is not KVPlayerSaveStore store) throw new InvalidOperationException("KV save store required.");
store.Bind(original, LoginLifetime);
PrepareProfile(result.Account);
BindPromptServices(original);
// Chat subscribes before the unique success event. No SDK or analytics initialization here.
GContext.container.Resolve<IChatService>().WaitingForLogin();
if (!KVHandoffReady || CustomId != requestedId) throw new InvalidOperationException("Stale KV login.");
server.EnableKVBusiness(epoch);
playerId = result.Account.PlayerId;
internalId = result.Account.InternalId;
published = true;
GContext.Publish(new EvtAPISvrLoginSuccess { playerId = playerId, internalId = internalId });
if (!IsAuthorized) return ELoginStep.Api;
_ = RunSaveScheduleAsync(store);
progress?.Invoke(2);
return ELoginStep.Done;
}
catch (Exception ex)
{
KVLastError = PlayerKVGameLoadError.Apply;
Debug.LogError("KV login failed: " + ex.GetType().Name);
Logout();
return ELoginStep.Api;
}
finally { loading = false; }
}
void PrepareProfile(PlayerKVAccountDescriptor account)
{
if (account.InternalId == 0 || account.CreateTimeUtc == DateTime.MinValue || account.ServerTimeUtc == DateTime.MinValue)
throw new InvalidOperationException("Authenticated account metadata required.");
CreateTime = account.CreateTimeUtc;
LoginTime = account.ServerTimeUtc;
var previous = Read("LastLoginTimeUtc");
LastLoginTime = ParseLastLoginTime(previous.Value, previous.Found.Value);
DisplayName = Read("DisplayName").Value;
if (string.IsNullOrEmpty(DisplayName)) DisplayName = GetDefaultName(account.PlayerId);
AvatarUrl = Read("AvatarUrl").Value ?? string.Empty;
Language = Read("Language").Value;
if (string.IsNullOrEmpty(Language)) Language = GContext.container.Resolve<ISettingService>().Lang;
var continent = Read("ContinentCode").Value;
ContinentCode = new[] { "AF", "AN", "AS", "EU", "NA", "OC", "SA" }.Contains(continent) ? continent : null;
}
internal static DateTime ParseLastLoginTime(string value, bool found)
{
if (!found) return DateTime.MinValue;
if (!DateTime.TryParseExact(value, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var time) ||
time == DateTime.MinValue || time.Kind == DateTimeKind.Unspecified)
throw new FormatException("Invalid LastLoginTimeUtc.");
return time.ToUniversalTime();
}
PlayerSaveReadResult Read(string key)
{
var result = SaveStore.Read(key);
if (!result.Success) throw new InvalidOperationException("KV profile read failed: " + result.Error);
return result;
}
void BindPromptServices(PlayerKVGameSessionHandle original)
{
if (promptsBound || !KVHandoffReady) throw new InvalidOperationException("Prompt binding is stale.");
var settings = GContext.container.Resolve<ISettingService>() as DefaultSettingService;
var notifications = GContext.container.Resolve<ILocalNotificationService>() as LocalNotificationService;
var club = GContext.container.Resolve<ClubService>();
if (settings == null || notifications == null || club == null ||
!ReferenceEquals(settings, GContext.container.Resolve<ISettingService>()) ||
!ReferenceEquals(notifications, GContext.container.Resolve<ILocalNotificationService>()) ||
!ReferenceEquals(club, GContext.container.Resolve<ClubService>()))
throw new InvalidOperationException("Formal prompt services required.");
settings.PreflightFormalPromptOwner(original);
club.PreflightFormalPromptOwner(original);
notifications.PreflightFormalPromptSettings(settings);
promptsBound = true;
settings.BindFormalPromptOwner(original);
club.BindFormalPromptOwner(original);
notifications.BindFormalPromptSettings(settings);
if (!settings.HasFormalPromptOwner(original) || !club.HasFormalPromptOwner(original) ||
!notifications.HasFormalPromptSettings(settings) || !KVHandoffReady)
throw new InvalidOperationException("Formal prompt binding changed.");
}
void OnUnauthorized(int version)
{
if (version == customServerMgr.SessionVersion) Logout();
}
public void Logout() => ObserveClose(CloseKVForValidationAsync());
async void ObserveClose(Task task)
{
try { await task; }
catch (Exception ex) { Debug.LogError("KV close failed: " + ex.GetType().Name); }
}
internal Task CloseKVForValidationAsync()
{
if (closeTask != null) return closeTask;
closing = true;
published = false;
(SaveStore as KVPlayerSaveStore)?.Dispose();
playerId = null; internalId = 0; handle = null; epoch = 0;
DisplayName = AvatarUrl = Language = ContinentCode = null;
ClearPlayInfos();
Application.quitting -= Logout;
if (customServerMgr is CustomServerMgr server)
{
server.KVUnauthorized -= OnUnauthorized;
server.DisableKVBusiness();
}
// Revoke first; a synchronous cancellation callback must already see invalid identity.
closeTask = coordinator?.Revoke() ?? Task.CompletedTask;
lifetime.Cancel();
return closeTask;
}
Task IKVUserLifecycle.CloseKVForValidationAsync() => CloseKVForValidationAsync();
internal Task<PlayerKVShutdownStatus> ObserveKVQuitAsync() =>
coordinator?.ObserveQuitAsync() ?? Task.FromResult(PlayerKVShutdownStatus.Ready);
Task<PlayerKVShutdownStatus> IKVUserLifecycle.ObserveKVQuitAsync() => ObserveKVQuitAsync();
void IKVUserLoadReceiver.ResetForKVLoad()
{
if ((!loading && !closing) || published || handle != null)
throw new InvalidOperationException("KV identity must be unpublished during load/reset.");
intents?.Discard(); intents = null;
EAccoutUrl = new Dictionary<string, string>(); KVTapJoyBalance = 0;
}
void IKVUserLoadReceiver.ReplaceFromKVDecoded(PlayerKVLoginState state, long loadEpoch, IReadOnlyDictionary<string, string> raw)
{
if (!loading || closing || published || handle != null || state == null || raw == null)
throw new InvalidOperationException("KV load receiver is not current.");
intents = new PlayerKVInitializationIntents(loadEpoch, state.Channel);
foreach (var pair in state.Normalizations) intents.Add(loadEpoch, pair.Key, pair.Value);
EAccoutUrl = state.AccountUrls.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal);
KVTapJoyBalance = state.TapJoyBalance;
}
void IKVUserLoadReceiver.DiscardKVIntents() => intents?.Discard();
async Task SaveProfile(string key, string value)
{
var store = SaveStore;
if (!IsAuthorized || store == null) throw new InvalidOperationException("KV identity unavailable.");
var result = await store.WriteAndConfirmAsync(key, value, LoginLifetime);
if (!IsAuthorized || !ReferenceEquals(store, SaveStore) || !store.IsCurrent || !result.Confirmed)
throw new InvalidOperationException("KV profile save was not confirmed.");
}
async Task RunSaveScheduleAsync(IPlayerSaveStore store)
{
try
{
while (IsAuthorized && store.IsCurrent)
{
await Task.Delay(TimeSpan.FromSeconds(40), LoginLifetime);
if (!IsAuthorized || !store.IsCurrent) return;
var result = await store.FlushAsync(LoginLifetime);
if (!result.Confirmed && !LoginLifetime.IsCancellationRequested)
Debug.LogWarning("KV scheduled save: " + result.Status);
}
}
catch (OperationCanceledException) { }
catch (Exception ex) { Debug.LogError("KV save scheduler failed: " + ex.GetType().Name); }
}
public async Task<string> ChangeDisplayName(string newName)
{
await SaveProfile("DisplayName", newName);
DisplayName = newName; GContext.Publish(new ChangeAvatarEvent()); UpdateProfile(); return newName;
}
public async Task<string> UpdateAvatarUrl(string url)
{
await SaveProfile("AvatarUrl", url);
AvatarUrl = url; GContext.Publish(new ChangeAvatarEvent()); UpdateProfile(); return url;
}
public async Task<string> UpdateLanguage(string code)
{ await SaveProfile("Language", code); Language = code; return code; }
public double CreateTotalSeconds() => (ZZTimeHelper.UtcNow() - CreateTime).TotalSeconds;
// The non-AGG channel has no account-link operation; never manufacture a successful binding.
public Task<(int code, int userid)> LinkAccount(EAccoutType type) => Task.FromResult((1, 0));
public Task<bool> GetLinkState(EAccoutType type) => Task.FromResult(false);
public string GetDefaultName(string id) => "Angler#" + (string.IsNullOrEmpty(id) ? string.Empty : id.Substring(0, Math.Min(5, id.Length)));
public void SetPlayerLv(string id, int level) => levels[id] = id == UserId ? GContext.container.Resolve<PlayerData>().lv : level;
public int GetPlayerLv(string id) => levels.TryGetValue(id, out var level) ? level : 1;
public Task<(string, int)> GetPlayLv(string id)
{
var level = id == UserId && IsAuthorized ? (int)GContext.container.Resolve<PlayerData>().lv :
id != null && levels.TryGetValue(id, out var cachedLevel) ? cachedLevel : -1;
return level >= 0 ? Task.FromResult((id, level)) :
Task.FromException<(string, int)>(new InvalidOperationException("Public player level has not been loaded."));
}
public PlayInfo GetPlayInfo(string id)
{
if (string.IsNullOrEmpty(id)) return null;
if (!profiles.TryGetValue(id, out var profile))
{
profile = new PlayInfo { name = GetDefaultName(id) };
profiles.Add(id, profile);
SetPlayInfo(id, profile);
}
return profile;
}
async void SetPlayInfo(string id, PlayInfo profile)
{
if (id == UserId)
{
profile.name = DisplayName;
profile.avatarUrl = AvatarUrl;
GContext.Publish(new GetClubPlayInfoEvent { id = id, name = profile.name, avatarUrl = profile.avatarUrl });
return;
}
var json = await customServerMgr.FriendRequest("SearchFriend", new SearchFriendReq { PlayFabId = id });
if (string.IsNullOrEmpty(json)) return;
SearchFriendResp result;
try { result = Newtonsoft.Json.JsonConvert.DeserializeObject<SearchFriendResp>(json); }
catch (Newtonsoft.Json.JsonException ex)
{
Debug.LogError("KV player profile response failed: " + ex.GetType().Name);
return;
}
if (result != null && result.Code > 0 && result.Info != null)
{
if (!string.IsNullOrEmpty(result.Info.DisplayName)) profile.name = result.Info.DisplayName;
profile.avatarUrl = result.Info.AvatarUrl;
GContext.Publish(new GetClubPlayInfoEvent { id = id, name = profile.name, avatarUrl = profile.avatarUrl });
}
}
public void SetPlayInfo(string id, string name, string avatarUrl)
{
if (string.IsNullOrEmpty(id)) return;
profiles[id] = new PlayInfo { name = string.IsNullOrEmpty(name) ? GetDefaultName(id) : name, avatarUrl = avatarUrl };
}
public void ClearPlayInfos() { profiles.Clear(); levels.Clear(); }
public void UpdateProfile()
{
if (IsAuthorized) customServerMgr.AccountRequest("UpdateProfile", new LoginRequest { playerId = ApiPlayerId,
playerProfile = new PlayerProfile { Level = GContext.container.Resolve<PlayerData>().lv, Avatar = AvatarUrl, DisplayName = DisplayName } });
}
public void UpdateProfileLv()
{
if (IsAuthorized) customServerMgr.AccountRequest("UpdateProfile", new LoginRequest { playerId = ApiPlayerId,
playerProfile = new PlayerProfile { Level = GContext.container.Resolve<PlayerData>().lv } });
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e146290e7ecd4db58dbeb536ace8a092

View File

@@ -0,0 +1,12 @@
using System.Threading.Tasks;
namespace game.PlayerDataStorage
{
internal interface IKVUserLifecycle
{
bool KVHandoffReady { get; }
PlayerKVGameLoadError KVLastError { get; }
Task CloseKVForValidationAsync();
Task<PlayerKVShutdownStatus> ObserveKVQuitAsync();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 12a766df765ed0d4981d8da38eead5e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace game.PlayerDataStorage
{
internal interface IKVUserLoadReceiver
{
void ResetForKVLoad();
void ReplaceFromKVDecoded(PlayerKVLoginState state, long epoch, IReadOnlyDictionary<string, string> raw);
void DiscardKVIntents();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8cda3d2b981ada043af2162ca707d8bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,62 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace game.PlayerDataStorage
{
// A store belongs to one login. Callers retain this instance across awaits.
public interface IPlayerSaveStore
{
bool IsCurrent { get; }
PlayerSaveReadResult Read(string key);
PlayerSaveWriteResult Write(string key, string value, bool remoteStore = true);
Task<PlayerSaveConfirmation> WriteAndConfirmAsync(string key, string value, CancellationToken token = default);
Task<PlayerSaveConfirmation> ConfirmAsync(PlayerSaveReceipt receipt, CancellationToken token = default);
Task<PlayerSaveConfirmation> FlushAsync(CancellationToken token = default);
}
public sealed class PlayerSaveReadResult
{
public bool Success => Error == null;
public bool? Found { get; }
public string Value { get; }
public string Error { get; }
internal PlayerSaveReadResult(bool found, string value) { Found = found; Value = value; }
internal PlayerSaveReadResult(string error) { Error = error ?? "ReadFailed"; }
public string ValueOrThrow()
{
if (!Success) throw new InvalidOperationException("Player save read failed: " + Error);
return Value;
}
}
public sealed class PlayerSaveReceipt
{
internal readonly IPlayerSaveStore Store;
internal readonly object Ticket;
internal PlayerSaveReceipt(IPlayerSaveStore store, object ticket) { Store = store; Ticket = ticket; }
}
public sealed class PlayerSaveWriteResult
{
public bool Accepted => Error == null;
public string Error { get; }
public PlayerSaveReceipt Receipt { get; }
internal PlayerSaveWriteResult(PlayerSaveReceipt receipt) { Receipt = receipt; }
internal PlayerSaveWriteResult(string error) { Error = error ?? "WriteRejected"; }
public void RequireAccepted()
{
if (!Accepted) throw new InvalidOperationException("Player save write rejected: " + Error);
}
}
// Buffered and LocalOnly are deliberately different from cloud confirmation.
public enum PlayerSaveStatus { Confirmed, Buffered, LocalOnly, Rejected, Superseded, Unknown, Cancelled }
public sealed class PlayerSaveConfirmation
{
public PlayerSaveStatus Status { get; }
public bool Confirmed => Status == PlayerSaveStatus.Confirmed;
public string Error { get; }
internal PlayerSaveConfirmation(PlayerSaveStatus status, string error = null) { Status = status; Error = error; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7a9c9b2dab8e5a4ca300f0bcf6637e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -17,6 +17,7 @@ namespace game.PlayerDataStorage
internal string CacheRoot { get; } internal string CacheRoot { get; }
internal int LoginObserveMs { get; } internal int LoginObserveMs { get; }
internal int QuitObserveMs { get; } internal int QuitObserveMs { get; }
internal bool RequireExistingArchive { get; }
internal Func<int,CancellationToken,Task> Delay { get; } internal Func<int,CancellationToken,Task> Delay { get; }
internal PlayerKVControlledLoadCapability LeaseCapability { get; private set; } internal PlayerKVControlledLoadCapability LeaseCapability { get; private set; }
readonly PlayerKVControlledLoadCapability auth,kv,lease; readonly PlayerKVControlledLoadCapability auth,kv,lease;
@@ -28,7 +29,8 @@ namespace game.PlayerDataStorage
bool claimed; bool claimed;
private PlayerKVGameLoadCapability(PlayerKVIdentitySnapshot identity,PlayerKVSchemaSnapshot snapshot,Uri api,string cacheRoot, private PlayerKVGameLoadCapability(PlayerKVIdentitySnapshot identity,PlayerKVSchemaSnapshot snapshot,Uri api,string cacheRoot,
int loginMs,int quitMs,Func<int,CancellationToken,Task> delay, int loginMs,int quitMs,Func<int,CancellationToken,Task> delay,
PlayerKVControlledLoadCapability auth,PlayerKVControlledLoadCapability kv,PlayerKVControlledLoadCapability lease) PlayerKVControlledLoadCapability auth,PlayerKVControlledLoadCapability kv,PlayerKVControlledLoadCapability lease,
bool requireExistingArchive=false)
{ {
Identity=identity ?? throw new ArgumentNullException(nameof(identity)); Snapshot=snapshot ?? throw new ArgumentNullException(nameof(snapshot)); Identity=identity ?? throw new ArgumentNullException(nameof(identity)); Snapshot=snapshot ?? throw new ArgumentNullException(nameof(snapshot));
if(api==null || !api.IsAbsoluteUri || (api.Scheme!="http" && api.Scheme!="https") || !string.IsNullOrEmpty(api.UserInfo) || if(api==null || !api.IsAbsoluteUri || (api.Scheme!="http" && api.Scheme!="https") || !string.IsNullOrEmpty(api.UserInfo) ||
@@ -36,6 +38,19 @@ namespace game.PlayerDataStorage
loginMs<1000 || loginMs>15000 || quitMs<1000 || quitMs>15000) throw new ArgumentException("Controlled load configuration invalid."); loginMs<1000 || loginMs>15000 || quitMs<1000 || quitMs>15000) throw new ArgumentException("Controlled load configuration invalid.");
Api=new Uri(api.AbsoluteUri.TrimEnd('/')+"/"); CacheRoot=cacheRoot; Api=new Uri(api.AbsoluteUri.TrimEnd('/')+"/"); CacheRoot=cacheRoot;
LoginObserveMs=loginMs; QuitObserveMs=quitMs; Delay=delay ?? Task.Delay; this.auth=auth; this.kv=kv; this.lease=lease; LoginObserveMs=loginMs; QuitObserveMs=quitMs; Delay=delay ?? Task.Delay; this.auth=auth; this.kv=kv; this.lease=lease;
RequireExistingArchive=requireExistingArchive;
}
internal static PlayerKVGameLoadCapability CreateForGame(PlayerKVIdentitySnapshot identity,PlayerKVSchemaSnapshot snapshot,
Uri api,string cacheRoot,Func<CookieContainer,HttpMessageHandler> auth,Func<CookieContainer,HttpMessageHandler> kv,
Func<CookieContainer,HttpMessageHandler> lease)
{
if(api==null || api.AbsoluteUri!="http://s101.local:18080/")
throw new ArgumentException("Formal KV endpoint mismatch.",nameof(api));
var capability=new PlayerKVGameLoadCapability(identity,snapshot,api,cacheRoot,10000,5000,null,
PlayerKVControlledLoadCapability.CreateForGame(auth),PlayerKVControlledLoadCapability.CreateForGame(kv),
PlayerKVControlledLoadCapability.CreateForGame(lease),true);
capability.LeaseCapability=PlayerKVControlledLoadCapability.CreateForGame(capability.TakeLease);
return capability;
} }
#if UNITY_EDITOR #if UNITY_EDITOR
internal static PlayerKVGameLoadCapability CreateForValidation(PlayerKVIdentitySnapshot identity,PlayerKVSchemaSnapshot snapshot, internal static PlayerKVGameLoadCapability CreateForValidation(PlayerKVIdentitySnapshot identity,PlayerKVSchemaSnapshot snapshot,
@@ -65,7 +80,7 @@ namespace game.PlayerDataStorage
{ {
// Validate all three returned handler graphs before the first authentication send. // Validate all three returned handler graphs before the first authentication send.
authentication=auth.CreateHandler(jar); pendingKV=kv.CreateHandler(jar); pendingLease=lease.CreateHandler(jar); authentication=auth.CreateHandler(jar); pendingKV=kv.CreateHandler(jar); pendingLease=lease.CreateHandler(jar);
if(ReferenceEquals(authentication,pendingKV) || ReferenceEquals(authentication,pendingLease) || ReferenceEquals(pendingKV,pendingLease)) if(ShareHandlerGraph(authentication,pendingKV,pendingLease))
throw new InvalidOperationException("Distinct transport ownership required."); throw new InvalidOperationException("Distinct transport ownership required.");
return authentication; return authentication;
} }
@@ -77,6 +92,17 @@ namespace game.PlayerDataStorage
} }
} }
} }
static bool ShareHandlerGraph(params HttpMessageHandler[] roots)
{
var seen=new System.Collections.Generic.List<HttpMessageHandler>();
foreach(var root in roots)
for(var node=root;node!=null;node=(node as DelegatingHandler)?.InnerHandler)
{
if(seen.Exists(previous=>ReferenceEquals(previous,node))) return true;
seen.Add(node);
}
return false;
}
HttpMessageHandler TakeKV(CookieContainer jar) HttpMessageHandler TakeKV(CookieContainer jar)
{ {
lock(gate) lock(gate)

View File

@@ -237,7 +237,7 @@ namespace game.PlayerDataStorage
var player=new GameCore.PlayerData(tables,null); var small=new GameCore.SmallGameData(tables,null,null); var fish=new GameCore.PlayerFishData(tables,null); var player=new GameCore.PlayerData(tables,null); var small=new GameCore.SmallGameData(tables,null,null); var fish=new GameCore.PlayerFishData(tables,null);
var shop=new GameCore.PlayerShopData(); var eventBase=new GameCore.FishingEventData(tables); var buffs=new BuffDataCenter(tables); var camp=new CampDataMM(tables,null); var shop=new GameCore.PlayerShopData(); var eventBase=new GameCore.FishingEventData(tables); var buffs=new BuffDataCenter(tables); var camp=new CampDataMM(tables,null);
var achievements=new AchievementDataManager(tables); var box=new FishingBoxData(); var guide=new GameCore.GuideDataCenter(null,null); var battle=new BattlePassData(tables); var achievements=new AchievementDataManager(tables); var box=new FishingBoxData(); var guide=new GameCore.GuideDataCenter(null,null); var battle=new BattlePassData(tables);
var sign=new GameCore.SigninData(tables); var trigger=new GameCore.TriggerPackData(tables); var club=new ClubService(); var challenge=new FishingChallengeManager(); var sign=new GameCore.SigninData(tables); var trigger=new GameCore.TriggerPackData(tables); var club=new ClubService(false); var challenge=new FishingChallengeManager();
var chains=new DataCenter.OfferChainsChestManager(); var disco=new DataCenter.OfferDiscoPartyManager(); var partner=new DataCenter.EventPartnerGatherManager(); var cube=new EventCubeMeltManager(); var chains=new DataCenter.OfferChainsChestManager(); var disco=new DataCenter.OfferDiscoPartyManager(); var partner=new DataCenter.EventPartnerGatherManager(); var cube=new EventCubeMeltManager();
var eventPack=new EventPackData(); var piggy=new PiggyBankPackData(); var bargain=new BargainPackData(); var foot=new FootPrintService(); var shooting=new EventShootingRangeData(); var eventPack=new EventPackData(); var piggy=new PiggyBankPackData(); var bargain=new BargainPackData(); var foot=new FootPrintService(); var shooting=new EventShootingRangeData();
var potion=new EventPotionDataManager(); var pinball=new EventPinballDataManager(); var scratch=new EventScratchTicketDataManager(); var potion=new EventPotionDataManager(); var pinball=new EventPinballDataManager(); var scratch=new EventScratchTicketDataManager();
@@ -924,7 +924,7 @@ namespace game.PlayerDataStorage
{ {
int assertions = 0; int assertions = 0;
void Check(bool condition) { assertions++; if (!condition) throw new InvalidOperationException("Club assertion failed."); } void Check(bool condition) { assertions++; if (!condition) throw new InvalidOperationException("Club assertion failed."); }
var club = new ClubService(); var club = new ClubService(false);
var snapshot = new PlayerKVSchemaSnapshot("M14",Array.Empty<int>(),Array.Empty<int>(),new Dictionary<string,int>(), var snapshot = new PlayerKVSchemaSnapshot("M14",Array.Empty<int>(),Array.Empty<int>(),new Dictionary<string,int>(),
CultureInfo.InvariantCulture,new DateTime(2026,9,23,0,0,0,DateTimeKind.Utc),0); CultureInfo.InvariantCulture,new DateTime(2026,9,23,0,0,0,DateTimeKind.Utc),0);
try try

View File

@@ -6,7 +6,7 @@ namespace game.PlayerDataStorage
{ {
internal enum PlayerKVGameLoadError internal enum PlayerKVGameLoadError
{ None, BackendNotEnabled, BackendUnsupported, Input, Prepare, Schema, Apply, Stale, SideEffect, GateBusy, PreviousClosePending, PreviousCloseFailed, ClosePending, CloseFailed } { None, BackendNotEnabled, BackendUnsupported, Input, Prepare, Schema, Apply, Stale, SideEffect, GateBusy, PreviousClosePending, PreviousCloseFailed, ClosePending, CloseFailed }
internal enum PlayerKVGameLoadState { Rejected, LoadedForValidation } internal enum PlayerKVGameLoadState { Rejected, LoadedForValidation, GameLoaded }
// Preserve stage evidence without retaining a successful Prepare payload on a later load failure. // Preserve stage evidence without retaining a successful Prepare payload on a later load failure.
internal sealed class PlayerKVPrepareEvidence internal sealed class PlayerKVPrepareEvidence
{ {
@@ -27,18 +27,21 @@ namespace game.PlayerDataStorage
} }
internal sealed class PlayerKVGameLoadResult internal sealed class PlayerKVGameLoadResult
{ {
internal PlayerKVGameLoadState State => Error==PlayerKVGameLoadError.None ? PlayerKVGameLoadState.LoadedForValidation : PlayerKVGameLoadState.Rejected; internal PlayerKVGameLoadState State { get; }
internal PlayerKVGameLoadError Error { get; } internal PlayerKVGameLoadError Error { get; }
internal PlayerKVGameLoadError OriginalError { get; } internal PlayerKVGameLoadError OriginalError { get; }
internal bool CleanupFailed => Error==PlayerKVGameLoadError.CloseFailed; internal bool CleanupFailed => Error==PlayerKVGameLoadError.CloseFailed;
internal PlayerKVSchemaVerdict? SchemaVerdict { get; } internal PlayerKVSchemaVerdict? SchemaVerdict { get; }
internal PlayerKVPrepareEvidence Prepare { get; } internal PlayerKVPrepareEvidence Prepare { get; }
internal long Epoch { get; } internal long Epoch { get; }
internal PlayerKVAccountDescriptor Account { get; }
internal int ModuleCount { get; } internal int ModuleCount { get; }
internal int IntentCount { get; } internal int IntentCount { get; }
internal PlayerKVGameLoadResult(PlayerKVGameLoadError error,long epoch,PlayerKVGameLoadError? original=null, internal PlayerKVGameLoadResult(PlayerKVGameLoadError error,long epoch,PlayerKVGameLoadError? original=null,
PlayerKVSchemaVerdict? verdict=null,PlayerKVLoginPrepareResult prepare=null,int modules=0,int intents=0) PlayerKVSchemaVerdict? verdict=null,PlayerKVLoginPrepareResult prepare=null,int modules=0,int intents=0,bool forGame=false,PlayerKVAccountDescriptor account=null)
{ Error=error; OriginalError=original ?? error; SchemaVerdict=verdict; Prepare=prepare==null ? null : new PlayerKVPrepareEvidence(prepare); Epoch=epoch; ModuleCount=modules; IntentCount=intents; } { Error=error; OriginalError=original ?? error; SchemaVerdict=verdict; Prepare=prepare==null ? null : new PlayerKVPrepareEvidence(prepare); Epoch=epoch; ModuleCount=modules; IntentCount=intents;
Account=error==PlayerKVGameLoadError.None && forGame ? account : null;
State=error!=PlayerKVGameLoadError.None ? PlayerKVGameLoadState.Rejected : forGame ? PlayerKVGameLoadState.GameLoaded : PlayerKVGameLoadState.LoadedForValidation; }
} }
internal sealed class PlayerKVGameLoadCoordinator internal sealed class PlayerKVGameLoadCoordinator
{ {
@@ -61,6 +64,7 @@ namespace game.PlayerDataStorage
Action clearAfterApply; Action clearAfterApply;
long epoch; long epoch;
bool loaded; bool loaded;
PlayerKVGameSessionHandle gameHandle;
internal bool LoadedForValidation => loaded && context!=null && context.IsCurrent && guard!=null && guard.IsCurrent && guard.Violations==0; internal bool LoadedForValidation => loaded && context!=null && context.IsCurrent && guard!=null && guard.IsCurrent && guard.Violations==0;
internal int QuitObserveMs => capability?.QuitObserveMs ?? 5000; internal int QuitObserveMs => capability?.QuitObserveMs ?? 5000;
internal PlayerKVGameLoadCoordinator(PlayerKVGameLoadTransaction transaction,Func<bool> unstarted,Action notify,Func<int> intentCount, internal PlayerKVGameLoadCoordinator(PlayerKVGameLoadTransaction transaction,Func<bool> unstarted,Action notify,Func<int> intentCount,
@@ -72,7 +76,29 @@ namespace game.PlayerDataStorage
this.unbind=unbind ?? throw new ArgumentNullException(nameof(unbind)); this.registry=registry ?? PlayerKVShutdownRegistry.Process; this.unbind=unbind ?? throw new ArgumentNullException(nameof(unbind)); this.registry=registry ?? PlayerKVShutdownRegistry.Process;
mainThread=Thread.CurrentThread.ManagedThreadId; mainContext=SynchronizationContext.Current; mainThread=Thread.CurrentThread.ManagedThreadId; mainContext=SynchronizationContext.Current;
} }
internal async Task<PlayerKVGameLoadResult> LoadAsync(string id,PlayerKVGameLoadCapability next,CancellationToken token=default,Func<bool> identityCurrent=null) internal Task<PlayerKVGameLoadResult> LoadAsync(string id,PlayerKVGameLoadCapability next,CancellationToken token=default,Func<bool> identityCurrent=null)
=> LoadCoreAsync(id,next,token,identityCurrent,false);
internal Task<PlayerKVGameLoadResult> LoadForGameAsync(string id,PlayerKVGameLoadCapability next,CancellationToken token=default,Func<bool> identityCurrent=null)
=> LoadCoreAsync(id,next,token,identityCurrent,true);
internal bool TryGetCurrentGameHandle(long expectedEpoch,out PlayerKVGameSessionHandle handle)
{
handle=null;
PlayerKVLoginContext owner; PlayerKVGameSessionHandle published;
lock(closeGate)
{
if(closing!=null || epoch!=expectedEpoch || gameHandle==null) return false;
owner=context; published=gameHandle;
}
// Never call the external identity predicate while holding a publication monitor.
if(!owner.TryAcquireGameHandle(expectedEpoch,out var original) || !ReferenceEquals(original,published)) return false;
lock(closeGate)
{
if(closing!=null || epoch!=expectedEpoch || !ReferenceEquals(context,owner) || !ReferenceEquals(gameHandle,published)) return false;
handle=published;
return true;
}
}
async Task<PlayerKVGameLoadResult> LoadCoreAsync(string id,PlayerKVGameLoadCapability next,CancellationToken token,Func<bool> identityCurrent,bool forGame)
{ {
if(next==null || Thread.CurrentThread.ManagedThreadId!=mainThread || string.IsNullOrEmpty(id)) if(next==null || Thread.CurrentThread.ManagedThreadId!=mainThread || string.IsNullOrEmpty(id))
return Result(PlayerKVGameLoadError.Input); return Result(PlayerKVGameLoadError.Input);
@@ -89,9 +115,13 @@ namespace game.PlayerDataStorage
if(token.IsCancellationRequested || !unstarted() || transaction.Poisoned) return Result(PlayerKVGameLoadError.Input); if(token.IsCancellationRequested || !unstarted() || transaction.Poisoned) return Result(PlayerKVGameLoadError.Input);
if(!PlayerKVGameLoadGuard.TryEnter(out var enteredGuard)) return Result(PlayerKVGameLoadError.GateBusy); if(!PlayerKVGameLoadGuard.TryEnter(out var enteredGuard)) return Result(PlayerKVGameLoadError.GateBusy);
lifetimeRegistration.Dispose(); lifetimeRegistration.Dispose();
guard=enteredGuard; capability=next; closing=null; loaded=false; lock(closeGate)
attempt=Interlocked.Increment(ref epoch); {
context=next.CreateContext(id,attempt,e=>Interlocked.Read(ref epoch)==e && !token.IsCancellationRequested && (identityCurrent==null || identityCurrent())); guard=enteredGuard; capability=next; closing=null; loaded=false; gameHandle=null;
attempt=Interlocked.Increment(ref epoch);
context=next.CreateContext(id,attempt,e=>Interlocked.Read(ref epoch)==e && !token.IsCancellationRequested && (identityCurrent==null || identityCurrent()));
}
var owner=context;
lifetimeRegistration=token.Register(()=> { _=Revoke(attempt); }); lifetimeRegistration=token.Register(()=> { _=Revoke(attempt); });
var work=context.PrepareAsync(token); var work=context.PrepareAsync(token);
var cancelled=new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); var cancelled=new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -103,6 +133,8 @@ namespace game.PlayerDataStorage
} }
if(!prepared.Prepared) return await Reject(prepared.CleanupFailed ? PlayerKVGameLoadError.CloseFailed : PlayerKVGameLoadError.Prepare,attempt,next,prepared,verdict); if(!prepared.Prepared) return await Reject(prepared.CleanupFailed ? PlayerKVGameLoadError.CloseFailed : PlayerKVGameLoadError.Prepare,attempt,next,prepared,verdict);
if(!context.IsCurrent || epoch!=attempt) return await Reject(PlayerKVGameLoadError.Stale,attempt,next,prepared,verdict); if(!context.IsCurrent || epoch!=attempt) return await Reject(PlayerKVGameLoadError.Stale,attempt,next,prepared,verdict);
if(next.RequireExistingArchive && (context.AccountDescriptor?.NewlyCreated!=false || prepared.Data==null || prepared.Data.Count==0))
return await Reject(PlayerKVGameLoadError.Schema,attempt,next,prepared,verdict);
var decoded=PlayerKVSaveSchema.Decode(prepared,context.AccountDescriptor,id,attempt,context.IsCurrent,next.Snapshot,next.Snapshot.Version,next.Identity); var decoded=PlayerKVSaveSchema.Decode(prepared,context.AccountDescriptor,id,attempt,context.IsCurrent,next.Snapshot,next.Snapshot.Version,next.Identity);
verdict=decoded.Verdict; verdict=decoded.Verdict;
if(decoded.Data==null) return await Reject(PlayerKVGameLoadError.Schema,attempt,next,prepared,verdict); if(decoded.Data==null) return await Reject(PlayerKVGameLoadError.Schema,attempt,next,prepared,verdict);
@@ -113,9 +145,26 @@ namespace game.PlayerDataStorage
finally { var pending=clearAfterApply; clearAfterApply=null; pending?.Invoke(); } finally { var pending=clearAfterApply; clearAfterApply=null; pending?.Invoke(); }
if(applied!=PlayerKVApplyError.None) return await Reject(applied==PlayerKVApplyError.Stale ? PlayerKVGameLoadError.Stale : if(applied!=PlayerKVApplyError.None) return await Reject(applied==PlayerKVApplyError.Stale ? PlayerKVGameLoadError.Stale :
applied==PlayerKVApplyError.SideEffect ? PlayerKVGameLoadError.SideEffect : PlayerKVGameLoadError.Apply,attempt,next,prepared,verdict); applied==PlayerKVApplyError.SideEffect ? PlayerKVGameLoadError.SideEffect : PlayerKVGameLoadError.Apply,attempt,next,prepared,verdict);
if(token.IsCancellationRequested || !context.IsCurrent || guard.Violations!=0) return await Reject(PlayerKVGameLoadError.Stale,attempt,next,prepared,verdict); if(token.IsCancellationRequested || !owner.IsCurrent || enteredGuard.Violations!=0) return await Reject(PlayerKVGameLoadError.Stale,attempt,next,prepared,verdict);
loaded=true; var intents=intentCount();
return new PlayerKVGameLoadResult(PlayerKVGameLoadError.None,attempt,verdict:verdict,modules:31,intents:intentCount()); PlayerKVGameSessionHandle original=null;
if(forGame && !owner.TryAcquireGameHandle(attempt,out original)) return await Reject(PlayerKVGameLoadError.Stale,attempt,next,prepared,verdict);
bool published=false;
lock(closeGate)
{
if(closing==null && epoch==attempt && !token.IsCancellationRequested && ReferenceEquals(context,owner) && enteredGuard.IsCurrent && enteredGuard.Violations==0)
{
if(forGame)
{
enteredGuard.Dispose();
// Dispose serializes with Reject; its final count cannot change afterwards.
if(enteredGuard.Violations==0) { gameHandle=original; published=true; }
}
else { loaded=true; published=true; }
}
}
if(!published) return await Reject(PlayerKVGameLoadError.Stale,attempt,next,prepared,verdict);
return new PlayerKVGameLoadResult(PlayerKVGameLoadError.None,attempt,verdict:verdict,modules:31,intents:intents,forGame:forGame,account:context.AccountDescriptor);
} }
catch { return await Reject(PlayerKVGameLoadError.Apply,attempt,next,prepared,verdict); } catch { return await Reject(PlayerKVGameLoadError.Apply,attempt,next,prepared,verdict); }
finally { loginGate.Release(); } finally { loginGate.Release(); }
@@ -148,7 +197,7 @@ namespace game.PlayerDataStorage
{ {
if(expectedEpoch.HasValue && expectedEpoch.Value!=Interlocked.Read(ref epoch)) return Task.CompletedTask; if(expectedEpoch.HasValue && expectedEpoch.Value!=Interlocked.Read(ref epoch)) return Task.CompletedTask;
if(closing!=null) return closing; if(closing!=null) return closing;
loaded=false; Interlocked.Increment(ref epoch); loaded=false; gameHandle=null; Interlocked.Increment(ref epoch);
if(context==null && guard==null) return Task.CompletedTask; if(context==null && guard==null) return Task.CompletedTask;
var completion=new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); var completion=new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
closing=completion.Task; closing=completion.Task;

View File

@@ -1,4 +1,4 @@
#if UNITY_EDITOR && !SERVICE_HOST #if !SERVICE_HOST
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
@@ -12,10 +12,10 @@ namespace game.PlayerDataStorage
{ {
static T Require<T>() where T:class => GContext.container.Resolve<T>() ?? static T Require<T>() where T:class => GContext.container.Resolve<T>() ??
throw new InvalidOperationException("KV game module is not registered: "+typeof(T).FullName); throw new InvalidOperationException("KV game module is not registered: "+typeof(T).FullName);
internal static PlayerKVSchemaSnapshot Snapshot(DateTime nowUtc) internal static PlayerKVSchemaSnapshot Snapshot(DateTime nowUtc,string version="kv-loop")
{ {
var tables=Require<cfg.Tables>(); var tables=Require<cfg.Tables>();
return new PlayerKVSchemaSnapshot("kv-loop",tables.TbRodData.DataMap.Keys,tables.TbMapData.DataMap.Keys, return new PlayerKVSchemaSnapshot(version,tables.TbRodData.DataMap.Keys,tables.TbMapData.DataMap.Keys,
Enum.GetNames(typeof(cfg.ConditionType)).ToDictionary(n=>n,n=>(int)Enum.Parse(typeof(cfg.ConditionType),n)),CultureInfo.InvariantCulture, Enum.GetNames(typeof(cfg.ConditionType)).ToDictionary(n=>n,n=>(int)Enum.Parse(typeof(cfg.ConditionType),n)),CultureInfo.InvariantCulture,
nowUtc,tables.TbGlobalConfig.InitWheelTicket, nowUtc,tables.TbGlobalConfig.InitWheelTicket,
tables.TbEventPackManager.DataMap.ToDictionary(p=>p.Key,p=>p.Value.PackType), tables.TbEventPackManager.DataMap.ToDictionary(p=>p.Key,p=>p.Value.PackType),
@@ -23,7 +23,7 @@ namespace game.PlayerDataStorage
tables.TbFishData.DataMap.ToDictionary(p=>p.Key,p=>new PlayerKVFishConfig(p.Key,p.Value.AreaID,p.Value.Quality,p.Value.FishCardID,p.Value.BaseWeight,p.Value.PointExtraParam,p.Value.WeightRandom)), tables.TbFishData.DataMap.ToDictionary(p=>p.Key,p=>new PlayerKVFishConfig(p.Key,p.Value.AreaID,p.Value.Quality,p.Value.FishCardID,p.Value.BaseWeight,p.Value.PointExtraParam,p.Value.WeightRandom)),
tables.TbFishCard.DataMap.ToDictionary(p=>p.Key,p=>new PlayerKVFishCardConfig(p.Key,p.Value.MaxLevel,p.Value.CardsRequiredList,p.Value.WeightMagList))); tables.TbFishCard.DataMap.ToDictionary(p=>p.Key,p=>new PlayerKVFishCardConfig(p.Key,p.Value.MaxLevel,p.Value.CardsRequiredList,p.Value.WeightMagList)));
} }
internal static PlayerKVGameLoadTransaction Bind(PlayFabUserService user) internal static PlayerKVGameLoadTransaction Bind(IKVUserLoadReceiver user)
{ {
var player=Require<GameCore.PlayerData>(); var small=Require<GameCore.SmallGameData>(); var fish=Require<GameCore.PlayerFishData>(); var player=Require<GameCore.PlayerData>(); var small=Require<GameCore.SmallGameData>(); var fish=Require<GameCore.PlayerFishData>();
var shop=Require<GameCore.PlayerShopData>(); var eventBase=Require<GameCore.FishingEventData>(); var buffs=Require<BuffDataCenter>(); var camp=Require<CampDataMM>(); var shop=Require<GameCore.PlayerShopData>(); var eventBase=Require<GameCore.FishingEventData>(); var buffs=Require<BuffDataCenter>(); var camp=Require<CampDataMM>();
@@ -53,7 +53,7 @@ namespace game.PlayerDataStorage
Add<PlayerKVProgressPackPair>(28,()=>{progress.ResetForKVLoad();migration.ResetForKVLoad();},p=>{progress.ReplaceFromKVDecoded(p.Current);migration.ReplaceFromKVDecoded(p.Migration);}); Add<PlayerKVProgressPackPair>(28,()=>{progress.ResetForKVLoad();migration.ResetForKVLoad();},p=>{progress.ReplaceFromKVDecoded(p.Current);migration.ReplaceFromKVDecoded(p.Migration);});
Add<PlayerKVPinataState>(29,pinata.ResetForKVLoad,pinata.ReplaceFromKVDecoded); Add<PlayerKVEmptyModuleState>(30,digging.ResetForKVLoad,digging.ReplaceFromKVDecoded); Add<PlayerKVPinataState>(29,pinata.ResetForKVLoad,pinata.ReplaceFromKVDecoded); Add<PlayerKVEmptyModuleState>(30,digging.ResetForKVLoad,digging.ReplaceFromKVDecoded);
modules.Add(new PlayerKVLoadModule(31,user.ResetForKVLoad,d=>user.ReplaceFromKVDecoded(d.Module<PlayerKVLoginState>(31),d.Epoch,d.Raw))); modules.Add(new PlayerKVLoadModule(31,user.ResetForKVLoad,d=>user.ReplaceFromKVDecoded(d.Module<PlayerKVLoginState>(31),d.Epoch,d.Raw)));
return new PlayerKVGameLoadTransaction(modules,System.Threading.Thread.CurrentThread.ManagedThreadId,()=>user.KVIntents?.Discard()); return new PlayerKVGameLoadTransaction(modules,System.Threading.Thread.CurrentThread.ManagedThreadId,user.DiscardKVIntents);
} }
} }
} }

View File

@@ -58,6 +58,21 @@ namespace game.PlayerDataStorage
internal PlayerKVAccountDescriptor AccountDescriptor => internal PlayerKVAccountDescriptor AccountDescriptor =>
Volatile.Read(ref prepared) != 0 && IsCurrent ? accountDescriptor : null; Volatile.Read(ref prepared) != 0 && IsCurrent ? accountDescriptor : null;
// Only the coordinator may publish this original handle after Apply and Guard release.
internal bool TryAcquireGameHandle(long expectedEpoch, out PlayerKVGameSessionHandle gameHandle)
{
gameHandle = null;
// IsCurrent can invoke external code and synchronously reenter CloseAsync.
if (expectedEpoch != epoch || !IsCurrent) return false;
lock (gate)
{
if (closing != null || revoked != 0 || prepared != 1 || handle == null || handle.Epoch != expectedEpoch)
return false;
gameHandle = handle;
return true;
}
}
// All issuance and Close admission share gate. The editor factory may not block on Close. // All issuance and Close admission share gate. The editor factory may not block on Close.
internal bool TryAcquireAccountLease(PlayerKVControlledLoadCapability capability, internal bool TryAcquireAccountLease(PlayerKVControlledLoadCapability capability,
out PlayerKVAccountLease lease) out PlayerKVAccountLease lease)
@@ -317,21 +332,26 @@ namespace game.PlayerDataStorage
} }
} }
/// <summary>Construction is absent from player builds; no default network handler is supplied.</summary> /// <summary>Controlled transport factory; formal and Editor validation modes remain separate.</summary>
internal sealed class PlayerKVControlledLoadCapability internal sealed class PlayerKVControlledLoadCapability
{ {
readonly Func<CookieContainer, HttpMessageHandler> leaseHandler; readonly Func<CookieContainer, HttpMessageHandler> leaseHandler;
private PlayerKVControlledLoadCapability(Func<CookieContainer, HttpMessageHandler> leaseHandler) readonly bool formal;
{ this.leaseHandler = leaseHandler ?? throw new ArgumentNullException(nameof(leaseHandler)); } private PlayerKVControlledLoadCapability(Func<CookieContainer, HttpMessageHandler> leaseHandler, bool formal)
{ this.leaseHandler = leaseHandler ?? throw new ArgumentNullException(nameof(leaseHandler)); this.formal = formal; }
internal bool IsFormal => formal;
internal static PlayerKVControlledLoadCapability CreateForGame(
Func<CookieContainer, HttpMessageHandler> handler) => new PlayerKVControlledLoadCapability(handler, true);
#if UNITY_EDITOR #if UNITY_EDITOR
internal static PlayerKVControlledLoadCapability CreateForValidation( internal static PlayerKVControlledLoadCapability CreateForValidation(
Func<CookieContainer, HttpMessageHandler> leaseHandler) => new PlayerKVControlledLoadCapability(leaseHandler); Func<CookieContainer, HttpMessageHandler> leaseHandler) => new PlayerKVControlledLoadCapability(leaseHandler, false);
#endif #endif
internal HttpMessageHandler CreateHandler(CookieContainer cookies) internal HttpMessageHandler CreateHandler(CookieContainer cookies)
{ {
var handler = leaseHandler(cookies) ?? throw new InvalidOperationException("Synthetic handler required."); var handler = leaseHandler(cookies) ?? throw new InvalidOperationException("Transport handler required.");
var seen = new HashSet<HttpMessageHandler>(); var seen = new HashSet<HttpMessageHandler>();
var node = handler; var node = handler;
while (node != null) while (node != null)
@@ -340,12 +360,23 @@ namespace game.PlayerDataStorage
// It never becomes a lease; the rejecting factory retains responsibility for that graph. // It never becomes a lease; the rejecting factory retains responsibility for that graph.
if (!seen.Add(node)) if (!seen.Add(node))
throw new InvalidOperationException("Cyclic transport is not a validation handler."); throw new InvalidOperationException("Cyclic transport is not a validation handler.");
if (node is HttpClientHandler || if (!formal && (node is HttpClientHandler ||
node.GetType().FullName == "System.Net.Http.SocketsHttpHandler") node.GetType().FullName == "System.Net.Http.SocketsHttpHandler"))
{ {
handler.Dispose(); handler.Dispose();
throw new InvalidOperationException("Real or cyclic transport is not a validation handler."); throw new InvalidOperationException("Real or cyclic transport is not a validation handler.");
} }
if (formal && node is HttpClientHandler real &&
(!real.UseCookies || !ReferenceEquals(real.CookieContainer, cookies) || real.AllowAutoRedirect))
{
handler.Dispose();
throw new InvalidOperationException("Formal transport must share the session cookies and reject redirects.");
}
if (formal && node.GetType().FullName == "System.Net.Http.SocketsHttpHandler")
{
handler.Dispose();
throw new InvalidOperationException("Unverified formal transport handler.");
}
node = (node as DelegatingHandler)?.InnerHandler; node = (node as DelegatingHandler)?.InnerHandler;
} }
return handler; return handler;
@@ -366,6 +397,8 @@ namespace game.PlayerDataStorage
{ {
readonly object gate = new object(); readonly object gate = new object();
readonly HttpClient client; readonly HttpClient client;
readonly CookieContainer cookies;
readonly bool formal;
readonly Func<bool> current; readonly Func<bool> current;
readonly Func<bool> admission; readonly Func<bool> admission;
readonly CancellationTokenSource lifetime = new CancellationTokenSource(); readonly CancellationTokenSource lifetime = new CancellationTokenSource();
@@ -374,6 +407,8 @@ namespace game.PlayerDataStorage
int active; int active;
internal Uri ApiBaseUri { get; } internal Uri ApiBaseUri { get; }
internal long Epoch { get; } internal long Epoch { get; }
internal bool IsFormal => formal;
internal CookieContainer FormalCookies => formal && IsCurrent ? cookies : null;
internal bool IsCurrent internal bool IsCurrent
{ {
get get
@@ -387,18 +422,24 @@ namespace game.PlayerDataStorage
PlayerKVControlledLoadCapability capability, Func<bool> current, Func<bool> admission) PlayerKVControlledLoadCapability capability, Func<bool> current, Func<bool> admission)
{ {
if (capability == null) throw new ArgumentNullException(nameof(capability)); if (capability == null) throw new ArgumentNullException(nameof(capability));
ApiBaseUri = api; Epoch = epoch; this.current = current; this.admission = admission; ApiBaseUri = api; Epoch = epoch; this.cookies = cookies;
formal = capability.IsFormal; this.current = current; this.admission = admission;
client = new HttpClient(capability.CreateHandler(cookies)) { BaseAddress = api }; client = new HttpClient(capability.CreateHandler(cookies)) { BaseAddress = api };
if (formal) client.Timeout = TimeSpan.FromSeconds(15);
} }
internal async Task<PlayerKVAccountResponse> SendAsync(HttpMethod method, string route, internal async Task<PlayerKVAccountResponse> SendAsync(HttpMethod method, string route,
string body = null, string contentType = "application/json", CancellationToken token = default) string body = null, string contentType = "application/json", CancellationToken token = default)
{ {
// Exact canonical relative paths: no query, fragment, encoding, traversal or absolute URL. if (formal)
if (route != "__kv_validation/ok" && route != "__kv_validation/unauthorized") {
if (!IsCanonicalBusinessRoute(route))
throw new ArgumentException("Only canonical relative business routes are allowed.", nameof(route));
}
else if (route != "__kv_validation/ok" && route != "__kv_validation/unauthorized")
throw new ArgumentException("Only canonical validation routes are allowed.", nameof(route)); throw new ArgumentException("Only canonical validation routes are allowed.", nameof(route));
if (method != HttpMethod.Get && method != HttpMethod.Post) if (method != HttpMethod.Get && method != HttpMethod.Post)
throw new ArgumentException("Unsupported validation method.", nameof(method)); throw new ArgumentException("Unsupported lease method.", nameof(method));
if (!IsCurrent) return new PlayerKVAccountResponse(false); if (!IsCurrent) return new PlayerKVAccountResponse(false);
lock (gate) lock (gate)
{ {
@@ -436,6 +477,27 @@ namespace game.PlayerDataStorage
} }
} }
static bool IsCanonicalBusinessRoute(string route)
{
if (string.IsNullOrEmpty(route) || route[0] == '/' || route[0] == '\\' ||
Uri.TryCreate(route, UriKind.Absolute, out _) ||
route.IndexOf("//", StringComparison.Ordinal) >= 0 ||
route.IndexOfAny(new[] { '?', '#', '%', '\\' }) >= 0)
return false;
var segments = route.Split('/');
foreach (var segment in segments)
{
if (segment.Length == 0 || segment == "." || segment == "..") return false;
foreach (var character in segment)
if (char.IsControl(character)) return false;
}
return !route.Equals("__kv_validation", StringComparison.OrdinalIgnoreCase) &&
!route.StartsWith("__kv_validation/", StringComparison.OrdinalIgnoreCase) &&
!route.Equals("account/Login", StringComparison.OrdinalIgnoreCase) &&
!route.Equals("playerdata", StringComparison.OrdinalIgnoreCase) &&
!route.StartsWith("playerdata/", StringComparison.OrdinalIgnoreCase);
}
internal Task CloseAsync() internal Task CloseAsync()
{ {
TaskCompletionSource<bool> completion; TaskCompletionSource<bool> completion;

View File

@@ -0,0 +1,45 @@
using System;
using System.Globalization;
namespace game.PlayerDataStorage
{
/// <summary>
/// Immutable process-start selection. Accounts 101 and 102 are the reviewed first cohort;
/// expanding it requires a new complete migration design review.
/// </summary>
internal sealed class PlayerKVRuntimeSelection
{
internal const string FormalApiUrl = "http://s101.local:18080/";
internal bool IsKV { get; }
internal Uri Api { get; }
private PlayerKVRuntimeSelection(bool isKV, Uri api)
{
IsKV = isKV;
Api = api;
}
internal static PlayerKVRuntimeSelection Capture(string backend, string api, bool agg)
{
if (backend == null || backend == "playfab")
return new PlayerKVRuntimeSelection(false, null);
if (backend != "kv")
throw new ArgumentException("Unsupported save backend.", nameof(backend));
if (agg)
throw new InvalidOperationException("KV login is not enabled for AGG channels.");
if (!string.Equals(api, FormalApiUrl, StringComparison.Ordinal))
throw new ArgumentException("KV API address does not match the reviewed endpoint.", nameof(api));
return new PlayerKVRuntimeSelection(true, new Uri(FormalApiUrl));
}
internal bool TryAdmitPlayer(string requestedId, out string canonicalId)
{
canonicalId = null;
if (!IsKV || !long.TryParse(requestedId, NumberStyles.None, CultureInfo.InvariantCulture, out var id) ||
(id != 101 && id != 102)) return false;
canonicalId = id.ToString(CultureInfo.InvariantCulture);
return true;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4d8ad9b0b9d34ce385e2b31813d24ff3

View File

@@ -88,27 +88,27 @@ namespace game.PlayerDataStorage
Check(PlayerKVGameLoadGuard.TryEnter(out guard)); guard.Dispose(); guard = null; Check(PlayerKVGameLoadGuard.TryEnter(out guard)); guard.Dispose(); guard = null;
Directory.CreateDirectory(cache); bridge = new PlayerKVGameSessionBridge(KV); Directory.CreateDirectory(cache); bridge = new PlayerKVGameSessionBridge(KV);
var a = await Open("101"); var a = await Open("101");
var present = PlayFabMgr.ReadKV(a, "k"); Check(present.Success && present.Found == true && present.Value == "A"); var present = KVMgr.ReadKV(a, "k"); Check(present.Success && present.Found == true && present.Value == "A");
Check(PlayFabMgr.ReadKV(a, "missing").Found == false); Check(KVMgr.ReadKV(a, "missing").Found == false);
Check(PlayFabMgr.ReadKV(a, "empty").Found == true && PlayFabMgr.ReadKV(a, "empty").Value == ""); Check(KVMgr.ReadKV(a, "empty").Found == true && KVMgr.ReadKV(a, "empty").Value == "");
Check(PlayFabMgr.ReadKV(a, null).Found == null); Check(KVMgr.ReadKV(a, null).Found == null);
var edit = PlayFabMgr.WriteKV(a, "k", "remote"); Check(edit.Accepted && edit.Ticket != null && writes == 0); var edit = KVMgr.WriteKV(a, "k", "remote"); Check(edit.Accepted && edit.Ticket != null && writes == 0);
var local = PlayFabMgr.WriteKV(a, "k", "local", false); Check(local.Accepted && local.Ticket == null); var local = KVMgr.WriteKV(a, "k", "local", false); Check(local.Accepted && local.Ticket == null);
Check(PlayFabMgr.ReadKV(a, "k").Value == "local"); Check(KVMgr.ReadKV(a, "k").Value == "local");
var flush = await PlayFabMgr.FlushKVAsync(a); Check(flush.Confirmed == 1 && remote["101"]["k"] == "remote"); var flush = await KVMgr.FlushKVAsync(a); Check(flush.Confirmed == 1 && remote["101"]["k"] == "remote");
Check((await PlayFabMgr.WaitForKVTicketAsync(a, edit.Ticket)).Status == PlayerKVTicketStatus.Confirmed); Check((await KVMgr.WaitForKVTicketAsync(a, edit.Ticket)).Status == PlayerKVTicketStatus.Confirmed);
await bridge.CloseAsync(); epoch++; await bridge.CloseAsync(); epoch++;
var b = await Open("102"); var writesBefore = writes; var b = await Open("102"); var writesBefore = writes;
Check(PlayFabMgr.ReadKV(a, "k").Error == PlayerKVKeyReadError.StaleOrClosing); Check(KVMgr.ReadKV(a, "k").Error == PlayerKVKeyReadError.StaleOrClosing);
Check(!PlayFabMgr.WriteKV(a, "k", "late").Accepted); Check(!KVMgr.WriteKV(a, "k", "late").Accepted);
Check((await PlayFabMgr.FlushKVAsync(a)).Blocked == 1); Check((await KVMgr.FlushKVAsync(a)).Blocked == 1);
Check((await PlayFabMgr.WaitForKVTicketAsync(b, edit.Ticket)).Status == PlayerKVTicketStatus.Blocked); Check((await KVMgr.WaitForKVTicketAsync(b, edit.Ticket)).Status == PlayerKVTicketStatus.Blocked);
Check(PlayFabMgr.ReadKV(b, "k").Value == "B" && writes == writesBefore); Check(KVMgr.ReadKV(b, "k").Value == "B" && writes == writesBefore);
Check(PlayerKVGameLoadGuard.TryEnter(out guard)); Check(PlayerKVGameLoadGuard.TryEnter(out guard));
Check(PlayFabMgr.ReadKV(null, null).Error == PlayerKVKeyReadError.LoadGuardActive); Check(KVMgr.ReadKV(null, null).Error == PlayerKVKeyReadError.LoadGuardActive);
Check(PlayFabMgr.WriteKV(null, null, null).StateResult.Error == PlayerKVChangeError.Blocked); Check(KVMgr.WriteKV(null, null, null).StateResult.Error == PlayerKVChangeError.Blocked);
Check((await PlayFabMgr.FlushKVAsync(b)).TransportError == PlayerKVError.Cancelled); Check((await KVMgr.FlushKVAsync(b)).TransportError == PlayerKVError.Cancelled);
Check((await PlayFabMgr.WaitForKVTicketAsync(b, null)).TransportError == PlayerKVError.Cancelled); Check((await KVMgr.WaitForKVTicketAsync(b, null)).TransportError == PlayerKVError.Cancelled);
Check(guard.Violations == 4 && guard.Count(PlayerKVGuardPoint.ReadLocal) == 1 && guard.Count(PlayerKVGuardPoint.Update) == 1 && guard.Count(PlayerKVGuardPoint.Sync) == 2); Check(guard.Violations == 4 && guard.Count(PlayerKVGuardPoint.ReadLocal) == 1 && guard.Count(PlayerKVGuardPoint.Update) == 1 && guard.Count(PlayerKVGuardPoint.Sync) == 2);
Check(auth == 2 && gets == 2 && writes == 1); Check(auth == 2 && gets == 2 && writes == 1);
await Close(); await Close();

View File

@@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace game.PlayerDataStorage
{
public sealed class KVPlayerSaveStore : IPlayerSaveStore, IDisposable
{
PlayerKVGameSessionHandle handle;
CancellationToken lifetime;
bool bound, disposed;
readonly object receiptsGate = new object();
readonly Dictionary<string, PlayerKVSyncTicket> pendingReceipts = new Dictionary<string, PlayerKVSyncTicket>(StringComparer.Ordinal);
public bool IsCurrent => bound && !disposed && !lifetime.IsCancellationRequested && handle.Status != PlayerKVSyncStatus.Closed;
internal void Bind(PlayerKVGameSessionHandle original, CancellationToken loginLifetime)
{
if (bound || disposed || original == null || loginLifetime.IsCancellationRequested)
throw new InvalidOperationException("Player save store cannot be rebound.");
// This also checks the load guard before permitting business use.
if (!KVMgr.ReadKV(original, "LastLoginTimeUtc").Success)
throw new InvalidOperationException("Player save handle is not ready.");
handle = original;
lifetime = loginLifetime;
bound = true;
}
public PlayerSaveReadResult Read(string key)
{
if (!IsCurrent) return new PlayerSaveReadResult("StaleOrNotReady");
var result = KVMgr.ReadKV(handle, key);
return result.Success ? new PlayerSaveReadResult(result.Found.Value, result.Value) :
new PlayerSaveReadResult(result.Error.ToString());
}
public PlayerSaveWriteResult Write(string key, string value, bool remoteStore = true)
{
lock (receiptsGate)
{
if (!IsCurrent)
{
var rejected = new PlayerSaveWriteResult("StaleOrNotReady");
return rejected;
}
var result = KVMgr.WriteKV(handle, key, value, remoteStore);
if (result.Accepted && result.Ticket != null) pendingReceipts[key] = result.Ticket;
var write = result.Accepted ? new PlayerSaveWriteResult(new PlayerSaveReceipt(this, result.Ticket)) :
new PlayerSaveWriteResult(result.StateResult.Error.ToString());
return write;
}
}
public async Task<PlayerSaveConfirmation> WriteAndConfirmAsync(string key, string value, CancellationToken token = default)
{
if (token.IsCancellationRequested) return Cancelled();
var write = Write(key, value);
if (!write.Accepted) return new PlayerSaveConfirmation(PlayerSaveStatus.Rejected, write.Error);
return await ConfirmAsync(write.Receipt, token);
}
public async Task<PlayerSaveConfirmation> ConfirmAsync(PlayerSaveReceipt receipt, CancellationToken token = default)
{
if (!IsCurrent || token.IsCancellationRequested) return Cancelled();
if (receipt == null || !ReferenceEquals(receipt.Store, this))
return new PlayerSaveConfirmation(PlayerSaveStatus.Rejected, "ForeignReceipt");
if (receipt.Ticket == null) return new PlayerSaveConfirmation(PlayerSaveStatus.LocalOnly);
using (var wait = CancellationTokenSource.CreateLinkedTokenSource(lifetime, token))
{
try
{
await KVMgr.FlushKVAsync(handle, wait.Token);
var ticket = await KVMgr.WaitForKVTicketAsync(handle, (PlayerKVSyncTicket)receipt.Ticket, wait.Token);
if (!IsCurrent || wait.IsCancellationRequested) return Cancelled();
if (ticket.Status == PlayerKVTicketStatus.Confirmed)
{
ForgetConfirmed((PlayerKVSyncTicket)receipt.Ticket);
return new PlayerSaveConfirmation(PlayerSaveStatus.Confirmed);
}
return new PlayerSaveConfirmation(ticket.Status == PlayerKVTicketStatus.Unknown ? PlayerSaveStatus.Unknown :
ticket.Status == PlayerKVTicketStatus.Superseded ? PlayerSaveStatus.Superseded :
ticket.Status == PlayerKVTicketStatus.Cancelled ? PlayerSaveStatus.Cancelled : PlayerSaveStatus.Rejected,
ticket.TransportError.ToString());
}
catch (OperationCanceledException) { return Cancelled(); }
}
}
public async Task<PlayerSaveConfirmation> FlushAsync(CancellationToken token = default)
{
if (!IsCurrent || token.IsCancellationRequested) return Cancelled();
PlayerKVSyncTicket[] captured;
lock (receiptsGate) captured = pendingReceipts.Values.ToArray();
using (var wait = CancellationTokenSource.CreateLinkedTokenSource(lifetime, token))
{
try
{
var result = await KVMgr.FlushKVAsync(handle, wait.Token);
if (!IsCurrent || wait.IsCancellationRequested) return Cancelled();
var status = result.Unknown > 0 || result.CacheStatus == PlayerKVSyncStatus.UnknownOutcome ?
PlayerSaveStatus.Unknown : result.Blocked > 0 || result.TransportError != PlayerKVError.None ||
result.CacheStatus != PlayerKVSyncStatus.Ready ? PlayerSaveStatus.Rejected : PlayerSaveStatus.Confirmed;
if (status != PlayerSaveStatus.Confirmed) return new PlayerSaveConfirmation(status, result.TransportError.ToString());
// Multiple writes before this flush are covered by the captured final receipt.
// A replacement during the await belongs to a later flush and cannot confirm it.
foreach (var receipt in captured)
{
var ticket = await KVMgr.WaitForKVTicketAsync(handle, receipt, wait.Token);
if (!IsCurrent || wait.IsCancellationRequested) return Cancelled();
if (ticket.Status != PlayerKVTicketStatus.Confirmed)
return new PlayerSaveConfirmation(ticket.Status == PlayerKVTicketStatus.Superseded ? PlayerSaveStatus.Superseded :
ticket.Status == PlayerKVTicketStatus.Unknown ? PlayerSaveStatus.Unknown :
ticket.Status == PlayerKVTicketStatus.Cancelled ? PlayerSaveStatus.Cancelled : PlayerSaveStatus.Rejected,
ticket.TransportError.ToString());
ForgetConfirmed(receipt);
}
return new PlayerSaveConfirmation(PlayerSaveStatus.Confirmed);
}
catch (OperationCanceledException) { return Cancelled(); }
}
}
static PlayerSaveConfirmation Cancelled() => new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled);
void ForgetConfirmed(PlayerKVSyncTicket receipt)
{
lock (receiptsGate)
if (pendingReceipts.TryGetValue(receipt.Key, out var latest) && ReferenceEquals(latest, receipt))
pendingReceipts.Remove(receipt.Key);
}
public void Dispose() { disposed = true; lock (receiptsGate) { pendingReceipts.Clear(); } }
}
public sealed class PlayFabPlayerSaveStore : IPlayerSaveStore
{
readonly PlayFabMgr manager;
readonly CancellationToken lifetime;
public PlayFabPlayerSaveStore(IUserService user) : this(PlayFabMgr.Instance, user.LoginLifetime) { }
internal PlayFabPlayerSaveStore(PlayFabMgr manager, CancellationToken lifetime)
{ this.manager = manager; this.lifetime = lifetime; }
public bool IsCurrent => manager != null && !lifetime.IsCancellationRequested;
public PlayerSaveReadResult Read(string key)
{
if (!IsCurrent) return new PlayerSaveReadResult("StaleOrNotReady");
try
{
var value = manager.GetLocalData(key);
return new PlayerSaveReadResult(value != null, value);
}
catch (Exception ex) { return new PlayerSaveReadResult(ex.GetType().Name); }
}
public PlayerSaveWriteResult Write(string key, string value, bool remoteStore = true)
{
if (!IsCurrent) return new PlayerSaveWriteResult("StaleOrNotReady");
try
{
manager.UpdateUserDataValue(key, value, remoteStore);
return new PlayerSaveWriteResult(new PlayerSaveReceipt(this, remoteStore ? (object)true : null));
}
catch (Exception ex) { return new PlayerSaveWriteResult(ex.GetType().Name); }
}
public async Task<PlayerSaveConfirmation> WriteAndConfirmAsync(string key, string value, CancellationToken token = default)
{
if (!IsCurrent || token.IsCancellationRequested) return new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled);
using (var wait = CancellationTokenSource.CreateLinkedTokenSource(lifetime, token))
{
var cancelled = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using (wait.Token.Register(() => cancelled.TrySetResult(false)))
{
try
{
var save = manager.SyncDataImmidiate(key, value);
var finished = await Task.WhenAny(save, cancelled.Task);
if (!IsCurrent || wait.IsCancellationRequested) return new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled);
return new PlayerSaveConfirmation(await finished ? PlayerSaveStatus.Confirmed : PlayerSaveStatus.Rejected);
}
catch (Exception ex) { return new PlayerSaveConfirmation(PlayerSaveStatus.Rejected, ex.GetType().Name); }
}
}
}
public Task<PlayerSaveConfirmation> ConfirmAsync(PlayerSaveReceipt receipt, CancellationToken token = default) =>
Task.FromResult(new PlayerSaveConfirmation(!IsCurrent || token.IsCancellationRequested ? PlayerSaveStatus.Cancelled :
receipt == null || !ReferenceEquals(receipt.Store, this) ? PlayerSaveStatus.Rejected :
receipt.Ticket == null ? PlayerSaveStatus.LocalOnly : PlayerSaveStatus.Buffered));
public Task<PlayerSaveConfirmation> FlushAsync(CancellationToken token = default)
{
if (!IsCurrent || token.IsCancellationRequested)
return Task.FromResult(new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled));
// The original manager owns its timer and callback-based buffered dispatch.
manager.FlushBufferedData();
return Task.FromResult(new PlayerSaveConfirmation(PlayerSaveStatus.Buffered));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b48427bc908093046aaca2238303cf40
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -3,6 +3,8 @@ using System.Threading.Tasks;
using asap.core; using asap.core;
using Game; using Game;
using UnityEngine; using UnityEngine;
using System;
using game.PlayerDataStorage;
namespace game namespace game
{ {
@@ -27,6 +29,61 @@ namespace game
int _unitOfWeight = 1; int _unitOfWeight = 1;
bool? _showNotice; bool? _showNotice;
readonly bool requiresFormalKV;
PlayerKVGameSessionHandle promptOwner;
internal PlayerKVEditResult LastPromptEdit { get; private set; }
internal bool HasKVPromptOwner => promptOwner != null;
// Preserve only our key/owner/error metadata across void UI boundaries, never platform payloads.
internal static InvalidOperationException PromptFailure(string message)
{
var error = new InvalidOperationException(message);
error.Data["B01PromptDiagnostic"] = message;
return error;
}
internal static void LogPromptFailure(Exception error) => Debug.LogError(
error.Data["B01PromptDiagnostic"] as string ?? "Notification prompt stopped: " + error.GetType().Name);
public DefaultSettingService() : this(false) { }
internal DefaultSettingService(bool formalKV) { requiresFormalKV = formalKV; }
internal DefaultSettingService(PlayerKVGameSessionHandle owner) : this(true)
{
promptOwner = owner ?? throw new ArgumentNullException(nameof(owner));
RequirePromptOwner();
}
// The frozen startup selection is supplied before login; this instance can never change owners.
internal void PreflightFormalPromptOwner(PlayerKVGameSessionHandle owner)
{
if (!requiresFormalKV || owner == null || (promptOwner != null && !ReferenceEquals(promptOwner, owner)))
throw PromptFailure("ShowNotice: formal owner mismatch.");
var read = KVMgr.ReadKV(owner, "ShowNotice");
if (!read.Success) throw PromptFailure($"ShowNotice owner={owner.OwnerInstanceId}: {read.Error}");
}
internal void BindFormalPromptOwner(PlayerKVGameSessionHandle owner)
{
PreflightFormalPromptOwner(owner);
if (promptOwner == null) promptOwner = owner;
RequirePromptOwner();
}
internal bool HasFormalPromptOwner(PlayerKVGameSessionHandle owner) =>
requiresFormalKV && ReferenceEquals(promptOwner, owner);
internal void RequirePromptOwner()
{
if (promptOwner != null) ReadPrompt();
else if (requiresFormalKV) throw PromptFailure("ShowNotice: formal owner not handed off.");
}
PlayerKVKeyReadResult ReadPrompt()
{
if (LastPromptEdit != null && !LastPromptEdit.Accepted)
throw PromptFailure($"ShowNotice owner={promptOwner.OwnerInstanceId}: {LastPromptEdit.StateResult.Error}; uncertain={LastPromptEdit.StateResult.LocalCommitMayHaveSucceeded}");
var read = KVMgr.ReadKV(promptOwner, "ShowNotice");
if (!read.Success)
throw PromptFailure($"ShowNotice owner={promptOwner.OwnerInstanceId}: {read.Error}");
return read;
}
public bool Music public bool Music
{ {
set set
@@ -105,11 +162,33 @@ namespace game
{ {
get get
{ {
if (requiresFormalKV && promptOwner == null) RequirePromptOwner();
if (promptOwner != null)
{
var read = ReadPrompt();
_showNotice = read.Found == true && read.Value == "1";
return _showNotice.Value;
}
_showNotice ??= PlayFabMgr.Instance.GetLocalData("ShowNotice") == "1"; _showNotice ??= PlayFabMgr.Instance.GetLocalData("ShowNotice") == "1";
return _showNotice.Value; return _showNotice.Value;
} }
set set
{ {
if (requiresFormalKV && promptOwner == null) RequirePromptOwner();
if (promptOwner != null)
{
var read = ReadPrompt();
var serialized = value ? "1" : "0";
if (read.Found != true || read.Value != serialized)
{
LastPromptEdit = KVMgr.WriteKV(promptOwner, "ShowNotice", serialized);
if (!LastPromptEdit.Accepted)
throw PromptFailure($"ShowNotice owner={promptOwner.OwnerInstanceId}: {LastPromptEdit.StateResult.Error}; uncertain={LastPromptEdit.StateResult.LocalCommitMayHaveSucceeded}");
}
RequirePromptOwner();
_showNotice = value;
return;
}
if (value != _showNotice) if (value != _showNotice)
{ {
_showNotice = value; _showNotice = value;
@@ -156,7 +235,9 @@ namespace game
{ {
return false; return false;
} }
if (GContext.container.Resolve<ILocalNotificationService>().IsNoticeOpen) var notification = GContext.container.Resolve<ILocalNotificationService>();
if (notification is LocalNotificationService local) local.RequirePromptOwner(this);
if (notification.IsNoticeOpen)
{ {
ShowNotice = true; ShowNotice = true;
return false; return false;

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -21,23 +21,25 @@ public class SurveyService
const string SurveyData = "SurveyData"; const string SurveyData = "SurveyData";
const string SurveyReadSidData = "SurveyReadSidData"; const string SurveyReadSidData = "SurveyReadSidData";
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore;
IUserService userService; IUserService userService;
cfg.Tables tables; cfg.Tables tables;
List<string> receivedReward = new List<string>(); List<string> receivedReward = new List<string>();
List<string> readSid = new List<string>(); List<string> readSid = new List<string>();
string access_token; string access_token;
public SurveyData tableData; public SurveyData tableData;
public SurveyService(IUserService userService, cfg.Tables tables) public SurveyService(IUserService userService, cfg.Tables tables, game.PlayerDataStorage.IPlayerSaveStore saveStore)
{ {
this.userService = userService; this.userService = userService;
this.saveStore = saveStore;
this.tables = tables; this.tables = tables;
tableData = null; tableData = null;
string surveyData = PlayFabMgr.Instance.GetLocalData(SurveyData); string surveyData = saveStore.Read(SurveyData).ValueOrThrow();
if (!string.IsNullOrEmpty(surveyData)) if (!string.IsNullOrEmpty(surveyData))
{ {
receivedReward = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>>(surveyData); receivedReward = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>>(surveyData);
} }
string surveyReadSidData = PlayFabMgr.Instance.GetLocalData(SurveyReadSidData); string surveyReadSidData = saveStore.Read(SurveyReadSidData).ValueOrThrow();
if (!string.IsNullOrEmpty(surveyReadSidData)) if (!string.IsNullOrEmpty(surveyReadSidData))
{ {
readSid = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>>(surveyReadSidData); readSid = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>>(surveyReadSidData);
@@ -53,7 +55,7 @@ public class SurveyService
PlayerData playerData = GContext.container.Resolve<PlayerData>(); PlayerData playerData = GContext.container.Resolve<PlayerData>();
List<SurveyData> surveyDataTable = tables.TbSurveyData.DataList; List<SurveyData> surveyDataTable = tables.TbSurveyData.DataList;
ulong userId = Convert.ToUInt64(userService.UserId, 16); ulong userId = Convert.ToUInt64(userService.UserId, saveStore is game.PlayerDataStorage.KVPlayerSaveStore ? 10 : 16);
tableData = null; tableData = null;
if (surveyDataTable.Count > 0) if (surveyDataTable.Count > 0)
{ {
@@ -109,7 +111,7 @@ public class SurveyService
string url = $"{SurveyUrl}s2/{tableData.ID}/{tableData.Hash}/?&uid={userService.CustomId}"; string url = $"{SurveyUrl}s2/{tableData.ID}/{tableData.Hash}/?&uid={userService.CustomId}";
//标记已读 //标记已读
readSid.Add(tableData.ID.ToString()); readSid.Add(tableData.ID.ToString());
PlayFabMgr.Instance.UpdateUserDataValue(SurveyReadSidData, Newtonsoft.Json.JsonConvert.SerializeObject(readSid)); saveStore.Write(SurveyReadSidData, Newtonsoft.Json.JsonConvert.SerializeObject(readSid)).RequireAccepted();
GetSurveyMatchlist(); GetSurveyMatchlist();
return url; return url;
} }
@@ -133,7 +135,7 @@ public class SurveyService
GContext.container.Resolve<PlayerItemData>().AddItemByDrop(tableData.Reward, null); GContext.container.Resolve<PlayerItemData>().AddItemByDrop(tableData.Reward, null);
GContext.Publish(new ShowData()); GContext.Publish(new ShowData());
receivedReward.Add(sid); receivedReward.Add(sid);
PlayFabMgr.Instance.UpdateUserDataValue(SurveyData, Newtonsoft.Json.JsonConvert.SerializeObject(receivedReward)); saveStore.Write(SurveyData, Newtonsoft.Json.JsonConvert.SerializeObject(receivedReward)).RequireAccepted();
GetSurveyMatchlist(); GetSurveyMatchlist();
} }
catch (Exception e) catch (Exception e)

View File

@@ -253,7 +253,7 @@ namespace game.PlayerDataStorage
using(var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(30))) using(var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(30)))
{ {
var owner=a.Consumption.Owner; var owner=a.Consumption.Owner;
var flush=await PlayFabMgr.FlushKVAsync(owner,timeout.Token); var flush=await KVMgr.FlushKVAsync(owner,timeout.Token);
if(!RewardCurrent(a) || flush.Unknown>0 || flush.Blocked>0 || flush.TransportError!=PlayerKVError.None) if(!RewardCurrent(a) || flush.Unknown>0 || flush.Blocked>0 || flush.TransportError!=PlayerKVError.None)
throw Stop("Reward flush: "+flush.TransportError); throw Stop("Reward flush: "+flush.TransportError);
foreach(var r in a.Records) foreach(var r in a.Records)
@@ -262,7 +262,7 @@ namespace game.PlayerDataStorage
var final=last[r.Key].Edit.Ticket; var final=last[r.Key].Edit.Ticket;
if(t.OwnerInstanceId!=owner.OwnerInstanceId || t.Epoch!=owner.Epoch || t.Key!=r.Key || if(t.OwnerInstanceId!=owner.OwnerInstanceId || t.Epoch!=owner.Epoch || t.Key!=r.Key ||
final.OwnerInstanceId!=t.OwnerInstanceId || final.Epoch!=t.Epoch) throw Stop("Reward ticket identity mismatch."); final.OwnerInstanceId!=t.OwnerInstanceId || final.Epoch!=t.Epoch) throw Stop("Reward ticket identity mismatch.");
var result=await PlayFabMgr.WaitForKVTicketAsync(owner,t,timeout.Token); var result=await KVMgr.WaitForKVTicketAsync(owner,t,timeout.Token);
if(!RewardCurrent(a)) throw Stop("Reward action changed while waiting."); if(!RewardCurrent(a)) throw Stop("Reward action changed while waiting.");
bool explained=result.Status==PlayerKVTicketStatus.Superseded && !ReferenceEquals(t,final) && final.Sequence>t.Sequence; bool explained=result.Status==PlayerKVTicketStatus.Superseded && !ReferenceEquals(t,final) && final.Sequence>t.Sequence;
if(result.Status!=PlayerKVTicketStatus.Confirmed && !explained) throw Stop("Reward ticket: "+result.Status); if(result.Status!=PlayerKVTicketStatus.Confirmed && !explained) throw Stop("Reward ticket: "+result.Status);

View File

@@ -1,4 +1,4 @@
#if UNITY_EDITOR && !SERVICE_HOST #if UNITY_EDITOR && !SERVICE_HOST
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@@ -152,6 +152,9 @@ namespace game.PlayerDataStorage
pfShell=new GameObject("KV inactive guide Init fixture");pfShell.SetActive(false);pfShell.hideFlags=HideFlags.HideAndDontSave; pfShell=new GameObject("KV inactive guide Init fixture");pfShell.SetActive(false);pfShell.hideFlags=HideFlags.HideAndDontSave;
var pf=pfShell.AddComponent<PlayFabMgr>(); var pf=pfShell.AddComponent<PlayFabMgr>();
Check(pf.IsUnstartedForKVLoad,"guide fixture never starts PF lifecycle");pfField.SetValue(null,pf); Check(pf.IsUnstartedForKVLoad,"guide fixture never starts PF lifecycle");pfField.SetValue(null,pf);
var store=new PlayFabPlayerSaveStore(pf,CancellationToken.None);
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; 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.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; Application.logMessageReceived+=onLog;
@@ -270,6 +273,9 @@ namespace game.PlayerDataStorage
typeof(PlayerFishData).GetField("mapFishData",fields).SetValue(fish,new Dictionary<int,Dictionary<int,FishWeightData>>{{102,data}}); typeof(PlayerFishData).GetField("mapFishData",fields).SetValue(fish,new Dictionary<int,Dictionary<int,FishWeightData>>{{102,data}});
pfShell=new GameObject("KV reward unstarted PF fixture");pfShell.SetActive(false);pfShell.hideFlags=HideFlags.HideAndDontSave; pfShell=new GameObject("KV reward unstarted PF fixture");pfShell.SetActive(false);pfShell.hideFlags=HideFlags.HideAndDontSave;
var pf=pfShell.AddComponent<PlayFabMgr>();Check(pf.IsUnstartedForKVLoad,"PF lifecycle never starts");pfField.SetValue(null,pf); var pf=pfShell.AddComponent<PlayFabMgr>();Check(pf.IsUnstartedForKVLoad,"PF lifecycle never starts");pfField.SetValue(null,pf);
var store=new PlayFabPlayerSaveStore(pf,CancellationToken.None);
container.RegisterInstance<IPlayerSaveStore>(store);
player.playerSaveStore=store;fish.playerSaveStore=store;
rewardAction.BeforeFishCount=scenario=="new-fish"?0:20; rewardAction.BeforeFishCount=scenario=="new-fish"?0:20;
using(BeginRewardSegment("strike")){}rewardAction.Ending=true;using(BeginRewardSegment("ending")){} using(BeginRewardSegment("strike")){}rewardAction.Ending=true;using(BeginRewardSegment("ending")){}
rewardAction.Applying=true; rewardAction.Applying=true;
@@ -519,8 +525,14 @@ namespace game.PlayerDataStorage
var pf=shell.AddComponent<PlayFabMgr>(); var pf=shell.AddComponent<PlayFabMgr>();
if(!pf.IsUnstartedForKVLoad)throw new InvalidOperationException("No PF lifecycle may start in this fixture."); if(!pf.IsUnstartedForKVLoad)throw new InvalidOperationException("No PF lifecycle may start in this fixture.");
PF.SetValue(null,pf); PF.SetValue(null,pf);
var store=new PlayFabPlayerSaveStore(pf,CancellationToken.None);
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 items=new PlayerItemData(tables);container.RegisterInstance(items);
var data=new BattlePassData(tables); var data=new BattlePassData(tables){playerSaveStore=store};
data.BattlePassRecord.ID=tables.TbFishingEvent.DataList.First(e=>tables.TbBattlePassMain.GetOrDefault(e.RedirectID)!=null).ID; data.BattlePassRecord.ID=tables.TbFishingEvent.DataList.First(e=>tables.TbBattlePassMain.GetOrDefault(e.RedirectID)!=null).ID;
data.BattlePassRecord.Level=1; 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); foreach(var t in tables.TbBattlePassTask.DataList.Where(t=>rewardConditions.Contains(t.Type)))data.BattleTask.dic_dailyTasks.Add(t.TaskID,0);

View File

@@ -1,4 +1,4 @@
#if UNITY_EDITOR && !SERVICE_HOST #if UNITY_EDITOR && !SERVICE_HOST
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@@ -31,6 +31,9 @@ namespace game.PlayerDataStorage
var oldLanguages=LocalizationMgr.enumLanguage; var oldLanguages=LocalizationMgr.enumLanguage;
var textTable=typeof(LocalizationMgr).GetField("table",System.Reflection.BindingFlags.Static|System.Reflection.BindingFlags.NonPublic); var textTable=typeof(LocalizationMgr).GetField("table",System.Reflection.BindingFlags.Static|System.Reflection.BindingFlags.NonPublic);
var oldTextTable=textTable.GetValue(null); var oldTextTable=textTable.GetValue(null);
GameObject saveShell=null;
var pfField=typeof(PlayFabMgr).GetField("_instance",System.Reflection.BindingFlags.Static|System.Reflection.BindingFlags.NonPublic);
var oldPF=pfField.GetValue(null);
FishingSubscribers subscribers=null; FishingSubscribers subscribers=null;
int checks=0,groups=0; var passed=new List<string>(); string result=null; int checks=0,groups=0; var passed=new List<string>(); string result=null;
void Check(bool yes,string reason) { checks++; if(!yes) throw new InvalidOperationException("Fishing validation: "+reason); } void Check(bool yes,string reason) { checks++; if(!yes) throw new InvalidOperationException("Fishing validation: "+reason); }
@@ -40,6 +43,7 @@ namespace game.PlayerDataStorage
player?.StopRecoveryEnergy(); player=null; subscription?.Dispose(); subscription=null; player?.StopRecoveryEnergy(); player=null; subscription?.Dispose(); subscription=null;
if(fixture!=null) await fixture.Close(); if(fixture!=null) await fixture.Close();
if(pending!=null) { try { await pending; } catch { } pending=null; } if(pending!=null) { try { await pending; } catch { } pending=null; }
pfField.SetValue(null,oldPF);if(saveShell!=null)UnityEngine.Object.DestroyImmediate(saveShell);saveShell=null;
FishingContextAccess.Use(oldContainer); FishingContextAccess.Events=oldEvents; container?.Dispose(); container=null; FishingContextAccess.Use(oldContainer); FishingContextAccess.Events=oldEvents; container?.Dispose(); container=null;
fixture=new EquipmentFixture(Path.Combine(folder,(++groups).ToString())); fixture=new EquipmentFixture(Path.Combine(folder,(++groups).ToString()));
var owner=await fixture.Open(); var owner=await fixture.Open();
@@ -177,12 +181,17 @@ namespace game.PlayerDataStorage
var tables=new cfg.Tables(n=>SimpleJSON.JSON.Parse(File.ReadAllText(Path.GetFullPath(Path.Combine(Application.dataPath,"../../cfgData",n+".json"))))); 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=new Container(); FishingContextAccess.Use(container); FishingContextAccess.Events=new EventAggregator();
container.RegisterInstance(tables); container.RegisterInstance(tables);
saveShell=new GameObject("KV inactive consumption storage fixture");saveShell.SetActive(false);saveShell.hideFlags=HideFlags.HideAndDontSave;
var pf=saveShell.AddComponent<PlayFabMgr>();Check(pf.IsUnstartedForKVLoad,"consumption fixture never starts PF lifecycle");pfField.SetValue(null,pf);
var store=new PlayFabPlayerSaveStore(pf,CancellationToken.None);
container.RegisterInstance<IPlayerSaveStore>(store);
LocalizationMgr.enumLanguage=tables.TbLanguageConfig.DataList.Select(x=>x.ID).ToList(); LocalizationMgr.enumLanguage=tables.TbLanguageConfig.DataList.Select(x=>x.ID).ToList();
textTable.SetValue(null,null); textTable.SetValue(null,null);
player=new PlayerData(tables,null); var small=new SmallGameData(tables,null,null); var fish=new PlayerFishData(tables,null); player=new PlayerData(tables,null); var small=new SmallGameData(tables,null,null); var fish=new PlayerFishData(tables,null);
var achievements=new AchievementDataManager(tables); var box=new FishingBoxData(); var events=new FishingEventData(tables); var achievements=new AchievementDataManager(tables); var box=new FishingBoxData(); var events=new FishingEventData(tables);
container.RegisterInstance(player); container.RegisterInstance(small); container.RegisterInstance(fish); container.RegisterInstance(player); container.RegisterInstance(small); container.RegisterInstance(fish);
container.RegisterInstance(achievements); container.RegisterInstance(box); container.RegisterInstance(events); container.RegisterInstance(achievements); container.RegisterInstance(box); container.RegisterInstance(events);
player.playerSaveStore=store;fish.playerSaveStore=store;small.playerSaveStore=store;events.playerSaveStore=store;
container.RegisterInstance<ILocalNotificationService>(new FishingNotifications()); container.RegisterInstance<ILocalNotificationService>(new FishingNotifications());
fish.ResetForKVLoad(); small.ResetForKVLoad(); achievements.ResetForKVLoad(); box.ResetForKVLoad(); fish.ResetForKVLoad(); small.ResetForKVLoad(); achievements.ResetForKVLoad(); box.ResetForKVLoad();
achievements.Init(); achievements.Init();
@@ -228,7 +237,7 @@ namespace game.PlayerDataStorage
Check(scenario=="cross-limit"?savedTime>new DateTime(2026,1,1):savedTime==new DateTime(2026,1,1),"recovery timestamp crossing behavior preserved"); Check(scenario=="cross-limit"?savedTime>new DateTime(2026,1,1):savedTime==new DateTime(2026,1,1),"recovery timestamp crossing behavior preserved");
if(fixture.HoldSet) if(fixture.HoldSet)
{ {
await PlayFabMgr.FlushKVAsync(action.Owner); await KVMgr.FlushKVAsync(action.Owner);
var recovered=JsonConvert.DeserializeObject<Dictionary<string,string>>(fixture.Remote["NewPlayerDataDic"]); var recovered=JsonConvert.DeserializeObject<Dictionary<string,string>>(fixture.Remote["NewPlayerDataDic"]);
Check(recovered["newData"].Split(',')[3]==(initialEnergy-2).ToString(),"later recovery has separate flush, not original action confirmation"); Check(recovered["newData"].Split(',')[3]==(initialEnergy-2).ToString(),"later recovery has separate flush, not original action confirmation");
} }
@@ -254,6 +263,7 @@ namespace game.PlayerDataStorage
player?.StopRecoveryEnergy(); subscription?.Dispose(); player?.StopRecoveryEnergy(); subscription?.Dispose();
if(fixture!=null) await fixture.Close(); if(fixture!=null) await fixture.Close();
if(pending!=null) { try { await pending; } catch(Exception e) { Debug.LogWarning("Observed fishing test failure: "+e.GetType().Name); } } if(pending!=null) { try { await pending; } catch(Exception e) { Debug.LogWarning("Observed fishing test failure: "+e.GetType().Name); } }
pfField.SetValue(null,oldPF);if(saveShell!=null)UnityEngine.Object.DestroyImmediate(saveShell);
FishingContextAccess.Use(oldContainer); FishingContextAccess.Events=oldEvents; container?.Dispose(); FishingContextAccess.Use(oldContainer); FishingContextAccess.Events=oldEvents; container?.Dispose();
LocalizationMgr.enumLanguage=oldLanguages; textTable.SetValue(null,oldTextTable); LocalizationMgr.enumLanguage=oldLanguages; textTable.SetValue(null,oldTextTable);
(Enabled,Ready,Error,Permit,RunPath,handle,onFault,onClose,collecting,mainThread,failed,mainContext,fishingAction)=saved; (Enabled,Ready,Error,Permit,RunPath,handle,onFault,onClose,collecting,mainThread,failed,mainContext,fishingAction)=saved;
@@ -280,8 +290,14 @@ namespace game.PlayerDataStorage
var pf=shell.AddComponent<PlayFabMgr>(); var pf=shell.AddComponent<PlayFabMgr>();
if(!pf.IsUnstartedForKVLoad)throw new InvalidOperationException("No PF lifecycle may start in this fixture."); if(!pf.IsUnstartedForKVLoad)throw new InvalidOperationException("No PF lifecycle may start in this fixture.");
PF.SetValue(null,pf); PF.SetValue(null,pf);
var store=container.Resolve<IPlayerSaveStore>();
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 items=new PlayerItemData(tables);container.RegisterInstance(items);
var data=new BattlePassData(tables); var data=new BattlePassData(tables){playerSaveStore=store};
data.BattlePassRecord.ID=tables.TbFishingEvent.DataList.First(e=>tables.TbBattlePassMain.GetOrDefault(e.RedirectID)!=null).ID; data.BattlePassRecord.ID=tables.TbFishingEvent.DataList.First(e=>tables.TbBattlePassMain.GetOrDefault(e.RedirectID)!=null).ID;
data.BattlePassRecord.Level=1; data.BattlePassRecord.Level=1;
data.BattleTask.dic_dailyTasks.Add(tables.TbBattlePassTask.DataList.First(t=>t.Type==cfg.ConditionType.ConsumeEnergy).TaskID,0); data.BattleTask.dic_dailyTasks.Add(tables.TbBattlePassTask.DataList.First(t=>t.Type==cfg.ConditionType.ConsumeEnergy).TaskID,0);

View File

@@ -131,7 +131,7 @@ namespace game.PlayerDataStorage
} }
internal static string Read(string key) internal static string Read(string key)
{ {
var result=PlayFabMgr.ReadKV(Capture(),key); var result=KVMgr.ReadKV(Capture(),key);
if(!result.Success) throw Stop("Read: "+result.Error); if(!result.Success) throw Stop("Read: "+result.Error);
return result.Found==true ? result.Value : null; return result.Found==true ? result.Value : null;
} }
@@ -142,7 +142,7 @@ namespace game.PlayerDataStorage
CheckRewardWrite(key,value); CheckRewardWrite(key,value);
var valueHash=value==null?null:PlayerKVLoopPermit.Hash(Encoding.UTF8.GetBytes(value)); var valueHash=value==null?null:PlayerKVLoopPermit.Hash(Encoding.UTF8.GetBytes(value));
Audit("write-attempt",new{key,remote,valueHash}); Audit("write-attempt",new{key,remote,valueHash});
var result=PlayFabMgr.WriteKV(owner,key,value,remote); var result=KVMgr.WriteKV(owner,key,value,remote);
if(!result.Accepted) throw Stop("Write: "+result.StateResult.Error+" uncertain="+result.StateResult.LocalCommitMayHaveSucceeded); if(!result.Accepted) throw Stop("Write: "+result.StateResult.Error+" uncertain="+result.StateResult.LocalCommitMayHaveSucceeded);
if(collecting!=null) if(collecting!=null)
{ {
@@ -220,7 +220,7 @@ namespace game.PlayerDataStorage
last.Values.Any(r=>!r.Remote || r.Edit.Ticket==null)) throw Stop("Fishing final writes require original remote tickets."); last.Values.Any(r=>!r.Remote || r.Edit.Ticket==null)) throw Stop("Fishing final writes require original remote tickets.");
using(var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(30))) using(var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(30)))
{ {
var flush=await PlayFabMgr.FlushKVAsync(batch.Owner,timeout.Token); var flush=await KVMgr.FlushKVAsync(batch.Owner,timeout.Token);
if(!FishingCurrent(action) || flush.Unknown>0 || flush.Blocked>0 || flush.TransportError!=PlayerKVError.None) if(!FishingCurrent(action) || flush.Unknown>0 || flush.Blocked>0 || flush.TransportError!=PlayerKVError.None)
throw Stop("Fishing flush: "+flush.TransportError); throw Stop("Fishing flush: "+flush.TransportError);
foreach(var record in batch.Records) foreach(var record in batch.Records)
@@ -231,7 +231,7 @@ namespace game.PlayerDataStorage
if(ticket.OwnerInstanceId!=batch.Owner.OwnerInstanceId || ticket.Epoch!=batch.Owner.Epoch || if(ticket.OwnerInstanceId!=batch.Owner.OwnerInstanceId || ticket.Epoch!=batch.Owner.Epoch ||
ticket.Key!=record.Key || final.OwnerInstanceId!=ticket.OwnerInstanceId || final.Epoch!=ticket.Epoch) ticket.Key!=record.Key || final.OwnerInstanceId!=ticket.OwnerInstanceId || final.Epoch!=ticket.Epoch)
throw Stop("Fishing ticket identity mismatch."); throw Stop("Fishing ticket identity mismatch.");
var result=await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,ticket,timeout.Token); var result=await KVMgr.WaitForKVTicketAsync(batch.Owner,ticket,timeout.Token);
if(!FishingCurrent(action)) throw Stop("Fishing action changed while waiting."); if(!FishingCurrent(action)) throw Stop("Fishing action changed while waiting.");
bool explained=result.Status==PlayerKVTicketStatus.Superseded && !ReferenceEquals(ticket,final) && final.Sequence>ticket.Sequence; bool explained=result.Status==PlayerKVTicketStatus.Superseded && !ReferenceEquals(ticket,final) && final.Sequence>ticket.Sequence;
if(result.Status!=PlayerKVTicketStatus.Confirmed && !explained) throw Stop("Fishing ticket: "+result.Status); if(result.Status!=PlayerKVTicketStatus.Confirmed && !explained) throw Stop("Fishing ticket: "+result.Status);
@@ -254,12 +254,12 @@ namespace game.PlayerDataStorage
{ {
if(primary?.Ticket==null || !batch.Edits.Contains(primary) || !ReferenceEquals(Capture(),batch.Owner)) if(primary?.Ticket==null || !batch.Edits.Contains(primary) || !ReferenceEquals(Capture(),batch.Owner))
throw Stop("Missing original equipment ticket."); throw Stop("Missing original equipment ticket.");
var flush=await PlayFabMgr.FlushKVAsync(batch.Owner); var flush=await KVMgr.FlushKVAsync(batch.Owner);
if(flush.Unknown>0 || flush.Blocked>0 || flush.TransportError!=PlayerKVError.None) throw Stop("Equipment flush: "+flush.TransportError); if(flush.Unknown>0 || flush.Blocked>0 || flush.TransportError!=PlayerKVError.None) throw Stop("Equipment flush: "+flush.TransportError);
foreach(var edit in batch.Edits) foreach(var edit in batch.Edits)
{ {
if(edit.Ticket==null) continue; // localOnly is audited, never claimed as remotely confirmed. if(edit.Ticket==null) continue; // localOnly is audited, never claimed as remotely confirmed.
var result=await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,edit.Ticket); var result=await KVMgr.WaitForKVTicketAsync(batch.Owner,edit.Ticket);
if(result.Status!=PlayerKVTicketStatus.Confirmed) throw Stop("Equipment ticket: "+result.Status); if(result.Status!=PlayerKVTicketStatus.Confirmed) throw Stop("Equipment ticket: "+result.Status);
} }
if(!ReferenceEquals(Capture(),batch.Owner)) throw Stop("Equipment session changed."); if(!ReferenceEquals(Capture(),batch.Owner)) throw Stop("Equipment session changed.");
@@ -270,7 +270,7 @@ namespace game.PlayerDataStorage
if(!Ready) return; if(!Ready) return;
try try
{ {
var result=await PlayFabMgr.FlushKVAsync(Capture()); var result=await KVMgr.FlushKVAsync(Capture());
if(result.Unknown>0 || result.Blocked>0 || result.TransportError!=PlayerKVError.None) throw Stop("Periodic flush: "+result.TransportError); if(result.Unknown>0 || result.Blocked>0 || result.TransportError!=PlayerKVError.None) throw Stop("Periodic flush: "+result.TransportError);
} }
catch(Exception e) { Fault(e.Message); } catch(Exception e) { Fault(e.Message); }
@@ -342,14 +342,14 @@ namespace game.PlayerDataStorage
Check(batch.Edits.Count==2 && ReferenceEquals(primary,batch.Edits[1]),"all original edits collected"); Check(batch.Edits.Count==2 && ReferenceEquals(primary,batch.Edits[1]),"all original edits collected");
Check(fixture.Sets==0,"write acceptance is local only"); Check(fixture.Sets==0,"write acceptance is local only");
await ConfirmEquipAsync(batch,primary); await ConfirmEquipAsync(batch,primary);
foreach(var edit in batch.Edits) Check((await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,edit.Ticket)).Status==PlayerKVTicketStatus.Confirmed,"each original ticket confirmed"); foreach(var edit in batch.Edits) Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,edit.Ticket)).Status==PlayerKVTicketStatus.Confirmed,"each original ticket confirmed");
Check(fixture.Remote["k"]=="nested" && fixture.Remote["NewPlayerDataDic"]=="whole-save" && Ready && fixture.Faults==0,"complete batch committed"); Check(fixture.Remote["k"]=="nested" && fixture.Remote["NewPlayerDataDic"]=="whole-save" && Ready && fixture.Faults==0,"complete batch committed");
await Start(); batch=BeginEquip(); await Start(); batch=BeginEquip();
var first=Write("k","first",true); Write("k","second",true); var first=Write("k","first",true); Write("k","second",true);
primary=Write("NewPlayerDataDic","whole-save",true); batch.Dispose(); primary=Write("NewPlayerDataDic","whole-save",true); batch.Dispose();
await Reject(ConfirmEquipAsync(batch,primary),"Superseded"); await Reject(ConfirmEquipAsync(batch,primary),"Superseded");
Check((await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,first.Ticket)).Status==PlayerKVTicketStatus.Superseded,"original nested ticket never replaced"); Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,first.Ticket)).Status==PlayerKVTicketStatus.Superseded,"original nested ticket never replaced");
await Start(); batch=BeginEquip(); await Start(); batch=BeginEquip();
Write("k","nested",true); primary=Write("NewPlayerDataDic","watermark",true); batch.Dispose(); Write("k","nested",true); primary=Write("NewPlayerDataDic","watermark",true); batch.Dispose();
@@ -362,8 +362,8 @@ namespace game.PlayerDataStorage
Check(batch.Edits.Count==2,"later write excluded from synchronous batch"); Check(batch.Edits.Count==2,"later write excluded from synchronous batch");
fixture.ReleaseSet.TrySetResult(true); await confirmation; pendingConfirmation=null; fixture.ReleaseSet.TrySetResult(true); await confirmation; pendingConfirmation=null;
Check(!fixture.Remote.ContainsKey("late"),"flush watermark excludes later write"); Check(!fixture.Remote.ContainsKey("late"),"flush watermark excludes later write");
await PlayFabMgr.FlushKVAsync(batch.Owner); await KVMgr.FlushKVAsync(batch.Owner);
Check((await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,late.Ticket)).Status==PlayerKVTicketStatus.Confirmed,"later write needs its own flush"); Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,late.Ticket)).Status==PlayerKVTicketStatus.Confirmed,"later write needs its own flush");
await Start(); batch=BeginEquip(); primary=Write("NewPlayerDataDic","unauthorized",true); batch.Dispose(); await Start(); batch=BeginEquip(); primary=Write("NewPlayerDataDic","unauthorized",true); batch.Dispose();
fixture.Unauthorized=true; fixture.Unauthorized=true;
@@ -374,7 +374,7 @@ namespace game.PlayerDataStorage
fixture.LoseResponse=true; fixture.LoseResponse=true;
await Reject(ConfirmEquipAsync(batch,primary),"Equipment flush"); await Reject(ConfirmEquipAsync(batch,primary),"Equipment flush");
Check(fixture.Remote["NewPlayerDataDic"]=="lost-response" && fixture.Sets==1,"unknown remote commit is not replayed"); Check(fixture.Remote["NewPlayerDataDic"]=="lost-response" && fixture.Sets==1,"unknown remote commit is not replayed");
Check((await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,primary.Ticket)).Status==PlayerKVTicketStatus.Unknown,"unknown original ticket preserved"); Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,primary.Ticket)).Status==PlayerKVTicketStatus.Unknown,"unknown original ticket preserved");
await Start(); batch=BeginEquip(); primary=Write("NewPlayerDataDic","closed",true); batch.Dispose(); await Start(); batch=BeginEquip(); primary=Write("NewPlayerDataDic","closed",true); batch.Dispose();
await fixture.Close(); await fixture.Close();

View File

@@ -1,8 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using asap.core;
using game.PlayerDataStorage; using game.PlayerDataStorage;
using UnityEngine;
namespace game namespace game
{ {
@@ -105,6 +110,14 @@ namespace game
EAccoutUrl=state.AccountUrls.ToDictionary(p=>p.Key,p=>p.Value,StringComparer.Ordinal); EAccoutUrl=state.AccountUrls.ToDictionary(p=>p.Key,p=>p.Value,StringComparer.Ordinal);
ReplaceKVLinked(state); ReplaceKVLinked(state);
} }
void IKVUserLoadReceiver.ResetForKVLoad() => ResetForKVLoad();
void IKVUserLoadReceiver.ReplaceFromKVDecoded(PlayerKVLoginState state,long epoch,IReadOnlyDictionary<string,string> raw)
=> ReplaceFromKVDecoded(state,epoch,raw);
void IKVUserLoadReceiver.DiscardKVIntents() => KVIntents?.Discard();
bool IKVUserLifecycle.KVHandoffReady => false;
PlayerKVGameLoadError IKVUserLifecycle.KVLastError => KVLastError;
Task IKVUserLifecycle.CloseKVForValidationAsync() => CloseKVForValidationAsync();
Task<PlayerKVShutdownStatus> IKVUserLifecycle.ObserveKVQuitAsync() => ObserveKVQuitAsync();
internal virtual void ReplaceKVLinked(PlayerKVLoginState state) { } internal virtual void ReplaceKVLinked(PlayerKVLoginState state) { }
} }
} }

View File

@@ -63,11 +63,10 @@ namespace game
string DisplayName { get; } string DisplayName { get; }
string AvatarUrl { get; } string AvatarUrl { get; }
Dictionary<string, string> EAccoutUrl { get; } Dictionary<string, string> EAccoutUrl { get; }
EntityKey entityKey { get; }
Task<string> UpdateAvatarUrl(string url); Task<string> UpdateAvatarUrl(string url);
string UserId { get; } string UserId { get; }
string ApiPlayerId { get; } string ApiPlayerId { get; }
string CustomId { get; } string CustomId { get; set; }
uint InternalId { get; } uint InternalId { get; }
string ContinentCode { get; } string ContinentCode { get; }
//string CountryCode { get; } //string CountryCode { get; }
@@ -99,7 +98,7 @@ namespace game
void UpdateProfileLv(); void UpdateProfileLv();
} }
public partial class PlayFabUserService : IUserService public partial class PlayFabUserService : IUserService, PlayerDataStorage.IKVUserLoadReceiver, PlayerDataStorage.IKVUserLifecycle
{ {
[Inject] [Inject]
public IConfig config { get; set; } public IConfig config { get; set; }
@@ -143,7 +142,7 @@ namespace game
const string MMTestSubKey = "MMFailParam"; const string MMTestSubKey = "MMFailParam";
public string GetDefaultName(string id) public string GetDefaultName(string id)
{ {
return $"Angler#{id[0..5]}"; return "Angler#" + (string.IsNullOrEmpty(id) ? string.Empty : id.Substring(0, Math.Min(5, id.Length)));
} }
public PlayFabUserService() : this(null) { } public PlayFabUserService() : this(null) { }
@@ -180,7 +179,9 @@ namespace game
if (generation != logoutGeneration || requestedId != CustomId) return ELoginStep.Api; if (generation != logoutGeneration || requestedId != CustomId) return ELoginStep.Api;
if (!long.TryParse(requestedId, System.Globalization.NumberStyles.None, if (!long.TryParse(requestedId, System.Globalization.NumberStyles.None,
System.Globalization.CultureInfo.InvariantCulture, out var numericId) || numericId <= 0) System.Globalization.CultureInfo.InvariantCulture, out var numericId) || numericId <= 0)
{
return ELoginStep.Api; return ELoginStep.Api;
}
return await LoginCore(progress, generation); return await LoginCore(progress, generation);
} }
finally { loginGate.Release(); } finally { loginGate.Release(); }

View File

@@ -139,11 +139,18 @@ namespace game
private AutoRestart _autoRestart; private AutoRestart _autoRestart;
private bool stopping; private bool stopping;
private PlayerDataStorage.IPlayerSaveStore sessionSaveStore;
public override async Task StartAsync() public override async Task StartAsync()
{ {
if (stopping || this == null) return; if (stopping || this == null) return;
var loginLifetime = UserService.LoginLifetime; var user = UserService;
var loginLifetime = user.LoginLifetime;
sessionSaveStore = GContext.container.Resolve<PlayerDataStorage.IPlayerSaveStore>();
var store = sessionSaveStore;
var kvUser = user as KVUserService;
if (kvUser != null && (!kvUser.IsAuthorized || !kvUser.KVHandoffReady || !store.IsCurrent))
throw new System.InvalidOperationException("KV first scene requires the current completed login.");
LoadingScreen.SetProgress(0.54f); LoadingScreen.SetProgress(0.54f);
await Awaiters.NextFrame; await Awaiters.NextFrame;
if (stopping || this == null || loginLifetime.IsCancellationRequested) return; if (stopping || this == null || loginLifetime.IsCancellationRequested) return;
@@ -177,6 +184,8 @@ namespace game
}); });
if (stopping || this == null || loginLifetime.IsCancellationRequested) return; if (stopping || this == null || loginLifetime.IsCancellationRequested) return;
//GContext.Publish() //GContext.Publish()
if (kvUser != null && (!kvUser.IsAuthorized || !kvUser.KVHandoffReady || !store.IsCurrent))
throw new System.InvalidOperationException("KV session changed before first scene initialization.");
stageData = new FishingStageData(); stageData = new FishingStageData();
LoadingScreen.SetProgress(0.75f); LoadingScreen.SetProgress(0.75f);
@@ -188,6 +197,23 @@ namespace game
InitData(); InitData();
InitManager(); InitManager();
if (kvUser != null)
{
if (!GContext.container.Resolve<TimeManager.TimeRefreshManager>().InitialRefreshSucceeded)
throw new System.InvalidOperationException("KV initial daily refresh failed; previous login must be retained.");
// Daily refresh consumes the previous login first. Save its actual player aggregate
// and pending module writes before advancing the persisted login timestamp.
GContext.container.Resolve<PlayerData>().SavePlayerData();
var initialization = await store.FlushAsync(loginLifetime);
if (stopping || this == null || loginLifetime.IsCancellationRequested) return;
if (!kvUser.IsAuthorized || !store.IsCurrent || !initialization.Confirmed)
throw new System.InvalidOperationException("KV initialization saves were not confirmed.");
var loginSaved = await store.WriteAndConfirmAsync("LastLoginTimeUtc",
kvUser.LoginTime.ToUniversalTime().ToString("O", System.Globalization.CultureInfo.InvariantCulture), loginLifetime);
if (stopping || this == null || loginLifetime.IsCancellationRequested) return;
if (!kvUser.IsAuthorized || !store.IsCurrent || !loginSaved.Confirmed)
throw new System.InvalidOperationException("KV login timestamp was not confirmed.");
}
GContext.container.RegisterInstance<IStageData>(stageData); GContext.container.RegisterInstance<IStageData>(stageData);
GContext.container.Register<SurveyService>().AsSingleton(); GContext.container.Register<SurveyService>().AsSingleton();
GContext.OnEvent<TargetAddData>().Subscribe(OnAddTarget).AddTo(disposable); GContext.OnEvent<TargetAddData>().Subscribe(OnAddTarget).AddTo(disposable);
@@ -254,16 +280,36 @@ namespace game
GContext.container.Resolve<FriendService>().GetInvitedListCount(); GContext.container.Resolve<FriendService>().GetInvitedListCount();
MainThreadDispatcher.OnApplicationPauseAsObservable() MainThreadDispatcher.OnApplicationPauseAsObservable()
.Where(paused => !paused) .Subscribe(paused =>
.Subscribe(paused => { _ = OnApplicationResumedAsync(); }) {
if (paused) _ = FlushPausedSaveAsync();
else _ = OnApplicationResumedAsync();
})
.AddTo(disposable); .AddTo(disposable);
} }
private async Task FlushPausedSaveAsync()
{
var user = UserService;
var store = sessionSaveStore;
if (stopping || user is not KVUserService || !user.IsAuthorized || store == null || !store.IsCurrent) return;
try
{
var result = await store.FlushAsync(user.LoginLifetime);
if (!stopping && store.IsCurrent && !result.Confirmed)
Debug.LogWarning("KV pause save: " + result.Status);
}
catch (System.Exception ex) { Debug.LogError("KV pause save failed: " + ex.GetType().Name); }
}
private async Task OnApplicationResumedAsync() private async Task OnApplicationResumedAsync()
{ {
var userService = UserService; var userService = UserService;
if (userService == null || !userService.IsLogin) if (userService == null || !userService.IsLogin)
return; return;
var lifetime = userService.LoginLifetime;
var store = sessionSaveStore;
if (userService is KVUserService && (!userService.IsAuthorized || store == null || !store.IsCurrent)) return;
var rtService = GContext.container.Resolve<IRTService>(); var rtService = GContext.container.Resolve<IRTService>();
var mailService = GContext.container.Resolve<IInGameMailService>(); var mailService = GContext.container.Resolve<IInGameMailService>();
@@ -277,6 +323,8 @@ namespace game
{ {
Debug.LogException(ex); Debug.LogException(ex);
} }
if (stopping || lifetime.IsCancellationRequested || !userService.IsLogin ||
(userService is KVUserService && !store.IsCurrent)) return;
await mailService.FetchMail(); await mailService.FetchMail();
} }
} }

View File

@@ -20,8 +20,20 @@ namespace game
#if AGG #if AGG
GContext.container.Register<IIAPService, AggIAPService>().AsSingleton(); GContext.container.Register<IIAPService, AggIAPService>().AsSingleton();
GContext.container.Register<IUserService, AggUserService>().AsSingleton(); GContext.container.Register<IUserService, AggUserService>().AsSingleton();
GContext.container.Register<PlayerDataStorage.IPlayerSaveStore, PlayerDataStorage.PlayFabPlayerSaveStore>().AsSingleton();
#else #else
GContext.container.Register<IUserService, PlayFabUserService>().AsSingleton(); if (SubBoostrap.RuntimeSelection?.IsKV == true)
{
if (PlayFabMgr.HasLiveInstanceForKVLoad)
throw new System.InvalidOperationException("KV startup found an active legacy save manager.");
GContext.container.Register<IUserService, KVUserService>().AsSingleton();
GContext.container.Register<PlayerDataStorage.IPlayerSaveStore, PlayerDataStorage.KVPlayerSaveStore>().AsSingleton();
}
else
{
GContext.container.Register<IUserService, PlayFabUserService>().AsSingleton();
GContext.container.Register<PlayerDataStorage.IPlayerSaveStore, PlayerDataStorage.PlayFabPlayerSaveStore>().AsSingleton();
}
GContext.container.Register<IIAPService, DevIAPService>().AsSingleton(); GContext.container.Register<IIAPService, DevIAPService>().AsSingleton();
#endif #endif
GContext.container.Register<PlayerFishData>().AsSingleton(); GContext.container.Register<PlayerFishData>().AsSingleton();
@@ -29,7 +41,10 @@ namespace game
GContext.container.Register<CampDataMM>().AsSingleton(); GContext.container.Register<CampDataMM>().AsSingleton();
GContext.container.Register<FishingEventData>().AsSingleton(); GContext.container.Register<FishingEventData>().AsSingleton();
#if UNITY_EDITOR && !SERVICE_HOST #if UNITY_EDITOR && !SERVICE_HOST
if(PlayerDataStorage.PlayerKVGameplayLoop.Enabled) if(PlayerDataStorage.PlayerKVGameplayLoop.Enabled || SubBoostrap.RuntimeSelection?.IsKV==true)
GContext.container.Register<EventShootingRangeData>().AsSingleton();
#else
if(SubBoostrap.RuntimeSelection?.IsKV==true)
GContext.container.Register<EventShootingRangeData>().AsSingleton(); GContext.container.Register<EventShootingRangeData>().AsSingleton();
#endif #endif
GContext.container.Register<SigninData>().AsSingleton(); GContext.container.Register<SigninData>().AsSingleton();
@@ -108,6 +123,8 @@ namespace game
protected override async Task RequestNextAct(string actId) protected override async Task RequestNextAct(string actId)
{ {
if(SubBoostrap.RuntimeSelection?.IsKV==true)
return;
await GContext.container.Resolve<IStageService>().SwitchStageAsync("FishingStage"); await GContext.container.Resolve<IStageService>().SwitchStageAsync("FishingStage");
} }
} }

View File

@@ -18,7 +18,7 @@ namespace game
[Inject] public IRTService rtService { get; set; } [Inject] public IRTService rtService { get; set; }
[Inject] public IIAPService iapService { get; set; } [Inject] public IIAPService iapService { get; set; }
private PlayFabUserService playFabUserService => userService as PlayFabUserService; private PlayerDataStorage.IKVUserLifecycle kvLifecycle => userService as PlayerDataStorage.IKVUserLifecycle;
[SerializeField] [SerializeField]
private GameObject customInput; private GameObject customInput;
@@ -85,15 +85,34 @@ namespace game
} }
else else
{ {
playFabUserService.CustomId = configCustomId; userService.CustomId = configCustomId;
} }
//source = new CancellationTokenSource(); //source = new CancellationTokenSource();
//LoadingAutoInc(source.Token, 1f); //LoadingAutoInc(source.Token, 1f);
var formalKV=SubBoostrap.RuntimeSelection?.IsKV==true;
rtService.WaitForAPISvrLogin(); rtService.WaitForAPISvrLogin();
var step = await userService.Login(OnLoginStep); var step = await userService.Login(OnLoginStep);
loginToken.ThrowIfCancellationRequested(); loginToken.ThrowIfCancellationRequested();
if(formalKV)
{
StopLoadingInfo();
LoadingScreen.HideProgressBar();
if(step==ELoginStep.Done && kvLifecycle?.KVHandoffReady==true)
LoadingScreen.SetLoadingInfo("KV account loaded. Gameplay is not enabled yet.");
else
{
var error=kvLifecycle?.KVLastError.ToString() ?? "Input";
dialog.Config(new SimpleDialog.Context
{
message="KV login unavailable",
message2="Login stopped ("+error+"). Sign out or restart before trying again.",
okAction=()=>userService.Logout(), closeAction=()=>userService.Logout()
});
}
return;
}
if (step != ELoginStep.Done) if (step != ELoginStep.Done)
{ {
#if UNITY_EDITOR && !SERVICE_HOST #if UNITY_EDITOR && !SERVICE_HOST
@@ -145,10 +164,7 @@ namespace game
{ {
PlayerPrefs.SetString("CustomID", tempInput); PlayerPrefs.SetString("CustomID", tempInput);
} }
if (playFabUserService != null) userService.CustomId = tempInput;
playFabUserService.CustomId = tempInput;
else
throw new NotImplementedException("playFabUserService is null");
customInput.SetActive(false); customInput.SetActive(false);
} }
@@ -294,6 +310,7 @@ namespace game
loginLifetime?.Cancel(); loginLifetime?.Cancel();
loginLifetime?.Dispose(); loginLifetime?.Dispose();
loginLifetime = null; loginLifetime = null;
if(SubBoostrap.RuntimeSelection?.IsKV==true) userService.Logout();
} }

View File

@@ -37,7 +37,7 @@ namespace game
#if UNITY_EDITOR && !SERVICE_HOST #if UNITY_EDITOR && !SERVICE_HOST
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) _=game.PlayerDataStorage.PlayerKVGameplayLoop.CloseAsync(); if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) _=game.PlayerDataStorage.PlayerKVGameplayLoop.CloseAsync();
#endif #endif
if(userService is PlayFabUserService closingKVUser) _=closingKVUser.CloseKVForValidationAsync(); if(userService is PlayerDataStorage.IKVUserLifecycle closingKVUser) _=closingKVUser.CloseKVForValidationAsync();
// Invalidate identity before any await can allow old login/HTTP callbacks to resume. // Invalidate identity before any await can allow old login/HTTP callbacks to resume.
if (QuitStage.Logout) if (QuitStage.Logout)
{ {
@@ -58,7 +58,7 @@ namespace game
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) await PlayerDataStorage.PlayerKVShutdownRegistry.Process.ObserveAsync(5000); if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) await PlayerDataStorage.PlayerKVShutdownRegistry.Process.ObserveAsync(5000);
else else
#endif #endif
if(userService is PlayFabUserService kvUser) await kvUser.ObserveKVQuitAsync(); if(userService is PlayerDataStorage.IKVUserLifecycle kvUser) await kvUser.ObserveKVQuitAsync();
adsService.Release(); adsService.Release();

View File

@@ -1,4 +1,4 @@
using AppsFlyerSDK; using AppsFlyerSDK;
using asap.core; using asap.core;
using asap.core.common; using asap.core.common;
@@ -24,10 +24,22 @@ using UnityEngine.Rendering.Universal;
public class SubBoostrap public class SubBoostrap
{ {
internal static game.PlayerDataStorage.PlayerKVRuntimeSelection RuntimeSelection { get; private set; }
public static async System.Threading.Tasks.Task RegisterServices(GameObject rootCtxGo) public static async System.Threading.Tasks.Task RegisterServices(GameObject rootCtxGo, IConfig config)
{ {
if(config==null) throw new ArgumentNullException(nameof(config));
#if AGG
const bool aggBuild=true;
#else
const bool aggBuild=false;
#endif
var selection=game.PlayerDataStorage.PlayerKVRuntimeSelection.Capture(
config.Get<string>("SaveBackend","playfab"),
config.Get<string>(GConstant.K_Event_API_URL,game.Account.S101AccountSession.DefaultApiUrl),aggBuild);
RuntimeSelection=selection;
var container = GContext.container; var container = GContext.container;
container.RegisterInstance(selection);
VibrationController vibrationController = new VibrationController(); VibrationController vibrationController = new VibrationController();
vibrationController.Init(); vibrationController.Init();
container.RegisterInstance(vibrationController); container.RegisterInstance(vibrationController);
@@ -88,7 +100,7 @@ public class SubBoostrap
#if UNITY_EDITOR && !SERVICE_HOST #if UNITY_EDITOR && !SERVICE_HOST
game.PlayerDataStorage.PlayerKVGameplayLoop.BeforeBootstrap(); game.PlayerDataStorage.PlayerKVGameplayLoop.BeforeBootstrap();
#endif #endif
rootCtxGo.AddComponent<PlayFabMgr>(); if(!selection.IsKV) rootCtxGo.AddComponent<PlayFabMgr>();
// TODO Replace DataCenter with seprated data class // TODO Replace DataCenter with seprated data class
// Create(); // Create();
// ----------------------------------------- // -----------------------------------------
@@ -118,7 +130,7 @@ public class SubBoostrap
GContext.container.Register<IRTService, SignalRService>().AsSingleton(); GContext.container.Register<IRTService, SignalRService>().AsSingleton();
GContext.container.RegisterInstance(new AvatarService()); GContext.container.RegisterInstance(new AvatarService());
var settings = new DefaultSettingService(); var settings = new DefaultSettingService(RuntimeSelection?.IsKV == true);
settings.Init(tables); settings.Init(tables);
GContext.container.RegisterInstance<ISettingService>(settings); GContext.container.RegisterInstance<ISettingService>(settings);
GameCore.LocalizationMgr.Init(); GameCore.LocalizationMgr.Init();
@@ -363,6 +375,21 @@ public class InternalEditorBoost
} }
private async Task Init() private async Task Init()
{ {
var selection = GContext.container.Resolve<game.PlayerDataStorage.PlayerKVRuntimeSelection>();
if (selection == null)
{
var startupConfig = GContext.container.Resolve<IConfig>();
var backend = startupConfig?.Get<string>("SaveBackend", "playfab");
if (!string.Equals(backend, "playfab", StringComparison.Ordinal))
throw new InvalidOperationException("Editor direct scene boot only supports playfab; KV requires RegisterServices.");
// Editor direct scene boot is PF-only; KV and kv-loop must use RegisterServices.
selection = game.PlayerDataStorage.PlayerKVRuntimeSelection.Capture("playfab", null, false);
GContext.container.RegisterInstance(selection);
if (!ReferenceEquals(GContext.container.Resolve<game.PlayerDataStorage.PlayerKVRuntimeSelection>(), selection))
throw new InvalidOperationException("Editor backend selection registration failed.");
}
if (selection.IsKV)
throw new InvalidOperationException("Editor direct scene boot cannot reuse a KV selection.");
var tables = await cfg.Tables.Init(x => { Debug.Log(x); }); var tables = await cfg.Tables.Init(x => { Debug.Log(x); });
GContext.container.RegisterInstance(tables); GContext.container.RegisterInstance(tables);
GContext.container.Register<IFaceUIService, FaceUIService>().AsSingleton(); GContext.container.Register<IFaceUIService, FaceUIService>().AsSingleton();
@@ -376,6 +403,7 @@ public class InternalEditorBoost
using (var scope = GContext.container.InjectionScope()) using (var scope = GContext.container.InjectionScope())
{ {
GContext.container.Register<game.PlayerDataStorage.IPlayerSaveStore, game.PlayerDataStorage.PlayFabPlayerSaveStore>().AsSingleton();
// IAdsService 在 AfterAssetUpdate 中手动注册并调用 Init(),此处不再重复注册 // IAdsService 在 AfterAssetUpdate 中手动注册并调用 Init(),此处不再重复注册
GContext.container.Register<PlayerFishData>().AsSingleton(); GContext.container.Register<PlayerFishData>().AsSingleton();
GContext.container.Register<PlayerItemData>().AsSingleton(); GContext.container.Register<PlayerItemData>().AsSingleton();
@@ -421,7 +449,7 @@ public class InternalEditorBoost
GContext.container.Register<Activity.ActivityScopeManager>().AsSingleton(); GContext.container.Register<Activity.ActivityScopeManager>().AsSingleton();
GContext.container.Register<TimeRefreshManager>().AsSingleton(); GContext.container.Register<TimeRefreshManager>().AsSingleton();
} }
var settings = new DefaultSettingService(); var settings = new DefaultSettingService(selection.IsKV);
settings.Init(tables); settings.Init(tables);
GContext.container.RegisterInstance<ISettingService>(settings); GContext.container.RegisterInstance<ISettingService>(settings);
GameCore.LocalizationMgr.Init(); GameCore.LocalizationMgr.Init();

View File

@@ -20,7 +20,7 @@ namespace TimeManager
_sessionStartTime = ZZTimeHelper.UtcNow().UtcNowOffset(); _sessionStartTime = ZZTimeHelper.UtcNow().UtcNowOffset();
_lastCheckedHour = _sessionStartTime.Hour; _lastCheckedHour = _sessionStartTime.Hour;
CheckLastLogin(); InitialRefreshSucceeded = CheckLastLogin();
_tickService.MinuteTick _tickService.MinuteTick
.Subscribe(_ => .Subscribe(_ =>
{ {
@@ -32,6 +32,7 @@ namespace TimeManager
private readonly ITimeTickService _tickService; private readonly ITimeTickService _tickService;
private readonly IUserService _userService; private readonly IUserService _userService;
public bool InitialRefreshSucceeded { get; }
private int _lastCheckedDayOfYear = -1; private int _lastCheckedDayOfYear = -1;
private int _lastCheckedHour = -1; private int _lastCheckedHour = -1;
@@ -40,11 +41,11 @@ namespace TimeManager
#region Check登录时间 #region Check登录时间
private void CheckLastLogin() private bool CheckLastLogin()
{ {
if (IsLastLoginToday()) return; if (IsLastLoginToday()) return true;
Debug.Log("上次登录不是今天"); Debug.Log("上次登录不是今天");
OnCrossDayRefresh(); return OnCrossDayRefresh();
} }
private void CheckSessionCrossDay() private void CheckSessionCrossDay()
{ {
@@ -105,7 +106,8 @@ namespace TimeManager
/// </summary> /// </summary>
public DateTime? GetLastLoginTime() public DateTime? GetLastLoginTime()
{ {
return _userService?.LastLoginTime; var value = _userService?.LastLoginTime;
return value == DateTime.MinValue ? null : value;
} }
/// <summary> /// <summary>
@@ -121,16 +123,18 @@ namespace TimeManager
/// <summary> /// <summary>
/// 跨天刷新逻辑 /// 跨天刷新逻辑
/// </summary> /// </summary>
private void OnCrossDayRefresh() private bool OnCrossDayRefresh()
{ {
try try
{ {
// 1. 发送活动跨天刷新事件 // 1. 发送活动跨天刷新事件
GContext.Publish(new ActivityCrossDayRefreshEvent()); GContext.Publish(new ActivityCrossDayRefreshEvent());
return true;
} }
catch (Exception e) catch (Exception e)
{ {
Debug.LogError($"[TimeRefreshManager] Error during cross day refresh: {e.Message}\n{e.StackTrace}"); Debug.LogError($"[TimeRefreshManager] Error during cross day refresh: {e.Message}\n{e.StackTrace}");
return false;
} }
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -14,6 +14,8 @@ namespace Assets.Scripts.UI.AdWheel
{ {
public class AdWheelManager:IDisposable public class AdWheelManager:IDisposable
{ {
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
public AdWheelManager() public AdWheelManager()
{ {
@@ -97,7 +99,7 @@ namespace Assets.Scripts.UI.AdWheel
private AdWheelData GetCachedData() private AdWheelData GetCachedData()
{ {
var dataStr = PlayFabMgr.Instance.GetLocalData(nameof(AdWheelData)); var dataStr = saveStore.Read(nameof(AdWheelData)).ValueOrThrow();
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"jsd AdWheelData LoadData sData {dataStr}"); Debug.Log($"jsd AdWheelData LoadData sData {dataStr}");
#endif #endif
@@ -117,7 +119,7 @@ namespace Assets.Scripts.UI.AdWheel
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"jsd AdWheelData SaveData {JsonConvert.SerializeObject(Data)}"); Debug.Log($"jsd AdWheelData SaveData {JsonConvert.SerializeObject(Data)}");
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue(nameof(AdWheelData), JsonConvert.SerializeObject(Data)); saveStore.Write(nameof(AdWheelData), JsonConvert.SerializeObject(Data)).RequireAccepted();
} }
private List<IWheelReward> _rewards = new List<IWheelReward>(); private List<IWheelReward> _rewards = new List<IWheelReward>();

View File

@@ -1,4 +1,4 @@
using System; using System;
using asap.core; using asap.core;
using Assets.Scripts.UI.AdWheel; using Assets.Scripts.UI.AdWheel;
using cfg; using cfg;
@@ -19,6 +19,8 @@ public class AdvertData
} }
public class AdvertPanel : MonoBehaviour public class AdvertPanel : MonoBehaviour
{ {
game.PlayerDataStorage.IPlayerSaveStore saveStore;
Button btn_close; Button btn_close;
TMP_Text txt_energy, txt_money, txt_ticket; TMP_Text txt_energy, txt_money, txt_ticket;
ulong _curGold; ulong _curGold;
@@ -35,6 +37,7 @@ public class AdvertPanel : MonoBehaviour
private void Awake() private void Awake()
{ {
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
txt_ticket = transform.Find("top_area/resource/icon_ticket/text_num").GetComponent<TMP_Text>(); txt_ticket = transform.Find("top_area/resource/icon_ticket/text_num").GetComponent<TMP_Text>();
txt_energy = transform.Find("top_area/resource/icon_energy/text_num").GetComponent<TMP_Text>(); txt_energy = transform.Find("top_area/resource/icon_energy/text_num").GetComponent<TMP_Text>();
txt_money = transform.Find("top_area/resource/icon_money/text_num").GetComponent<TMP_Text>(); txt_money = transform.Find("top_area/resource/icon_money/text_num").GetComponent<TMP_Text>();
@@ -94,7 +97,7 @@ public class AdvertPanel : MonoBehaviour
// 判断是否满足{seconds}没有充值了 // 判断是否满足{seconds}没有充值了
private bool Condition_NoRechargeSeconds(int timeSeconds) private bool Condition_NoRechargeSeconds(int timeSeconds)
{ {
var lastPaymentStr = PlayFabMgr.Instance.GetLocalData("LastPaymentTime"); var lastPaymentStr = saveStore.Read("LastPaymentTime").ValueOrThrow();
DateTime curTime = ZZTimeHelper.UtcNow().UtcNowOffset(); DateTime curTime = ZZTimeHelper.UtcNow().UtcNowOffset();
DateTime endTime = curTime; DateTime endTime = curTime;
if (string.IsNullOrEmpty(lastPaymentStr) || !lastPaymentStr.Contains("__")) if (string.IsNullOrEmpty(lastPaymentStr) || !lastPaymentStr.Contains("__"))

Some files were not shown because too many files have changed in this diff Show More