diff --git a/MUNGA/APP.cpp b/MUNGA/APP.cpp index ad438b5..93bd652 100644 --- a/MUNGA/APP.cpp +++ b/MUNGA/APP.cpp @@ -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; } } + } } } diff --git a/MUNGA/INPUTSCRIPT.cpp b/MUNGA/INPUTSCRIPT.cpp new file mode 100644 index 0000000..b7a1b0e --- /dev/null +++ b/MUNGA/INPUTSCRIPT.cpp @@ -0,0 +1,182 @@ +#include "munga.h" +#pragma hdrstop + +#include "inputscript.h" + +#include +#include +#include + +//########################################################################## +// 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; +} diff --git a/MUNGA/INPUTSCRIPT.h b/MUNGA/INPUTSCRIPT.h new file mode 100644 index 0000000..71af3f0 --- /dev/null +++ b/MUNGA/INPUTSCRIPT.h @@ -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 + ); diff --git a/MUNGA_L4/Munga_L4.vcxproj b/MUNGA_L4/Munga_L4.vcxproj index f4479f7..c0d2432 100644 --- a/MUNGA_L4/Munga_L4.vcxproj +++ b/MUNGA_L4/Munga_L4.vcxproj @@ -151,6 +151,7 @@ + diff --git a/RP/VTVMPPR.cpp b/RP/VTVMPPR.cpp index d1fed26..5ce6f61 100644 --- a/RP/VTVMPPR.cpp +++ b/RP/VTVMPPR.cpp @@ -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 diff --git a/RP_L4/RPL4ENVIRON.cpp b/RP_L4/RPL4ENVIRON.cpp index 2730510..04c4eac 100644 --- a/RP_L4/RPL4ENVIRON.cpp +++ b/RP_L4/RPL4ENVIRON.cpp @@ -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"