using System; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using game.Account; namespace game.PlayerDataStorage { // The lease-only capability remains source-compatible with the independent 4B1 regression host. // This separate capability supplies every dependency required by a complete controlled load. internal sealed class PlayerKVGameLoadCapability { internal PlayerKVIdentitySnapshot Identity { get; } internal PlayerKVSchemaSnapshot Snapshot { get; } 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; } readonly PlayerKVControlledLoadCapability auth,kv,lease; 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(PlayerKVIdentitySnapshot identity,PlayerKVSchemaSnapshot snapshot,Uri api,string cacheRoot, int loginMs,int quitMs,Func delay, PlayerKVControlledLoadCapability auth,PlayerKVControlledLoadCapability kv,PlayerKVControlledLoadCapability lease, bool requireExistingArchive=false) { Identity=identity ?? throw new ArgumentNullException(nameof(identity)); Snapshot=snapshot ?? throw new ArgumentNullException(nameof(snapshot)); 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; this.auth=auth; this.kv=kv; this.lease=lease; RequireExistingArchive=requireExistingArchive; } internal static PlayerKVGameLoadCapability CreateForGame(PlayerKVIdentitySnapshot identity,PlayerKVSchemaSnapshot snapshot, Uri api,string cacheRoot,Func auth,Func kv, Func lease) { if(api==null || api.AbsoluteUri!="http://s101.local:18080/") throw new ArgumentException("Formal KV endpoint mismatch.",nameof(api)); var capability=new PlayerKVGameLoadCapability(identity,snapshot,api,cacheRoot,10000,5000,null, PlayerKVControlledLoadCapability.CreateForGame(auth),PlayerKVControlledLoadCapability.CreateForGame(kv), PlayerKVControlledLoadCapability.CreateForGame(lease),true); capability.LeaseCapability=PlayerKVControlledLoadCapability.CreateForGame(capability.TakeLease); return capability; } #if UNITY_EDITOR internal static PlayerKVGameLoadCapability CreateForValidation(PlayerKVIdentitySnapshot identity,PlayerKVSchemaSnapshot snapshot, Uri api,string cacheRoot,Func auth,Func kv, Func lease,int loginMs=10000,int quitMs=5000,Func delay=null) { var capability=new PlayerKVGameLoadCapability(identity,snapshot,api,cacheRoot,loginMs,quitMs,delay, PlayerKVControlledLoadCapability.CreateForValidation(auth),PlayerKVControlledLoadCapability.CreateForValidation(kv), PlayerKVControlledLoadCapability.CreateForValidation(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)); } 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=auth.CreateHandler(jar); pendingKV=kv.CreateHandler(jar); pendingLease=lease.CreateHandler(jar); 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); } } } }