#if PLUGIN_SESSION_RECOVERY
namespace XFEEDToolBox.Plugin;
internal static unsafe partial class SessionRecoveryRuntime
{
private struct TradePauseAuthorization
{internal string? owner,run;internal ulong epoch,expires,entity;}
private struct TradePauseResumeWork
{internal string? id,owner,run;internal ulong epoch,expires,sequence;internal bool pending,applied,failed;}
private static TradePauseAuthorization tradePauseAuthorization;
private static TradePauseResumeWork tradePauseWork;
private static readonly object tradePauseDispatchLock=new();
private struct TradePauseIdentity
{internal ulong entity,tick,utc,sequence;internal double hull,shield;internal bool verified;}
private static TradePauseIdentity tradePauseIdentity;
private static void ObserveTradePauseIdentity(bool force=false)
{
if(!enabled||GetCurrentThreadId()!=expectedThread)return;
Root current;TradePauseIdentity previous;lock(gate){current=root;previous=tradePauseIdentity;}
var now=GetTickCount64();if(!force&&previous.tick!=0&&now>=previous.tick&&now-previous.tick<50)return;
var valid=current.valid&&Fresh(current.tick,now)&¤t.state==current.next&¤t.state is "InGame" or "Pause";
ulong entity=0;double hull=double.NaN,shield=double.NaN;
valid=valid&&Navigation.TryReadTradePauseShipHealth(out entity,out hull,out shield);
var observed=new TradePauseIdentity{entity=entity,hull=hull,shield=shield,tick=now,utc=Utc(),sequence=previous.sequence+1,verified=valid};
lock(gate)tradePauseIdentity=observed;
}
private static bool TradePauseIdentityReady(ulong entity,ulong now)
{
lock(gate)return tradePauseIdentity.verified&&tradePauseIdentity.entity==entity&&entity!=0&&Fresh(tradePauseIdentity.tick,now)
&&double.IsFinite(tradePauseIdentity.hull)&&tradePauseIdentity.hull>=.5;
}
private static JsonObject TradePauseIdentityDescription()
{
lock(gate)
{
var value=tradePauseIdentity;
return Obj(("verified",value.verified&&Fresh(value.tick,GetTickCount64())),("entityId",value.entity.ToString()),("sequence",value.sequence),
("observedAtUnixMilliseconds",value.utc),("hullFraction",double.IsFinite(value.hull)?(double?)value.hull:null),
("shieldFraction",double.IsFinite(value.shield)?(double?)value.shield:null),("source","current-main-ship-on-original-menu-thread"),("physicsTelemetryRequired",false));
}
}
private static bool TradePauseKey(string value)=>value.Length==32&&value.All(c=>c is >= '0' and <= '9' or >= 'a' and <= 'f');
private static bool TradePauseMenuReady(Root r,Menu m,ulong now)
=>enabled&&r.valid&&r.thread==expectedThread&&m.thread==expectedThread&&r.flow==m.flow
&&r.state=="Pause"&&r.next=="Pause"&&m.categoryName=="Pause"&&m.kind==1&&m.ready&&Fresh(r.tick,now)&&Fresh(m.tick,now)
&&m.names.Count(name=>name=="InGame")==1&&m.names.Where((name,index)=>name=="InGame"&&m.allowed[index]).Count()==1;
private static bool TradePauseAuthorized(TradePauseAuthorization authorization,TradePauseResumeWork wanted,ulong now)
=>authorization.expires>now&&authorization.entity!=0&&authorization.owner==wanted.owner&&authorization.run==wanted.run
&&authorization.epoch==wanted.epoch&&wanted.pending&&wanted.expires>now;
private static JsonObject TradePauseDescription()
{
lock(gate)
{
var now=GetTickCount64();var authorization=tradePauseAuthorization;var wanted=tradePauseWork;
return Obj(("schema",1),("scopeActive",authorization.expires>now),("owner",authorization.owner),("runId",authorization.run),
("entityId",authorization.entity.ToString()),("scopeExpiresInMilliseconds",authorization.expires>now?authorization.expires-now:0),
("requestId",wanted.id),("pending",wanted.pending&&wanted.expires>now&&authorization.expires>now),
("applied",wanted.applied),("failed",wanted.failed||wanted.pending&&(wanted.expires<=now||authorization.expires<=now)),
("action","InGame"),("onlyStablePauseCategory",true));
}
}
private static void CancelTradePauseScope(string? owner=null,string? run=null)
{
lock(tradePauseDispatchLock)lock(gate)
{
if(owner is not null&&(tradePauseAuthorization.owner!=owner||tradePauseAuthorization.run!=run))return;
tradePauseAuthorization.expires=0;++tradePauseAuthorization.epoch;tradePauseWork.pending=false;
}
}
private static JsonNode TradePauseRequest(string op,JsonElement request)
{
var owner=request.Str("owner");var run=request.Str("runId");
if(!TradePauseKey(owner)||!TradePauseKey(run))throw new InvalidOperationException("An explicit running trade scope is required");
if(op=="trade.pause.end"){CancelTradePauseScope(owner,run);return Description();}
lock(tradePauseDispatchLock)
{
var now=GetTickCount64();Root observed;lock(gate)observed=root;
if(!enabled||!observed.valid||!Fresh(observed.tick,now)||observed.thread!=expectedThread||observed.state!=observed.next
||observed.state is not("InGame" or "Pause"))throw new InvalidOperationException("A stable current game or Pause context is required");
if(op=="trade.pause.renew")
{
var ttl=request.Int("ttlMilliseconds");var entity=request.U64("entityId");
if(ttl is <1 or >750||entity==0||!TradePauseIdentityReady(entity,now))throw new InvalidOperationException("The running trade main ship is not verified");
lock(gate)
{
if(work.pending&&work.expires>now)throw new InvalidOperationException("An explicit session workflow owns the original menu");
var before=tradePauseAuthorization;
if(before.expires>now&&(before.owner!=owner||before.run!=run))throw new InvalidOperationException("Another running trade scope owns pause recovery");
if(before.expires>now&&before.entity!=entity&&observed.state!="InGame")throw new InvalidOperationException("A changed ship cannot be admitted from a Pause menu");
if(before.owner!=owner||before.run!=run||before.entity!=entity||before.expires<=now)
{++before.epoch;tradePauseWork.pending=false;}
before.owner=owner;before.run=run;before.entity=entity;before.expires=now+(ulong)ttl;tradePauseAuthorization=before;
}
return Description();
}
if(op!="trade.pause.resume")throw new InvalidOperationException("Unknown running-trade pause operation");
var id=request.Str("requestId");var expected=request.U64("menuSequence");var deadline=request.Int("ttlMilliseconds");
if(!TradePauseKey(id)||expected==0||deadline is <1 or >150)throw new InvalidOperationException("A bounded original Pause resume request is required");
lock(gate)
{
if(tradePauseWork.id==id)return Description();
var authorization=tradePauseAuthorization;
if(authorization.owner!=owner||authorization.run!=run||authorization.expires<=now||authorization.entity==0
||work.pending&&work.expires>now||tradePauseWork.pending&&tradePauseWork.expires>now||!TradePauseMenuReady(root,menu,now)
||expected>menu.sequence||menu.sequence-expected>32)throw new InvalidOperationException("The running trade Pause menu changed before resume");
tradePauseWork=new(){id=id,owner=owner,run=run,epoch=authorization.epoch,expires=Math.Min(now+(ulong)deadline,menu.tick+200),sequence=expected,pending=true};
}
return Description();
}
}
private static void PollTradePauseResume(nuint activity,Menu observed,Root r)
{
lock(tradePauseDispatchLock)
{
var now=GetTickCount64();TradePauseAuthorization authorization;TradePauseResumeWork wanted;Work other;
lock(gate){authorization=tradePauseAuthorization;wanted=tradePauseWork;other=work;}
if(!TradePauseAuthorized(authorization,wanted,now))return;
if(other.pending&&other.expires>now||!TradePauseMenuReady(r,observed,now)||observed.sequence<wanted.sequence
||GetCurrentThreadId()!=expectedThread)return;
ObserveTradePauseIdentity(force:true);
if(!TradePauseIdentityReady(authorization.entity,GetTickCount64()))return;
var fresh=CaptureMenu(activity);Root current;lock(gate)current=root;
if(!TradePauseMenuReady(current,fresh,GetTickCount64())||current.flow!=r.flow||current.csm!=r.csm
||Name(Pointer(Pointer(current.csm+0x100)))!="Pause"||Name(Pointer(Pointer(current.csm+0x108)))!="Pause")return;
var index=Array.FindIndex(fresh.names,name=>name=="InGame");if(index<0||!fresh.allowed[index])return;
lock(gate)
{
if(tradePauseWork.id!=wanted.id||!TradePauseAuthorized(tradePauseAuthorization,tradePauseWork,GetTickCount64()))return;
tradePauseWork.pending=false;
}
var applied=false;
try{Call(image+PauseSelect,(ulong)(activity+0x80),0,(ulong)index);applied=true;}
finally{lock(gate)if(tradePauseWork.id==wanted.id){tradePauseWork.applied=applied;tradePauseWork.failed=!applied;}}
}
}
private static bool TradePauseGuardSelfTest()
{
var previousEnabled=enabled;var previousThread=expectedThread;var previousIdentity=tradePauseIdentity;
try
{
enabled=true;expectedThread=7;
var r=new Root{valid=true,state="Pause",next="Pause",flow=0x10000,tick=1000,thread=7};
var m=new Menu{ready=true,flow=r.flow,tick=1000,sequence=9,thread=7,categoryName="Pause",kind=1,names=["InGame","ExitOptions"],allowed=[true,true]};
if(!TradePauseMenuReady(r,m,1100))return false;
foreach(var state in new[]{"InGame","MainMenu","ExitOptions","Rebirth","NetworkError","Pause Options"})
{var wrong=r;wrong.state=wrong.next=state;if(TradePauseMenuReady(wrong,m,1100))return false;}
var transitional=r;transitional.next="InGame";if(TradePauseMenuReady(transitional,m,1100))return false;
m.allowed[0]=false;if(TradePauseMenuReady(r,m,1100))return false;m.allowed[0]=true;
m.names=["InGame","InGame"];if(TradePauseMenuReady(r,m,1100))return false;m.names=["InGame","ExitOptions"];
m.kind=2;if(TradePauseMenuReady(r,m,1100))return false;m.kind=1;
if(TradePauseMenuReady(r,m,1201))return false;
// A stopped physics producer is immaterial: this separate current
// menu-thread identity must still be fresh, healthy and identical.
tradePauseIdentity=new(){verified=true,entity=42,tick=1000,hull=1};
if(!TradePauseIdentityReady(42,1100)||TradePauseIdentityReady(43,1100)||TradePauseIdentityReady(42,1201))return false;
tradePauseIdentity.hull=.49;if(TradePauseIdentityReady(42,1100))return false;
tradePauseIdentity.hull=1;tradePauseIdentity.verified=false;if(TradePauseIdentityReady(42,1100))return false;
var authorization=new TradePauseAuthorization{owner="one",run="run",entity=42,epoch=3,expires=1200};
var wanted=new TradePauseResumeWork{owner="one",run="run",epoch=3,expires=1150,pending=true};
if(!TradePauseAuthorized(authorization,wanted,1100)||TradePauseAuthorized(authorization,wanted,1150))return false;
authorization.epoch++;if(TradePauseAuthorized(authorization,wanted,1100))return false;
authorization.epoch=3;authorization.expires=0;return !TradePauseAuthorized(authorization,wanted,1100);
}
finally{enabled=previousEnabled;expectedThread=previousThread;tradePauseIdentity=previousIdentity;}
}
}
#endif
#if PLUGIN_SESSION_RECOVERY
namespace XFEEDToolBox.Plugin;
internal static unsafe partial class SessionRecoveryRuntime
{
private struct TradePauseAuthorization
{internal string? owner,run;internal ulong epoch,expires,entity;}
private struct TradePauseResumeWork
{internal string? id,owner,run;internal ulong epoch,expires,sequence;internal bool pending,applied,failed;}
private static TradePauseAuthorization tradePauseAuthorization;
private static TradePauseResumeWork tradePauseWork;
private static readonly object tradePauseDispatchLock=new();
private struct TradePauseIdentity
{internal ulong entity,tick,utc,sequence;internal double hull,shield;internal bool verified;}
private static TradePauseIdentity tradePauseIdentity;
private static void ObserveTradePauseIdentity(bool force=false)
{
if(!enabled||GetCurrentThreadId()!=expectedThread)return;
Root current;TradePauseIdentity previous;lock(gate){current=root;previous=tradePauseIdentity;}
var now=GetTickCount64();if(!force&&previous.tick!=0&&now>=previous.tick&&now-previous.tick<50)return;
var valid=current.valid&&Fresh(current.tick,now)&¤t.state==current.next&¤t.state is "InGame" or "Pause";
ulong entity=0;double hull=double.NaN,shield=double.NaN;
valid=valid&&Navigation.TryReadTradePauseShipHealth(out entity,out hull,out shield);
var observed=new TradePauseIdentity{entity=entity,hull=hull,shield=shield,tick=now,utc=Utc(),sequence=previous.sequence+1,verified=valid};
lock(gate)tradePauseIdentity=observed;
}
private static bool TradePauseIdentityReady(ulong entity,ulong now)
{
lock(gate)return tradePauseIdentity.verified&&tradePauseIdentity.entity==entity&&entity!=0&&Fresh(tradePauseIdentity.tick,now)
&&double.IsFinite(tradePauseIdentity.hull)&&tradePauseIdentity.hull>=.5;
}
private static JsonObject TradePauseIdentityDescription()
{
lock(gate)
{
var value=tradePauseIdentity;
return Obj(("verified",value.verified&&Fresh(value.tick,GetTickCount64())),("entityId",value.entity.ToString()),("sequence",value.sequence),
("observedAtUnixMilliseconds",value.utc),("hullFraction",double.IsFinite(value.hull)?(double?)value.hull:null),
("shieldFraction",double.IsFinite(value.shield)?(double?)value.shield:null),("source","current-main-ship-on-original-menu-thread"),("physicsTelemetryRequired",false));
}
}
private static bool TradePauseKey(string value)=>value.Length==32&&value.All(c=>c is >= '0' and <= '9' or >= 'a' and <= 'f');
private static bool TradePauseMenuReady(Root r,Menu m,ulong now)
=>enabled&&r.valid&&r.thread==expectedThread&&m.thread==expectedThread&&r.flow==m.flow
&&r.state=="Pause"&&r.next=="Pause"&&m.categoryName=="Pause"&&m.kind==1&&m.ready&&Fresh(r.tick,now)&&Fresh(m.tick,now)
&&m.names.Count(name=>name=="InGame")==1&&m.names.Where((name,index)=>name=="InGame"&&m.allowed[index]).Count()==1;
private static bool TradePauseAuthorized(TradePauseAuthorization authorization,TradePauseResumeWork wanted,ulong now)
=>authorization.expires>now&&authorization.entity!=0&&authorization.owner==wanted.owner&&authorization.run==wanted.run
&&authorization.epoch==wanted.epoch&&wanted.pending&&wanted.expires>now;
private static JsonObject TradePauseDescription()
{
lock(gate)
{
var now=GetTickCount64();var authorization=tradePauseAuthorization;var wanted=tradePauseWork;
return Obj(("schema",1),("scopeActive",authorization.expires>now),("owner",authorization.owner),("runId",authorization.run),
("entityId",authorization.entity.ToString()),("scopeExpiresInMilliseconds",authorization.expires>now?authorization.expires-now:0),
("requestId",wanted.id),("pending",wanted.pending&&wanted.expires>now&&authorization.expires>now),
("applied",wanted.applied),("failed",wanted.failed||wanted.pending&&(wanted.expires<=now||authorization.expires<=now)),
("action","InGame"),("onlyStablePauseCategory",true));
}
}
private static void CancelTradePauseScope(string? owner=null,string? run=null)
{
lock(tradePauseDispatchLock)lock(gate)
{
if(owner is not null&&(tradePauseAuthorization.owner!=owner||tradePauseAuthorization.run!=run))return;
tradePauseAuthorization.expires=0;++tradePauseAuthorization.epoch;tradePauseWork.pending=false;
}
}
private static JsonNode TradePauseRequest(string op,JsonElement request)
{
var owner=request.Str("owner");var run=request.Str("runId");
if(!TradePauseKey(owner)||!TradePauseKey(run))throw new InvalidOperationException("An explicit running trade scope is required");
if(op=="trade.pause.end"){CancelTradePauseScope(owner,run);return Description();}
lock(tradePauseDispatchLock)
{
var now=GetTickCount64();Root observed;lock(gate)observed=root;
if(!enabled||!observed.valid||!Fresh(observed.tick,now)||observed.thread!=expectedThread||observed.state!=observed.next
||observed.state is not("InGame" or "Pause"))throw new InvalidOperationException("A stable current game or Pause context is required");
if(op=="trade.pause.renew")
{
var ttl=request.Int("ttlMilliseconds");var entity=request.U64("entityId");
if(ttl is <1 or >750||entity==0||!TradePauseIdentityReady(entity,now))throw new InvalidOperationException("The running trade main ship is not verified");
lock(gate)
{
if(work.pending&&work.expires>now)throw new InvalidOperationException("An explicit session workflow owns the original menu");
var before=tradePauseAuthorization;
if(before.expires>now&&(before.owner!=owner||before.run!=run))throw new InvalidOperationException("Another running trade scope owns pause recovery");
if(before.expires>now&&before.entity!=entity&&observed.state!="InGame")throw new InvalidOperationException("A changed ship cannot be admitted from a Pause menu");
if(before.owner!=owner||before.run!=run||before.entity!=entity||before.expires<=now)
{++before.epoch;tradePauseWork.pending=false;}
before.owner=owner;before.run=run;before.entity=entity;before.expires=now+(ulong)ttl;tradePauseAuthorization=before;
}
return Description();
}
if(op!="trade.pause.resume")throw new InvalidOperationException("Unknown running-trade pause operation");
var id=request.Str("requestId");var expected=request.U64("menuSequence");var deadline=request.Int("ttlMilliseconds");
if(!TradePauseKey(id)||expected==0||deadline is <1 or >150)throw new InvalidOperationException("A bounded original Pause resume request is required");
lock(gate)
{
if(tradePauseWork.id==id)return Description();
var authorization=tradePauseAuthorization;
if(authorization.owner!=owner||authorization.run!=run||authorization.expires<=now||authorization.entity==0
||work.pending&&work.expires>now||tradePauseWork.pending&&tradePauseWork.expires>now||!TradePauseMenuReady(root,menu,now)
||expected>menu.sequence||menu.sequence-expected>32)throw new InvalidOperationException("The running trade Pause menu changed before resume");
tradePauseWork=new(){id=id,owner=owner,run=run,epoch=authorization.epoch,expires=Math.Min(now+(ulong)deadline,menu.tick+200),sequence=expected,pending=true};
}
return Description();
}
}
private static void PollTradePauseResume(nuint activity,Menu observed,Root r)
{
lock(tradePauseDispatchLock)
{
var now=GetTickCount64();TradePauseAuthorization authorization;TradePauseResumeWork wanted;Work other;
lock(gate){authorization=tradePauseAuthorization;wanted=tradePauseWork;other=work;}
if(!TradePauseAuthorized(authorization,wanted,now))return;
if(other.pending&&other.expires>now||!TradePauseMenuReady(r,observed,now)||observed.sequence<wanted.sequence
||GetCurrentThreadId()!=expectedThread)return;
ObserveTradePauseIdentity(force:true);
if(!TradePauseIdentityReady(authorization.entity,GetTickCount64()))return;
var fresh=CaptureMenu(activity);Root current;lock(gate)current=root;
if(!TradePauseMenuReady(current,fresh,GetTickCount64())||current.flow!=r.flow||current.csm!=r.csm
||Name(Pointer(Pointer(current.csm+0x100)))!="Pause"||Name(Pointer(Pointer(current.csm+0x108)))!="Pause")return;
var index=Array.FindIndex(fresh.names,name=>name=="InGame");if(index<0||!fresh.allowed[index])return;
lock(gate)
{
if(tradePauseWork.id!=wanted.id||!TradePauseAuthorized(tradePauseAuthorization,tradePauseWork,GetTickCount64()))return;
tradePauseWork.pending=false;
}
var applied=false;
try{Call(image+PauseSelect,(ulong)(activity+0x80),0,(ulong)index);applied=true;}
finally{lock(gate)if(tradePauseWork.id==wanted.id){tradePauseWork.applied=applied;tradePauseWork.failed=!applied;}}
}
}
private static bool TradePauseGuardSelfTest()
{
var previousEnabled=enabled;var previousThread=expectedThread;var previousIdentity=tradePauseIdentity;
try
{
enabled=true;expectedThread=7;
var r=new Root{valid=true,state="Pause",next="Pause",flow=0x10000,tick=1000,thread=7};
var m=new Menu{ready=true,flow=r.flow,tick=1000,sequence=9,thread=7,categoryName="Pause",kind=1,names=["InGame","ExitOptions"],allowed=[true,true]};
if(!TradePauseMenuReady(r,m,1100))return false;
foreach(var state in new[]{"InGame","MainMenu","ExitOptions","Rebirth","NetworkError","Pause Options"})
{var wrong=r;wrong.state=wrong.next=state;if(TradePauseMenuReady(wrong,m,1100))return false;}
var transitional=r;transitional.next="InGame";if(TradePauseMenuReady(transitional,m,1100))return false;
m.allowed[0]=false;if(TradePauseMenuReady(r,m,1100))return false;m.allowed[0]=true;
m.names=["InGame","InGame"];if(TradePauseMenuReady(r,m,1100))return false;m.names=["InGame","ExitOptions"];
m.kind=2;if(TradePauseMenuReady(r,m,1100))return false;m.kind=1;
if(TradePauseMenuReady(r,m,1201))return false;
// A stopped physics producer is immaterial: this separate current
// menu-thread identity must still be fresh, healthy and identical.
tradePauseIdentity=new(){verified=true,entity=42,tick=1000,hull=1};
if(!TradePauseIdentityReady(42,1100)||TradePauseIdentityReady(43,1100)||TradePauseIdentityReady(42,1201))return false;
tradePauseIdentity.hull=.49;if(TradePauseIdentityReady(42,1100))return false;
tradePauseIdentity.hull=1;tradePauseIdentity.verified=false;if(TradePauseIdentityReady(42,1100))return false;
var authorization=new TradePauseAuthorization{owner="one",run="run",entity=42,epoch=3,expires=1200};
var wanted=new TradePauseResumeWork{owner="one",run="run",epoch=3,expires=1150,pending=true};
if(!TradePauseAuthorized(authorization,wanted,1100)||TradePauseAuthorized(authorization,wanted,1150))return false;
authorization.epoch++;if(TradePauseAuthorized(authorization,wanted,1100))return false;
authorization.epoch=3;authorization.expires=0;return !TradePauseAuthorized(authorization,wanted,1100);
}
finally{enabled=previousEnabled;expectedThread=previousThread;tradePauseIdentity=previousIdentity;}
}
}
#endif