Files
RP412/RP/VTV.h
T
CydandClaude Fable 5 c479cd48e9 The death cycle is deterministic
A scripted lap - full throttle, a steer, a crash at speed, the burn,
the tumble, death, respawn, a second crash, a second respawn - now
plays out bit-identical between identical runs and across 30 and 144
fps under RP412PHYSICSHZ. Ninety of ninety samples exact in the repro
pair, sixty of sixty across frame rates, max difference 0.000000. The
crash was already deterministic; this makes the RECOVERY deterministic,
and it took five pieces, every one found by measurement:

- The respawn teleport moves onto the vehicle's own step grid.
  VTV::ScheduleRespawn stores it and BeginStep applies it at the first
  step whose clock reaches the due time, teleport and turn-toward-goal
  together, because the goal flip reads the POST-reset heading. The old
  path applied the Reset from the event queue, which runs on wall
  clock, and identical runs diverged on the first step after the pod
  stood back up.

- The handler keeps its Reset for the FIRST spawn of a mission, gated
  by a flag rather than by mode. A Mover is born in StasisState and the
  first Reset is what wakes it; gating on "is fixed stepping on" - the
  first attempt - skipped that wake-up and parked the pod frozen at its
  spawn point for an entire race. The scripted-lap harness caught it in
  one run.

- The vehicle stamps its own death clock, at the single site that sets
  BurningState - inside the step machinery, which is why the crash
  measured exact. The schedule anchors to the death, the last
  step-exact event in the chain.

- The due time is quantized to a half-second grid ANCHORED AT THE
  DEATH. The instrument showed the naive anchor was four seconds stale
  by scheduling time: the fry chain reposts itself at wall-clock
  Now()+2.0 and the drop-zone reply lands about five sim-seconds after
  death, jittered by a few steps of queue timing. Firing "next step"
  inherited that jitter whole. Rounding up to the next half-second
  after the death puts hundredths of jitter against tenths of headroom,
  so every run lands in the same cell - and the felt delay stays the
  six-ish seconds it has always been.

- The out-of-world tumble draws from a per-vehicle random stream seeded
  by creation order. The global Random is shared with the frame loop's
  consumers - particles, mostly - so its position at the moment a
  burning pod drew from it depended on how many frames had rendered,
  and the kick went straight into angular velocity. Last wall-clocked
  input in the whole death cycle.

The respawn scheduling and firing log under RP412PHYSTRACE in
run-comparable terms - pad identity, due offset, lateness - because
those lines are what cracked this: "due in -4.06 sim-s" said more in
one glance than three rounds of hypothesis.

Still outside the claim: multi-vehicle contact (DynamicBounce writes
the victim's state from the striker's step) and network play. That is
the lockstep frontier, and it now has a harness waiting for it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 21:42:04 -05:00

609 lines
13 KiB
C++

#pragma once
#include "..\munga\jmover.h"
#include "..\munga\vector2d.h"
#include "..\munga\controls.h"
#include "..\munga\normal.h"
#include "..\munga\damage.h"
#include "..\munga\reticle.h"
#include "rptool.h"
class VTVControlsMapper;
class VTV;
class RPPlayer;
#define ZIPPY 2.3f
#define MAX_THRUSTERS 6
//##########################################################################
//#################### VTV::DamageZone ############################
//##########################################################################
class VTV__DamageZone :
public DamageZone
{
public:
VTV__DamageZone(
VTV *vtv,
int damage_zone_index,
MemoryStream *damage_zone_stream
);
static Logical
CreateStreamedDamageZone(
NotationFile *model_file,
const char *model_name,
NotationFile *skl_file,
const char *damage_zone_name,
MemoryStream *damage_zone_stream,
NotationFile *dmg_file,
const ResourceDirectories *directories
);
void
TakeDamage(Damage& damage);
};
//##########################################################################
//#################### VTV::UpdateMessage ############################
//##########################################################################
struct VTV__UpdateRecord:
public JointedMover::UpdateRecord
{
public:
Enumeration
collisionState,
collisionMaterialType;
Normal
collisionNormal;
Scalar
collisionSpeed;
Logical
boosterOn;
Vector3D
boosterScale;
Scalar
boosterSmokeDensity;
int
hornBlast; // HACK - ECH 7/31/95 - For replicants
};
//##########################################################################
//###################### VTV::ModelResource ##########################
//##########################################################################
struct VTV__ModelResource:
public JointedMover::ModelResource
{
Scalar
maxAcceleration,
groundEffectDomainSquared,
groundEffectRange,
maxThrusterRotationRate,
maxYawVelocity,
angularSpringFactor,
bankFactor,
deathSpeed,
deathScoreLoss,
maxImpactSpeed,
bottomArmorScale,
powerDive;
Vector3D
maxAngularAcceleration;
int
jointIndex[MAX_THRUSTERS],
segmentIndex[MAX_THRUSTERS];
Reticle::ModelResource
reticleResource;
};
//##########################################################################
//##################### Mover::MakeMessage ##########################
//##########################################################################
class VTV__MakeMessage:
public JointedMover::MakeMessage
{
public:
char
vehicleBadge[20],
vehicleColor[20];
VTV__MakeMessage(
Receiver::MessageID message_ID,
size_t length,
Entity::ClassID class_ID,
const EntityID &owner_ID,
ResourceDescription::ResourceID resource_ID,
LWord instance_flags,
const Origin &origin,
const Motion &velocity,
const Motion &acceleration,
const char* badge,
const char* color
):
JointedMover::MakeMessage(
message_ID,
length,
class_ID,
owner_ID,
resource_ID,
instance_flags,
origin,
velocity,
acceleration
)
{
Str_Copy(vehicleBadge, badge, sizeof(vehicleBadge));
Str_Copy(vehicleColor, color, sizeof(vehicleColor));
}
VTV__MakeMessage(
Receiver::MessageID message_ID,
size_t length,
const EntityID &entity_ID,
Entity::ClassID class_ID,
const EntityID &owner_ID,
ResourceDescription::ResourceID resource_ID,
LWord instance_flags,
const Origin &origin,
const Motion &velocity,
const Motion &acceleration,
const char* badge,
const char* color
):
JointedMover::MakeMessage(
message_ID,
length,
entity_ID,
class_ID,
owner_ID,
resource_ID,
instance_flags,
origin,
velocity,
acceleration
)
{
Str_Copy(vehicleBadge, badge, sizeof(vehicleBadge));
Str_Copy(vehicleColor, color, sizeof(vehicleColor));
}
};
//##########################################################################
//############################# VTV ##################################
//##########################################################################
class VTV:
public JointedMover
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data support
//
public:
static Derivation *GetClassDerivations();
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Message Support
//
public:
enum {
KavorkianMessageID = JointedMover::NextMessageID,
MarkSpotMessageID,
RestoreToSpotMessageID,
ZoomInMessageID,
ZoomOutMessageID,
NextMessageID
};
private:
static const HandlerEntry MessageHandlerEntries[];
protected:
static MessageHandlerSet MessageHandlers;
void
TakeDamageMessageHandler(TakeDamageMessage *message);
void
ZoomInMessageHandler(
ReceiverDataMessageOf<ControlsButton> *message
);
void
ZoomOutMessageHandler(
ReceiverDataMessageOf<ControlsButton> *message
);
void
KavorkianMessageHandler(
ReceiverDataMessageOf<ControlsButton> *message
);
void
MarkSpotMessageHandler(
ReceiverDataMessageOf<ControlsButton> *message
);
void
RestoreToSpotMessageHandler(
ReceiverDataMessageOf<ControlsButton> *message
);
void
PlayerLinkMessageHandler(PlayerLinkMessage *message);
Origin savedOrigin;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support
//
public:
enum {
HeightAboveTerrainAttributeID = JointedMover::NextAttributeID,
ThrusterPitchAttributeID,
CollisionStateAttributeID,
CollisionMaterialTypeAttributeID,
CollisionNormalAttributeID,
CollisionSpeedAttributeID,
EyepointRotationAttributeID,
BoosterOnAttributeID,
BoosterScaleAttributeID,
BoosterSmokeDensityAttributeID,
ForwardVelocityAttributeID,
CompassHeadingAttributeID,
TargetReticleAttributeID,
NavigationRangeAttributeID,
NavigationLinearPositionAttributeID,
NavigationAngularPositionAttributeID,
ScoreStateAttributeID,
ScoreTypeAttributeID,
HornBlastAttributeID, // HACK - ECH 7/31/95 - For replicants
NextAttributeID
};
virtual Vector3D
GetWorldLinearVelocity()
{return worldLinearVelocity;}
private:
static const IndexEntry AttributePointers[];
protected:
//static AttributeIndexSet AttributeIndex
static AttributeIndexSet& GetAttributeIndex();
//
// public attribute declarations go here
//
public:
int hornBlast; // HACK - ECH 7/31/95 - For replicants
Scalar heightAboveTerrain;
Hinge thrusterPitch;
EulerAngles eyepointRotation;
Logical boosterOn;
Vector3D boosterScale;
Scalar boosterSmokeDensity;
Scalar forwardVelocity;
YawPitchRoll compassHeading;
Reticle targetReticle;
Scalar navigationRange;
Point3D
*navigationLinearPosition; // ptr to linear pos of object or NULL
Quaternion
*navigationAngularPosition; // ptr to angular pos of object or NULL
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Model support
//
public:
enum {
BurningState = JointedMover::StateCount,
StateCount
};
enum {
ControlsMapperSubsystem,
PowerSubsystem,
Thruster1Subsystem,
Thruster2Subsystem,
Thruster3Subsystem,
Thruster4Subsystem,
Thruster5Subsystem,
Thruster6Subsystem,
JointSubsystemID,
BasicSubsystemCount
};
typedef void
(VTV::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
SetMappingSubsystem(VTVControlsMapper *mapper);
void
MoveAndCollide(Scalar time_slice);
Logical
BoosterOn();
protected:
typedef VTV__UpdateRecord UpdateRecord;
void
WriteUpdateRecord(
Simulation::UpdateRecord *message,
int update_model
);
void
ReadUpdateRecord(Simulation::UpdateRecord *message);
public:
Scalar
groundEffectDomainSquared,
groundEffectRange,
angularSpringFactor,
bankFactor,
maxYawVelocity,
powerDive,
thrusterAngle;
Vector3D
maxAngularAcceleration;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Collision support
//
public:
enum {
NoCollisionState = 0,
InitialHitState,
SlideState,
RestState,
CollisionStateCount
};
Enumeration
collisionTemporaryState;
StateIndicator
collisionState;
Enumeration
collisionMaterialType;
Normal
collisionNormal;
Scalar
collisionSpeed,
bottomArmorScale;
protected:
void
PerformAndWatch(
const Time& till,
MemoryStream *update_stream
);
void
ProcessCollision(
Scalar time_slice,
BoxedSolidCollision &collision,
const Point3D &old_position,
Damage *damage
);
Time
lastDoorHit;
Vector3D
doorHitNormal;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Damage support
//
public:
Scalar
deathSpeed,
deathScoreLoss;
Scalar
deathConstant;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Scoring support
//
public:
//
// HACK - ECH 7/6/95 - Allow the player vehicle to respond to score
// messages, allows attribute system to be used for scoring
//
void
RespondToScoreMessage(Message *message);
StateIndicator
scoreState;
Enumeration
scoreType;
Logical
insideWorld;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
typedef VTV__ModelResource ModelResource;
typedef VTV__MakeMessage MakeMessage;
typedef VTV__DamageZone DamageZone;
static VTV*
Make(MakeMessage *creation_message);
static ResourceDescription::ResourceID
CreateModelResource(
ResourceFile *resource_file,
const char *model_name,
NotationFile *model_file,
const ResourceDirectories *directories,
ModelResource* model=NULL
);
static ResourceDescription::ResourceID
CreateSubsystemStream(
ResourceFile *resource_file,
const char *model_name,
NotationFile *model_file,
const ResourceDirectories *directories
);
static ResourceDescription::ResourceID
CreateDamageZoneStream(
ResourceFile *resource_file,
const char *dzone_name,
NotationFile *dmg_file,
const ResourceDirectories *directories
);
typedef Logical
(*FindNameFunction)(
const char *control_name,
ControlsMapping *mapping
);
static Simulation::SharedData*
GetSubsystemSharedData(const char *name);
static ResourceDescription::ResourceID
CreateControlMappingStream(
const char *mapping_name,
NotationFile *mapping_file,
FindNameFunction find_name,
ResourceFile *resource_file,
const char *model_name,
NotationFile *model_file,
const ResourceDirectories *directories,
PlatformTool *current_tool
);
VTV(
MakeMessage *creation_message,
SharedData &virtual_data = DefaultData
);
~VTV();
Logical
TestInstance() const;
enum {
RegularReset,
FootballReset,
MissionReviewReset
};
void
Reset(const Origin &new_origin, int reset_command);
//
// A respawn that lands on this vehicle's own step grid. Under fixed
// stepping the player's death recovery cannot ride the event queue -
// the queue runs on wall clock, and a Reset that fires at a wall
// instant lands between different sim steps on every run. Scheduled
// here instead, BeginStep applies it at the first step whose clock
// reaches 'due': same step count after death, every run, every
// frame rate. The goal point rides along because the turn-to-face-
// the-scorezone flip depends on the POST-reset heading, so it has
// to happen in the same step as the teleport.
//
void
ScheduleRespawn(
const Origin &new_origin,
const Time &due,
const Point3D &face_toward,
Logical have_goal
);
void
BeginStep();
//
// The instant this vehicle died, on its own step clock - stamped at
// the single site that sets BurningState, which runs inside the step
// machinery and is therefore already deterministic. The respawn
// schedule anchors HERE rather than at the moment the drop-zone
// reply happens to come off the event queue: the death is the last
// step-exact event in the chain, so "one second after death" is the
// same step count on every run. Marked once per death; consuming it
// re-arms it for the next one.
//
void
MarkDeathClock()
{
if (!deathClockValid)
{
deathClock = GetLastPerformance();
deathClockValid = True;
}
}
Logical
ConsumeDeathClock(Time *when_out)
{
if (!deathClockValid)
{
return False;
}
*when_out = deathClock;
deathClockValid = False;
return True;
}
void
DeathShutdown(int shutdown_command);
char
vehicleBadge[20],
vehicleColor[20];
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Navigation support
protected:
// the pending step-grid respawn - see ScheduleRespawn
Origin
respawnOrigin;
Time
respawnDue,
deathClock;
Point3D
respawnGoal;
Logical
respawnPending,
respawnHaveGoal,
deathClockValid;
//
// The out-of-world tumble's own random stream. The global Random is
// shared with frame-cadence consumers - particles above all - so its
// position when a burning pod draws from it depends on how many
// frames have rendered, which is wall clock, which makes the tumble
// differ between identical runs. A per-vehicle generator seeded by
// creation order keeps the tumble looking random while drawing the
// same kicks at the same steps every run.
//
unsigned long
tumbleSeed;
Scalar
TumbleRandom()
{
tumbleSeed = tumbleSeed * 1103515245UL + 12345UL;
return (Scalar)((tumbleSeed >> 16) & 0x7FFF) / (Scalar) 32767;
}
Scalar
targetRangeExponent,
currentRangeExponent;
public:
Scalar
maxImpactSpeed;
};