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>
183 lines
3.7 KiB
C++
183 lines
3.7 KiB
C++
#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;
|
|
}
|