diff --git a/RP/RPPLAYER.cpp b/RP/RPPLAYER.cpp index 40f13f4..45a6503 100644 --- a/RP/RPPLAYER.cpp +++ b/RP/RPPLAYER.cpp @@ -382,6 +382,96 @@ void RPPlayer::ResetAfterDeath(DropZone::ReplyMessage *message) ForceUpdate(); SetSimulationState(DropZoneAcquiredState); dropZoneLocation = message->dropZoneLocation; + + // + //------------------------------------------------------------------ + // Fixed-step: the RECOVERY goes on the vehicle's own step grid. + // + // The event queue runs on wall clock, so a Reset fired from it + // lands between different sim steps on every run - the crash was + // measured bit-identical between runs and the first divergence was + // the step after the pod stood back up. The vehicle applies the + // teleport itself at the first step one SIM second after now; the + // message still makes its round trip below, but only for the + // player-state bookkeeping - the handler leaves the physics to the + // schedule it can see is pending. + //------------------------------------------------------------------ + // + if (Simulation::FixedStep() > (Scalar) 0 && + playerVehicle != NULL && playerVehicle->GetClassID() == VTVClassID) + { + VTV *vtv = (VTV *) playerVehicle; + + // + // Anchored to the DEATH and quantized to a half-second grid. + // + // This handler runs when the drop-zone reply finally comes off + // the event queue, and the whole death-to-here chain is wall + // clock - the fry retries repost at Now()+2.0, and the measured + // arrival is about five sim-seconds after death, give or take a + // few STEPS of queue jitter. An anchor of death+1.0 is long past + // by then, so "fire at the next step" inherited the jitter + // whole. + // + // The death stamp is the last step-exact event in the chain, so: + // take the measured gap, add the second the old code waited, and + // round UP to the next half-second AFTER THE DEATH. The jitter + // is hundredths; the nearest grid boundary is tenths away; every + // run lands in the same cell and fires on the same step. The + // felt delay is the same six-ish seconds it has always been. + // + Time due; + Time death_mark; + if (vtv->ConsumeDeathClock(&death_mark)) + { + Scalar gap = vtv->GetLastPerformance() - death_mark; + const Scalar quantum = (Scalar) 0.5; + int cells = (int)((gap + (Scalar) 1.0) / quantum) + 1; + + due = death_mark; + due += quantum * (Scalar) cells; + } + else + { + due = vtv->GetLastPerformance(); + due += 1.0f; + } + vtv->ScheduleRespawn( + message->dropZoneLocation, due, + (goalEntity != NULL) + ? goalEntity->localOrigin.linearPosition + : Point3D(0.0f, 0.0f, 0.0f), + (goalEntity != NULL) ? True : False); + respawnScheduled = True; + + // + // Under the trace, say what was scheduled in run-comparable + // terms: the pad (identity by position), and how far ahead of + // the vehicle's clock the due time sits. Two runs that disagree + // here diverge before the physics gets a vote. + // + { + static int diag = -1; + if (diag < 0) + { + const char *setting = getenv("RP412PHYSTRACE"); + diag = (setting != NULL && atoi(setting) != 0) ? 1 : 0; + } + if (diag) + { + char buffer[160]; + sprintf(buffer, + "PhysTrace: respawn #%d scheduled, pad %.2f,%.2f " + "due in %.4f sim-s\n", + deathCount, + (double) message->dropZoneLocation.linearPosition.x, + (double) message->dropZoneLocation.linearPosition.z, + (double)(Scalar)(due - vtv->GetLastPerformance())); + DEBUG_STREAM << buffer << std::flush; + } + } + } + Time when = Now(); when += 1.0f; application->Post(HighEventPriority, this, message, when); @@ -422,6 +512,7 @@ void } AlwaysExecute(); deathCount = 0; + respawnScheduled = False; } // @@ -467,6 +558,26 @@ void { VTV *vtv = (VTV*)playerVehicle; Check(vtv); + + // + // A SCHEDULED respawn means the vehicle already holds - or has + // already applied - its teleport and goal-flip, on its own step + // grid. Resetting it AGAIN here, at whatever wall instant this + // message came off the queue, would re-teleport it mid-step and + // put the nondeterminism straight back. + // + // The flag, not "is fixed stepping on": this same leg also runs + // for the FIRST spawn of the mission, where the Reset below is + // what wakes a Mover out of its initial stasis. Gating on the + // mode alone skipped that wake-up and parked the pod, frozen at + // exactly its spawn point, for an entire race. + // + if (respawnScheduled) + { + respawnScheduled = False; + Check_Fpu(); + return; + } vtv->Reset(message->dropZoneLocation, VTV::RegularReset); } diff --git a/RP/RPPLAYER.h b/RP/RPPLAYER.h index dfe1eb2..f67dd76 100644 --- a/RP/RPPLAYER.h +++ b/RP/RPPLAYER.h @@ -291,6 +291,18 @@ public: Entity *goalEntity; + // + // True between ResetAfterDeath handing the recovery to the vehicle's + // step grid (fixed-step only) and the bookkeeping message coming back + // round. The message handler must NOT Reset the vehicle again in that + // window - but it MUST still Reset on the first spawn of the mission, + // which is what wakes a Mover out of its initial stasis. Gating on + // "is fixed stepping on" instead of on this flag skipped that wake-up + // and froze the pod on its pad for the whole race. + // + Logical + respawnScheduled; + private: static const IndexEntry AttributePointers[]; diff --git a/RP/VTV.cpp b/RP/VTV.cpp index d94e49f..91e0af0 100644 --- a/RP/VTV.cpp +++ b/RP/VTV.cpp @@ -803,9 +803,17 @@ void Check(*damageZones); (*damageZones)->TakeDamage(collision_damage); - localVelocity.angularMotion.x += 8.0f * Random - 4.0f; - localVelocity.angularMotion.y += 8.0f * Random - 4.0f; - localVelocity.angularMotion.z += 8.0f * Random - 4.0f; + // + // The tumble draws from the vehicle's OWN stream, not the + // global Random - the global one is shared with the frame + // loop's consumers (particles, mostly), so its position here + // depended on how many frames had rendered. This kick goes + // straight into physics state; it was the last wall-clocked + // input left in the whole death cycle. + // + localVelocity.angularMotion.x += 8.0f * TumbleRandom() - 4.0f; + localVelocity.angularMotion.y += 8.0f * TumbleRandom() - 4.0f; + localVelocity.angularMotion.z += 8.0f * TumbleRandom() - 4.0f; } } worldLinearAcceleration = zippy_accel; @@ -1273,6 +1281,18 @@ VTV::VTV( boosterSmokeDensity = 0.0f; doorHitNormal = Vector3D::Identity; lastDoorHit = Time::Null; + respawnPending = False; + respawnHaveGoal = False; + deathClockValid = False; + + // + // Creation order is deterministic, so each vehicle's tumble stream + // is too - see TumbleRandom in the header. + // + { + static unsigned long tumble_births = 0; + tumbleSeed = 0x52503431UL + 7919UL * ++tumble_births; + } heightAboveTerrain = 0.0f; forwardVelocity = 0.0f; hornBlast = -1; @@ -1685,6 +1705,101 @@ void Check_Fpu(); } +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +void + VTV::ScheduleRespawn( + const Origin &new_origin, + const Time &due, + const Point3D &face_toward, + Logical have_goal + ) +{ + Check(this); + Check(&new_origin); + + respawnOrigin = new_origin; + respawnDue = due; + respawnGoal = face_toward; + respawnHaveGoal = have_goal; + respawnPending = True; + Check_Fpu(); +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// The pending respawn lands here, at the top of the first STEP whose clock +// has reached its due time - the same step count after death on every run +// and every frame rate. The old path applied the Reset from the event +// queue, which runs on wall clock: the crash was deterministic and the +// recovery was not, measured as two identical runs diverging on the first +// step after the pod stood back up. +// +// The turn-to-face-the-scorezone flip is the same arithmetic +// RPPlayer::PointVTVTowardGoal does, done here because it reads the +// POST-reset heading - it belongs to the same step as the teleport. +// +void + VTV::BeginStep() +{ + Check(this); + + if (respawnPending && !(GetLastPerformance() < respawnDue)) + { + respawnPending = False; + + { + static int diag = -1; + if (diag < 0) + { + const char *setting = getenv("RP412PHYSTRACE"); + diag = (setting != NULL && atoi(setting) != 0) ? 1 : 0; + } + if (diag) + { + char buffer[120]; + sprintf(buffer, + "PhysTrace: respawn fired, %.4f sim-s late, pad %.2f,%.2f\n", + (double)(Scalar)(GetLastPerformance() - respawnDue), + (double) respawnOrigin.linearPosition.x, + (double) respawnOrigin.linearPosition.z); + DEBUG_STREAM << buffer << std::flush; + } + } + + Reset(respawnOrigin, RegularReset); + + if (respawnHaveGoal) + { + Vector3D to_goal; + to_goal.Subtract(respawnGoal, localOrigin.linearPosition); + + UnitVector current_heading; + localToWorld.GetFromAxis(Z_Axis, ¤t_heading); + + Scalar length_to_goal = to_goal.LengthSquared(); + if (length_to_goal > SMALL) + { + Scalar dot_prod = + (to_goal * current_heading) / Sqrt(length_to_goal); + if (dot_prod >= 0.0f) + { + Quaternion turn_around; + Quaternion y_roll(0.0f, 1.0f, 0.0f, 0.0); + + turn_around.Multiply( + localOrigin.angularPosition, y_roll); + localOrigin.angularPosition = turn_around; + localToWorld = localOrigin; + } + } + ForceUpdate(); + } + } + Mover::BeginStep(); + Check_Fpu(); +} + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // void @@ -3100,6 +3215,16 @@ void if (damageLevel >= 1.0f) { vtv->SetSimulationState(VTV::BurningState); + + // + // Stamp the death on the vehicle's own step clock, here at the + // one site that declares it dead. The respawn schedule anchors + // to this instant - the last step-exact event in the death + // chain - so the recovery lands the same number of steps after + // the crash on every run. Marked once; repeated damage while + // already burning does not move it. + // + vtv->MarkDeathClock(); } // diff --git a/RP/VTV.h b/RP/VTV.h index 67df69a..fd3a644 100644 --- a/RP/VTV.h +++ b/RP/VTV.h @@ -504,6 +504,58 @@ public: 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); @@ -514,6 +566,38 @@ public: //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // 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;