Files
ft/Client/Assets/Scripts/Core/CustomServerMgr.cs
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

289 lines
12 KiB
C#

using asap.core;
using LitJson;
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using game.Account;
using game.PlayerDataStorage;
public interface ICustomServerMgr
{
Task<string> GetStringAsync(string url);
Task<string> CustomServerPost(string url, string bodyString, string contentType = "application/json");
Task<string> AccountRequest(string url, object data);
Task<string> EventRequest(string url, object data);
Task<string> ClubRequest(string url, object data);
Task<string> BombRequest(string url, object data);
Task<string> DuelRequest(string url, object data);
Task<string> CommonDataRequest(string url, object data);
Task<string> FriendRequest(string url, object data);
Task<T> EventPartnerRequest<T>(string url, object data = null);
Task<string> EventPartnerRequest(string url, object data);
CookieContainer Cookie { get; }
Uri ApiBaseUri { get; }
int SessionVersion { get; }
event Action SessionChanged;
void UseAccountSession(S101AccountSession session);
void ClearAccountSession();
}
public class CustomServerMgr : ICustomServerMgr
{
IConfig config { get; set; }
//网络客户端
HttpClient client;
private CookieContainer _cookie;
private readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
private readonly SynchronizationContext ownerContext;
public int SessionVersion { get; private set; }
public event Action SessionChanged;
PlayerKVAccountLease kvLease;
bool kvOnly;
bool kvBusinessReady;
bool kvBusinessPublished;
int pfRequests;
internal event Action<int> KVUnauthorized;
internal bool IsFreshForKVLoad => pfRequests==0 && kvLease==null;
internal void BindKVForValidation(PlayerKVAccountLease lease)
{
if(lease==null || lease.IsFormal || !lease.IsCurrent || lease.ApiBaseUri!=ApiBaseUri || !IsFreshForKVLoad)
throw new InvalidOperationException("Fresh matching 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 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()
{
kvBusinessReady=false; SessionVersion++; var previous=kvLease; kvLease=null; _cookie=null;
var closing=previous?.CloseAsync() ?? Task.CompletedTask;
SessionChanged?.Invoke(); return closing;
}
internal async Task<PlayerKVAccountResponse> SendKVForValidationAsync(string route)
{
var lease=kvLease; int version=SessionVersion;
if(!kvOnly || lease==null) return new PlayerKVAccountResponse(false);
var response=await lease.SendAsync(HttpMethod.Get,route);
if(version!=SessionVersion || !ReferenceEquals(lease,kvLease) || !response.IsCurrent) return new PlayerKVAccountResponse(false);
if(response.StatusCode==401 || response.StatusCode==403)
{
KVUnauthorized?.Invoke(version);
return new PlayerKVAccountResponse(false,response.StatusCode);
}
return response;
}
public CookieContainer Cookie => kvOnly
? (kvBusinessReady && kvLease?.IsFormal==true ? kvLease.FormalCookies : null) ?? throw new NullReferenceException()
: _cookie ?? throw new NullReferenceException();
public Uri ApiBaseUri { get; }
public CustomServerMgr(IConfig config) : this(config, null) { }
public CustomServerMgr(IConfig config, Func<CookieContainer, HttpMessageHandler> handlerFactory)
{
ownerContext=SynchronizationContext.Current;
this.handlerFactory = handlerFactory ?? (cookies => new HttpClientHandler
{
CookieContainer = cookies, UseCookies = true, AllowAutoRedirect = false
});
this.config = config;
var url = config.Get<string>(GConstant.K_Event_API_URL, S101AccountSession.DefaultApiUrl);
if (string.IsNullOrWhiteSpace(url)) url = S101AccountSession.DefaultApiUrl;
ApiBaseUri = new Uri(url.TrimEnd('/') + "/");
ClearAccountSession();
}
public void UseAccountSession(S101AccountSession session)
{
if(kvOnly) throw new InvalidOperationException("Controlled container cannot switch to PlayFab.");
if (session == null || !session.IsAuthenticated || session.ApiBaseUri != ApiBaseUri)
throw new ArgumentException("An authenticated session for this API is required.", nameof(session));
ReplaceTransport(session.Cookies);
}
public void ClearAccountSession()
{
if(kvOnly) { PlayerKVShutdownRegistry.Process.Register(ClearKVForValidation()); return; }
ReplaceTransport(new CookieContainer());
}
private void ReplaceTransport(CookieContainer cookies)
{
SessionVersion++;
var previous = client;
_cookie = cookies;
client = new HttpClient(handlerFactory(cookies)) { BaseAddress = ApiBaseUri, Timeout = TimeSpan.FromSeconds(15) };
previous?.Dispose();
SessionChanged?.Invoke();
}
public Task<string> CustomServerPost(string url, string bodyString,
string contentType = "application/json") => SendAsync(HttpMethod.Post, url, bodyString, contentType);
public Task<string> GetStringAsync(string url) => SendAsync(HttpMethod.Get, url, null, null);
// 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)
{
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++;
var transport = client;
var version = SessionVersion;
try
{
using (var request = new HttpRequestMessage(method, url))
{
if (body != null) request.Content = new StringContent(body, Encoding.UTF8, contentType);
using (var response = await transport.SendAsync(request))
{
var result = await response.Content.ReadAsStringAsync();
if (version != SessionVersion) return null;
if (response.StatusCode == HttpStatusCode.OK) return result;
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
GContext.Publish(new game.EvtAPISvrUnauthorized { SessionVersion = version });
else
Debug.LogError($"API request failed: HTTP {(int)response.StatusCode}");
return null;
}
}
}
catch (Exception e)
{
if (version == SessionVersion) Debug.LogError($"API request failed: {e.GetType().Name}");
return null;
}
}
public async Task<string> AccountRequest(string url, object data)
{
return await CustomServerPost($"account/{url}",
Newtonsoft.Json.JsonConvert.SerializeObject(data));
}
public async Task<string> EventRequest(string url, object data)
{
return await CustomServerPost($"event/{url}",
Newtonsoft.Json.JsonConvert.SerializeObject(data));
}
public async Task<string> ClubRequest(string url, object data)
{
return await CustomServerPost($"club/{url}",
Newtonsoft.Json.JsonConvert.SerializeObject(data));
}
public async Task<string> BombRequest(string url, object data)
{
return await CustomServerPost($"bomb/{url}",
Newtonsoft.Json.JsonConvert.SerializeObject(data));
}
public async Task<string> CommonDataRequest(string url, object data)
{
return await CustomServerPost($"commondata/{url}",
Newtonsoft.Json.JsonConvert.SerializeObject(data));
}
public async Task<string> DuelRequest(string url, object data)
{
return await CustomServerPost($"duel/{url}",
Newtonsoft.Json.JsonConvert.SerializeObject(data));
}
public async Task<string> FriendRequest(string url, object data)
{
return await CustomServerPost($"friend/{url}",
Newtonsoft.Json.JsonConvert.SerializeObject(data));
}
// 伙伴建造请求
public async Task<string> EventPartnerRequest(string url, object data)
{
// throw new NotImplementedException();
var body = Newtonsoft.Json.JsonConvert.SerializeObject(data);
return await CustomServerPost($"eventpartnergather/{url}", body);
}
public async Task<T> EventPartnerRequest<T>(string url, object data = null)
{
var body = Newtonsoft.Json.JsonConvert.SerializeObject(data);
var responseString = await CustomServerPost($"eventpartner/{url}", body);
try
{
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(responseString);
}
catch (Exception e)
{
Debug.LogError(
$"<color=red>[EventPartner] Response {responseString} failed to be deserialized: {e.Message}</color>");
return default(T);
}
}
}