保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
124 lines
7.5 KiB
Plaintext
124 lines
7.5 KiB
Plaintext
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using asap.core;
|
|
using game.PlayerDataStorage;
|
|
using UnityEngine;
|
|
|
|
namespace game
|
|
{
|
|
public partial class PlayFabUserService
|
|
{
|
|
readonly bool kvControlledInstance;
|
|
PlayerKVGameLoadCoordinator kvCoordinator;
|
|
Action<IDictionary<string,string>> kvReceiver;
|
|
int kvReceiverUsers;
|
|
internal PlayerKVGameLoadError KVLastError { get; private set; }
|
|
internal string CaptureSaveBackend() => config==null ? "playfab" : config.Get<string>("SaveBackend","playfab");
|
|
internal bool RejectSaveBackend(string backend)
|
|
{
|
|
#if UNITY_EDITOR && !SERVICE_HOST
|
|
if(PlayerKVGameplayLoop.Enabled && backend=="kv-loop" && !kvControlledInstance)
|
|
{ KVLastError=PlayerKVGameLoadError.None; return false; }
|
|
#endif
|
|
KVLastError=backend=="kv" ? PlayerKVGameLoadError.BackendNotEnabled : backend!="playfab" ? PlayerKVGameLoadError.BackendUnsupported :
|
|
kvControlledInstance ? PlayerKVGameLoadError.Input : PlayerKVGameLoadError.None;
|
|
return KVLastError!=PlayerKVGameLoadError.None;
|
|
}
|
|
internal void ConfigureKVForValidation(PlayerKVGameLoadTransaction transaction,PlayFabMgr pf,CustomServerMgr server,Func<bool> unstarted)
|
|
{
|
|
if(!kvControlledInstance || kvCoordinator!=null || pf==null || !pf.IsUnstartedForKVLoad || PlayFabMgr.HasLiveInstanceForKVLoad ||
|
|
server==null || !server.IsFreshForKVLoad || unstarted==null || !unstarted()) throw new InvalidOperationException("Fresh controlled services required.");
|
|
kvCoordinator=new PlayerKVGameLoadCoordinator(transaction,()=>pf.IsUnstartedForKVLoad && InternalId==0 && unstarted(),
|
|
NotifyKVLoaded,()=>KVIntents?.Count ?? 0,server.BindKVForValidation,server.ClearKVForValidation);
|
|
server.KVUnauthorized+=version=> { if(version==server.SessionVersion) { InternalId=0; _=kvCoordinator.Revoke(); } };
|
|
}
|
|
internal virtual async Task<PlayerKVGameLoadResult> LoadKVForValidationAsync(PlayerKVGameLoadCapability capability)
|
|
{
|
|
if(!kvControlledInstance || kvCoordinator==null || capability==null)
|
|
return new PlayerKVGameLoadResult(PlayerKVGameLoadError.Input,0);
|
|
try
|
|
{
|
|
int loginMs=config==null ? 10000 : PlayerKVSaveSchema.Int32(config.Get<string>("KVLoginCloseObserveMs","10000"));
|
|
int quitMs=config==null ? 5000 : PlayerKVSaveSchema.Int32(config.Get<string>("KVQuitCloseObserveMs","5000"));
|
|
if(loginMs!=capability.LoginObserveMs || quitMs!=capability.QuitObserveMs || loginMs<1000 || loginMs>15000 || quitMs<1000 || quitMs>15000)
|
|
return new PlayerKVGameLoadResult(PlayerKVGameLoadError.Input,0);
|
|
}
|
|
catch { return new PlayerKVGameLoadResult(PlayerKVGameLoadError.Input,0); }
|
|
var requestedId=CustomId; var generation=logoutGeneration;
|
|
var result=await kvCoordinator.LoadAsync(requestedId,capability,LoginLifetime,()=>requestedId==CustomId && generation==logoutGeneration);
|
|
if(generation!=logoutGeneration || requestedId!=CustomId)
|
|
{
|
|
_=kvCoordinator.Revoke();
|
|
if(result.State==PlayerKVGameLoadState.LoadedForValidation) result=new PlayerKVGameLoadResult(PlayerKVGameLoadError.Stale,result.Epoch);
|
|
}
|
|
KVLastError=result.Error; return result;
|
|
}
|
|
internal Task CloseKVForValidationAsync() => kvCoordinator?.Revoke() ?? Task.CompletedTask;
|
|
internal Task<PlayerKVShutdownStatus> ObserveKVQuitAsync() => kvCoordinator?.ObserveQuitAsync() ?? Task.FromResult(PlayerKVShutdownStatus.Ready);
|
|
internal bool KVLoadedForValidation => kvCoordinator?.LoadedForValidation ?? false;
|
|
internal int KVReceiverCountForValidation => kvReceiverUsers;
|
|
internal void EnterKVReceiver(Action<IDictionary<string,string>> receiver)
|
|
{
|
|
if(!kvControlledInstance || receiver==null || (kvReceiver!=null && kvReceiver!=receiver)) throw new InvalidOperationException("Controlled receiver required.");
|
|
if(kvReceiverUsers++==0) { kvReceiver=receiver; OnInitData+=receiver; }
|
|
}
|
|
internal void ExitKVReceiver()
|
|
{ if(--kvReceiverUsers==0) { OnInitData-=kvReceiver; kvReceiver=null; } }
|
|
void NotifyKVLoaded()
|
|
{
|
|
var listeners=OnInitData?.GetInvocationList();
|
|
if(listeners!=null && (listeners.Length!=1 || kvReceiver==null || !listeners[0].Equals(kvReceiver)))
|
|
throw new InvalidOperationException("Unregistered controlled receiver.");
|
|
OnInitData?.Invoke(KVLoadingCopy.ToDictionary(p=>p.Key,p=>p.Value,StringComparer.Ordinal));
|
|
}
|
|
internal int KVTapJoyBalance { get; private set; }
|
|
internal PlayerKVInitializationIntents KVIntents { get; private set; }
|
|
internal IReadOnlyDictionary<string,string> KVLoadingCopy { get; private set; }
|
|
internal int MMTestForKVValidation => MMTestValue;
|
|
internal bool IsControlledKVInstance => kvControlledInstance;
|
|
|
|
internal virtual void ResetForKVLoad()
|
|
{
|
|
bool admitted=kvControlledInstance, sessionAllowed=apiSession==null;
|
|
#if UNITY_EDITOR && !SERVICE_HOST
|
|
admitted=admitted || KVLoopLoading;
|
|
sessionAllowed=sessionAllowed || KVLoopLoading;
|
|
#endif
|
|
if(!admitted || InternalId!=0 || !sessionAllowed)
|
|
throw new InvalidOperationException("Unstarted controlled identity required.");
|
|
KVIntents?.Discard(); KVIntents=null; KVLoadingCopy=null;
|
|
MMTestValue=0; EAccoutUrl=new Dictionary<string,string>(StringComparer.Ordinal); KVTapJoyBalance=0;
|
|
}
|
|
internal void ReplaceFromKVDecoded(PlayerKVLoginState state,long epoch,IReadOnlyDictionary<string,string> raw)
|
|
{
|
|
bool admitted=kvControlledInstance;
|
|
#if UNITY_EDITOR && !SERVICE_HOST
|
|
admitted=admitted || KVLoopLoading;
|
|
#endif
|
|
if(!admitted || state==null || raw==null) throw new InvalidOperationException("Controlled login state required.");
|
|
var copy=raw.ToDictionary(p=>p.Key,p=>p.Value,StringComparer.Ordinal);
|
|
var intents=new PlayerKVInitializationIntents(epoch,state.Channel);
|
|
foreach(var pair in state.Normalizations) { copy[pair.Key]=pair.Value; intents.Add(epoch,pair.Key,pair.Value); }
|
|
KVIntents?.Discard(); KVIntents=intents;
|
|
KVLoadingCopy=new System.Collections.ObjectModel.ReadOnlyDictionary<string,string>(copy);
|
|
MMTestValue=state.MMTest; KVTapJoyBalance=state.TapJoyBalance;
|
|
EAccoutUrl=state.AccountUrls.ToDictionary(p=>p.Key,p=>p.Value,StringComparer.Ordinal);
|
|
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) { }
|
|
}
|
|
}
|