using System; using System.Collections.Generic; using System.Linq; using System.Threading; namespace game.PlayerDataStorage { internal interface IPlayerKVLoadModule { int Id { get; } void Reset(); void Replace(PlayerKVDecodedSave decoded); } internal sealed class PlayerKVLoadModule : IPlayerKVLoadModule { readonly Action reset; readonly Action replace; public int Id { get; } internal PlayerKVLoadModule(int id,Action reset,Action replace) { Id=id; this.reset=reset ?? throw new ArgumentNullException(nameof(reset)); this.replace=replace ?? throw new ArgumentNullException(nameof(replace)); } public void Reset() => reset(); public void Replace(PlayerKVDecodedSave decoded) => replace(decoded); } internal enum PlayerKVApplyError { None, Input, Stale, Apply, SideEffect, Poisoned } internal sealed class PlayerKVGameLoadTransaction { readonly IPlayerKVLoadModule[] modules; readonly int mainThread; readonly Action discard; bool applying; internal bool IsApplying => applying; internal bool Poisoned { get; private set; } internal int AppliedModules { get; private set; } internal PlayerKVGameLoadTransaction(IEnumerable modules,int mainThread,Action discard) { this.modules=modules?.ToArray() ?? throw new ArgumentNullException(nameof(modules)); if(this.modules.Length!=31 || !this.modules.Select(m=>m?.Id ?? 0).SequenceEqual(Enumerable.Range(1,31))) throw new ArgumentException("Exactly 31 ordered modules required."); this.mainThread=mainThread; this.discard=discard ?? throw new ArgumentNullException(nameof(discard)); } // Synchronous main-thread region: no callback may yield between Reset and publication. internal PlayerKVApplyError Apply(PlayerKVDecodedSave decoded,Func current,Func clean,Action notify) { if(Poisoned) return PlayerKVApplyError.Poisoned; if(applying || Thread.CurrentThread.ManagedThreadId!=mainThread || decoded==null || current==null || clean==null || notify==null) return PlayerKVApplyError.Input; applying=true; AppliedModules=0; PlayerKVApplyError error=PlayerKVApplyError.Apply; try { if(!current()) error=PlayerKVApplyError.Stale; else if(!clean()) error=PlayerKVApplyError.SideEffect; else if(!ResetAll()) error=PlayerKVApplyError.Poisoned; else { foreach(var module in modules) { if(!current()) { error=PlayerKVApplyError.Stale; break; } if(!clean()) { error=PlayerKVApplyError.SideEffect; break; } module.Replace(decoded); AppliedModules++; } if(AppliedModules==31) { // The last module can revoke the epoch or trip a guard; never publish in that gap. if(!current()) error=PlayerKVApplyError.Stale; else if(!clean()) error=PlayerKVApplyError.SideEffect; else { notify(); error=!current() ? PlayerKVApplyError.Stale : !clean() ? PlayerKVApplyError.SideEffect : PlayerKVApplyError.None; } } } } catch { error=PlayerKVApplyError.Apply; } finally { applying=false; } if(error!=PlayerKVApplyError.None) { if(!ResetAll()) error=PlayerKVApplyError.Poisoned; try { discard(); } catch { Poisoned=true; error=PlayerKVApplyError.Poisoned; } AppliedModules=0; } return error; } internal bool Clear() { if(applying || Thread.CurrentThread.ManagedThreadId!=mainThread) throw new InvalidOperationException("Main thread required."); bool cleared=ResetAll(); try { discard(); } catch { Poisoned=true; cleared=false; } AppliedModules=0; return cleared; } bool ResetAll() { bool ok=true; foreach(var module in modules) try { module.Reset(); } catch { ok=false; } if(!ok) Poisoned=true; return ok; } } }