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>
463 lines
10 KiB
C++
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());
|
|
}
|