The callsign and loadout outlive the session
The loadout has always survived a race - gPersistSelection is why the setup screen reopens the way you left it - but only for as long as the process lived. Closing the game was a reset, and the callsign is the one thing on that screen a player types rather than picks, so it was the one they had to type again every launch. pilot.cfg beside bindings.txt now holds both, KEY=VALUE like environ.ini, one line per group. BT411 solved this first, in fe_last.ini, and its own comment says why RP412 never grew the file: BT411 relaunches the process between missions and would otherwise forget the loadout mid-evening, while RP412 stays in one process. That made the gap invisible from inside a session and total across two. Same idea, two differences worth naming: BT411 saves only on a launch - it returns before SavePersisted when the player quits. That loses a callsign typed by somebody who then changed their mind, which is exactly the moment this feature exists for, so this writes on the way out however the menu is left: launching, stepping into a lobby, or EXIT GAME. BT411 takes the stored name as-is. A callsign here is quoted into frontend.egg, joined into a comma-separated list for the results screen, and published as Steam lobby member data, so a comma alone would split one pilot into two on the score sheet. SanitizeCallsign drops what could end a token early and is applied to what is typed as well as to what is read, so the file cannot hold what the game will not accept. Every index is range-checked on the way in, against the group's real size rather than a constant - the track list is the one that moves, since football and the death race carry different maps, so it answers for whichever scenario is selected. The track is re-checked after the whole file is read as well, because the file is parsed in the order it happens to be written and the scenario may arrive second. Written unconditionally rather than only on a change: it is a few hundred bytes, and writing every time means a value hand-edited out of range comes back corrected instead of being quietly re-rejected on every launch forever. Verified by round trip. A callsign typed and then abandoned via EXIT GAME is in the file and back in the box next launch. A file carrying Ba"d,Na#me loads as BadName; an empty one falls back to Pilot. A full loadout round-trips value for value; vehicle=999 and color=-3 come back 0 with the rest untouched; and track=9 under football falls back to 0 both when the scenario is read first and when it is read second, which is the case the second check exists for. One correction to my own test rig on the way: cross-process SetWindowText on an EDIT updates the cached caption, which an external GetWindowText then reads back happily, while leaving the control's own buffer alone - so the harness looked right and the game correctly saw the old name. WM_SETTEXT is marshalled properly and shows the truth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+275
-10
@@ -231,6 +231,241 @@ namespace
|
||||
Logical gHavePersist = False;
|
||||
char gLastPilotName[24] = "Pilot";
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// What the player set last time, in pilot.cfg beside bindings.txt:
|
||||
// the callsign they typed and the loadout they picked.
|
||||
//
|
||||
// The loadout has always survived a race - gPersistSelection below
|
||||
// is why the menu reopens the way you left it - but only for as
|
||||
// long as the process lives. BT411 keeps the same things in
|
||||
// fe_last.ini and had to, since it relaunches between missions;
|
||||
// RP412 stayed in one process and so never needed a file. Closing
|
||||
// the game was still a reset, which is what this fixes.
|
||||
//
|
||||
// KEY=VALUE like environ.ini, one line per group.
|
||||
//---------------------------------------------------------------
|
||||
const char kPilotFileName[] = "pilot.cfg";
|
||||
|
||||
//
|
||||
// Stable file keys, independent of the on-screen headings - those
|
||||
// carry spaces ("TIME OF DAY") and are free to be reworded.
|
||||
//
|
||||
struct GroupKey
|
||||
{
|
||||
int group;
|
||||
const char *key;
|
||||
};
|
||||
const GroupKey kGroupKeys[] =
|
||||
{
|
||||
{ GroupScenario, "scenario" }, { GroupMap, "track" },
|
||||
{ GroupVehicle, "vehicle" }, { GroupColor, "color" },
|
||||
{ GroupBadge, "badge" }, { GroupTeam, "team" },
|
||||
{ GroupPosition, "position" }, { GroupTime, "time" },
|
||||
{ GroupWeather, "weather" }, { GroupLength, "length" },
|
||||
};
|
||||
|
||||
//
|
||||
// How many rows a group offers, so a stale or hand-edited index
|
||||
// cannot select past the end of a list. The track list is the one
|
||||
// that moves - football and the death race carry different maps -
|
||||
// so it answers for whichever scenario is selected.
|
||||
//
|
||||
int GroupSize(int group, const int *selection)
|
||||
{
|
||||
switch (group)
|
||||
{
|
||||
case GroupScenario: return FE_COUNT(kScenarios);
|
||||
case GroupMap:
|
||||
{
|
||||
int count = 0;
|
||||
ActiveMaps(selection, &count);
|
||||
return count;
|
||||
}
|
||||
case GroupVehicle: return FE_COUNT(kVehicles);
|
||||
case GroupColor: return FE_COUNT(kColors);
|
||||
case GroupBadge: return FE_COUNT(kBadges);
|
||||
case GroupTeam: return FE_COUNT(kTeams);
|
||||
case GroupPosition: return FE_COUNT(kPositions);
|
||||
case GroupTime: return FE_COUNT(kTimes);
|
||||
case GroupWeather: return FE_COUNT(kWeather);
|
||||
case GroupLength: return FE_COUNT(kLengths);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
//
|
||||
// A callsign is quoted into frontend.egg, joined into a
|
||||
// comma-separated list for the results screen, and published as
|
||||
// Steam lobby member data. Anything that could end a token early
|
||||
// therefore has to go - a comma alone would split one pilot into
|
||||
// two on the score sheet. Applied to what is typed as well as to
|
||||
// what is read back, so the file cannot hold what the game will
|
||||
// not accept.
|
||||
//
|
||||
void SanitizeCallsign(char *name, int size)
|
||||
{
|
||||
char clean[64];
|
||||
int out = 0;
|
||||
for (int i = 0; name[i] != '\0' && out < (int) sizeof(clean) - 1; ++i)
|
||||
{
|
||||
unsigned char c = (unsigned char) name[i];
|
||||
if (c < 32 || c > 126) continue; // controls, high bytes
|
||||
if (c == ',' || c == '"') continue; // egg and CSV delimiters
|
||||
if (c == '#' || c == ';') continue; // pilot.cfg comment marks
|
||||
clean[out++] = (char) c;
|
||||
}
|
||||
clean[out] = '\0';
|
||||
|
||||
char *start = clean;
|
||||
while (*start == ' ' || *start == '\t')
|
||||
{
|
||||
++start;
|
||||
}
|
||||
int end = (int) strlen(start);
|
||||
while (end > 0 && (start[end - 1] == ' ' || start[end - 1] == '\t'))
|
||||
{
|
||||
start[--end] = '\0';
|
||||
}
|
||||
|
||||
if (start[0] == '\0')
|
||||
{
|
||||
strcpy(start, "Pilot");
|
||||
}
|
||||
strncpy(name, start, size - 1);
|
||||
name[size - 1] = '\0';
|
||||
}
|
||||
|
||||
void SavePilotSettings(const char *name, const int *selection)
|
||||
{
|
||||
FILE *file = fopen(kPilotFileName, "wt");
|
||||
if (file == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "FrontEnd: could not write " << kPilotFileName
|
||||
<< "\n" << std::flush;
|
||||
return;
|
||||
}
|
||||
fputs("# RP412 pilot settings, written by the game on the way out of\n"
|
||||
"# the setup screen. Delete this file to start over.\n", file);
|
||||
fprintf(file, "callsign=%s\n", name);
|
||||
if (selection != NULL)
|
||||
{
|
||||
for (int i = 0; i < FE_COUNT(kGroupKeys); ++i)
|
||||
{
|
||||
fprintf(file, "%s=%d\n", kGroupKeys[i].key,
|
||||
selection[kGroupKeys[i].group]);
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
DEBUG_STREAM << "FrontEnd: saved callsign \"" << name << "\""
|
||||
<< ((selection != NULL) ? " and loadout" : "")
|
||||
<< " to " << kPilotFileName << "\n" << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// Read once per run. Absent or unreadable simply leaves the
|
||||
// built-in defaults in place - a missing file is a first run, not
|
||||
// an error, and every value is range-checked so a hand-edited or
|
||||
// out-of-date file cannot select past the end of a list.
|
||||
//
|
||||
void EnsurePilotSettingsLoaded()
|
||||
{
|
||||
static Logical loaded = False;
|
||||
if (loaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
loaded = True;
|
||||
|
||||
FILE *file = fopen(kPilotFileName, "rt");
|
||||
if (file == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int selection[GroupCount];
|
||||
memset(selection, 0, sizeof(selection));
|
||||
selection[GroupLength] = 2; // 5:00, as the menu defaults
|
||||
Logical have_loadout = False;
|
||||
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), file) != NULL)
|
||||
{
|
||||
char *cursor = line;
|
||||
while (*cursor == ' ' || *cursor == '\t')
|
||||
{
|
||||
++cursor;
|
||||
}
|
||||
if (*cursor == '#' || *cursor == ';')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
char *equals = strchr(cursor, '=');
|
||||
if (equals == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
*equals = '\0';
|
||||
char *key = cursor;
|
||||
char *value = equals + 1;
|
||||
while (*value == ' ' || *value == '\t')
|
||||
{
|
||||
++value;
|
||||
}
|
||||
char *newline = strpbrk(value, "\r\n");
|
||||
if (newline != NULL)
|
||||
{
|
||||
*newline = '\0';
|
||||
}
|
||||
|
||||
if (_stricmp(key, "callsign") == 0)
|
||||
{
|
||||
char candidate[24];
|
||||
strncpy(candidate, value, sizeof(candidate) - 1);
|
||||
candidate[sizeof(candidate) - 1] = '\0';
|
||||
SanitizeCallsign(candidate, sizeof(candidate));
|
||||
strcpy(gLastPilotName, candidate);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < FE_COUNT(kGroupKeys); ++i)
|
||||
{
|
||||
if (_stricmp(key, kGroupKeys[i].key) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int index = atoi(value);
|
||||
if (index >= 0 && index < GroupSize(kGroupKeys[i].group, selection))
|
||||
{
|
||||
selection[kGroupKeys[i].group] = index;
|
||||
have_loadout = True;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
|
||||
//
|
||||
// The track list belongs to the scenario, and the file is read in
|
||||
// whatever order it was written, so re-check the track once the
|
||||
// scenario is settled - the same clamp the menu applies when the
|
||||
// scenario is switched by hand.
|
||||
//
|
||||
if (have_loadout)
|
||||
{
|
||||
int map_count = 0;
|
||||
ActiveMaps(selection, &map_count);
|
||||
if (selection[GroupMap] >= map_count)
|
||||
{
|
||||
selection[GroupMap] = 0;
|
||||
}
|
||||
memcpy(gPersistSelection, selection, sizeof(gPersistSelection));
|
||||
gHavePersist = True;
|
||||
}
|
||||
|
||||
DEBUG_STREAM << "FrontEnd: callsign \"" << gLastPilotName << "\""
|
||||
<< (have_loadout ? " and loadout" : "")
|
||||
<< " from " << kPilotFileName << "\n" << std::flush;
|
||||
}
|
||||
|
||||
// [pilots]-order names of the last launched race (owner first),
|
||||
// comma separated - the network console labels results with them
|
||||
char gLastPilotNamesCsv[256] = "";
|
||||
@@ -1219,6 +1454,10 @@ Logical
|
||||
{
|
||||
gLastLaunchMode = FELaunchSingle;
|
||||
|
||||
// before the lobby branch below: a member rejoining a room publishes
|
||||
// the callsign as member data without the menu ever opening
|
||||
EnsurePilotSettingsLoaded();
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Coming back from a race while still in a lobby: straight to
|
||||
// the room (the lobby outlives races - single binary payoff)
|
||||
@@ -1248,6 +1487,7 @@ Logical
|
||||
{
|
||||
FEState fe;
|
||||
memset(&fe, 0, sizeof(fe));
|
||||
EnsurePilotSettingsLoaded();
|
||||
strcpy(fe.pilotName, gLastPilotName);
|
||||
if (gHavePersist)
|
||||
{
|
||||
@@ -1346,20 +1586,41 @@ Logical
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Harvest the loadout whichever way we leave the menu - the
|
||||
// lobby publishes it as member data, launches build from it
|
||||
// Harvest the callsign whichever way we leave the menu, INCLUDING
|
||||
// a close: typing a name and then quitting is how somebody sets it
|
||||
// for next time, and losing it there would be the one case that
|
||||
// makes the whole thing feel unreliable. The loadout still only
|
||||
// persists on a real exit - it is picked, not typed, and the menu
|
||||
// reopens with it visible anyway.
|
||||
//---------------------------------------------------------------
|
||||
if (!fe.closed)
|
||||
if (fe.nameEdit != NULL)
|
||||
{
|
||||
fe.pilotName[0] = '\0';
|
||||
GetWindowTextA(fe.nameEdit, fe.pilotName, sizeof(fe.pilotName) - 1);
|
||||
if (fe.pilotName[0] == '\0')
|
||||
{
|
||||
strcpy(fe.pilotName, "Pilot");
|
||||
}
|
||||
strcpy(gLastPilotName, fe.pilotName);
|
||||
memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection));
|
||||
gHavePersist = True;
|
||||
SanitizeCallsign(fe.pilotName, sizeof(fe.pilotName));
|
||||
}
|
||||
else
|
||||
{
|
||||
strcpy(fe.pilotName, gLastPilotName);
|
||||
}
|
||||
|
||||
strcpy(gLastPilotName, fe.pilotName);
|
||||
memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection));
|
||||
gHavePersist = True;
|
||||
|
||||
//
|
||||
// Written on the way out however the player leaves - launching,
|
||||
// stepping into a lobby, or quitting. BT411 saves only on a
|
||||
// launch, which loses a callsign typed by somebody who then
|
||||
// changed their mind, and that is the one moment this feature
|
||||
// exists for.
|
||||
//
|
||||
// Unconditionally, rather than only when something changed: the
|
||||
// file is a few hundred bytes, and writing it every time means a
|
||||
// value that was hand-edited out of range comes back corrected
|
||||
// instead of being quietly re-rejected on every launch forever.
|
||||
//
|
||||
SavePilotSettings(gLastPilotName, gPersistSelection);
|
||||
|
||||
Logical launched = fe.launched;
|
||||
Logical closed = fe.closed;
|
||||
@@ -1825,6 +2086,10 @@ Logical
|
||||
return False;
|
||||
}
|
||||
|
||||
// this screen labels a row with the pilot's own callsign, and in the
|
||||
// -egg and lobby paths it can be the first screen of the session
|
||||
EnsurePilotSettingsLoaded();
|
||||
|
||||
ResultsState rs;
|
||||
memset(&rs, 0, sizeof(rs));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user