101 lines
4.5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|