Retain spawn replication through admission and nearby crate use

This commit is contained in:
2026-09-20 18:00:34 +02:00
parent e8d472c6ff
commit 173faae6d4
8 changed files with 193 additions and 24 deletions
+7 -6
View File
@@ -11,7 +11,7 @@ methods = '\n'.join(body(source, key) for key in keys)
methods = methods.replace('protected ', '').replace('Replication.IsServer()', 'Replication::IsServer()')
methods = methods.replace('ERR_GameMode.Get()', 'Mode::Get()')
methods = methods.replace('RplComponent component = RplComponent.Cast(Replication.FindItem(id));', 'RplComponent* component = Replication::FindItem(id);')
methods = methods.replace('component.GetEntity()', 'component->GetEntity()')
methods = methods.replace('component.GetEntity()', 'component->GetEntity()').replace('RplId.Invalid()', '-1')
methods = methods.replace('foreach (RplId id :', 'for (RplId id :')
methods = methods.replace('GetGame().GetWorld().GetWorldTime()', 'game.now')
methods = methods.replace('GetGame().GetCallqueue().Remove(ERR_CheckClientSpawnScene)', 'game.removes++')
@@ -31,8 +31,9 @@ template<typename... T>void PrintFormat(const char*,T...){}
struct Probe{
int m_ERRSceneToken=4,m_ERRClientSceneToken=4,acks=0,lastAck=0;
bool m_ERRScenePending=false,m_ERRSceneAcknowledged=false,m_ERRSceneTimedOut=false,m_ERRClientScenePending=false;
bool m_ERRSceneReleased=false,m_ERRSceneVerified=false,m_ERRClientSceneFinal=false,lastFinal=false;float m_ERRClientSceneStableSince=-1;
bool m_ERRSceneFinalRequested=false,m_ERRSceneVerified=false,m_ERRClientSceneFinal=false,lastFinal=false;float m_ERRClientSceneStableSince=-1;
float m_ERRSceneDeadline=1000,m_ERRSceneStartedAt=0;Nodes m_ERRClientSceneNodes,m_ERRClientStockOwners,m_ERRClientStockItems;
void ERR_ReportStockWait(int,int,int){}
bool stockReady=true;bool ERR_ClientStockReady(){return stockReady;}void ERR_RefreshSceneInventory(){}
int GetPlayerId(){return 1;}void SendAck(int token,bool finalCheck){acks++;lastAck=token;lastFinal=finalCheck;}
'''+methods+r'''
@@ -59,7 +60,7 @@ int main(){
Probe handshake;handshake.m_ERRScenePending=true;
handshake.ERR_AcknowledgeSpawnScene(4,true);assert(!handshake.ERR_SpawnSceneVerified());
handshake.ERR_AcknowledgeSpawnScene(4,false);handshake.ERR_AcknowledgeSpawnScene(4,true);assert(!handshake.ERR_SpawnSceneVerified());
handshake.m_ERRSceneReleased=true;handshake.ERR_AcknowledgeSpawnScene(3,true);assert(!handshake.ERR_SpawnSceneVerified());
handshake.m_ERRSceneFinalRequested=true;handshake.ERR_AcknowledgeSpawnScene(3,true);assert(!handshake.ERR_SpawnSceneVerified());
handshake.ERR_AcknowledgeSpawnScene(4,true);assert(handshake.ERR_SpawnSceneVerified());
Probe finalTimeout;finalTimeout.m_ERRScenePending=true;finalTimeout.m_ERRSceneAcknowledged=true;
finalTimeout.ERR_SpawnSceneMayFinish();assert(finalTimeout.m_ERRSceneTimedOut&&!finalTimeout.ERR_SpawnSceneVerified());
@@ -89,7 +90,7 @@ for old, new in [('override ', ''), ('SCR_SpawnRequestComponent requestComponent
run(r'''
#include <cassert>
enum class SCR_ESpawnResult{OK,SPAWN_NOT_ALLOWED,CANNOT_ASSIGN};
struct Request{bool ready=false,mayFinish=false,verified=false;bool ERR_SpawnSceneVerified(){return ready&&verified;}bool ERR_VerifySceneRelease(){return !ready||verified;}int ends=0;bool ERR_SpawnSceneReady(){return ready;}bool ERR_SpawnSceneMayFinish(){return mayFinish;}bool preloadRequired=true;bool ERR_TakeScenePreload(){if(!ready||!preloadRequired)return false;preloadRequired=false;return true;}void ERR_EndSpawnScene(){ends++;}};
struct Request{bool ready=false,mayFinish=false,verified=false;bool ERR_SpawnSceneVerified(){return ready&&verified;}bool ERR_VerifyLoadedScene(){return !ready||verified;}int ends=0,retained=0;void ERR_RetainSpawnScene(void*){retained++;}bool ERR_SpawnSceneReady(){return ready;}bool ERR_SpawnSceneMayFinish(){return mayFinish;}bool preloadRequired=true;bool ERR_TakeScenePreload(){if(!ready||!preloadRequired)return false;preloadRequired=false;return true;}void ERR_EndSpawnScene(){ends++;}};
struct Data{};struct Actor{};struct Ticket{};
struct Mode{static bool current;static bool Get(){return current;}};bool Mode::current=true;
struct Logic{Ticket ticket;bool hasTicket=true,allowed=true;Ticket* TakeSpawnTicket(Request*,Data*,Actor*){return hasTicket?&ticket:nullptr;}bool MayFinalizeSpawn(Ticket*){return allowed;}};
@@ -108,7 +109,7 @@ int main(){
h.preloadReady=true;assert(!h.CanFinalize_S(&request,&data,&actor));request.verified=true;assert(h.CanFinalize_S(&request,&data,&actor)&&h.preloadStarts==1);
h.storage.allowed=false;
assert(h.FinalizeRequest_S(&request,&data,&actor)==SCR_ESpawnResult::SPAWN_NOT_ALLOWED&&h.assignments==0&&h.rejects==2);
h.storage.allowed=true;assert(h.FinalizeRequest_S(&request,&data,&actor)==SCR_ESpawnResult::OK&&h.assignments==1&&request.ends==3);
h.storage.allowed=true;assert(h.FinalizeRequest_S(&request,&data,&actor)==SCR_ESpawnResult::OK&&h.assignments==1&&request.ends==3&&request.retained==1);
h.storage.hasTicket=false;assert(h.FinalizeRequest_S(&request,&data,&actor)==SCR_ESpawnResult::SPAWN_NOT_ALLOWED&&h.assignments==1&&h.rejects==2);
Mode::current=false;h.logic=nullptr;assert(h.FinalizeRequest_S(&request,&data,&actor)==SCR_ESpawnResult::OK&&h.assignments==2);
}
@@ -127,7 +128,7 @@ struct Items:std::vector<int>{using std::vector<int>::vector;void Clear(){clear(
struct Controller{int GetControlledEntity(){return 42;}};
struct Probe{
Items m_ERRSceneEntities{1,2},m_ERRSceneRoots,m_ERRSceneStockOwners,m_ERRSceneStockItems;
bool m_ERRSceneReleased=true,m_ERRSceneVerified=true;bool m_ERRSceneObserver=true,m_ERRScenePending=true,m_ERRSceneAcknowledged=true,m_ERRSceneTimedOut=true,m_ERRScenePreloadRequired=true;
bool m_ERRSceneFinalRequested=true,m_ERRSceneVerified=true;bool m_ERRSceneObserver=true,m_ERRScenePending=true,m_ERRSceneAcknowledged=true,m_ERRSceneTimedOut=true,m_ERRScenePreloadRequired=true;
int m_ERRSceneToken=7,restored=0,cancelled=0,observed=0;Controller controller;bool connected=true;
Controller* GetPlayerController(){return connected?&controller:nullptr;}
void ERR_StreamScene(int,bool normal){assert(normal);restored++;}
+71
View File
@@ -0,0 +1,71 @@
"""Exercise production retention lifetime and the post-preload handoff."""
from policy_harness import ROOT, body, run
source = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnSceneLease.c'
methods = '\n'.join(body(source, key) for key in ('void Start(', 'protected void Check(', 'void Stop('))
for old, new in [
('protected ', ''), ('SCR_PlayerController controller', 'Controller* controller'),
('IEntity actor', 'Actor* actor'), ('vector position', 'float position'),
('array<IEntity> entities', 'Items entities'), ('ERR_GameMode mode = ERR_GameMode.Get()', 'Mode* mode = currentMode'),
('mode.Active()', 'mode->active'), ('m_Controller.GetControlledEntity()', 'm_Controller->actor'),
('vector.DistanceXZ(m_Actor.GetOrigin(), m_Position)', 'std::abs(m_Actor->position - m_Position)'),
('GetGame().GetCallqueue().CallLater(Check, 500, true)', 'starts++'),
('if (GetGame()) GetGame().GetCallqueue().Remove(Check)', 'removes++'),
('foreach (IEntity entity :', 'for (int entity :'), ('null', 'nullptr')]:
methods = methods.replace(old, new)
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
struct Items:std::vector<int>{using std::vector<int>::vector;void Clear(){clear();}void Copy(Items other){assign(other.begin(),other.end());}};
struct Actor{float position=0;};struct Controller{Actor* actor=nullptr;};
struct Mode{bool active=true;}mode;Mode* currentMode=&mode;
struct Lease{
Controller* m_Controller=nullptr;Actor* m_Actor=nullptr;float m_Position=0;Items m_Entities;
int starts=0,removes=0,released=0;void Stream(Controller*,int,bool normal){assert(normal);released++;}
'''+methods+r'''
};
int main(){
Actor actor,other;Controller controller{&actor};Lease lease;
lease.Start(&controller,&actor,0,{11,12,13});
for(int i=0;i<1000;i++){lease.Check();}assert(lease.released==0&&lease.m_Entities.size()==3);
actor.position=120;lease.Check();assert(lease.released==0);
actor.position=121;lease.Check();assert(lease.released==3&&lease.m_Entities.empty());
lease.Stop();assert(lease.released==3);
actor.position=0;lease.Start(&controller,&actor,0,{21});controller.actor=&other;lease.Check();assert(lease.released==4);
controller.actor=&actor;lease.Start(&controller,&actor,0,{22});currentMode=nullptr;lease.Check();assert(lease.released==5);currentMode=&mode;
lease.Start(&controller,&actor,0,{23});mode.active=false;lease.Check();assert(lease.released==6);mode.active=true;
lease.Start(&controller,&actor,0,{24});lease.m_Controller=nullptr;lease.Check();assert(lease.m_Entities.empty());
lease.Start(&controller,&actor,0,{25});lease.Start(&controller,&actor,0,{26});assert(lease.released==8);lease.Stop();assert(lease.released==9);
}
''')
scene = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnClientScene.c'
verify = body(scene, 'bool ERR_VerifyLoadedScene(')
for old, new in [('ObserversSystem observers', 'Observers* observers'),
('observers.InsertObserverMP', 'observers->InsertObserverMP'),
('GetPlayerController().GetRplIdentity()', '1'), ('null', 'nullptr'),
('Rpc(ERR_ConfirmClientSpawnScene, m_ERRSceneToken)', 'confirms++')]:
verify = verify.replace(old, new)
run(r'''
#include <cassert>
template<typename... T>void PrintFormat(const char*,T...){}
struct Observers{int calls=0;void InsertObserverMP(int,float,float,void*){calls++;}};
struct Probe{
bool m_ERRSceneTimedOut=false,m_ERRScenePreloadRequired=true,m_ERRSceneFinalRequested=false,m_ERRSceneVerified=false,ready=false,preloading=false;
int m_ERRSceneToken=1,confirms=0;float m_ERRScenePosition[3]={1,0,2};Observers observer;
bool ERR_SpawnSceneReady(){return ready;}bool IsPreloading(){return preloading;}
Observers* GetObserversSystem(){return &observer;}bool GetPlayerController(){return true;}int GetPlayerId(){return 1;}
// Releasing nodes here would break dedicated final readiness: intentionally
// no streaming mutator in this adapter. The production method must retain them.
'''+verify+r'''
};
int main(){
Probe p;assert(!p.ERR_VerifyLoadedScene());p.ready=true;assert(!p.ERR_VerifyLoadedScene());
p.m_ERRScenePreloadRequired=false;p.preloading=true;assert(!p.ERR_VerifyLoadedScene());p.preloading=false;
assert(!p.ERR_VerifyLoadedScene()&&p.confirms==1&&p.observer.calls==1);
assert(!p.ERR_VerifyLoadedScene()&&p.confirms==1);p.m_ERRSceneVerified=true;assert(p.ERR_VerifyLoadedScene());
Probe timeout;timeout.m_ERRSceneTimedOut=true;assert(timeout.ERR_VerifyLoadedScene()&&timeout.confirms==0);
}
''')
print('PASS: spawn scene remains held through final acknowledgement and nearby use; leaving, reassignment, disconnect and teardown release it')