diff --git a/.gitignore b/.gitignore index fff37de..f621c59 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,10 @@ rpl4.log # on somebody's desk. mfd_layout.cfg +# The pilot's callsign, remembered between sessions. Whoever is sitting +# at this machine, which is not the repo's business. +pilot.cfg + # Build-output static libs that land in lib/ (the two committed dependency # libs, OpenAL32.lib and libsndfile-1.lib, stay tracked). /lib/Munga_L4.lib diff --git a/RP_L4/RPL4FE.cpp b/RP_L4/RPL4FE.cpp index 1a382ad..0792105 100644 --- a/RP_L4/RPL4FE.cpp +++ b/RP_L4/RPL4FE.cpp @@ -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)); diff --git a/pack-dist.ps1 b/pack-dist.ps1 index b5b9ef0..9024bfd 100644 --- a/pack-dist.ps1 +++ b/pack-dist.ps1 @@ -123,6 +123,11 @@ Set-Content -Path "$dist\environ.ini" -Encoding ascii -Value @" # # Input bindings live in bindings.txt beside the exe (written with the # full documented layout on first run; delete it to restore defaults). +# +# Your callsign and loadout are remembered in pilot.cfg beside the exe. +# Set them on the setup screen once and they come back every session, +# however you left - launching, joining a lobby, or quitting. Delete +# that file to start over. # ---- Core (the shipped configuration) --------------------------------------