The first cut hung the snapshot off Mover::BeginStep, which is inside Entity::PerformAndWatch's fixed-step interleave - and that interleave sits entirely inside "if (GetInstance() != ReplicantInstance)". A replicant never runs it; it reaches the step loop through Simulation::PerformAndWatch instead. Every remote pod is a replicant, so on a Live Cam the camera was being interpolated while the car it was watching still stepped. Smoother, and most of the way to nowhere - which is exactly what "still some hitching" was. So the hooks move to Simulation::PerformTo, where both paths meet: SnapshotRenderOrigin before each Perform, SetRenderStepFraction after the loop, two virtuals that do nothing by default and are overridden by Entity because Entity owns the origin. Mover::BeginStep goes back to what it was, so there is now one mechanism instead of two. Taking the snapshot inside the step loop is also strictly better placed than BeginStep was: it lands immediately before the integration, and still after any BeginStep teleport, so a VTV's scheduled respawn stays a cut. Entity::PerformAndWatch keeps computing the fraction itself after its interleave, because there PerformTo is called once per step with a till one step ahead and so sees no leftover at all - it needs the FRAME's till, which only the interleave has. Determinism re-proved, and more thoroughly than the first time. The scripted lap at 240 fps, interpolation on and off, on both the old build and this one: all four runs agree to the last decimal at the same simulation time - pos -15.06739 3.01541 388.02603 at t=15.260. The one "differing" sample in the raw comparison was the trace sampling at t=1.260 in one run and t=1.280 in the other and then realigning, not divergence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
675 lines
16 KiB
C++
675 lines
16 KiB
C++
#pragma once
|
|
|
|
#include "simulate.h"
|
|
#include "origin.h"
|
|
#include "linmtrx.h"
|
|
|
|
class Player;
|
|
class NotationFile;
|
|
class DamageZone;
|
|
class Damage;
|
|
class Subsystem;
|
|
|
|
#define ENTITY_CONTINUATION
|
|
#include "entity2.h"
|
|
|
|
//##########################################################################
|
|
//########################### Entity #################################
|
|
//##########################################################################
|
|
|
|
class Entity:
|
|
public Simulation
|
|
{
|
|
friend class Entity__StaticVideoSocketIterator;
|
|
friend class Entity__DynamicVideoSocketIterator;
|
|
friend class Entity__AudioSocketIterator;
|
|
friend class Entity__GaugeSocketIterator;
|
|
friend class Entity__AudioLocationIterator;
|
|
|
|
//##########################################################################
|
|
// Shared Data support
|
|
//
|
|
public:
|
|
typedef Entity__SharedData SharedData;
|
|
SharedData*
|
|
GetSharedData();
|
|
|
|
static Derivation *GetClassDerivations();
|
|
|
|
static SharedData DefaultData;
|
|
|
|
//##########################################################################
|
|
// Message Support
|
|
//
|
|
public:
|
|
enum {
|
|
MakeMessageID = Simulation::NextMessageID,
|
|
MakeReadyMessageID,
|
|
RemakeEntityMessageID,
|
|
RemakeReadyMessageID,
|
|
RemakeCompleteMessageID,
|
|
RemakeIncompleteMessageID,
|
|
DestroyEntityMessageID,
|
|
TransferEntityMessageID,
|
|
TransferCompleteMessageID,
|
|
TransferIncompleteMessageID,
|
|
BecomeInterestingMessageID,
|
|
BecomeUninterestingMessageID,
|
|
UpdateMessageID,
|
|
SubscribeReplicantMessageID,
|
|
UnsubscribeReplicantMessageID,
|
|
TakeDamageMessageID,
|
|
TakeDamageStreamMessageID,
|
|
PlayerLinkMessageID,
|
|
NextMessageID
|
|
};
|
|
|
|
typedef Entity__Message Message;
|
|
typedef Entity__MakeMessage MakeMessage;
|
|
typedef Entity__MakeReadyMessage MakeReadyMessage;
|
|
typedef Entity__RemakeEntityMessage RemakeEntityMessage;
|
|
typedef Entity__RemakeReadyMessage RemakeReadyMessage;
|
|
typedef Entity__RemakeCompleteMessage RemakeCompleteMessage;
|
|
typedef Entity__RemakeIncompleteMessage RemakeIncompleteMessage;
|
|
typedef Entity__DestroyEntityMessage DestroyEntityMessage;
|
|
typedef Entity__TransferEntityMessage TransferEntityMessage;
|
|
typedef Entity__TransferCompleteMessage TransferCompleteMessage;
|
|
typedef Entity__TransferIncompleteMessage TransferIncompleteMessage;
|
|
typedef Entity__BecomeInterestingMessage BecomeInterestingMessage;
|
|
typedef Entity__BecomeUninterestingMessage BecomeUninterestingMessage;
|
|
typedef Entity__UpdateMessage UpdateMessage;
|
|
typedef Entity__SubscribeReplicantMessage SubscribeReplicantMessage;
|
|
typedef Entity__UnsubscribeReplicantMessage UnsubscribeReplicantMessage;
|
|
typedef Entity__TakeDamageMessage TakeDamageMessage;
|
|
typedef Entity__TakeDamageStreamMessage TakeDamageStreamMessage;
|
|
typedef Entity__PlayerLinkMessage PlayerLinkMessage;
|
|
typedef Entity__DynamicMessage DynamicMessage;
|
|
typedef Entity__MakeMapMessage MakeMapMessage;
|
|
|
|
|
|
void
|
|
Receive(Event *event);
|
|
|
|
void
|
|
LocalDispatch(Message *what)
|
|
{Receiver::Receive(Cast_Object(Receiver::Message*,what));}
|
|
|
|
void
|
|
Dispatch(Receiver::Message *what);
|
|
|
|
void
|
|
DispatchToReplicant(
|
|
Message *what,
|
|
HostID host
|
|
);
|
|
|
|
void
|
|
DispatchToReplicants(Message *what);
|
|
|
|
private:
|
|
static const HandlerEntry MessageHandlerEntries[];
|
|
|
|
protected:
|
|
//static MessageHandlerSet MessageHandlers;
|
|
static MessageHandlerSet& GetMessageHandlers();
|
|
|
|
//##########################################################################
|
|
// Attribute Support
|
|
//
|
|
public:
|
|
enum {
|
|
LocalToWorldAttributeID = Simulation::NextAttributeID,
|
|
LocalOriginAttributeID,
|
|
DamageZoneCountAttributeID,
|
|
DamageZonesAttributeID,
|
|
NextAttributeID
|
|
};
|
|
|
|
private:
|
|
static const IndexEntry AttributePointers[];
|
|
|
|
protected:
|
|
//static AttributeIndexSet AttributeIndex
|
|
static AttributeIndexSet& GetAttributeIndex();
|
|
|
|
//
|
|
// public attribute declarations go here
|
|
//
|
|
public:
|
|
LinearMatrix localToWorld;
|
|
Origin localOrigin;
|
|
int damageZoneCount;
|
|
DamageZone **damageZones;
|
|
|
|
//######################################################################
|
|
//################### Render interpolation #########################
|
|
//######################################################################
|
|
//
|
|
// The simulation advances in whole fixed steps (RP412PHYSICSHZ) and the
|
|
// renderer draws whenever it can, so at any frame rate that is not the
|
|
// step rate the drawn position only changes 50 times a second and is
|
|
// held for however many frames fall inside a step. That is visible as
|
|
// stepping, and it gets WORSE the faster the machine: at 240 fps each
|
|
// position is held for nearly five frames.
|
|
//
|
|
// So drawing interpolates. renderPreviousOrigin is this entity's origin
|
|
// at the START of the step it is currently in, snapshotted by
|
|
// Mover::BeginStep, and renderStepFraction is how far through that step
|
|
// the frame being drawn falls. GetRenderToWorld blends the two.
|
|
//
|
|
// RENDER ONLY. localOrigin and localToWorld are untouched, so physics,
|
|
// collision, scoring, the nav map's entity queries and the network
|
|
// update records all keep seeing exact stepped values - which is what
|
|
// keeps the simulation identical on every machine at every frame rate.
|
|
// RP412PHYSTRACE is the proof of that and must not move.
|
|
//
|
|
// The picture therefore trails the simulation by up to one step (20 ms
|
|
// at 50 Hz), which is the standard price and much the lesser evil:
|
|
// extrapolating FORWARD instead has to guess, and overshoots into
|
|
// shimmer every time the guess is corrected.
|
|
//
|
|
// A teleport must not be smoothed - sliding a pod 300 metres across the
|
|
// map over 20 ms would be far worse than the cut it replaces. That
|
|
// falls out for free: VTV::BeginStep applies a scheduled respawn and
|
|
// THEN calls Mover::BeginStep, so the snapshot is taken after the
|
|
// teleport and the blend has nothing to travel.
|
|
//
|
|
Origin renderPreviousOrigin;
|
|
Scalar renderStepFraction;
|
|
|
|
//
|
|
// The transform to DRAW with. Falls back to localToWorld verbatim when
|
|
// interpolation is off, when there is no fixed step to interpolate
|
|
// within, or before the first snapshot exists - so the unfixed-step
|
|
// path behaves exactly as it always did.
|
|
//
|
|
void
|
|
GetRenderToWorld(LinearMatrix *out);
|
|
|
|
//
|
|
// Filled by Simulation::PerformTo around each fixed step, for locally
|
|
// simulated entities and replicants alike.
|
|
//
|
|
void
|
|
SnapshotRenderOrigin();
|
|
void
|
|
SetRenderStepFraction(Scalar fraction);
|
|
|
|
|
|
int
|
|
GetDamageZoneIndex(const CString &damage_zone_name) const;
|
|
|
|
|
|
//
|
|
// virtual access
|
|
//
|
|
public:
|
|
virtual Vector3D
|
|
GetWorldLinearVelocity()
|
|
{return Vector3D(0.0f, 0.0f, 0.0f);}
|
|
virtual Vector3D
|
|
GetWorldLinearAcceleration()
|
|
{return Vector3D(0.0f, 0.0f, 0.0f);}
|
|
|
|
//##########################################################################
|
|
// Model Support
|
|
//
|
|
public:
|
|
UpdateMessage*
|
|
Execute(const Time &till);
|
|
|
|
Subsystem*
|
|
GetSubsystem(int index)
|
|
{
|
|
Check(this); Verify((unsigned)index < subsystemCount);
|
|
return subsystemArray[index];
|
|
}
|
|
Simulation*
|
|
GetSimulation(int index);
|
|
int
|
|
GetSubsystemCount()
|
|
{Check(this); return subsystemCount;}
|
|
Subsystem*
|
|
FindSubsystem(const char* name);
|
|
|
|
typedef void
|
|
(Entity::*Performance)(Scalar time_slice);
|
|
|
|
typedef Entity__UpdateRecord UpdateRecord;
|
|
|
|
void
|
|
SetPerformance(Performance performance)
|
|
{
|
|
Check(this);
|
|
activePerformance = (Simulation::Performance)performance;
|
|
}
|
|
|
|
enum {
|
|
EntitySubsystemID = -1
|
|
};
|
|
|
|
static int
|
|
FindSubsytemID(
|
|
const char *model_name,
|
|
const char *subsystem_name
|
|
);
|
|
public:
|
|
|
|
enum {
|
|
DamageZoneUpdateModelBit = Simulation::NextUpdateModelBit,
|
|
NextUpdateModelBit
|
|
};
|
|
|
|
enum {
|
|
DamageZoneUpdateModelFlag = 1 << DamageZoneUpdateModelBit
|
|
};
|
|
|
|
Logical DamageZoneUpdateModelFlagSet()
|
|
{
|
|
Check(this);
|
|
return ((updateModel & DamageZoneUpdateModelFlag) != 0);
|
|
}
|
|
|
|
protected:
|
|
|
|
void
|
|
WriteUpdateRecord(
|
|
Simulation::UpdateRecord *message,
|
|
int update_model
|
|
);
|
|
void
|
|
UpdateMessageHandler(UpdateMessage *message);
|
|
|
|
void
|
|
ReadUpdateRecord(Simulation::UpdateRecord *message);
|
|
|
|
void
|
|
ReadDamageUpdateRecord(Simulation::UpdateRecord *update_record);
|
|
|
|
void
|
|
WriteDamageUpdateRecord(Simulation::UpdateRecord *update_record);
|
|
|
|
void
|
|
PerformAndWatch(
|
|
const Time &till,
|
|
MemoryStream *update_stream
|
|
);
|
|
|
|
int
|
|
subsystemCount;
|
|
Subsystem
|
|
**subsystemArray;
|
|
Origin
|
|
updateOrigin;
|
|
|
|
void
|
|
TakeDamageStreamMessageHandler(TakeDamageStreamMessage *message);
|
|
void
|
|
TakeDamageMessageHandler(TakeDamageMessage *message);
|
|
void
|
|
PlayerLinkMessageHandler(PlayerLinkMessage *message);
|
|
|
|
//##########################################################################
|
|
// Renderer Support
|
|
//
|
|
public:
|
|
void
|
|
AddStaticVideoComponent(Component *component);
|
|
void
|
|
AddDynamicVideoComponent(Component *component);
|
|
void
|
|
AddAudioComponent(Component *component);
|
|
|
|
typedef Entity__StaticVideoSocketIterator
|
|
StaticVideoSocketIterator;
|
|
typedef Entity__DynamicVideoSocketIterator
|
|
DynamicVideoSocketIterator;
|
|
typedef Entity__AudioSocketIterator
|
|
AudioSocketIterator;
|
|
|
|
virtual Enumeration
|
|
GetAudioRepresentation(Entity *linked_entity);
|
|
|
|
private:
|
|
SChainOf<Component*>
|
|
staticVideoSocket;
|
|
SChainOf<Component*>
|
|
dynamicVideoSocket;
|
|
SChainOf<Component*>
|
|
audioSocket;
|
|
|
|
//##########################################################################
|
|
// Flag Support
|
|
//
|
|
public:
|
|
//
|
|
// MasterInstance - created on owning host, replicated on remote hosts,
|
|
// sends updates to replicants
|
|
// ReplicantInstance - created on remote hosts, receives updates from
|
|
// MasterInstance
|
|
// IndependantInstance - created on owning host, replicated on remote
|
|
// hosts, never receives updates from MasterInstance
|
|
// HermitInstance - created on owning host, is not replicated, does
|
|
// send updates
|
|
//
|
|
enum {
|
|
InstanceBits = Simulation::NextBit,
|
|
ValidBit = InstanceBits+2,
|
|
TransferableBit,
|
|
InterestBit,
|
|
InterestLockedBit,
|
|
DynamicBit,
|
|
TrappedBit,
|
|
StatueBit,
|
|
MapBit,
|
|
PreRunBit,
|
|
CondemnedBit,
|
|
NextBit
|
|
};
|
|
enum Instance
|
|
{
|
|
MasterInstance=0,
|
|
ReplicantInstance=1<<InstanceBits,
|
|
IndependantInstance=2<<InstanceBits,
|
|
HermitInstance=3<<InstanceBits
|
|
};
|
|
enum {
|
|
InstanceMask = HermitInstance,
|
|
ValidFlag = 1<<ValidBit,
|
|
TransferableFlag = 1<<TransferableBit,
|
|
InterestFlag = 1<<InterestBit,
|
|
InterestLockedFlag = 1<<InterestLockedBit,
|
|
DynamicFlag = 1<<DynamicBit,
|
|
TrappedFlag = 1<<TrappedBit,
|
|
StatueFlag = 1<<StatueBit,
|
|
MapFlag = 1<<MapBit,
|
|
PreRunFlag = 1<<PreRunBit,
|
|
CondemnedFlag = 1<<CondemnedBit,
|
|
TypeMask = DynamicFlag|TrappedFlag|MapFlag,
|
|
// DefaultFlags = MasterInstance
|
|
DefaultFlags = DynamicFlag|MasterInstance
|
|
};
|
|
enum Type {
|
|
StaticType = 0,
|
|
DynamicType = DynamicFlag,
|
|
TrappedType = DynamicFlag|TrappedFlag,
|
|
StatueType = DynamicFlag|TrappedFlag|StatueFlag,
|
|
MapType = DynamicFlag|MapFlag,
|
|
TrappedMapType = DynamicFlag|TrappedFlag|MapFlag,
|
|
};
|
|
|
|
//##########################################################################
|
|
// Instance Type support
|
|
//
|
|
public:
|
|
void
|
|
SetInstance(Instance type)
|
|
{Check(this); simulationFlags &= ~InstanceMask; simulationFlags |= type;}
|
|
Instance
|
|
GetInstance()
|
|
{Check(this); return (Instance)(simulationFlags&InstanceMask);}
|
|
|
|
static LWord
|
|
EntityFlagsSetInstance(LWord entity_flags, Instance type)
|
|
{
|
|
entity_flags &= ~InstanceMask;
|
|
entity_flags |= type;
|
|
return entity_flags;
|
|
}
|
|
static Instance
|
|
EntityFlagsGetInstance(LWord entity_flags)
|
|
{return (Instance)(entity_flags&InstanceMask);}
|
|
|
|
//##########################################################################
|
|
// Validity Support
|
|
//
|
|
public:
|
|
void
|
|
SetValidFlag()
|
|
{Check(this); simulationFlags |= ValidFlag;}
|
|
void
|
|
SetInvalidFlag()
|
|
{Check(this); simulationFlags &= ~ValidFlag;}
|
|
Logical
|
|
IsValid()
|
|
{Check(this); return (simulationFlags&ValidFlag) != 0;}
|
|
|
|
void
|
|
SetPreRunFlag()
|
|
{Check(this); simulationFlags |= PreRunFlag;}
|
|
void
|
|
SetNoPreRunFlag()
|
|
{Check(this); simulationFlags &= ~PreRunFlag;}
|
|
Logical
|
|
IsPreRunnable()
|
|
{Check(this); return (simulationFlags&PreRunFlag) != 0;}
|
|
|
|
void
|
|
SetCondemnedFlag()
|
|
{Check(this); simulationFlags |= CondemnedFlag;}
|
|
Logical
|
|
IsCondemned()
|
|
{Check(this); return (simulationFlags&CondemnedFlag) != 0;}
|
|
|
|
//##########################################################################
|
|
// Transference Support
|
|
//
|
|
public:
|
|
void
|
|
SetTransferableFlag()
|
|
{Check(this); simulationFlags |= TransferableFlag;}
|
|
void
|
|
SetNontransferableFlag()
|
|
{Check(this); simulationFlags &= ~TransferableFlag;}
|
|
Logical
|
|
IsTransferable()
|
|
{Check(this); return (simulationFlags&TransferableFlag) != 0;}
|
|
|
|
const EntityID&
|
|
GetEntityID()
|
|
{return entityID;}
|
|
HostID
|
|
GetOwnerID()
|
|
{return ownerID;}
|
|
|
|
EntityID
|
|
entityID;
|
|
HostID
|
|
ownerID;
|
|
|
|
Player
|
|
*playerLink;
|
|
|
|
Player*
|
|
GetPlayerLink()
|
|
{return playerLink;}
|
|
|
|
//##########################################################################
|
|
// Interest Support
|
|
//
|
|
public:
|
|
void
|
|
BecomeInteresting();
|
|
void
|
|
BecomeInterestingMessageHandler(const Receiver::Message*);
|
|
void
|
|
BecomeUninterestingMessageHandler();
|
|
|
|
Logical
|
|
IsInteresting()
|
|
{Check(this); return interestCount != 0;}
|
|
|
|
void
|
|
SetInterestLockedFlag();
|
|
void
|
|
SetInterestUnlockedFlag();
|
|
Logical
|
|
IsInterestLocked()
|
|
{Check(this); return (simulationFlags&InterestLockedFlag) != 0;}
|
|
|
|
void
|
|
SetInterestZoneID(InterestZoneID interest_zone_ID)
|
|
{interestZoneID = interest_zone_ID;}
|
|
InterestZoneID
|
|
GetInterestZoneID()
|
|
{return interestZoneID;}
|
|
|
|
Logical
|
|
IsStatic()
|
|
{Check(this); return (simulationFlags&DynamicFlag) == 0;}
|
|
Logical
|
|
IsDynamic()
|
|
{Check(this); return (simulationFlags&DynamicFlag) != 0;}
|
|
|
|
void
|
|
SetTrappedFlag()
|
|
{Check(this); simulationFlags |= TrappedFlag;}
|
|
Logical
|
|
IsTrapped()
|
|
{Check(this); return (simulationFlags&TrappedFlag) != 0;}
|
|
|
|
Logical
|
|
IsStatue()
|
|
{Check(this); return (simulationFlags&StatueFlag) != 0;}
|
|
|
|
Logical
|
|
IsMap()
|
|
{Check(this); return (simulationFlags&MapFlag) != 0;}
|
|
|
|
static Logical
|
|
EntityFlagsIsMap(LWord entity_flags)
|
|
{return (entity_flags&MapFlag) != 0;}
|
|
|
|
Type
|
|
GetType()
|
|
{Check(this); return (Type)(simulationFlags&TypeMask);}
|
|
|
|
virtual Enumeration
|
|
GetInterestPriority(Entity *linked_entity);
|
|
|
|
InterestZoneID
|
|
interestZoneID;
|
|
int
|
|
interestCount;
|
|
Time
|
|
creationTime;
|
|
|
|
//##########################################################################
|
|
// Camera Support
|
|
//
|
|
protected:
|
|
|
|
Origin
|
|
cameraOffset;
|
|
|
|
public:
|
|
Origin
|
|
GetCameraOffset() const
|
|
{Check(this); return cameraOffset; }
|
|
|
|
//##########################################################################
|
|
// 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
|
|
//
|
|
virtual void
|
|
RespondToScoreMessage(Message *message) {}
|
|
|
|
//##########################################################################
|
|
// Construction and Destruction
|
|
//
|
|
public:
|
|
typedef Entity* (*MakeHandler)(MakeMessage *);
|
|
|
|
static Entity*
|
|
Make(MakeMessage *creation_message);
|
|
|
|
//
|
|
// Warning... This function requires a properly set up strtok!!!!
|
|
//
|
|
static Logical
|
|
CreateMakeMessage(
|
|
MakeMessage *creation_message,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories *directories
|
|
);
|
|
|
|
static Logical
|
|
CreateMakeMapMessage(
|
|
MakeMapMessage *creation_message,
|
|
ResourceDescription::ResourceID resource_id,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories * //directories
|
|
);
|
|
|
|
static ResourceDescription::ResourceID
|
|
CreateDamageZoneStream(
|
|
ResourceFile *resource_file,
|
|
const char *model_name,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories *directories
|
|
);
|
|
|
|
static ResourceDescription::ResourceID
|
|
CreateExplosionTableStream(
|
|
ResourceFile *resource_file,
|
|
const char *model_name,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories *directories
|
|
);
|
|
|
|
ResourceDescription::ResourceID
|
|
GetResourceID()
|
|
{Check(this); return resourceID;}
|
|
Player*
|
|
GetOwningPlayer()
|
|
{Check(this); return owningPlayer;}
|
|
|
|
void
|
|
SetupNetworkMessage(Entity::Message *message);
|
|
|
|
protected:
|
|
Entity(
|
|
MakeMessage *creation_message,
|
|
SharedData &virtual_data
|
|
);
|
|
|
|
ResourceDescription::ResourceID
|
|
resourceID;
|
|
Player
|
|
*owningPlayer;
|
|
|
|
void
|
|
DestroyEntityMessageHandler(Message *message);
|
|
|
|
public:
|
|
~Entity();
|
|
|
|
void
|
|
CondemnToDeathRow();
|
|
|
|
//##########################################################################
|
|
// Test Support
|
|
//
|
|
public:
|
|
Logical
|
|
TestInstance() const;
|
|
static Logical
|
|
TestClass();
|
|
};
|
|
|
|
inline void
|
|
Entity::SetupNetworkMessage(Entity::Message *message)
|
|
{
|
|
Check(this);
|
|
Check(message);
|
|
message->entityID = entityID;
|
|
message->interestZoneID = interestZoneID;
|
|
message->ownerID = ownerID;
|
|
}
|
|
|
|
#include "entity3.h"
|
|
#undef ENTITY_CONTINUATION |