Files
ft/Client/Assets/Scripts/Services/PlayerData/PlayerKVGameLoadTransaction.cs

101 lines
4.5 KiB
C#

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<PlayerKVDecodedSave> replace;
public int Id { get; }
internal PlayerKVLoadModule(int id,Action reset,Action<PlayerKVDecodedSave> 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<IPlayerKVLoadModule> 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<bool> current,Func<bool> 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;
}
}
}