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.KV; public interface ICustomServerMgr { Task GetStringAsync(string url); Task CustomServerPost(string url, string bodyString, string contentType = "application/json"); Task AccountRequest(string url, object data); Task EventRequest(string url, object data); Task ClubRequest(string url, object data); Task BombRequest(string url, object data); Task DuelRequest(string url, object data); Task CommonDataRequest(string url, object data); Task FriendRequest(string url, object data); Task EventPartnerRequest(string url, object data = null); Task EventPartnerRequest(string url, object data); CookieContainer Cookie { get; } Uri ApiBaseUri { get; } int SessionVersion { get; } event Action SessionChanged; void UseAccountSession(GameServerAccountSession session); void ClearAccountSession(); } public class CustomServerMgr : ICustomServerMgr { public const string DefaultApiUrl = "http://s101.local:18080/"; IConfig config { get; set; } //网络客户端 HttpClient client; private CookieContainer _cookie; private readonly Func handlerFactory; private readonly SynchronizationContext ownerContext; public int SessionVersion { get; private set; } public event Action SessionChanged; private KvAccountLease kvLease; private bool kvOnly; private bool kvBusinessReady; private bool kvBusinessPublished; private int pfRequests; internal event Action KVUnauthorized; internal bool IsFreshForKVLoad => pfRequests==0 && kvLease==null; internal void BindKVForGame(KvAccountLease lease) { if (lease == null || !lease.IsCurrent || lease.ApiBaseUri.AbsoluteUri != ApiBaseUri.AbsoluteUri || !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.IsCurrent || kvLease.Epoch != epoch || kvLease.ApiBaseUri.AbsoluteUri != ApiBaseUri.AbsoluteUri) { 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() { kvBusinessReady = false; SessionVersion++; var previous = kvLease; kvLease = null; _cookie = null; var closing = previous?.CloseAsync() ?? Task.CompletedTask; SessionChanged?.Invoke(); return closing; } public CookieContainer Cookie => kvOnly ? (kvBusinessReady && kvLease != null ? 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 handlerFactory) { ownerContext=SynchronizationContext.Current; this.handlerFactory = handlerFactory ?? (cookies => new HttpClientHandler { CookieContainer = cookies, UseCookies = true, AllowAutoRedirect = false }); this.config = config; var url = config.Get(GConstant.K_Event_API_URL, DefaultApiUrl); if (string.IsNullOrWhiteSpace(url)) { url = DefaultApiUrl; } ApiBaseUri = new Uri(url.TrimEnd('/') + "/"); ClearAccountSession(); } public void UseAccountSession(GameServerAccountSession 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) { KvShutdownRegistry.Process.Register(ClearKVForGame()); 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 CustomServerPost(string url, string bodyString, string contentType = "application/json") => SendAsync(HttpMethod.Post, url, bodyString, contentType); public Task 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 SendAsync(HttpMethod method, string url, string body, string contentType) { if(kvOnly) { if(ownerContext!=null && !ReferenceEquals(SynchronizationContext.Current,ownerContext)) { var completion=new TaskCompletionSource(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.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 AccountRequest(string url, object data) { return await CustomServerPost($"account/{url}", Newtonsoft.Json.JsonConvert.SerializeObject(data)); } public async Task EventRequest(string url, object data) { return await CustomServerPost($"event/{url}", Newtonsoft.Json.JsonConvert.SerializeObject(data)); } public async Task ClubRequest(string url, object data) { return await CustomServerPost($"club/{url}", Newtonsoft.Json.JsonConvert.SerializeObject(data)); } public async Task BombRequest(string url, object data) { return await CustomServerPost($"bomb/{url}", Newtonsoft.Json.JsonConvert.SerializeObject(data)); } public async Task CommonDataRequest(string url, object data) { return await CustomServerPost($"commondata/{url}", Newtonsoft.Json.JsonConvert.SerializeObject(data)); } public async Task DuelRequest(string url, object data) { return await CustomServerPost($"duel/{url}", Newtonsoft.Json.JsonConvert.SerializeObject(data)); } public async Task FriendRequest(string url, object data) { return await CustomServerPost($"friend/{url}", Newtonsoft.Json.JsonConvert.SerializeObject(data)); } // 伙伴建造请求 public async Task EventPartnerRequest(string url, object data) { // throw new NotImplementedException(); var body = Newtonsoft.Json.JsonConvert.SerializeObject(data); return await CustomServerPost($"eventpartnergather/{url}", body); } public async Task EventPartnerRequest(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(responseString); } catch (Exception e) { Debug.LogError( $"[EventPartner] Response {responseString} failed to be deserialized: {e.Message}"); return default(T); } } }