using System; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using game.Account; namespace game.PlayerDataStorage { // Prebuild distinct transports with one cookie jar, then transfer each owned handler once. internal sealed class PlayerKVGameLoadCapability { internal Uri Api { get; } internal string CacheRoot { get; } internal int LoginObserveMs { get; } internal int QuitObserveMs { get; } internal bool RequireExistingArchive { get; } internal Func Delay { get; } internal PlayerKVControlledLoadCapability LeaseCapability { get; private set; } private readonly Func auth; private readonly Func kv; private readonly Func lease; private readonly bool prepareOriginalComparison; readonly object gate=new object(); CookieContainer cookies; HttpMessageHandler pendingKV,pendingLease; Exception cleanupFailure; readonly System.Collections.Generic.List failedHandlers=new System.Collections.Generic.List(); bool claimed; private PlayerKVGameLoadCapability(Uri api, string cacheRoot, int loginMs, int quitMs, Func delay, Func auth, Func kv, Func lease, bool requireExistingArchive = false, bool prepareOriginalComparison = false) { // Preserve the former wrappers' null-check order, diagnostic and configuration precedence. this.auth = auth ?? throw new ArgumentNullException("leaseHandler"); this.kv = kv ?? throw new ArgumentNullException("leaseHandler"); this.lease = lease ?? throw new ArgumentNullException("leaseHandler"); if (api == null || !api.IsAbsoluteUri || (api.Scheme != "http" && api.Scheme != "https") || !string.IsNullOrEmpty(api.UserInfo) || !string.IsNullOrEmpty(api.Query) || !string.IsNullOrEmpty(api.Fragment) || string.IsNullOrEmpty(cacheRoot) || loginMs < 1000 || loginMs > 15000 || quitMs < 1000 || quitMs > 15000) { throw new ArgumentException("Controlled load configuration invalid."); } Api = new Uri(api.AbsoluteUri.TrimEnd('/') + "/"); CacheRoot = cacheRoot; LoginObserveMs = loginMs; QuitObserveMs = quitMs; Delay = delay ?? Task.Delay; RequireExistingArchive = requireExistingArchive; this.prepareOriginalComparison = prepareOriginalComparison; } internal static PlayerKVGameLoadCapability CreateForGame(Uri api, string cacheRoot, Func auth, Func kv, Func lease, bool prepareOriginalComparison = false) { if (api == null || api.AbsoluteUri != "http://s101.local:18080/") { throw new ArgumentException("Formal KV endpoint mismatch.", nameof(api)); } var capability = new PlayerKVGameLoadCapability(api, cacheRoot, 10000, 5000, null, auth, kv, lease, true, prepareOriginalComparison); capability.LeaseCapability = PlayerKVControlledLoadCapability.CreateForGame(capability.TakeLease); return capability; } #if UNITY_EDITOR internal static PlayerKVGameLoadCapability CreateForValidation(Uri api, string cacheRoot, Func auth, Func kv, Func lease, int loginMs = 10000, int quitMs = 5000, Func delay = null) { var capability = new PlayerKVGameLoadCapability(api, cacheRoot, loginMs, quitMs, delay, auth, kv, lease); capability.LeaseCapability = PlayerKVControlledLoadCapability.CreateForValidation(capability.TakeLease); return capability; } #endif internal PlayerKVLoginContext CreateContext(string id,long epoch,Func current) { lock(gate) { if(claimed) throw new InvalidOperationException("Controlled capability already used."); claimed=true; } return new PlayerKVLoginContext(id,CacheRoot,epoch,current,Api, ()=>new S101AccountSession(Api,CreateAuthentication),()=>new PlayerKVGameSessionBridge(TakeKV, prepareOriginalComparison)); } private HttpMessageHandler CreateAuthentication(CookieContainer jar) { lock (gate) { if (cookies != null) { throw new InvalidOperationException("Repeated authentication factory."); } cookies = jar; HttpMessageHandler authentication = null; try { // Validate all three returned handler graphs before the first authentication send. authentication = PlayerKVControlledLoadCapability.CreateHandler(auth, jar, LeaseCapability.IsFormal); pendingKV = PlayerKVControlledLoadCapability.CreateHandler(kv, jar, LeaseCapability.IsFormal); pendingLease = PlayerKVControlledLoadCapability.CreateHandler(lease, jar, LeaseCapability.IsFormal); if (ShareHandlerGraph(authentication, pendingKV, pendingLease)) { throw new InvalidOperationException("Distinct transport ownership required."); } return authentication; } catch { try { authentication?.Dispose(); } catch (Exception ex) { cleanupFailure = ex; failedHandlers.Add(authentication); } try { CloseUnused(); } catch { } throw; } } } static bool ShareHandlerGraph(params HttpMessageHandler[] roots) { var seen=new System.Collections.Generic.List(); 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) { lock(gate) { if(!ReferenceEquals(jar,cookies) || pendingKV==null) throw new InvalidOperationException("KV transport ownership invalid."); var result=pendingKV; pendingKV=null; return result; } } HttpMessageHandler TakeLease(CookieContainer jar) { lock(gate) { if(!ReferenceEquals(jar,cookies) || pendingLease==null) throw new InvalidOperationException("Lease transport ownership invalid."); var result=pendingLease; pendingLease=null; return result; } } internal void CloseUnused() { lock(gate) { // Attempt every owned cleanup once; retain a failure even if a later Dispose succeeds. var ownedLease=pendingLease; pendingLease=null; var ownedKV=pendingKV; pendingKV=null; try { ownedLease?.Dispose(); } catch(Exception ex) { cleanupFailure=cleanupFailure ?? ex; failedHandlers.Add(ownedLease); } try { ownedKV?.Dispose(); } catch(Exception ex) { cleanupFailure=cleanupFailure ?? ex; failedHandlers.Add(ownedKV); } if(cleanupFailure!=null) throw new InvalidOperationException("Controlled transport cleanup failed.",cleanupFailure); } } } }