The pod can drive a scripted lap

RP412INPUTSCRIPT names a timeline file - one row per change, throttle,
stick X and Y, pedals, held until the next row's time - and the pod
drives it instead of listening to the controls. Times are SIMULATION
seconds from the green light, evaluated per step in the one place every
mapper funnels through (VTVControlsMapper::InterpretControls), so the
same script is the same lap at any frame rate. Rows hold rather than
interpolate on purpose: interpolation would sample differently at
different physics rates, and nothing on this path is allowed to.

The script shares the green-light anchor with RP412PHYSTRACE - its
clock starts at the instant the vehicle is stopped dead - because a
timeline that starts when the loader happens to finish is a different
lap every run.

A race is only deterministic if somebody DRIVES it, and a human cannot
drive the same lap twice. The first scripted lap - full throttle, a
steer, a crash at speed - earned the harness immediately:

- The drive, the crash, the death and the respawn teleport were all
  BIT-EXACT between identical runs, through t=13.5. Collisions with
  world geometry and the damage path are step-deterministic, which is
  better news than the code reading suggested.

- The first divergence is the step AFTER the respawn: the DropZoneReply
  that stands a dead pod back up is posted at wall-clock Now()+1.0
  (RPPLAYER.cpp), so the reset lands on a different sim step every run
  and everything after is time-shifted. The crash is deterministic; the
  RECOVERY is not. That is the next fix, and it is now a measurement,
  not a theory.

Values are clamped at load, once and visibly, so a script asking for
throttle 2.0 cannot trip the mapper's own range Verifies. Off unless
the environment names a file; it would be a cheat in a real race.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-09 21:03:03 -05:00
co-authored by Claude Fable 5
parent c2e2df1dce
commit af52476603
6 changed files with 291 additions and 1 deletions
+16 -1
View File
@@ -16,6 +16,7 @@
#include "console.h"
#include "appmsg.h"
#include "evtstat.h"
#include "inputscript.h"
#if defined(TRACE_FOREGROUND_PROCESSING)
BitTrace Foreground_Processing("Foreground Processing");
@@ -631,7 +632,14 @@ Time endUpdate = Now();
const char *setting = getenv("RP412PHYSTRACE");
physTrace = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
}
if (physTrace && GetApplicationState() == RunningMission)
//
// The scripted-input harness shares this anchor: its clock has to
// start at the same instant the vehicle is stopped, or the script
// timeline shifts against the settling transient by however long
// the load happened to take.
//
if ((physTrace || RPInputScript_Active()) &&
GetApplicationState() == RunningMission)
{
static Logical traceStarted = False;
static Time traceOrigin;
@@ -686,11 +694,17 @@ Time endUpdate = Now();
//
traceOrigin = reset_mover->GetLastPerformance();
// the script's t=0 is this same instant
RPInputScript_Arm(traceOrigin);
DEBUG_STREAM << "PhysTrace: vehicle stopped "
<< "at the green light\n" << std::flush;
}
}
if (physTrace)
{
//
// Sampled on the SIMULATION's clock - the vehicle's own
// lastPerformance, which advances in whole fixed steps - so two
@@ -730,6 +744,7 @@ Time endUpdate = Now();
DEBUG_STREAM << buffer << std::flush;
}
}
}
}
}
+182
View File
@@ -0,0 +1,182 @@
#include "munga.h"
#pragma hdrstop
#include "inputscript.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//##########################################################################
// RP412INPUTSCRIPT - see the header for what and why. This file is the
// how: a timeline of rows parsed once, held in a fixed array, evaluated
// by walking to the last row at or before the asked-for time.
//##########################################################################
namespace
{
enum { inputScriptMaxRows = 256 };
struct InputScriptRow
{
float t;
float throttle;
float stickX;
float stickY;
float pedals;
};
InputScriptRow gRows[inputScriptMaxRows];
int gRowCount = 0;
int gLoaded = -1; // -1 not tried, 0 no script, 1 loaded
Logical gArmed = False;
Time gOrigin;
float ClampInto(float value, float low, float high)
{
if (value < low) return low;
if (value > high) return high;
return value;
}
void Load()
{
gLoaded = 0;
const char *path = getenv("RP412INPUTSCRIPT");
if (path == NULL || *path == '\0')
{
return;
}
FILE *file = fopen(path, "rt");
if (file == NULL)
{
DEBUG_STREAM << "InputScript: cannot read '" << path
<< "' - driving unscripted\n" << std::flush;
return;
}
char line[256];
float last_t = -1.0f;
while (fgets(line, sizeof(line), file) != NULL &&
gRowCount < inputScriptMaxRows)
{
InputScriptRow row;
if (sscanf(line, " %f %f %f %f %f",
&row.t, &row.throttle, &row.stickX,
&row.stickY, &row.pedals) != 5)
{
continue; // comments, blanks, ragged lines
}
//
// Clamped HERE, not at sample time, so a script asking for
// throttle 2.0 is corrected once and visibly rather than
// silently every step - and the mapper's own Verify range
// checks can never trip on scripted input.
//
row.throttle = ClampInto(row.throttle, 0.0f, 1.0f);
row.stickX = ClampInto(row.stickX, -1.0f, 1.0f);
row.stickY = ClampInto(row.stickY, -1.0f, 1.0f);
row.pedals = ClampInto(row.pedals, -1.0f, 1.0f);
if (row.t < last_t)
{
DEBUG_STREAM << "InputScript: row at t=" << row.t
<< " is out of order - dropped\n" << std::flush;
continue;
}
last_t = row.t;
gRows[gRowCount++] = row;
}
fclose(file);
if (gRowCount > 0)
{
gLoaded = 1;
DEBUG_STREAM << "InputScript: '" << path << "', " << gRowCount
<< " row(s), last at t=" << gRows[gRowCount - 1].t
<< "s\n" << std::flush;
}
else
{
DEBUG_STREAM << "InputScript: '" << path
<< "' held no usable rows - driving unscripted\n" << std::flush;
}
}
}
int
RPInputScript_Active()
{
if (gLoaded < 0)
{
Load();
}
return (gLoaded == 1) ? 1 : 0;
}
void
RPInputScript_Arm(const Time &origin)
{
if (!RPInputScript_Active())
{
return;
}
gOrigin = origin;
gArmed = True;
DEBUG_STREAM << "InputScript: armed at the green light\n" << std::flush;
}
int
RPInputScript_Sample(
const Time &now,
float *throttle_out,
float *stick_x_out,
float *stick_y_out,
float *pedals_out
)
{
if (!gArmed || gLoaded != 1)
{
return 0;
}
Scalar t = now - gOrigin;
if (t < (Scalar) 0)
{
t = (Scalar) 0;
}
//
// The last row at or before t holds; before the first row, neutral.
// A linear walk, but the list is tiny and already ordered.
//
const InputScriptRow *current = NULL;
for (int i = 0; i < gRowCount; ++i)
{
if (gRows[i].t <= (float) t)
{
current = &gRows[i];
}
else
{
break;
}
}
if (current == NULL)
{
*throttle_out = 0.0f;
*stick_x_out = 0.0f;
*stick_y_out = 0.0f;
*pedals_out = 0.0f;
}
else
{
*throttle_out = current->throttle;
*stick_x_out = current->stickX;
*stick_y_out = current->stickY;
*pedals_out = current->pedals;
}
return 1;
}
+52
View File
@@ -0,0 +1,52 @@
#pragma once
//##########################################################################
// RP412INPUTSCRIPT - scripted analog input, on the simulation's clock.
//
// A race cannot be called deterministic until somebody DRIVES it, and a
// human cannot drive the same lap twice. This feeds the four analog
// channels the controls mapper interprets - throttle, stick X/Y, pedals -
// from a timeline file instead, evaluated against the mapper's own step
// clock, so the same script produces the same race at any frame rate.
//
// The file named by RP412INPUTSCRIPT= holds one row per change:
//
// # t throttle stickX stickY pedals
// 0.0 0.0 0 0 0
// 2.0 1.0 0 0 0
// 6.0 1.0 0.5 0 0
//
// Times are seconds of SIMULATION time from the green light. Each row
// HOLDS until the next row's time - a step function, no interpolation,
// because interpolation would sample differently at different physics
// rates and the whole point is that nothing does.
//
// Armed by the green-light anchor in Application::ExecuteForeground (the
// same instant RP412PHYSTRACE stops the pod dead), so the script clock,
// the trace clock and the vehicle's state all start together.
//
// Test harness: off unless the environment names a file, costs nothing
// when off, and it would be a cheat in a real race.
//##########################################################################
class Time;
// is a script named and readable? (parsed once, on first ask)
int
RPInputScript_Active();
// the green light: script time zero is this instant
void
RPInputScript_Arm(const Time &origin);
// evaluate at 'now' (a simulation clock, normally GetLastPerformance()).
// Returns 0 before Arm or with no script - callers leave their own
// values alone. Outputs are clamped to the mapper's legal ranges.
int
RPInputScript_Sample(
const Time &now,
float *throttle_out, // 0..1
float *stick_x_out, // -1..1
float *stick_y_out, // -1..1
float *pedals_out // -1..1
);
+1
View File
@@ -151,6 +151,7 @@
<ClCompile Include="..\MUNGA\INTEREST.cpp" />
<ClCompile Include="..\MUNGA\INTORGN.cpp" />
<ClCompile Include="..\MUNGA\ITERATOR.cpp" />
<ClCompile Include="..\MUNGA\INPUTSCRIPT.cpp" />
<ClCompile Include="..\MUNGA\JMOVER.cpp" />
<ClCompile Include="..\MUNGA\JOINT.cpp" />
<ClCompile Include="..\MUNGA\LAMP.cpp" />
+31
View File
@@ -5,6 +5,7 @@
#include "vtvpwr.h"
#include "..\munga\icom.h"
#include "..\munga\app.h"
#include "..\munga\inputscript.h"
#include "rpplayer.h"
#include "vtv.h"
@@ -414,6 +415,36 @@ void
VTVPower *power_system =
Cast_Object(VTVPower*, vtv->GetSubsystem(VTV::PowerSubsystem));
//
//----------------------------------------------------------------
// RP412INPUTSCRIPT: scripted driving, on this subsystem's own step
// clock.
//
// This is the one place every mapper - RIO, Thrustmaster, pad -
// funnels through, and it runs per SIMULATION STEP, so a scripted
// value lands on the same step of every run whatever the frame
// rate. Overriding at the RIO or the controls manager would key
// the timeline to the frame loop, which is wall clock, which is
// the thing the whole harness exists to keep out of the physics.
//
// Only the player's own vehicle: replicants get their state from
// the network, and the mapper does not run for them anyway.
//----------------------------------------------------------------
//
if (RPInputScript_Active())
{
float script_throttle, script_x, script_y, script_pedals;
if (RPInputScript_Sample(GetLastPerformance(),
&script_throttle, &script_x, &script_y, &script_pedals))
{
throttlePosition = script_throttle;
stickPosition.x = script_x;
stickPosition.y = script_y;
pedalsPosition = script_pedals;
}
}
//
//----------------------------------------------
// Make sure the control inputs are within range
+9
View File
@@ -299,6 +299,15 @@ namespace
"# taken, so it cannot wedge.\n"
"#RP412SPAWNZONE=3\n"
"\n"
"# Drive the pod from a timeline file instead of the controls - the\n"
"# same lap, exactly, every run. One row per change, held until the\n"
"# next row: time-in-seconds throttle stickX stickY pedals, values\n"
"# 0..1 for throttle and -1..1 elsewhere, # for comments. Times are\n"
"# SIMULATION seconds from the green light, so with RP412PHYSICSHZ set\n"
"# the same script is the same race at any frame rate - this is how\n"
"# driving, not just settling, gets verified bit-identical.\n"
"#RP412INPUTSCRIPT=testlap.txt\n"
"\n"
"# 1 = log the XInput-class controllers the generic-joystick scan skips\n"
"# (attached DirectInput devices are always logged). For debugging a pad\n"
"# that answers twice or a stick that does not answer at all.\n"