Doors run on the mission clock instead of being replicated

A door's position was an integrated countdown owned by whichever machine
the map-entity round-robin happened to deal it to. That left doors one
one-way-latency behind on every other machine, re-acquired at each state
change; drifting permanently on any frame hitch over a second, which the
old code dropped outright rather than clamping; and frozen mid-cycle,
collision volumes included, when their owning peer left, since ownership
transfer is not implemented.

Doors are clockwork with no inputs, and door/VTV physics is already local
pointer access - VTV::ProcessCollision reads door->currentVelocity off
the local object and the crush test is local VTV state - so a door does
not need an owner at all. Door::SlideDoor is now a phase function of
Application::GetMissionElapsed(), anchored so phase zero reproduces the
original DefaultState entry: fully open, starting to close. Every host
builds its own doorframe out of the map stream as a HermitInstance, the
instance kind DynamicEntityCreation does not broadcast, so nothing is
sent, nothing is received, and a peer leaving takes no doors with it.

Verified against a copy of the old integrator at 25fps with the real 10s
travel / 3s dead timings: identical 26s cycle, a constant one-frame
offset, and no drift across a 3s stall that leaves the old code
permanently 3 seconds out of phase.

Also fixes a latent bug found on the way: UpdateManager iterates the
dynamic master socket, which holds Independant and Hermit instances as
well as masters, and handed all of them to EntityUpdateReplicants, which
asserts MasterInstance.

Doorframes no longer consume a slot in the map-entity ownership cursor,
which shifts who owns every map entity dealt after them, so this cannot
share a session with an older build. The lobby publishes a simulation
revision and refuses to launch a mixed room.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-07 16:25:42 -05:00
co-authored by Claude Opus 5
parent 0e39075a20
commit 6e829f815e
8 changed files with 202 additions and 164 deletions
+65 -140
View File
@@ -72,172 +72,96 @@ Door::AttributeIndexSet& Door::GetAttributeIndex()
//#############################################################################
// Model Support
//
void
Door::ReadUpdateRecord(Simulation::UpdateRecord *message)
{
Check(this);
Check_Pointer(message);
Subsystem::ReadUpdateRecord(message);
UpdateRecord* record = (UpdateRecord*) message;
percentOpen = record->percentOpen;
switch (GetSimulationState())
{
case Opening:
case Closing:
phaseTimeRemaining = travelTime;
break;
case Opened:
case Closed:
phaseTimeRemaining = deadTime;
break;
}
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door updated to state "
// << GetSimulationState() << " @ "
// << application->GetSecondsRemainingInGame() << endl;
MoveCollisionVolume(percentOpen);
Check_Fpu();
}
// There is no ReadUpdateRecord/WriteUpdateRecord pair here on purpose. Doors
// are Hermit instances built independently on every host, so no door state is
// ever sent or received - the phase function below is the only thing that
// decides where a door is, and it reaches the same answer everywhere.
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
Door::WriteUpdateRecord(Simulation::UpdateRecord *record, int update_model)
{
Check(this);
Check_Pointer(record);
Subsystem::WriteUpdateRecord(record, update_model);
UpdateRecord *update = (UpdateRecord*)record;
update->percentOpen = percentOpen;
update->recordLength = sizeof(*update);
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
Door::SlideDoor(Scalar time_slice)
Door::SlideDoor(Scalar)
{
Check(this);
//
//------------------------------------------------------------
// Advance the clock, then branch based upon our current state
//------------------------------------------------------------
//--------------------------------------------------------------------------
// The door is clockwork. Its position is a function of how long the race
// has been running, not of a countdown integrated frame by frame, so:
//
int new_state;
if (time_slice > 1.0f)
// - every machine puts this door in the same place from the same mission
// clock, without a byte crossing the wire,
// - a frame hitch of any length costs nothing, because there is no
// accumulated state left to fall behind (the old code dropped any slice
// over a second outright and never got that time back).
//
// Phase zero is the instant the door starts to close, fully open, which is
// where the original state machine began from DefaultState:
//
// [0, travel) Closing 1 -> 0
// [travel, travel+dead) Closed 0
// [travel+dead, 2travel+dead) Opening 0 -> 1
// [2travel+dead, cycle) Opened 1
//--------------------------------------------------------------------------
//
if (cycleTime <= 0.0f)
{
MoveCollisionVolume(0.0f);
SetSimulationState(Closed);
Check_Fpu();
return;
}
phaseTimeRemaining -= time_slice;
Scalar percent_open;
switch (GetSimulationState())
Check(application);
Scalar phase = fmod(application->GetMissionElapsed() - phaseOffset, cycleTime);
if (phase < 0.0f)
{
phase += cycleTime;
}
//
//------------------------------------------------------------------------
// If the door is not done opening, set its new position, otherwise branch
// to the opened state
//------------------------------------------------------------------------
//--------------------------------------------------------------------------
// Pick the band. Each division below is guarded by the comparison that
// selected the branch, so a door with a zero travelTime or deadTime simply
// loses that band rather than dividing by zero.
//--------------------------------------------------------------------------
//
case Opening:
Door_Opening:
new_state = Opening;
if (phaseTimeRemaining > 0.0f)
{
percent_open = 1.0f - phaseTimeRemaining/travelTime;
}
else
{
phaseTimeRemaining += deadTime;
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door opened @ "
// << application->GetSecondsRemainingInGame() << endl;
goto Door_Opened;
}
currentVelocity.Subtract(
worldExtent,
GetEntity()->localOrigin.linearPosition
);
currentVelocity /= travelTime;
Check_Fpu();
break;
Scalar open_start = travelTime + deadTime;
int new_state;
Scalar percent_open;
//
//-------------------------------------------------------------
// If the door is ready to start closing, jump to closing state
//-------------------------------------------------------------
//
case Opened:
Door_Opened:
new_state = Opened;
if (phaseTimeRemaining <= 0.0f)
{
phaseTimeRemaining += travelTime;
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door closing @ "
// << application->GetSecondsRemainingInGame() << endl;
goto Door_Closing;
}
percent_open = 1.0f;
currentVelocity = Vector3D::Identity;
Check_Fpu();
break;
//
//------------------------------------------------------------------------
// If the door is not done closing, set its new position, otherwise branch
// to the closed state
//------------------------------------------------------------------------
//
case DefaultState:
phaseTimeRemaining = travelTime;
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door default @ "
// << application->GetSecondsRemainingInGame() << endl;
case Closing:
Door_Closing:
if (phase < travelTime)
{
new_state = Closing;
if (phaseTimeRemaining > 0.0f)
{
percent_open = phaseTimeRemaining/travelTime;
}
else
{
phaseTimeRemaining += deadTime;
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door closed @ "
// << application->GetSecondsRemainingInGame() << endl;
goto Door_Closed;
}
percent_open = 1.0f - phase/travelTime;
currentVelocity.Subtract(
GetEntity()->localOrigin.linearPosition,
worldExtent
);
currentVelocity /= travelTime;
Check_Fpu();
break;
//
//-------------------------------------------------------------
// If the door is ready to start opening, jump to opening state
//-------------------------------------------------------------
//
case Closed:
Door_Closed:
}
else if (phase < open_start)
{
new_state = Closed;
if (phaseTimeRemaining <= 0.0f)
{
phaseTimeRemaining += travelTime;
// DEBUG_STREAM << GetEntity()->GetEntityID() << " door opening @ "
// << application->GetSecondsRemainingInGame() << endl;
goto Door_Opening;
}
percent_open = 0.0f;
currentVelocity = Vector3D::Identity;
Check_Fpu();
break;
}
else if (phase < open_start + travelTime)
{
new_state = Opening;
percent_open = (phase - open_start)/travelTime;
currentVelocity.Subtract(
worldExtent,
GetEntity()->localOrigin.linearPosition
);
currentVelocity /= travelTime;
}
else
{
new_state = Opened;
percent_open = 1.0f;
currentVelocity = Vector3D::Identity;
}
//
@@ -344,7 +268,8 @@ Door::Door(
//
// Initialize variables
//
phaseTimeRemaining = 0.0f;
phaseOffset = 0.0f;
cycleTime = 2.0f*(travelTime + deadTime);
currentPosition = Point3D::Identity;
SetPerformance(&Door::SlideDoor);