Files
RP412/MUNGA/SIMULATE.cpp
T
CydandClaude Opus 5 d9149a6a8c A quiet pod coasts a second, then waits to be told
Build 2's first two items, L4 and L5, picked ahead of the rest of the
latency tier because the field data argues for them: a five-pod race on
4.12.233 kept time well - send interval median and p95 both ~32 ms - but
every pod saw gaps of one to two seconds, and corrections as large as
2702 metres against a mean under a metre. That is not a timing problem,
it is a pod flying most of the way across the map on stale velocity and
being yanked back through whatever it passed.

L5, the extrapolation clamp. Both dead reckoners carried
lastPerformance - lastUpdate into the projection unbounded once past the
expected update. They now stop at kMaximumExtrapolationSeconds, one
second, so a replicant coasts and then parks. A pod that stops and snaps
once reads as the dropped connection it is; a pod sliding confidently
through scenery reads as a broken game, and is far more expensive to
collide with. It matters more in the accelerated reckoner, which also
carries a*t*t/2 and so grows the error as the SQUARE of the silence.
RP412NETCOAST sets the seconds, 0 restores the unbounded coast for an
A/B on one connection. MUST MATCH across a race, same class as
RP412NETPREDICT - it moves replicants, so it decides where they collide.

L4, the stale-record guard. Simulation now remembers the sender's stamp
on the last record it accepted, and the record walk skips one stamped
earlier - such a record winds lastUpdate backwards and has the reckoner
extrapolate from a position the sender has already left. A regression
larger than five seconds is not a late record but a different stream (a
rejoin, a restart, a clock that was set), so that resyncs instead. The
counter NetRaceStats::staleCount has been sitting there reading zero
with a comment saying "until the stale guard ships"; it is now fed.

The guard is asked by the caller rather than done inside
ReadUpdateRecord, because that is virtual and not every override chains
to the base.

Verified: clean Release build; two-pod loopback race green, both pods
scoring, and stale 0 on both - which is the reading that says the guard
does not fire on an ordered stream. The clamp is NOT demonstrated by
that run: the harness parks its pods, so there is no velocity to coast
on. Pod B did see a 9.167 s gap in it, which on a moving pod is the
shape of the field's kilometre corrections.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 21:35:22 -05:00

1205 lines
30 KiB
C++

#include "munga.h"
#pragma hdrstop
#include "simulate.h"
#include "update.h"
#include "app.h"
#if defined(TRACE_EXECUTE_WATCHERS)
static BitTrace Execute_Watchers("Execute Watchers");
#define SET_EXECUTE_WATCHERS() Execute_Watchers.Set()
#define CLEAR_EXECUTE_WATCHERS() Execute_Watchers.Clear()
#else
#define SET_EXECUTE_WATCHERS()
#define CLEAR_EXECUTE_WATCHERS()
#endif
//#############################################################################
//######################## StateIndicator ###############################
//#############################################################################
//#############################################################################
// Construction and Destruction
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
StateIndicator::StateIndicator():
audioWatcherSocket(NULL),
videoWatcherSocket(NULL),
gaugeWatcherSocket(NULL)
{
Check_Pointer(this);
stateCount = 0;
oldState = 0;
currentState = 0;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
StateIndicator::StateIndicator(unsigned max_states):
audioWatcherSocket(NULL),
videoWatcherSocket(NULL),
gaugeWatcherSocket(NULL)
{
Check_Pointer(this);
stateCount = max_states;
oldState = max_states;
currentState = max_states;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
StateIndicator::StateIndicator(const StateIndicator &state_indicator):
audioWatcherSocket(NULL),
videoWatcherSocket(NULL),
gaugeWatcherSocket(NULL)
{
Check_Pointer(this);
// Do not perform deep copy of watchers
stateCount = state_indicator.stateCount;
oldState = state_indicator.oldState;
currentState = state_indicator.currentState;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
StateIndicator::~StateIndicator()
{
Check(this);
//
// Manual deletion of existing watchers
//
{
SChainIteratorOf<Component*> iterator(&audioWatcherSocket);
#if DEBUG_LEVEL>2
Component *component;
while ((component = iterator.ReadAndNext()) != NULL)
{
Check(component);
Dump(component->GetClassID());
}
Warn(iterator.GetSize() != 0);
#endif
iterator.DeletePlugs();
}
{
SChainIteratorOf<Component*> iterator(&videoWatcherSocket);
#if DEBUG_LEVEL>2
Component *component;
while ((component = iterator.ReadAndNext()) != NULL)
{
Check(component);
Dump(component->GetClassID());
}
Warn(iterator.GetSize() != 0);
#endif
iterator.DeletePlugs();
}
{
SChainIteratorOf<Component*> iterator(&gaugeWatcherSocket);
#if DEBUG_LEVEL>2
Component *component;
while ((component = iterator.ReadAndNext()) != NULL)
{
Check(component);
Dump(component->GetClassID());
}
Warn(iterator.GetSize() != 0);
#endif
iterator.DeletePlugs();
}
Check_Fpu();
}
//#############################################################################
// State stuff
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
StateIndicator&
StateIndicator::operator=(const StateIndicator &state_indicator)
{
// Do not perform assignment of watchers
stateCount = state_indicator.stateCount;
oldState = state_indicator.oldState;
currentState = state_indicator.currentState;
Check_Fpu();
return *this;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Logical
StateIndicator::operator==(const StateIndicator &state_indicator) const
{
Check_Fpu();
return
(
stateCount == state_indicator.stateCount &&
oldState == state_indicator.oldState &&
currentState == state_indicator.currentState
);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
StateIndicator::SetState(unsigned new_state)
{
Check(this);
Verify(new_state < stateCount);
//
//--------------------------------------------------------------------------
// See if the state really changes
//
// NOTE - the old state does change to the current state, simulating a loop
// in the state engine. If it turns out that someone is watching the
// level of the state indicator and doing their own edge detection,
// this might possibly maybe screw something up
//--------------------------------------------------------------------------
//
oldState = currentState;
if (new_state == currentState)
{
return;
}
//
//-------------------------------------------------------------------
// If the state has changed, update the state values and then run any
// watchers
//-------------------------------------------------------------------
//
currentState = new_state;
Component *watcher;
SET_EXECUTE_WATCHERS();
// Audio
{
SChainIteratorOf<Component*> iterator(audioWatcherSocket);
Check(&iterator);
while ((watcher = iterator.ReadAndNext()) != NULL)
{
watcher->Execute();
}
}
// Video
{
SChainIteratorOf<Component*> iterator(videoWatcherSocket);
Check(&iterator);
while ((watcher = iterator.ReadAndNext()) != NULL)
{
watcher->Execute();
}
}
// Gauge
{
SChainIteratorOf<Component*> iterator(gaugeWatcherSocket);
Check(&iterator);
while ((watcher = iterator.ReadAndNext()) != NULL)
{
watcher->Execute();
}
}
CLEAR_EXECUTE_WATCHERS();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
std::ostream& operator << (std::ostream &strm, const StateIndicator &state_indicator)
{
Check(&state_indicator);
strm << "[" << state_indicator.stateCount << ",";
strm << state_indicator.oldState << ",";
strm << state_indicator.currentState << "]";
return strm;
}
//#############################################################################
// Test Support
//
Logical
StateIndicator::TestInstance() const
{
return True;
}
//#############################################################################
//########################## Simulation #################################
//#############################################################################
//#############################################################################
// Virtual Data support
//
Derivation* Simulation::GetClassDerivations()
{
static Derivation classDerivations(Receiver::GetClassDerivations(), "Simulation");
return &classDerivations;
}
Simulation::SharedData
Simulation::DefaultData(
Simulation::GetClassDerivations(),
Simulation::GetMessageHandlers(),
Simulation::GetAttributeIndex(),
Simulation::StateCount
);
//#############################################################################
// Model support
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//##########################################################################
// Net clock - see SIMULATE.h for why the sender's timestamp is estimated
// rather than used as it stands.
//##########################################################################
namespace
{
enum
{
netClockMaxPeers = 16,
// Samples per rolling minimum. A peer sends one record per
// simulation per frame, so at eight vehicles and 60 fps this is
// well under a second - fast enough to follow a route change,
// long enough that the minimum means something.
netClockWindow = 128,
// The furthest back we will believe a timestamp. Beyond this the
// packet is stale or the estimate is wrong, and extrapolating a
// vehicle half a second forward does more harm than the lag we
// are correcting.
netClockMaxLagTicks = 500
};
struct PeerClock
{
HostID host;
Logical inUse;
Logical settled;
long offsetTicks; // our clock - their clock
long windowMinTicks;
int windowCount;
//
// Race totals for the RP412NETLOG summary: how often a window
// close actually moved the estimate (>5 ms), and the worst such
// step. This is the decision input for whether a bounded slew is
// ever needed - a step mid-race means SDR rerouted or a clock
// drifted, and today it lands as one hard jump.
//
unsigned long raceStepCount;
long raceStepWorstMs;
};
PeerClock gPeerClocks[netClockMaxPeers];
HostID gUpdateSender = 0;
Logical gUpdateSenderValid = False;
Logical NetClockEnabled()
{
static int enabled = -1;
if (enabled < 0)
{
const char *setting = getenv("RP412NETCLOCK");
enabled = (setting != NULL && atoi(setting) == 0) ? 0 : 1;
if (!enabled)
{
DEBUG_STREAM << "NetClock: disabled by RP412NETCLOCK=0 - "
<< "replicants dead-reckon from arrival time\n" << std::flush;
}
}
return enabled ? True : False;
}
PeerClock *FindPeer(HostID host)
{
PeerClock *free_slot = NULL;
for (int i = 0; i < netClockMaxPeers; ++i)
{
if (gPeerClocks[i].inUse)
{
if (gPeerClocks[i].host == host)
{
return &gPeerClocks[i];
}
}
else if (free_slot == NULL)
{
free_slot = &gPeerClocks[i];
}
}
if (free_slot != NULL)
{
free_slot->inUse = True;
free_slot->host = host;
free_slot->settled = False;
free_slot->offsetTicks = 0;
free_slot->windowMinTicks = 0;
free_slot->windowCount = 0;
free_slot->raceStepCount = 0;
free_slot->raceStepWorstMs = 0;
}
return free_slot;
}
}
void NetClock_BeginUpdate(HostID sender)
{
gUpdateSender = sender;
gUpdateSenderValid = True;
}
void NetClock_EndUpdate()
{
gUpdateSenderValid = False;
}
void NetClock_Reset()
{
memset(gPeerClocks, 0, sizeof(gPeerClocks));
gUpdateSenderValid = False;
}
//
// Race-end summary, one line per peer (RP412NETLOG). The offset's
// absolute value only says how far apart the two processes launched
// (clocks are ms-since-process-start); the movement counters are the
// health signal - a mid-race step means SDR rerouted or a clock
// drifted, and today each one lands as a hard jump in every replicant
// from that peer. This is the decision input for the deferred bounded
// slew (plan item D2).
//
void NetClock_ReportRaceStats()
{
for (int i = 0; i < netClockMaxPeers; ++i)
{
PeerClock *peer = &gPeerClocks[i];
if (peer->inUse && peer->settled)
{
DEBUG_STREAM << "NetLog: peer " << peer->host
<< ": netclock offset " << peer->offsetTicks
<< " ms, window steps >5ms " << peer->raceStepCount
<< ", worst " << peer->raceStepWorstMs << " ms\n" << std::flush;
}
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// A record older than the one we last applied is a record that would
// wind this simulation backwards - the sender has already told us where
// it went next. Applying it drags lastUpdate back, and the dead reckoner
// then extrapolates from a position the sender has left, which is a
// correction the moment the next record lands.
//
// Today's streams are ordered, so this should read zero; the counter
// says so in the race summary and a nonzero reading is itself the
// finding. It is here because ordering is a property of the channel, not
// of the game, and the queue work has already made the channel something
// we change.
//
// A LARGE regression is not a late record, it is a different stream: a
// rejoin, a restart, a machine whose clock was set. Five seconds is well
// past any plausible reordering and well short of a session, so beyond
// it we take the new stamp as the truth and resync rather than ignoring
// the sender forever. Same reasoning as the predictor's own reset.
//
static const long kStaleRegressionLimitTicks = 5000; // ms; Time is ms here
Logical
Simulation::AcceptUpdateStamp(const Time &stamp)
{
Check(this);
if (lastSenderStampValid)
{
long regression = lastSenderStamp.ticks - stamp.ticks;
if (regression > 0 && regression < kStaleRegressionLimitTicks)
{
return False;
}
}
lastSenderStampValid = True;
lastSenderStamp = stamp;
return True;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
Simulation::ReadUpdateRecord(UpdateRecord *message)
{
Check(this);
Check_Pointer(message);
//
//------------------------------------------------------------------
// When this update arrived is not when it was taken. Put lastUpdate
// at the sender's sampling moment, expressed in our clock, so the
// dead reckoner extrapolates over the network latency instead of
// starting from scratch once it has already elapsed.
//------------------------------------------------------------------
//
long now_ticks = Now().ticks;
long local_ticks = now_ticks;
PeerClock *peer = gUpdateSenderValid && NetClockEnabled()
? FindPeer(gUpdateSender) : NULL;
if (peer != NULL)
{
//
// sample = trueOffset + oneWayLatency, so the running minimum
// converges on the offset from above.
//
long sample = now_ticks - message->timeStamp.ticks;
if (!peer->settled)
{
peer->settled = True;
peer->offsetTicks = sample;
peer->windowMinTicks = sample;
peer->windowCount = 0;
DEBUG_STREAM << "NetClock: host " << peer->host
<< " first sample, offset " << sample << " ms\n" << std::flush;
}
else
{
if (sample < peer->windowMinTicks)
{
peer->windowMinTicks = sample;
}
if (sample < peer->offsetTicks)
{
peer->offsetTicks = sample; // a shorter path: believe it now
}
if (++peer->windowCount >= netClockWindow)
{
//
// Close the window: adopt its minimum even if it is
// LARGER than the running estimate, which is how the
// figure follows clock drift and a route that got
// slower rather than staying pinned to one old packet.
//
long moved = peer->windowMinTicks - peer->offsetTicks;
long moved_abs = (moved < 0) ? -moved : moved;
if (moved_abs > 5)
{
// race totals for the NetLog summary (see PeerClock)
peer->raceStepCount++;
if (moved_abs > peer->raceStepWorstMs)
{
peer->raceStepWorstMs = moved_abs;
}
}
if (moved > 50 || moved < -50)
{
DEBUG_STREAM << "NetClock: host " << peer->host
<< " offset " << peer->offsetTicks << " -> "
<< peer->windowMinTicks << " ms\n" << std::flush;
}
peer->offsetTicks = peer->windowMinTicks;
peer->windowMinTicks = sample;
peer->windowCount = 0;
}
}
local_ticks = message->timeStamp.ticks + peer->offsetTicks;
//
// Never ahead of our own clock, and never further back than we
// are willing to extrapolate.
//
if (local_ticks > now_ticks)
{
local_ticks = now_ticks;
}
else if (now_ticks - local_ticks > netClockMaxLagTicks)
{
local_ticks = now_ticks - netClockMaxLagTicks;
}
}
lastUpdate.ticks = local_ticks;
SetSimulationState(message->simulationState);
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
Simulation::WriteUpdateRecord(
UpdateRecord *message,
int update_model
)
{
Check(this);
Check_Pointer(message);
message->timeStamp = lastPerformance;
message->simulationState = GetSimulationState();
message->recordLength = sizeof(*message);
message->recordID = (Word)update_model;
lastUpdate = lastPerformance;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
Simulation::WriteSimulationUpdate(MemoryStream *update_stream)
{
Check(this);
Check(update_stream);
//
//-----------------
// Write the update
//-----------------
//
int bit=0;
int update_model = updateModel;
updateModel = 0;
while (update_model)
{
if (update_model & 1)
{
UpdateRecord* update = (UpdateRecord*)update_stream->GetPointer();
WriteUpdateRecord(update, bit);
update_stream->AdvancePointer(update->recordLength);
}
update_model >>= 1;
++bit;
}
Check_Fpu();
}
//#############################################################################
// Construction and Destruction
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Simulation::Simulation(
Simulation::ClassID class_ID,
Simulation::SharedData &virtual_data
):
Receiver(class_ID, virtual_data),
simulationState(GetSharedData()->stateCount),
audioWatcherSocket(NULL),
videoWatcherSocket(NULL),
gaugeWatcherSocket(NULL),
effectWatcherSocket(NULL)
{
Check_Pointer(this);
SetSimulationState(DefaultState);
lastPerformance = Now();
lastUpdate = lastPerformance;
lastSenderStampValid = False;
activePerformance = &Simulation::DoNothingOnce;
updateModel = 0;
simulationFlags = 0;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Simulation::~Simulation()
{
Check(this);
//
// Watchers should be deleted by renderers by now
//
#if DEBUG_LEVEL>0
{
SChainIteratorOf<Component*> iterator(&audioWatcherSocket);
Verify(iterator.GetSize() == 0);
}
{
SChainIteratorOf<Component*> iterator(&videoWatcherSocket);
Verify(iterator.GetSize() == 0);
}
{
SChainIteratorOf<Component*> iterator(&gaugeWatcherSocket);
Verify(iterator.GetSize() == 0);
}
#endif
Check_Fpu();
}
//#############################################################################
// Attribute Support
//
const Simulation::AttributePointer
Simulation::NullAttribute = NULL;
const Simulation::IndexEntry
Simulation::AttributePointers[]=
{
{
Simulation::SimulationStateAttributeID,
"SimulationState",
(Simulation::AttributePointer)&Simulation::simulationState
}
};
Simulation::AttributeIndexSet& Simulation::GetAttributeIndex()
{
static Simulation::AttributeIndexSet attributeIndex(ELEMENTS(Simulation::AttributePointers),
Simulation::AttributePointers
);
return attributeIndex;
}
void*
Simulation::GetAttributePointer(Simulation::AttributeID attribute)
{
Check(this);
AttributePointer attr =
GetSharedData()->activeAttributeIndex->Find(attribute);
Check_Fpu();
if (attr == NullAttribute)
{
return NULL;
}
else
{
return &(this->*attr);
}
}
void*
Simulation::GetAttributePointer(const char* attribute_name)
{
Check(this);
AttributePointer attr =
GetSharedData()->activeAttributeIndex->Find(attribute_name);
Check_Fpu();
if (attr == NullAttribute)
{
return NULL;
}
else
{
return &(this->*attr);
}
}
//#############################################################################
// RP412PHYSICSHZ - the size of one simulation step, as a rate in hertz.
//
// The engine simulates TO a timestamp: every entity keeps a lastPerformance
// marking how far it has been simulated, and PerformAndWatch hands Perform()
// the difference. That difference used to be however long the last frame
// took, which made the frame rate part of the physics - explicitly so, since
// Mover scales its bounce and penetration thresholds by delta_t.
//
// Advancing lastPerformance in fixed steps instead makes it the accumulator
// a fixed-step loop needs, and every Perform() in the game gets an identical
// dt without one of them being touched.
//
// 0 restores the old behaviour for comparison. The RATE is a game-feel
// decision, not a technical one: thirty years of handling constants were
// tuned against the DOS build's 40 ms steps, and RP412 has been running
// ~18 ms variable ones, so the feel has already drifted. Whatever is chosen
// here becomes the canonical physics for pods and PCs alike.
//#############################################################################
static Scalar
FixedPhysicsStep()
{
static Scalar
step = (Scalar) -1;
if (step < (Scalar) 0)
{
const char
*setting = getenv("RP412PHYSICSHZ");
//
// 50 Hz is the default: a 20 ms step, exact on the millisecond
// clock, and the rate whose settled hover ride height measured
// closest to the frame-coupled physics the game has always run.
// Proven before it was defaulted - a scripted lap with a crash,
// a burn and two respawns runs bit-identical at 30, 60 and 144
// fps, and identical runs reproduce exactly. 0 restores the
// original frame-coupled behaviour, where the frame rate is
// part of the physics.
//
int rate = (setting != NULL) ? atoi(setting) : 50;
//
// Guard the arithmetic rather than the taste: a rate below the
// frame rate is a legitimate choice (the pods ran at 25), but a
// step of zero or a negative one is not a choice at all.
//
if (rate < 0)
{
rate = 0;
}
if (rate > 1000)
{
rate = 1000;
}
step = (rate > 0) ? ((Scalar) 1 / (Scalar) rate) : (Scalar) 0;
DEBUG_STREAM << "Physics: ";
if (rate > 0)
{
DEBUG_STREAM << "fixed step, " << rate << " Hz";
//
// The engine's clock counts MILLISECONDS, so a step is
// really round(1000/rate) ms. A rate that does not divide
// 1000 evenly therefore runs at a neighbouring rate wearing
// this one's name - 60 asks for 16.67 ms and gets 17, which
// is 58.8 Hz. Say so, and name the rates that mean what
// they say.
//
if ((1000 % rate) != 0)
{
int step_ms = (1000 + rate / 2) / rate;
DEBUG_STREAM << " - NOT millisecond-exact, steps will run "
<< step_ms << " ms (" << (1000.0f / (float) step_ms)
<< " Hz). 25, 50 and 100 are exact";
}
}
else
{
DEBUG_STREAM << "frame-coupled (RP412PHYSICSHZ=0)";
}
DEBUG_STREAM << "\n" << std::flush;
}
return step;
}
//
// How far behind one frame may catch up: a quarter second of simulation,
// whatever the rate - enough to ride out a texture load or an alt-tab,
// short of letting a stalled machine spiral. Counted in steps because the
// loop is, so 6 steps at 25 Hz, 12 at 50, 25 at 100.
//
static int
MaximumCatchUpSteps(Scalar step)
{
int steps = (int)((Scalar) 0.25 / step);
return (steps < 4) ? 4 : steps;
}
// how many fixed steps the whole simulation has taken - the trace prints it,
// so 'is the step actually fixed' is answered by measurement not by reading
long gPhysicsStepsTaken = 0;
//#############################################################################
// Simulation Support
//
Scalar
Simulation::FixedStep()
{
return FixedPhysicsStep();
}
void
Simulation::PerformTo(const Time& till)
{
Check(this);
Check(&till);
Scalar step = FixedPhysicsStep();
if (step > (Scalar) 0)
{
//
//------------------------------------------------------------------
// Fixed step. The simulation advances in whole steps of the same
// size on every machine, and whatever is left over waits for the
// next frame - lastPerformance is the accumulator, and always was.
//
// Before this, the slice was simply however long the last frame
// took, so a 30 fps machine integrated gravity in 33 ms steps and
// a 144 fps machine in 7 ms ones. Nothing in any Perform()
// changes: it is handed a dt it can rely on instead of one that
// depended on the graphics card.
//
// NOTE the caller decides the interleaving. An entity's spring
// forces are computed from its subsystems (the VTV reads its
// thrusters' measured heights), so the subsystems and the entity
// must advance TOGETHER, one step at a time -
// Entity::PerformAndWatch owns that loop and hands everyone the
// same sub-frame 'till'. Stepping a subsystem all the way to the
// frame boundary before its owner moves at all is how the first
// attempt at this produced a pod that climbed at 30 fps and flew
// level at 144: two spring impulses from one stale height sample.
//------------------------------------------------------------------
//
Scalar behind = till - lastPerformance;
int taken = 0;
int max_steps = MaximumCatchUpSteps(step);
while (behind >= step && taken < max_steps)
{
//
// Where this step STARTED, for drawing. Taken here rather than
// in BeginStep so it covers replicants too, and taken after any
// BeginStep teleport (a VTV's scheduled respawn) so a jump
// stays a cut instead of becoming a slide.
//
SnapshotRenderOrigin();
Perform(step);
++gPhysicsStepsTaken;
lastPerformance += step;
behind -= step;
++taken;
}
//
// How far past the last completed step the frame being drawn falls.
// Entity::PerformAndWatch computes this again from the FRAME's till
// after its interleave, because there this function is called once
// per step and sees no leftover at all.
//
{
Scalar fraction = behind / step;
if (fraction < (Scalar) 0) fraction = (Scalar) 0;
if (fraction > (Scalar) 1) fraction = (Scalar) 1;
SetRenderStepFraction(fraction);
}
//
// A machine that cannot keep up must not try to buy back the whole
// backlog next frame - that costs more time, which makes a bigger
// backlog. Drop what could not be run and carry on: the game slows
// down rather than seizing, and it does so identically everywhere.
//
if (taken >= max_steps && behind >= step)
{
lastPerformance = till;
}
}
else
{
Scalar slice = till - lastPerformance;
lastPerformance = till;
Perform(slice);
}
Check_Fpu();
}
void
Simulation::BeginStep()
{
// nothing by default - see the header
}
void
Simulation::SnapshotRenderOrigin()
{
// nothing by default - only an Entity has an origin to snapshot
}
void
Simulation::SetRenderStepFraction(Scalar)
{
// nothing by default - see the header
}
void
Simulation::WatchAndWrite(MemoryStream *update_stream)
{
Check(this);
if (!AreWatchersDelayed())
{
ExecuteWatchers();
}
WriteSimulationUpdate(update_stream);
Check_Fpu();
}
void
Simulation::PerformAndWatch(
const Time& till,
MemoryStream *update_stream
)
{
Check(this);
Check(&till);
PerformTo(till);
WatchAndWrite(update_stream);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
Simulation::DoNothingOnce(Scalar)
{
NeverExecute();
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
Simulation::DoNothing(Scalar)
{
Check_Fpu();
}
//#############################################################################
// Watcher Support
//
void
Simulation::ExecuteWatchers()
{
SET_EXECUTE_WATCHERS();
Component *watcher;
// Audio
{
SChainIteratorOf<Component*> iterator(audioWatcherSocket);
while ((watcher = iterator.ReadAndNext()) != NULL)
{
watcher->Execute();
}
}
// Video
{
SChainIteratorOf<Component*> iterator(videoWatcherSocket);
while ((watcher = iterator.ReadAndNext()) != NULL)
{
watcher->Execute();
}
}
// Gauge
{
SChainIteratorOf<Component*> iterator(gaugeWatcherSocket);
while ((watcher = iterator.ReadAndNext()) != NULL)
{
watcher->Execute();
}
}
// Effect
{
SChainIteratorOf<Component*> iterator(effectWatcherSocket);
while ((watcher = iterator.ReadAndNext()) != NULL)
{
watcher->Execute();
}
}
CLEAR_EXECUTE_WATCHERS();
}
//#############################################################################
// Test Support
//
Logical
Simulation::TestInstance() const
{
return IsDerivedFrom(*GetClassDerivations());
}
//#############################################################################
//################### Simulation::AttributeIndexSet #####################
//#############################################################################
const Simulation::AttributeIndexSet
Simulation::AttributeIndexSet::NullSet;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Simulation__AttributeIndexSet::~Simulation__AttributeIndexSet()
{
if (attributeIndex)
{
Unregister_Pointer(attributeIndex);
delete[] attributeIndex;
}
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
Simulation::AttributeIndexSet::Build(
Simulation::AttributeID count,
const Simulation::IndexEntry index_table[],
const Simulation::AttributeIndexSet *inheritance
)
{
//
//-------------------------------------------------------
// Find out the highest message type we have to deal with
//-------------------------------------------------------
//
Check(this);
Check_Pointer(index_table);
entryCount = 0;
Simulation::AttributeID i;
for (i=0; i<count; ++i)
{
if (index_table[i].entryID > entryCount)
{
entryCount = index_table[i].entryID;
}
}
if (inheritance)
{
Check(inheritance);
if (entryCount<inheritance->entryCount)
{
entryCount = inheritance->entryCount;
}
#if DEBUG_LEVEL>0
else if (entryCount > inheritance->entryCount)
{
i = inheritance->entryCount+1;
goto Check_Table;
}
#endif
}
else
{
Verify(entryCount == count);
#if DEBUG_LEVEL>0
i = 1;
Check_Table:
while (i <= entryCount)
{
int j;
for (j=0; j<count; ++j)
{
if (index_table[j].entryID == i)
{
break;
}
}
if (j == count)
{
break;
}
++i;
}
Verify(i > count);
#endif
}
//
//-----------------------------------------------------------------------
// Allocate the memory for the new handler set, and copy the inherited
// handlers to the new table. We are guaranteed to have enough space for
// the inherited table
//-----------------------------------------------------------------------
//
attributeIndex = new Simulation::IndexEntry[entryCount];
Check_Pointer(attributeIndex);
Register_Pointer(attributeIndex);
i = 0;
if (inheritance)
{
for (; i<inheritance->entryCount; ++i)
{
attributeIndex[i] = inheritance->attributeIndex[i];
}
}
//
//----------------------------------------------------------------------
// Step through the new table supplied, placing each handler in the slot
// determined by the message type
//----------------------------------------------------------------------
//
for (i=0; i<count; ++i)
{
Verify(!inheritance || index_table[i].entryID > inheritance->entryCount);
attributeIndex[index_table[i].entryID-1] = index_table[i];
}
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Simulation::AttributePointer
Simulation::AttributeIndexSet::Find(const char* attribute_name) const
{
Check(this);
Check_Pointer(attribute_name);
for (int attribute=0; attribute<entryCount; ++attribute)
{
if (!strcmp(attribute_name, attributeIndex[attribute].entryName))
{
Check_Fpu();
return attributeIndex[attribute].entryAddress;
}
}
Check_Fpu();
return Simulation::NullAttribute;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
const Simulation::IndexEntry*
Simulation::AttributeIndexSet::FindEntry(const char* attribute_name) const
{
Check(this);
Check_Pointer(attribute_name);
for (int attribute=0; attribute<entryCount; ++attribute)
{
if (!strcmp(attribute_name, attributeIndex[attribute].entryName))
{
Check_Fpu();
return &attributeIndex[attribute];
}
}
Check_Fpu();
return NULL;
}
void
Simulation::RequestEncore(Encore encore)
{
Check(this);
SetWatcherDelay();
Check(application);
UpdateManager *updater = application->GetUpdateManager();
Check(updater);
updater->RequestEncore(this, encore);
}
#if defined(TEST_CLASS) && 0
#include "model.tcp"
#endif