Files
RP412/MUNGA/SIMULATE.h
T
CydandClaude Opus 5 82e733c1a6 Replicants reckon from when an update was sent
Simulation::ReadUpdateRecord threw away the sender's timestamp and
stamped lastUpdate with its own arrival time. The line carried the
original authors' own note: "HACK - should be based upon
message->timeStamp".

The dead reckoner extrapolates a replicant over
(lastPerformance - lastUpdate), so starting that clock at ARRIVAL rather
than at SEND leaves every remote vehicle exactly one network latency
behind where it should be. On the 1 ms LAN inside an arcade that is
nothing. Over Steam Datagram Relay it is 50-150 ms of positional lag on
every other player - a constant bias, not jitter, and the information
needed to remove it was already in the packet.

The timestamp cannot be used as it stands: both machines run
QueryPerformanceCounter since their own boot, so the two clocks share no
epoch. The offset is estimated per peer instead. Each record gives

    sample = ourNow - theirStamp = trueOffset + oneWayLatency

and latency is never negative, so the smallest sample seen is the
closest to the truth. A rolling minimum over 128 samples follows crystal
drift and re-adapts when a route gets slower, rather than being pinned
forever by one lucky packet; a shorter path is believed immediately.

Applied with two clamps: never ahead of our own clock, and never further
back than 500 ms. Past that the packet is stale or the estimate is
wrong, and throwing a vehicle half a second forward does more damage
than the lag being corrected.

Entity::UpdateMessageHandler is the only point on the receive path that
knows whose update this is - records carry a timestamp but not an owner -
so it publishes the sender around the loop, and only for entities
somebody else owns. Offsets are forgotten in CreateMission: the hosts in
the next race are not the hosts in the last one and a HostID gets reused.

RP412NETCLOCK=0 restores the arrival-time behaviour, documented in
environ.ini, so a test machine can compare the two without a rebuild.
The estimate is logged per host when it first settles and whenever it
moves more than 50 ms, which is what a three-machine session should be
read against.

WHAT IS AND IS NOT VERIFIED. A full single-player race runs unchanged -
the path is never entered without replicants, which is the regression
risk that reaches everybody. The behaviour this exists for needs real
latency between real machines and is therefore untested: a two-instance
loopback race would only have exercised the zero-latency case, where the
correction is a no-op by construction. Expect remote vehicles to sit
further forward than before, and watch for overshoot when somebody
changes direction sharply - that is the tradeoff this makes, and the
clamp above is what bounds it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:50:28 -05:00

423 lines
11 KiB
C++

#pragma once
#include "state.h"
#include "receiver.h"
#include "time.h"
#include "resource.h"
#include "hostid.h"
//##########################################################################
//########################### Net clock ##############################
//##########################################################################
//
// Aligning a peer's clock with ours, so a replicant is dead-reckoned from
// when its update was SENT rather than when it happened to arrive.
//
// Every update record carries the sender's own timestamp. The receiver
// used to throw it away and stamp lastUpdate with its own Now() - the
// original code says so: "HACK - should be based upon message->timeStamp".
// The dead reckoner then extrapolates over (lastPerformance - lastUpdate),
// so starting that clock at ARRIVAL rather than at SEND leaves every
// remote vehicle exactly one network latency behind where it should be.
// On the 1 ms arcade LAN that was invisible. Over Steam Datagram Relay it
// is a constant 50-150 ms of positional lag - a bias, not jitter.
//
// The timestamp cannot be used raw: two machines' clocks share no epoch,
// both being QueryPerformanceCounter since their own boot. So we estimate
// the offset per peer. Each arriving record gives
//
// sample = ourNow - theirStamp = trueOffset + oneWayLatency
//
// and since latency is never negative, the SMALLEST sample seen is the
// closest to the true offset. Taking a minimum over a short rolling
// window tracks crystal drift and re-adapts when the route changes,
// instead of being pinned forever by one lucky packet.
//
// RP412NETCLOCK=0 turns the whole thing off and restores the arrival-time
// behaviour, so a test machine can A/B it without a rebuild.
//
void NetClock_BeginUpdate(HostID sender); // around one message's records
void NetClock_EndUpdate();
void NetClock_Reset(); // forget every peer (new mission)
class Simulation__SharedData;
class Simulation__IndexData;
struct Simulation__IndexEntry;
class Simulation__AttributeIndexSet;
class MemoryStream;
//##########################################################################
//################# Simulation::UpdateRecord #########################
//##########################################################################
struct Simulation__UpdateRecord
{
public:
size_t recordLength;
Word subsystemID;
Word recordID;
Time timeStamp;
Enumeration simulationState;
};
//##########################################################################
//####################### Simulation #################################
//##########################################################################
class Simulation:
public Receiver
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
public:
typedef Simulation__SharedData SharedData;
SharedData*
GetSharedData();
static Derivation *GetClassDerivations();
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction Support
//
protected:
Simulation(
ClassID class_ID,
SharedData &shared_data
);
public:
~Simulation();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support
//
public:
typedef Enumeration AttributeID;
typedef Simulation__IndexData IndexData;
typedef Simulation__IndexEntry IndexEntry;
typedef Simulation__AttributeIndexSet AttributeIndexSet;
typedef int Simulation::*AttributePointer;
enum {
AnyAttributeID = 0,
SimulationStateAttributeID,
NextAttributeID
};
static const AttributePointer NullAttribute;
void*
GetAttributePointer(AttributeID attribute);
void*
GetAttributePointer(const char* attribute_name);
private:
static const IndexEntry AttributePointers[];
protected:
//static AttributeIndexSet AttributeIndex
static AttributeIndexSet& GetAttributeIndex();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation Support
//
public:
typedef void
(Simulation::*Performance)(Scalar time_slice);
typedef void
(Simulation::*Encore)();
typedef Simulation__UpdateRecord UpdateRecord;
void
SetPerformance(Performance performance)
{Check(this); activePerformance = performance;}
void
Perform(Scalar time_slice)
{
Check(this);
(this->*activePerformance)(time_slice);
}
virtual void
PerformAndWatch(
const Time& till,
MemoryStream *update_stream
);
void
DoNothingOnce(Scalar time_slice);
void
DoNothing(Scalar time_slice);
void
SetLastPerformance(const Time& when)
{Check(this); Check(&when); lastPerformance = when;}
void
RequestEncore(Encore encore);
virtual void
ReadUpdateRecord(UpdateRecord *message);
virtual void
WriteUpdateRecord(
UpdateRecord *message,
int update_model
);
void
WriteSimulationUpdate(MemoryStream *update_stream);
enum {
DefaultUpdateModelBit=0,
NextUpdateModelBit
};
enum {
DefaultUpdateModelFlag = 1<<DefaultUpdateModelBit
};
void
ForceUpdate(Word model=DefaultUpdateModelFlag)
{Check(this); updateModel |= model;}
protected:
Time
lastPerformance;
Time
lastUpdate;
Word
updateModel;
Performance
activePerformance;
//##########################################################################
// Flag Support
//
public:
enum {
DelayWatchersBit,
DontExecuteBit,
NextBit
};
enum {
DelayWatchersFlag = 1<<DelayWatchersBit,
DontExecuteFlag = 1<<DontExecuteBit
};
LWord simulationFlags;
void
SetWatcherDelay()
{Check(this); simulationFlags |= DelayWatchersFlag;}
void
ClearWatcherDelay()
{Check(this); simulationFlags &= ~DelayWatchersFlag;}
Logical
AreWatchersDelayed()
{Check(this); return (simulationFlags & DelayWatchersFlag) != 0;}
void
NeverExecute()
{Check(this); simulationFlags |= DontExecuteFlag;}
void
ExecuteOnUpdate()
{Check(this); simulationFlags |= DontExecuteFlag;}
void
AlwaysExecute()
{Check(this); simulationFlags &= ~DontExecuteFlag;}
Logical
IsReplicantExecutable()
{
Check(this);
return
(simulationFlags&DontExecuteFlag) == 0
|| lastUpdate >= lastPerformance;
}
Logical
IsNonReplicantExecutable()
{Check(this); return (simulationFlags&DontExecuteFlag) == 0;}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// State support
//
public:
enum {
DefaultState = 0,
StateCount
};
unsigned
GetSimulationState()
{Check(this); return simulationState.GetState();}
unsigned
GetOldSimulationState()
{Check(this); return simulationState.GetOldState();}
void
SetSimulationState(unsigned new_state)
{Check(this); simulationState.SetState(new_state);}
StateIndicator
simulationState;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Watcher Support
//
public:
void
AddAudioWatcher(Component *watcher)
{Check(&audioWatcherSocket);audioWatcherSocket.Add(watcher);}
void
AddVideoWatcher(Component *watcher)
{Check(&videoWatcherSocket);videoWatcherSocket.Add(watcher);}
void
AddGaugeWatcher(Component *watcher)
{Check(&gaugeWatcherSocket);gaugeWatcherSocket.Add(watcher);}
void
AddEffectWatcher(Component *watcher)
{Check(&effectWatcherSocket); effectWatcherSocket.Add(watcher);}
void
ExecuteWatchers();
private:
SChainOf<Component*>
audioWatcherSocket;
SChainOf<Component*>
videoWatcherSocket;
SChainOf<Component*>
gaugeWatcherSocket;
SChainOf<Component*>
effectWatcherSocket;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Support
//
public:
Logical
TestInstance() const;
static Logical
TestClass();
};
//##########################################################################
//################### Simulation::IndexEntry #########################
//##########################################################################
struct Simulation__IndexEntry
{
Enumeration
entryID;
const char *
entryName;
Simulation::AttributePointer
entryAddress;
};
#define ATTRIBUTE_ENTRY(class,name,attribute)\
{\
class::name##AttributeID,\
#name,\
(Simulation::AttributePointer) &class::attribute\
}
//##########################################################################
//################# Simulation::AttributeIndexSet ####################
//##########################################################################
class Simulation__AttributeIndexSet:
public Receiver::InheritanceSet
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
Simulation__AttributeIndexSet(
Simulation::AttributeID count,
const Simulation::IndexEntry index_table[],
const Simulation::AttributeIndexSet &inheritance
)
{Build(count, index_table, &inheritance);}
Simulation__AttributeIndexSet(
Simulation::AttributeID count,
const Simulation::IndexEntry index_table[]
)
{Build(count, index_table, NULL);}
Simulation__AttributeIndexSet()
{attributeIndex = NULL; entryCount = 0;}
~Simulation__AttributeIndexSet();
protected:
void
Build(
Simulation::AttributeID count,
const Simulation::IndexEntry index_table[],
const Simulation::AttributeIndexSet *inheritance
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// AttributeIndexSet Functionality
//
protected:
Simulation::IndexEntry
*attributeIndex;
public:
Simulation::AttributePointer
Find(Simulation::AttributeID attribute) const
{
Check(this);
Verify(attribute > 0);
if (attribute<=entryCount)
return attributeIndex[attribute-1].entryAddress;
else
return Simulation::NullAttribute;
}
Simulation::AttributePointer
Find(const char* attribute_name) const;
const Simulation::IndexEntry*
FindEntry(const char* attribute_name) const;
static const Simulation::AttributeIndexSet
NullSet;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Support
//
public:
static Logical
TestClass();
};
//##########################################################################
//################### Simulation::SharedData #########################
//##########################################################################
class Simulation__SharedData:
public Receiver::SharedData
{
public:
Simulation__SharedData(
Derivation* derivation,
Receiver::MessageHandlerSet &message_handlers,
Simulation::AttributeIndexSet &attribute_index,
int state_count
):
Receiver::SharedData(derivation, message_handlers),
activeAttributeIndex(&attribute_index),
stateCount(state_count)
{}
Simulation::AttributeIndexSet* activeAttributeIndex;
int stateCount;
};
inline Simulation::SharedData*
Simulation::GetSharedData()
{return Cast_Object(SharedData*,sharedData);}