diff --git a/MUNGA/ENTITY.cpp b/MUNGA/ENTITY.cpp index f4538b7..f74bbc4 100644 --- a/MUNGA/ENTITY.cpp +++ b/MUNGA/ENTITY.cpp @@ -406,7 +406,21 @@ void Check_Pointer(update); Simulation *simulation = GetSimulation((int)update->subsystemID-1); Check(simulation); - simulation->ReadUpdateRecord(update); + + // + // Skip a record the sender stamped before the one we already + // applied - see Simulation::AcceptUpdateStamp. Counted, because + // on an ordered stream this should never fire and the number + // saying so is the point of it. + // + if (simulation->AcceptUpdateStamp(update->timeStamp)) + { + simulation->ReadUpdateRecord(update); + } + else + { + netRaceStats.staleCount++; + } stream.AdvancePointer(update->recordLength); } diff --git a/MUNGA/MOVER.cpp b/MUNGA/MOVER.cpp index 47ad378..18cf1c0 100644 --- a/MUNGA/MOVER.cpp +++ b/MUNGA/MOVER.cpp @@ -84,6 +84,29 @@ static const Scalar kMaximumPredictedInterval = 0.25f; static const Scalar kLongGapThreshold = 0.200f; static const Scalar kQueuedGapThreshold = 0.005f; +// +// How far a replicant will coast on a sender that has gone quiet. +// +// Past the moment the next update was due, velocity times elapsed time +// stops being a prediction and becomes a guess that nobody is correcting. +// The field logs bear it out: gaps of one to two seconds on every pod in +// an eight-minute race, and worst-case corrections of one to two and a +// half KILOMETRES against a mean under a metre. That is a pod that flew +// most of the way across the map on stale velocity and then got yanked +// back - through whatever it passed through on the way. +// +// So it coasts for a second and then parks, waiting to be told. A pod +// that stops and then snaps once reads as a dropped connection, which is +// what it is; a pod sliding confidently through scenery reads as a +// broken game. Parking is also far cheaper to collide with. +// +// MUST MATCH on every machine in a race. This moves replicants rather +// than merely drawing them, so it decides where they collide - the same +// class as RP412NETPREDICT, and the lobby's build guard already keeps a +// mismatched build out of the room. +// +static const Scalar kMaximumExtrapolationSeconds = 1.0f; + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // RP412NETPREDICT=0 restores the original single-sample prediction, so the @@ -123,6 +146,31 @@ static Logical return cached ? True : False; } +// +// RP412NETCOAST, in seconds; 0 restores the original unbounded coast so +// the two can be compared on one build and one connection. See +// kMaximumExtrapolationSeconds - MUST MATCH across the race. +// +static Scalar + MaximumExtrapolationSeconds() +{ + static Logical read = False; + static Scalar seconds = kMaximumExtrapolationSeconds; + + if (!read) + { + read = True; + + const char *setting = getenv("RP412NETCOAST"); + if (setting != NULL && *setting != '\0') + { + Scalar asked = (Scalar) atof(setting); + seconds = (asked > 0.0f) ? asked : 0.0f; // 0 = no limit + } + } + return seconds; +} + //############################################################################# //############################### Mover ################################# //############################################################################# @@ -484,8 +532,18 @@ Logical } else { + // + // Past the expected update: coasting, not interpolating. Bound + // it - see kMaximumExtrapolationSeconds. + // time_slice = lastPerformance - lastUpdate; lerp_mode = False; + + Scalar coast_limit = MaximumExtrapolationSeconds(); + if (coast_limit > 0.0f && time_slice > coast_limit) + { + time_slice = coast_limit; + } } // @@ -538,8 +596,21 @@ Logical } else { + // + // Past the expected update: coasting, not interpolating. Bound + // it - see kMaximumExtrapolationSeconds. It matters more here + // than in the linear reckoner: this branch also carries a*t*t/2, + // so an unbounded slice grows the error as the SQUARE of the + // silence. + // time_slice = lastPerformance - lastUpdate; lerp_mode = False; + + Scalar coast_limit = MaximumExtrapolationSeconds(); + if (coast_limit > 0.0f && time_slice > coast_limit) + { + time_slice = coast_limit; + } } // diff --git a/MUNGA/SIMULATE.cpp b/MUNGA/SIMULATE.cpp index ea57d16..05486fa 100644 --- a/MUNGA/SIMULATE.cpp +++ b/MUNGA/SIMULATE.cpp @@ -403,6 +403,48 @@ void NetClock_ReportRaceStats() } } +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// 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 @@ -575,6 +617,7 @@ Simulation::Simulation( SetSimulationState(DefaultState); lastPerformance = Now(); lastUpdate = lastPerformance; + lastSenderStampValid = False; activePerformance = &Simulation::DoNothingOnce; updateModel = 0; simulationFlags = 0; diff --git a/MUNGA/SIMULATE.h b/MUNGA/SIMULATE.h index 78d93d3..2259a0a 100644 --- a/MUNGA/SIMULATE.h +++ b/MUNGA/SIMULATE.h @@ -209,6 +209,15 @@ public: void RequestEncore(Encore encore); + // + // True if this record should be applied, False if it is an older + // one arriving late. Ask BEFORE ReadUpdateRecord: it is the caller + // that has to skip the record, because ReadUpdateRecord is virtual + // and not every override chains to the base. + // + Logical + AcceptUpdateStamp(const Time &stamp); + virtual void ReadUpdateRecord(UpdateRecord *message); virtual void @@ -237,6 +246,17 @@ protected: lastPerformance; Time lastUpdate; + + // + // The sender's own stamp on the last record we accepted, for the + // stale guard in AcceptUpdateStamp. Kept in the sender's clock, not + // ours - it is only ever compared against another stamp from the + // same sender, so no offset is needed and none is applied. + // + Time + lastSenderStamp; + Logical + lastSenderStampValid; Word updateModel; diff --git a/RP_L4/RPL4ENVIRON.cpp b/RP_L4/RPL4ENVIRON.cpp index 3726ce1..3167b60 100644 --- a/RP_L4/RPL4ENVIRON.cpp +++ b/RP_L4/RPL4ENVIRON.cpp @@ -274,6 +274,22 @@ namespace "# same on every machine in a race.\n" "#RP412NETPREDICT=0\n" "\n" +"# How many seconds another player's pod will coast when their updates\n" +"# stop arriving, before it parks and waits. Default 1.\n" +"#\n" +"# Between updates a pod is carried forward on its last known velocity.\n" +"# That is a good guess for the length of one gap and a worse one every\n" +"# moment after: a race log from four pods showed gaps of one to two\n" +"# seconds each, and pods being corrected by as much as two and a half\n" +"# kilometres - most of the way across the map, through whatever was in\n" +"# the way, then snapped back. Stopping after a second reads as what it\n" +"# actually is, somebody's connection going quiet.\n" +"#\n" +"# 0 restores the old behaviour of coasting indefinitely. Like\n" +"# RP412NETPREDICT this changes how other pods MOVE and so where they\n" +"# collide - keep it the same on every machine in a race.\n" +"#RP412NETCOAST=1\n" +"\n" "# How long one background pass may spend drawing cockpit gauges, in\n" "# milliseconds. The gauges and the MFD/map displays are redrawn in the\n" "# time left over after the 3D view; on a big, busy map there is none\n"