Files
ft/CodexOutput/upserver/B3BatchMigration/M2/C2d修改前快照/069-UserService.KVLoad.cs.txt
Liubing\LB 6813da149a feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
2026-09-30 19:19:05 +08:00

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) { }
}
}