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>
609 lines
13 KiB
C++
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;
|
|
};
|