Utility class that maps station aliases to authoritative TestTrack station keys, resolves station positions and facings, maps car id strings to CarDefinition, and provides defaults per maneuver.
namespace VehicleProto;
/// <summary>
/// Canonical station/car resolution, shared by the play-mode pilot, the editor McpTools
/// (<c>Editor/VpTools.cs</c>), <see cref="CarSwitcher"/>, and the part-kit console commands.
/// Reconciles the station ids used by <c>specs/maneuvers/*.json</c> + <c>vp_test.py</c> with the
/// authoritative <see cref="TestTrack.Stations"/> registry, maps car ids to
/// <see cref="CarDefinitions"/>, and picks a default station per maneuver for the ConVar on-ramp.
///
/// Extracted verbatim from <see cref="VehiclePilot"/> in the pilot decomposition.
/// <see cref="VehiclePilot.ResolveCar"/> /
/// <see cref="VehiclePilot.ResolveStation"/> remain as thin forwarders so external callers that
/// are out of that task's edit scope (VpTools, PartKitCommands) keep compiling unchanged — this is
/// the ONE home for the logic, the pilot just re-exports the seam.
/// </summary>
public static class StationCarRegistry
{
/// <summary>Alias table: spec/station ids used by specs/maneuvers/*.json and vp_test.py's
/// STATIONS set, mapped onto the authoritative TestTrack.Stations keys. Chosen over renaming
/// the registry so both the drafted specs AND the descriptive registry keys keep working.</summary>
static readonly Dictionary<string, string> StationAliases = new( StringComparer.OrdinalIgnoreCase )
{
["openpad"] = "jturnpad",
["jturn"] = "jturnpad",
["hillgrade"] = "hillclimb",
// crashwall -> the RESERVED plot only (crash/destruction is out of scope for this kit). The
// station is kept as a reference/reserved spawn point; no `crash` maneuver consumes it here.
["crashwall"] = "crashwall_reserved",
};
/// <summary>Every station id <see cref="ResolveStation"/> accepts — the authoritative
/// <see cref="TestTrack.Stations"/> keys plus the spec aliases. Empty until the world is built.
/// vp_status returns this as the measurement-world census so the runner can fail closed on a
/// referenced-but-absent station BEFORE it drives (audit 2026-07-13 HIGH).</summary>
public static IEnumerable<string> ResolvableStationIds()
{
var stations = TestTrack.Stations;
if ( stations is null )
yield break;
foreach ( var k in stations.Keys )
yield return k;
// aliases only resolve when their target station exists
foreach ( var kv in StationAliases )
if ( stations.ContainsKey( kv.Value ) )
yield return kv.Key;
}
public static bool ResolveStation( string id, out Vector3 posMeters, out Rotation facing )
{
posMeters = Vector3.Zero;
facing = Rotation.Identity;
if ( string.IsNullOrEmpty( id ) )
return false;
var key = id.Trim();
var stations = TestTrack.Stations;
if ( stations is null || stations.Count == 0 )
return false;
if ( !stations.TryGetValue( key, out var s ) )
{
if ( !StationAliases.TryGetValue( key, out var aliased ) || !stations.TryGetValue( aliased, out s ) )
return false;
}
posMeters = s.posMeters;
facing = s.facing;
return true;
}
public static CarDefinition ResolveCar( string id )
=> (id ?? "").Trim().ToLowerInvariant() switch
{
"kart" => CarDefinitions.Kart,
"coupe" => CarDefinitions.Coupe,
"pickup" => CarDefinitions.Pickup, // pickup part kit
_ => CarDefinitions.Hatch, // default + "hatch" = the hatch_kit part kit
};
/// <summary>Strict car resolution for AUTOMATION callers (vp_spawn/vp_drive): an UNKNOWN id
/// returns false rather than silently selecting the hatch (audit 2026-07-13 MEDIUM — an
/// acknowledgement must mean the requested car was valid). The player-facing UI paths
/// (<see cref="CarSwitcher"/>, session menu) keep the forgiving <see cref="ResolveCar"/>
/// fallback-to-hatch on purpose.</summary>
public static readonly string[] KnownCarIds = { "hatch", "coupe", "kart", "pickup" };
public static bool TryResolveCar( string id, out CarDefinition def )
{
var key = (id ?? "").Trim().ToLowerInvariant();
if ( Array.IndexOf( KnownCarIds, key ) < 0 )
{
def = null;
return false;
}
def = ResolveCar( key );
return true;
}
public static string DefaultStationFor( string man ) => man switch
{
"launch" or "topspeed" => "dragstrip",
"brake" => "brakezone",
"skidpad" or "figure8" => "skidpad",
"slalom" => "slalom",
"jturn" or "spinrecovery" => "jturnpad",
"jump" => "ramps",
"washboard" => "washboard",
"hillclimb" => "hillclimb",
"liftoff" => "bankedcurve",
// "crash" is out of scope for this kit (no destruction sim) — no default station here; a
// crash maneuver is unknown in this repo and the runner rejects it before drive.
_ => "dragstrip",
};
}