Files
RP412/MUNGA/DOOR.cpp
T
CydandClaude Opus 5 6e829f815e 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>
2026-08-07 16:25:42 -05:00

463 lines
10 KiB
C++

#include "munga.h"
#pragma hdrstop
#include "door.h"
#include "fileutil.h"
#include "boxsolid.h"
#include "doorfram.h"
#include "app.h"
#include "notation.h"
//#############################################################################
// Shared Data Support
//
Door::SharedData
Door::DefaultData(
Door::GetClassDerivations(),
Door::GetMessageHandlers(),
Door::GetAttributeIndex(),
Door::StateCount
);
Derivation* Door::GetClassDerivations()
{
static Derivation classDerivations(Subsystem::GetClassDerivations(), "Door");
return &classDerivations;
}
//#############################################################################
// Messaging Support
//
//#############################################################################
// Attribute Support
//
const Door::IndexEntry
Door::AttributePointers[]=
{
{
Door::CurrentPositionAttributeID,
"CurrentPosition",
(Simulation::AttributePointer)&Door::currentPosition
},
{
Door::VideoResourceAttributeID,
"VideoResource",
(Simulation::AttributePointer)&Door::videoResource
},
{
Door::PercentOpenAttributeID,
"PercentOpen",
(Simulation::AttributePointer)&Door::percentOpen
},
{
Door::CurrentVelocityAttributeID,
"CurrentVelocity",
(Simulation::AttributePointer)&Door::currentVelocity
}
};
Door::AttributeIndexSet& Door::GetAttributeIndex()
{
static Door::AttributeIndexSet attributeIndex(ELEMENTS(Door::AttributePointers),
Door::AttributePointers,
Subsystem::GetAttributeIndex()
);
return attributeIndex;
}
//#############################################################################
// Model Support
//
// 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::SlideDoor(Scalar)
{
Check(this);
//
//--------------------------------------------------------------------------
// 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:
//
// - 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;
}
Check(application);
Scalar phase = fmod(application->GetMissionElapsed() - phaseOffset, cycleTime);
if (phase < 0.0f)
{
phase += cycleTime;
}
//
//--------------------------------------------------------------------------
// 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.
//--------------------------------------------------------------------------
//
Scalar open_start = travelTime + deadTime;
int new_state;
Scalar percent_open;
if (phase < travelTime)
{
new_state = Closing;
percent_open = 1.0f - phase/travelTime;
currentVelocity.Subtract(
GetEntity()->localOrigin.linearPosition,
worldExtent
);
currentVelocity /= travelTime;
}
else if (phase < open_start)
{
new_state = Closed;
percent_open = 0.0f;
currentVelocity = Vector3D::Identity;
}
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;
}
//
//-------------------------------------------------
// Move the collision volumes and set the new state
//-------------------------------------------------
//
MoveCollisionVolume(percent_open);
SetSimulationState(new_state);
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
Door::MoveCollisionVolume(Scalar percent_open)
{
Check(this);
//
//---------------------------------------------------------
// If the door hasn't moved, don't bother updating anything
//---------------------------------------------------------
//
Entity *entity = GetEntity();
Check(entity);
if (
GetSimulationState() != simulationState.GetOldState()
&& entity->GetInstance() == Entity::MasterInstance
&& entity->IsDynamic()
)
{
ForceUpdate();
}
if (percent_open == percentOpen)
{
return;
}
//
//-----------------------------------
// Otherwise, lerp the local position
//-----------------------------------
//
percentOpen = percent_open;
currentPosition.Lerp(Point3D::Identity, localExtent, percentOpen);
//
//-----------------------------------------------------------
// Now, lerp the world position and set the collision volumes
//-----------------------------------------------------------
//
Point3D world;
world.Lerp(
GetEntity()->localOrigin.linearPosition,
worldExtent,
percentOpen
);
BoxedSolid* temp = collisionTemplates;
BoxedSolid* vol = collisionVolumes;
while (temp)
{
Check(temp);
Check(vol);
vol->minX = temp->minX + world.x;
vol->maxX = temp->maxX + world.x;
vol->minY = temp->minY + world.y;
vol->maxY = temp->maxY + world.y;
vol->minZ = temp->minZ + world.z;
vol->maxZ = temp->maxZ + world.z;
vol = vol->GetNextSolid();
temp = temp->GetNextSolid();
}
Verify(!vol);
Check_Fpu();
}
//#############################################################################
// Constructer/Destructor Support
//
Door::Door(
DoorFrame *entity,
int subsystem_ID,
SubsystemResource *subsystem_resource
):
Subsystem(entity, subsystem_ID, subsystem_resource, DefaultData)
{
Check_Pointer(this);
Check(entity);
Check_Pointer(subsystem_resource);
//
// GetStream data NOTE::This must be in the same order as below!!!
//
localExtent = subsystem_resource->motionExtent;
travelTime = subsystem_resource->travelTime;
deadTime = subsystem_resource->deadTime;
//
// Initialize variables
//
phaseOffset = 0.0f;
cycleTime = 2.0f*(travelTime + deadTime);
currentPosition = Point3D::Identity;
SetPerformance(&Door::SlideDoor);
SetSimulationState(DefaultState);
collisionVolumeCount = 0;
collisionTemplates = NULL;
collisionVolumes = NULL;
percentOpen = -1.0f;
//
//-------------------------------------
// Establish the worldspace coordinates
//-------------------------------------
//
worldExtent.Multiply(localExtent, entity->localToWorld);
Origin local_origin = entity->localOrigin;
local_origin.linearPosition = Point3D::Identity;
//
//------------------------------
// Read in the collision volumes
//------------------------------
//
Check(application);
ResourceFile *res_file = application->GetResourceFile();
Check(res_file);
ResourceDescription *res =
res_file->FindResourceDescription(subsystem_resource->collisionID);
Check(res);
res->Lock();
BoxedSolidResource* box =
(BoxedSolidResource*)res->resourceAddress;
Check_Pointer(box);
collisionVolumeCount = res->resourceSize / sizeof(BoxedSolidResource);
for (int i=0; i<collisionVolumeCount; ++i)
{
BoxedSolidResource world_box,local_box;
world_box.Instance(*box,entity->localOrigin);
local_box.Instance(*box,local_origin);
collisionTemplates =
BoxedSolid::MakeBoxedSolid(&local_box, this, collisionTemplates);
Register_Object(collisionTemplates);
collisionVolumes =
BoxedSolid::MakeBoxedSolid(&world_box, this, collisionVolumes);
Register_Object(collisionVolumes);
++box;
}
res->Unlock();
MoveCollisionVolume(0.0f);
Check(this);
}
Door::~Door()
{
BoxedSolid *box = collisionTemplates;
while (box)
{
BoxedSolid *next_box = box->GetNextSolid();
Unregister_Object(box);
delete box;
box = next_box;
}
box = collisionVolumes;
while (box)
{
BoxedSolid *next_box = box->GetNextSolid();
Unregister_Object(box);
delete box;
box = next_box;
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem
//
Logical
Door::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char* subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
ResourceFile *res_file
)
{
if (
!Subsystem::CreateStreamedSubsystem(
model_file,
model_name,
subsystem_name,
subsystem_resource,
subsystem_file,
directories
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource);
subsystem_resource->classID = RegisteredClass::DoorClassID;
//
// Get the motion Extent
//
const char *extent_data;
if(
!subsystem_file->GetEntry(
subsystem_name,
"MotionExtent",
&extent_data
)
)
{
std::cerr << subsystem_name << " missing MotionExtent!!\n";
return -1;
}
sscanf(
extent_data,
"%f %f %f",
&subsystem_resource->motionExtent.x,
&subsystem_resource->motionExtent.y,
&subsystem_resource->motionExtent.z
);
//
// Get the travelTime
//
if(
!subsystem_file->GetEntry(
subsystem_name,
"TravelTime",
&subsystem_resource->travelTime
)
)
{
std::cerr << subsystem_name << " missing TravelTime!\n";
return -1;
}
//
// Read in the Deadtime
//
if(
!subsystem_file->GetEntry(
subsystem_name,
"DeadTime",
&subsystem_resource->deadTime
)
)
{
std::cerr << subsystem_name << " missing DeadTime!\n";
return False;
}
const char* collision_file;
if (
!subsystem_file->GetEntry(
subsystem_name,
"Collision",
&collision_file
)
)
{
std::cerr << subsystem_name << " missing Collision!\n";
return False;
}
subsystem_resource->collisionID =
BoxedSolidResource::CreateBoxedSolidStream(
collision_file,
res_file,
directories
);
return True;
}
Logical
Door::TestInstance() const
{
return IsDerivedFrom(*GetClassDerivations());
}