From 4f34684b16234bed38e4791b91c6cd441dd8e5c0 Mon Sep 17 00:00:00 2001 From: Cyd Date: Wed, 5 Aug 2026 14:46:18 -0500 Subject: [PATCH] environ.ini is written on first run, not shipped Packing one into every zip meant a tester who unzipped a new build over their folder got their configuration replaced. bindings.txt has never had that problem, because the exe carries the template and writes the file only when it is absent. environ.ini now works the same way, so a new build can land on an existing folder and every setting survives. The 245-line template moves out of pack-dist.ps1 and into RPL4ENVIRON.cpp as the exe's own literal, which also means the exe alone can produce a working install. It was lifted mechanically rather than retyped, and the file it writes is line-for-line identical to the one we have been shipping - only the line endings changed, from a mongrel 243 LF plus one stray CRLF that PowerShell's Set-Content left on the end, to the uniform LF the game already writes bindings.txt with. It cannot simply become optional. Without environ.ini, L4GAUGE is unset - which disables the gauge renderer and takes every MFD with it - and L4MFDSPLIT is unset, which is the packed-window arcade layout rather than the glass cockpit. The shipped values ARE the desktop game; the built-in getenv fallbacks are the 1995 pod. So the game writes the file rather than tolerating its absence. The cost of a file that is never overwritten is that a tester carrying one across many builds stops being offered new options. Nothing breaks - an option added later defaults to "behave as before" - but it goes unnoticed, and "the podium does not work" is a confusing bug report when the real answer is that their environ.ini predates RP412PODIUM. So the load names every template key the player's file has never mentioned, and says they are at built-in defaults and that deleting the file brings the documented one back. A stale seven-line file lists all 40. The file is read, never rewritten. The mention test is deliberately generous - a key counts as known if it appears in any form, commented or not - because the failure it guards against is worse than a missed notice: environ.ini is applied line by line, so a second copy of a key appearing later in the file would silently override the player's own. The version line also moves to the top of WinMain. It used to print after the environment was loaded, so the first thing in rpl4.log was a message about environ.ini rather than which build wrote it. Verified: the written file matches the old shipped one line for line; an edited file with a hand-added comment survives another run untouched; a seven-line file from an older build boots and names all 40 options it has never heard of; and a full mission on a self-written file brings up the glass cockpit at 125% with the virtual RIO active and nothing alarming in the log. Co-Authored-By: Claude Opus 5 (1M context) --- BUILD.md | 7 +- README.md | 10 +- RP_L4/RPL4.CPP | 48 ++-- RP_L4/RPL4ENVIRON.cpp | 508 +++++++++++++++++++++++++++++++++++++++ RP_L4/RPL4ENVIRON.h | 44 ++++ RP_L4/RP_L4.vcxproj | 2 + docs/rp412-handbook.html | 23 +- pack-dist.ps1 | 254 +------------------- 8 files changed, 604 insertions(+), 292 deletions(-) create mode 100644 RP_L4/RPL4ENVIRON.cpp create mode 100644 RP_L4/RPL4ENVIRON.h diff --git a/BUILD.md b/BUILD.md index 023357f..32d6650 100644 --- a/BUILD.md +++ b/BUILD.md @@ -74,8 +74,11 @@ outside a git checkout stamps `4.12.x (no repository)` rather than inventing a number that would sort against real ones. **Packaging:** [pack-dist.ps1](pack-dist.ps1) assembles a runnable game into -`dist\` (exe + PDB, game data, OpenAL/libsndfile runtimes, desktop -`environ.ini`, `start-windowed.bat`, README). Pass `-Zip` to also produce +`dist\` (exe + PDB, game data, OpenAL/libsndfile runtimes, launch scripts, +HANDBOOK.html, README). It deliberately does **not** write `environ.ini` — +the exe carries that template and writes it on first run +([RP_L4/RPL4ENVIRON.cpp](RP_L4/RPL4ENVIRON.cpp)), so a tester can drop a new +build over an old folder without losing their settings. Pass `-Zip` to also produce `RedPlanet-.zip` for handing to someone else. It reads the version from `rpl4build.h` rather than asking git again, so the package and the binary inside it cannot disagree, and it warns if the build it is packing diff --git a/README.md b/README.md index c6c0ebc..e825ea9 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,13 @@ team/position columns and its own track list). Steam multiplayer: see [docs/STEAM-3-MACHINE-TEST.md](docs/STEAM-3-MACHINE-TEST.md) (until RP412 has its own AppID it runs under Spacewar, 480). -The two config files beside the exe are self-documenting: **environ.ini** -(every engine option, commented) and **bindings.txt** (every key, pad -button, and axis; written with the full default layout on first run). +The config files beside the exe are self-documenting and none of them +ship: the game writes each one the first time it needs it and then leaves +it alone, so a new build dropped over an existing folder keeps every +setting. **environ.ini** is every engine option, commented; **bindings.txt** +every key, pad button and axis; **pilot.cfg** your callsign and loadout; +**mfd_layout.cfg** where you dragged the windows. Delete any of them to +start that part over with the current defaults. Default controls: numpad flies (8/2/4/6 stick, 7/9 pedals, 0 trigger), Shift/Ctrl throttle, Alt reverse, arrows look, Space fires, letter rows are the MFD button banks as printed on the panel. **Alt+Q** aborts a diff --git a/RP_L4/RPL4.CPP b/RP_L4/RPL4.CPP index f0deb20..c8cbe63 100644 --- a/RP_L4/RPL4.CPP +++ b/RP_L4/RPL4.CPP @@ -22,6 +22,7 @@ #include "rpl4pb.h" #include "rpl4fe.h" +#include "rpl4environ.h" #include "rpl4console.h" #include "rpl4lobby.h" #include "..\munga_l4\l4steamtransport.h" @@ -150,6 +151,15 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine SetUnhandledExceptionFilter(RPL4CrashDumpFilter); + // + // Which build this is, before anything else can fail. The patch number + // is this repository's commit count and the hash beside it names the + // commit, so a log from a test machine says exactly where it came from. + // A trailing '+' means the tree had uncommitted changes when it was + // built. See stamp-version.ps1. + // + DEBUG_STREAM << "Red Planet " << RP412_VERSION_LONG << std::endl << std::flush; + // load up our environment variables //controls if(getenv("L4CONTROLS") == NULL) @@ -164,38 +174,14 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine putenv("TARGETFPS=60"); if(getenv("MAXPARTICLES") == NULL) putenv("MAXPARTICLES=8192"); - FILE *file; - char line[1024]; - if (fopen_s(&file, "environ.ini", "r") == 0) - { - while (!feof(file)) - { - if (fgets(line, sizeof(line), file)) - { - for (int i = strlen(line); i >= 0; i--) - if (line[i] == '\n' || line[i] == '\r') - line[i] = 0; - // the file is self-documenting: skip comments, blanks, - // and anything that is not KEY=VALUE - char *setting = line; - while (*setting == ' ' || *setting == '\t') - ++setting; - if (*setting == '\0' || *setting == '#' || *setting == ';' || - strchr(setting, '=') == NULL) - continue; - putenv(setting); - } - } - fclose(file); - } + // + // environ.ini: written on first run and read here. The exe owns the + // template rather than the packaging script laying one down on every + // unzip, so a tester can drop a new build over an old folder and keep + // their settings. See rpl4environ.h. + // + RPL4Environ_Load(); - // - // The patch number is this repository's commit count and the hash beside - // it names the commit, so a log from a test machine says exactly which - // build it came from. A trailing '+' means the tree had uncommitted - // changes when it was built. See stamp-version.ps1. - // - DEBUG_STREAM << "Red Planet " << RP412_VERSION_LONG << std::endl << std::flush; DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << std::endl << std::flush; #ifdef RP412_STEAM diff --git a/RP_L4/RPL4ENVIRON.cpp b/RP_L4/RPL4ENVIRON.cpp new file mode 100644 index 0000000..7e17514 --- /dev/null +++ b/RP_L4/RPL4ENVIRON.cpp @@ -0,0 +1,508 @@ +#include "rpl4.h" +#pragma hdrstop + +#include "rpl4environ.h" + +#include +#include +#include + +//######################################################################## +// environ.ini - see rpl4environ.h for why the exe owns this rather than +// the packaging script. +//######################################################################## + +namespace +{ + const char kEnvironFileName[] = "environ.ini"; + + //------------------------------------------------------------------- + // The shipped configuration, verbatim. Lifted out of pack-dist.ps1 + // so there is one source of truth and the exe alone can produce a + // working install. + //------------------------------------------------------------------- + const char kEnvironTemplate[] = +"# ============================================================================\n" +"# environ.ini - Red Planet 4.12 configuration\n" +"# ============================================================================\n" +"# One KEY=VALUE per line, read at game start. Lines starting with # or ;\n" +"# are comments; anything without an = is ignored. Delete a line (or\n" +"# comment it out) to fall back to the built-in default.\n" +"#\n" +"# Input bindings live in bindings.txt beside the exe (written with the\n" +"# full documented layout on first run; delete it to restore defaults).\n" +"#\n" +"# Your callsign and loadout are remembered in pilot.cfg beside the exe.\n" +"# Set them on the setup screen once and they come back every session,\n" +"# however you left - launching, joining a lobby, or quitting. Delete\n" +"# that file to start over.\n" +"\n" +"# ---- Core (the shipped configuration) --------------------------------------\n" +"\n" +"# Control stack: tokens separated by ; or , processed left to right.\n" +"# PAD the virtual RIO (XInput controller + keyboard,\n" +"# rebindable via bindings.txt)\n" +"# RIO real serial cockpit hardware on COM1\n" +"# RIO:COMn same, on another port (RIO:COM3, ...)\n" +"# KEYBOARD the engine keyboard handler\n" +"# MOUSE, JOYSTICK, FLIGHTSTICKPRO, THRUSTMASTER, DIJOYSTICK\n" +"# legacy pointer/joystick drivers (untested here)\n" +"# Unset falls back to KEYBOARD alone.\n" +"L4CONTROLS=PAD;KEYBOARD\n" +"\n" +"# Renderer bring-up argument. Only its presence is checked (the DPL\n" +"# resolution parsing it once fed is gone) and the game refuses to start\n" +"# without it - any non-empty value works. Leave as shipped.\n" +"DPLARG=1\n" +"\n" +"# DPL (renderer/scene) configuration file, searched beside the exe.\n" +"# Any notation file name; RPDPL.INI is the one that ships.\n" +"L4DPLCFG=RPDPL.INI\n" +"\n" +"# Gauge (MFD/instrument) canvas. Must name a page of GAUGE\\L4GAUGE.INI:\n" +"# 640x480x8 | 640x480x16 | 800x600x16\n" +"# Unset disables the gauge renderer (and with it all MFDs).\n" +"L4GAUGE=640x480x16\n" +"\n" +"# Plasma display.\n" +"# SCREEN render the pod's plasma glass in-window (currently\n" +"# parked off-layout)\n" +"# COM1, COM2... drive real plasma glass on that serial port\n" +"# (9600 baud, N81)\n" +"# Unset = no plasma display.\n" +"L4PLASMA=SCREEN\n" +"\n" +"# 0 = classic separate gauge windows; 1 = the single-window glass\n" +"# cockpit (all seven displays composed on a locked 1920x1080 canvas\n" +"# around the viewscreen); 2 = exploded diagnostic view (each display\n" +"# in its own native-resolution desktop window - MFDs 640x480, map\n" +"# 480x640 - decoded exactly as the pod's VDB split them, no downscale).\n" +"L4MFDSPLIT=1\n" +"\n" +"# The game window - and in the exploded view (L4MFDSPLIT=2) each display\n" +"# window - is placed fresh every launch, so moving one somewhere useful\n" +"# never survived the menu-race-menu loop. This remembers where you put\n" +"# them, in mfd_layout.cfg beside this file:\n" +"# off / 0 / unset computed placement only, no file (default)\n" +"# load put the windows back where they were saved\n" +"# save the same, and re-save on every finished drag\n" +"# The game window gets its size back too, so you can size the cockpit to\n" +"# suit your monitor once and keep it. The display windows get position\n" +"# only: their size follows their content and their button banks, so an\n" +"# old one is never restored over them. Arrange everything once with\n" +"# save, then leave it on load.\n" +"#\n" +"# The plasma display window takes part too, under \"Plasma Display\".\n" +"#\n" +"# Each line in mfd_layout.cfg reads =<x>,<y>,<w>,<h>, and you can\n" +"# append ,noframe to take that window's title bar and border off - a\n" +"# cockpit that fills the monitor edge to edge without -fit taking the\n" +"# whole screen. Put the window where you want it first: a bare window\n" +"# has nothing to drag by. Delete the flag to get the frame back.\n" +"#RP412MFDLAYOUT=off\n" +"\n" +"# Size of the six secondary displays in the glass cockpit, as a\n" +"# percentage of their pod size. The pod bolted them down at one size;\n" +"# on a big panel there is room to trade viewscreen for instrument, so\n" +"# turn these up if you want to actually read the other displays while\n" +"# you fly. 100 = as the pod had them. Range 25-200 (out-of-range and\n" +"# unreadable values fall back to the group setting, then to 100).\n" +"#\n" +"# The scaling is applied in canvas units, before the cockpit is fitted\n" +"# to your window, so a given number looks the same on every monitor.\n" +"# The layout stays legal whatever you ask for - the panes are clamped\n" +"# against their actual neighbours, shrinking uniformly so a display\n" +"# never comes out stretched. They do overlap the viewscreen, exactly\n" +"# as the pod's bezels did, but never each other.\n" +"#\n" +"# L4MFDSCALE sets all five green MFDs at once.\n" +"L4MFDSCALE=100\n" +"\n" +"# ...and any single display can override it. Uncomment one to size it\n" +"# on its own - useful if you only care about, say, the damage readout.\n" +"# UL upper left UC upper center UR upper right\n" +"# LL lower left LR lower right\n" +"#L4MFDSCALE_UL=100\n" +"#L4MFDSCALE_UC=100\n" +"#L4MFDSCALE_UR=100\n" +"#L4MFDSCALE_LL=100\n" +"#L4MFDSCALE_LR=100\n" +"\n" +"# The portrait radar/map, sized on its own (it already sits at 1.35x\n" +"# the MFDs by default). It shares the canvas with whichever MFD is\n" +"# above it, so at extreme settings one of the two gives way.\n" +"L4RADARSCALE=100\n" +"\n" +"# Where the radar sits:\n" +"# CENTER bottom centre, under the viewscreen, as the pod had it\n" +"# (default; BOTTOM and CENTRE mean the same)\n" +"# LEFT bottom left corner (or BOTTOMLEFT)\n" +"# RIGHT bottom right corner (or BOTTOMRIGHT)\n" +"# MIDLEFT left edge, halfway up (or LEFTCENTER / LEFTCENTRE)\n" +"# MIDRIGHT right edge, halfway up (or RIGHTCENTER / RIGHTCENTRE)\n" +"# Anywhere but CENTER stops it blocking the middle of the road, which\n" +"# is worth having on a wide screen.\n" +"#\n" +"# In a bottom corner it is one of three panes along the bottom, and the\n" +"# lower MFD whose corner it takes slides inboard beside it. Halfway up\n" +"# a side it leaves the bottom row entirely and sits between that side's\n" +"# two MFDs - roomy on a tall radar, but if the MFDs on that side are\n" +"# also scaled up, the radar is the one that gives way (it has to clear\n" +"# both of them, and it grows from the middle in both directions).\n" +"L4RADARPOS=CENTER\n" +"\n" +"# The Winners Circle: at the end of a race the finishers are stood on\n" +"# the award platform in finishing order, with each pilot's callsign on\n" +"# the plate beside their spot, and held there for a few seconds before\n" +"# the results screen. 1 = show it, 0 = straight to the results.\n" +"RP412PODIUM=1\n" +"\n" +"# The shot is framed for you, but these move the camera if you want it\n" +"# somewhere else. Distances are in game units, measured from the middle\n" +"# of the group of finishers.\n" +"# STANDOFF how far out in front of the stand the camera sits\n" +"# HEIGHT how far above the group\n" +"# AIM height of the point it looks at, relative to the group -\n" +"# negative tilts down, positive tilts up\n" +"# ASPECT the stand was composed for a 4:3 pod monitor, so the shot\n" +"# is cropped to that shape with black either side. 0 runs it\n" +"# full width instead.\n" +"# FADEIN seconds to come up out of the black after the race fades\n" +"# CAM 0 watches from your own cockpit rather than off the stand\n" +"#RP412PODIUMSTANDOFF=36\n" +"#RP412PODIUMHEIGHT=12\n" +"#RP412PODIUMAIM=2\n" +"#RP412PODIUMASPECT=1.333\n" +"#RP412PODIUMFADEIN=0.45\n" +"#RP412PODIUMCAM=1\n" +"\n" +"# Override the game length the menu picked, in seconds. The shortest the\n" +"# menu offers is 3:00, which is a long wait when what you are testing is\n" +"# what happens at the buzzer. Unset = use the menu's choice.\n" +"#RP412MISSIONSECONDS=20\n" +"\n" +"# Simulation/render frame rate, integer frames/second. The desktop\n" +"# default is 60; the arcade pods shipped at 25.\n" +"TARGETFPS=60\n" +"\n" +"# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client\n" +"# running and steam_appid.txt beside the exe; without them the game\n" +"# logs the reason and falls back to plain TCP. 0 = TCP only.\n" +"RP412STEAM=1\n" +"\n" +"# ---- Optional ---------------------------------------------------------------\n" +"\n" +"# RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound to\n" +"# lamp buttons glow with the panel, flash modes and all.\n" +"# Unset or nonzero = on (the default); 0 = off.\n" +"#RP412KEYLIGHT=0\n" +"\n" +"# Invert the stick on top of whatever bindings.txt produces:\n" +"# X = invert X only, Y = invert Y only, XY = both (case-insensitive).\n" +"#L4PADFLIP=XY\n" +"\n" +"# Anti-aliasing sample count, passed straight to Direct3D 9:\n" +"# 0 = off, else 2..16 as the GPU supports (1 selects the driver's\n" +"# \"nonmaskable\" mode; unsupported counts fail device creation).\n" +"#MULTISAMPLE=0\n" +"\n" +"# Particle budget, integer. Default 8192.\n" +"#MAXPARTICLES=8192\n" +"\n" +"# On-screen plasma glass (L4PLASMA=SCREEN only). SCALE = integer pixel\n" +"# size 1..16, default 4 (out-of-range values are ignored). POS = window\n" +"# top-left as X,Y screen coordinates; unset = auto, parked below the\n" +"# main window.\n" +"#L4PLASMASCALE=4\n" +"#L4PLASMAPOS=0,0\n" +"\n" +"# Fixed random seed (repeatable runs): any unsigned integer.\n" +"# Unset seeds from the clock.\n" +"#RANDOM=12345\n" +"\n" +"# ---- LAN play without Steam -------------------------------------------------\n" +"# Host a race over plain TCP: list the member pods' console channels\n" +"# (members run: rpl4opt.exe -windowed -res 1920 1080 -net 1501).\n" +"# RP412HOSTPODS comma-separated IP[:port] list, one entry per member\n" +"# pod; port defaults to 1501 per entry\n" +"# RP412HOSTPORT this machine's console port, integer > 0\n" +"# (default 1501)\n" +"# RP412HOSTADDR this machine's LAN IP as members can reach it\n" +"# (default 127.0.0.1)\n" +"#RP412HOSTPODS=192.168.1.20:1501,192.168.1.21:1501\n" +"#RP412HOSTPORT=1501\n" +"#RP412HOSTADDR=192.168.1.10\n" +"\n" +"# ---- Developer / testing ----------------------------------------------------\n" +"\n" +"# Nonzero arms the debug keys: Alt+W wireframe, Alt+V predator vision,\n" +"# Alt+F frame dump, Alt+/ perf stats, Alt+E event-queue dump.\n" +"# 0 or unset = off. (Alt+Q, the mission abort, is always live.)\n" +"#RP412DEVKEYS=1\n" +"\n" +"# Console race-length override, integer seconds (short test races).\n" +"# Values <= 0 are ignored.\n" +"#L4CONSOLELEN=30\n" +"\n" +"# Nonzero = Steam transport loopback self-test at boot (logs PASS/FAIL).\n" +"#RP412STEAMSELFTEST=1\n" +"\n" +"# ---- Arcade heritage (multi-monitor pods; not used on the desktop) ----------\n" +"# PRIMGAUGE / SECGAUGE / MFDGAUGE / MFDGAUGE2 pin a display to a monitor\n" +"# by adapter index (0, 1, 2...). SPANDISABLE: 0 = let the MFDs span one\n" +"# wide surface, nonzero = separate windows (setting MFDGAUGE2 alone also\n" +"# forces spanning off). L4EYES = \"x y z xrot yrot zrot [type]\" floats\n" +"# for a detached camera; a type starting with r offsets it relative to\n" +"# the pod. L4INTERCOM enables the crew intercom - only its presence\n" +"# matters (traditionally COM2). NOMODES skips the mode/lamp programming;\n" +"# presence alone triggers it, even NOMODES=0. LOGSIZE > 0 sizes the\n" +"# trace log in dev builds compiled with tracing.\n" +"#PRIMGAUGE=1\n" +"#SECGAUGE=2\n" +"#MFDGAUGE=3\n" +"#MFDGAUGE2=4\n" +"#SPANDISABLE=1\n" +"#L4EYES=1\n" +"#L4INTERCOM=COM2\n" +"#NOMODES=1\n" +"#LOGSIZE=1000000\n" + ; + + //------------------------------------------------------------------- + // Does the player's file mention this key at all - set, or commented + // out, or with whitespace in front of it? + // + // Deliberately generous: a key that is mentioned in ANY form is left + // alone. The alternative failure is worse than a missed notice, since + // environ.ini is applied line by line and a second copy of a key + // further down the file would silently override the player's own. + //------------------------------------------------------------------- + Logical FileMentionsKey(const char *text, const char *key, int key_length) + { + const char *cursor = text; + while ((cursor = strstr(cursor, key)) != NULL) + { + // + // Must be a whole key: preceded by start-of-line, whitespace + // or a comment mark, and followed by '='. + // + const char *after = cursor + key_length; + Logical starts_token = + (cursor == text) || + (cursor[-1] == '\n') || (cursor[-1] == '\r') || + (cursor[-1] == ' ') || (cursor[-1] == '\t') || + (cursor[-1] == '#') || (cursor[-1] == ';'); + if (starts_token) + { + const char *scan = after; + while (*scan == ' ' || *scan == '\t') + { + ++scan; + } + if (*scan == '=') + { + return True; + } + } + cursor = after; + } + return False; + } + + //------------------------------------------------------------------- + // Name every template key the player's file has never heard of. Not + // a fix - their file stays theirs - but it puts the reason for a + // missing feature in the log we already ask testers for. + //------------------------------------------------------------------- + void ReportUnmentionedKeys(const char *file_text) + { + char missing[1024]; // what gets printed + char seen[1024]; // the same keys as "KEY=", so the mention + // test above can dedupe against them + missing[0] = '\0'; + seen[0] = '\0'; + int count = 0; // how many are missing + int listed = 0; // how many fitted in the line + + const char *cursor = kEnvironTemplate; + while (*cursor != '\0') + { + const char *line = cursor; + const char *end = strchr(line, '\n'); + int length = (end != NULL) ? (int)(end - line) : (int) strlen(line); + cursor = (end != NULL) ? (end + 1) : (line + length); + + // + // A template key line is "KEY=..." or "#KEY=..." - the + // commented ones are options that ship switched off, and a + // player who has never seen them wants to know they exist. + // + const char *scan = line; + int remaining = length; + if (remaining > 0 && *scan == '#') + { + ++scan; + --remaining; + } + if (remaining <= 0 || !(isalpha((unsigned char) *scan) || *scan == '_')) + { + continue; + } + int key_length = 0; + while (key_length < remaining && + (isalnum((unsigned char) scan[key_length]) || scan[key_length] == '_')) + { + ++key_length; + } + if (key_length >= remaining || scan[key_length] != '=' || key_length > 60) + { + continue; + } + + char key[64]; + memcpy(key, scan, key_length); + key[key_length] = '\0'; + + if (FileMentionsKey(file_text, key, key_length)) + { + continue; + } + // + // Templates list some keys twice (documented once, shown + // again in an example); do not name one twice. + // + if (FileMentionsKey(seen, key, key_length)) + { + continue; + } + ++count; + if (strlen(seen) + key_length + 3 < sizeof(seen)) + { + strcat(seen, key); + strcat(seen, "=\n"); + } + if (strlen(missing) + key_length + 3 < sizeof(missing)) + { + if (missing[0] != '\0') + { + strcat(missing, ", "); + } + strcat(missing, key); + ++listed; + } + } + + if (count > 0) + { + // + // Say when the list is short of the count rather than letting + // a full buffer quietly shorten the answer. + // + DEBUG_STREAM << "Environ: " << kEnvironFileName << " does not mention " + << count << " option(s) this build knows: " << missing; + if (listed < count) + { + DEBUG_STREAM << ", and " << (count - listed) << " more"; + } + DEBUG_STREAM << "\nEnviron: they are at their built-in defaults - delete " + << kEnvironFileName << " to get the documented file back\n" + << std::flush; + } + } +} + +void + RPL4Environ_Load() +{ + // + // First run: lay down the documented default. From here on the file + // belongs to whoever is sitting at this machine. + // + FILE *file = fopen(kEnvironFileName, "rb"); + if (file == NULL) + { + FILE *out = fopen(kEnvironFileName, "wb"); + if (out != NULL) + { + fwrite(kEnvironTemplate, 1, strlen(kEnvironTemplate), out); + fclose(out); + DEBUG_STREAM << "Environ: wrote default " << kEnvironFileName + << "\n" << std::flush; + } + else + { + DEBUG_STREAM << "Environ: could not write " << kEnvironFileName + << " - running on built-in defaults\n" << std::flush; + } + file = fopen(kEnvironFileName, "rb"); + } + if (file == NULL) + { + return; + } + + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + if (size <= 0) + { + fclose(file); + return; + } + char *text = new char[size + 1]; + size_t read = fread(text, 1, size, file); + text[read] = '\0'; + fclose(file); + + // + // One KEY=VALUE per line. Comments, blanks and anything without an + // '=' are skipped; everything else goes into the environment, which + // is why a line here beats a variable set in the shell. + // + int applied = 0; + char line[1024]; + const char *cursor = text; + while (*cursor != '\0') + { + int length = 0; + while (cursor[length] != '\0' && cursor[length] != '\n' && + length < (int) sizeof(line) - 1) + { + line[length] = cursor[length]; + ++length; + } + line[length] = '\0'; + cursor += length; + while (*cursor == '\n' || *cursor == '\r') + { + ++cursor; + } + for (int i = length - 1; i >= 0; --i) + { + if (line[i] == '\r' || line[i] == '\n') + { + line[i] = '\0'; + } + } + + char *setting = line; + while (*setting == ' ' || *setting == '\t') + { + ++setting; + } + if (*setting == '\0' || *setting == '#' || *setting == ';' || + strchr(setting, '=') == NULL) + { + continue; + } + putenv(setting); + ++applied; + } + + DEBUG_STREAM << "Environ: " << applied << " setting(s) from " + << kEnvironFileName << "\n" << std::flush; + + ReportUnmentionedKeys(text); + + delete[] text; +} diff --git a/RP_L4/RPL4ENVIRON.h b/RP_L4/RPL4ENVIRON.h new file mode 100644 index 0000000..185ceae --- /dev/null +++ b/RP_L4/RPL4ENVIRON.h @@ -0,0 +1,44 @@ +//===========================================================================// +// File: rpl4environ.h // +// Project: MUNGA Brick: Red Planet LBE Application // +// Contents: environ.ini - written on first run, then the player's // +//---------------------------------------------------------------------------// +// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. // +// PROPRIETARY AND CONFIDENTIAL // +//===========================================================================// + +#pragma once + +#include "..\munga\style.h" + +//######################################################################## +// +// environ.ini is the game's configuration: one KEY=VALUE per line, read +// once at startup and pushed into the environment, so every option the +// engine reads through getenv can be set from a file a player can open. +// +// The exe owns the template and writes it when the file is absent, the +// same way bindings.txt works, rather than the packaging script laying +// one down on every unzip. That is what lets a tester drop a new build +// over an old folder and keep their settings: the file is theirs from +// the moment it exists, and nothing overwrites it. +// +// It cannot simply be optional. Without it L4GAUGE is unset, which +// disables the gauge renderer and takes every MFD with it, and +// L4MFDSPLIT is unset, which is the packed-window arcade layout rather +// than the glass cockpit. The shipped values are the desktop game; the +// built-in getenv fallbacks are the 1995 pod. +// +// The cost of a file that is never overwritten is that a tester carrying +// one across many builds stops being offered new options. Options added +// later default to "behave as before", so nothing breaks - but it does +// go unnoticed, so the load names any template key the player's file +// does not mention. That line in rpl4.log is what turns "the podium does +// not work" into "your environ.ini predates RP412PODIUM". +// +//######################################################################## + +// Write environ.ini if it is not there, then read it into the +// environment. Call once, before anything reads a setting. +void + RPL4Environ_Load(); diff --git a/RP_L4/RP_L4.vcxproj b/RP_L4/RP_L4.vcxproj index 86f02c8..fb06fe5 100644 --- a/RP_L4/RP_L4.vcxproj +++ b/RP_L4/RP_L4.vcxproj @@ -118,6 +118,7 @@ <ClCompile Include=".\RPL4APP.cpp" /> <ClCompile Include=".\RPL4CONSOLE.cpp" /> <ClCompile Include=".\RPL4FE.cpp" /> + <ClCompile Include=".\RPL4ENVIRON.cpp" /> <ClCompile Include=".\RPL4LOBBY.cpp" /> <ClCompile Include=".\RPL4ARND.cpp" /> <ClCompile Include=".\RPL4GAUG.cpp" /> @@ -156,6 +157,7 @@ <ClInclude Include=".\RPL4APP.h" /> <ClInclude Include=".\RPL4CONSOLE.h" /> <ClInclude Include=".\RPL4FE.h" /> + <ClInclude Include=".\rpl4environ.h" /> <ClInclude Include=".\RPL4LOBBY.h" /> <ClInclude Include=".\RPL4ARND.h" /> <ClInclude Include=".\RPL4GAUG.h" /> diff --git a/docs/rp412-handbook.html b/docs/rp412-handbook.html index 8e5c122..3f4e82a 100644 --- a/docs/rp412-handbook.html +++ b/docs/rp412-handbook.html @@ -1316,9 +1316,11 @@ <section> <h2>Files beside the exe</h2> <p class="sub"> - Four files in the game folder are yours. Only one of them ships — the - game writes the other three when it first needs them, so a fresh unzip - has none of them and nothing is lost by deleting one. + Four files in the game folder are yours. <b>None of them ship.</b> The + game writes each one the first time it needs it and then never touches + it again, so a new build dropped over this folder keeps everything you + have set — and deleting any of them simply starts that part over with + the current defaults. </p> <div class="tbl-scroll"> @@ -1329,8 +1331,9 @@ <th class="mono">environ.ini</th> <td> Every engine option, commented in place — displays, renderer, - Steam, the podium, the lot. The only one that ships, and the one - to read first. + Steam, the podium, the lot. The one to read first. Written on + first run; without it the MFDs do not come up at all, so the + game will always put one back. </td> </tr> <tr> @@ -1363,10 +1366,12 @@ <strong>Two that catch people out.</strong> <code>environ.ini</code> is applied <i>over</i> the environment, so a variable you set in a shell loses to an uncommented line in the file — - comment the line out rather than fighting it. And - <code>bindings.txt</code> is never overwritten once it exists, which - is what protects your edits: after an update, <b>delete it</b> to pick - up new default bindings. + comment the line out rather than fighting it. And nothing here is ever + overwritten once it exists, which is exactly what lets you keep a + folder across builds — but it means a file carried through several + updates stops being offered new options. The game names any it has not + heard of in <code>rpl4.log</code>; <b>delete the file</b> to get the + fully documented current one back. </p> </div> diff --git a/pack-dist.ps1 b/pack-dist.ps1 index a76babe..5e77272 100644 --- a/pack-dist.ps1 +++ b/pack-dist.ps1 @@ -8,7 +8,7 @@ # TEST.EGG) - but not the arcade launch scripts or the old 4.10 exe # - libsndfile-1.dll beside the exe; OpenAL32.dll copied from the system # when installed, with oalinst.exe included as the fallback installer -# - a desktop environ.ini (PAD;KEYBOARD controls, on-screen plasma) +# (environ.ini is NOT shipped - the exe writes it on first run) # - start/joyconfig scripts, HANDBOOK.html, CONTROLS.txt and a README # # Usage: powershell -ExecutionPolicy Bypass -File pack-dist.ps1 [-Zip] @@ -134,252 +134,12 @@ if (Test-Path $openal) { } # --- desktop configuration ------------------------------------------------- -Set-Content -Path "$dist\environ.ini" -Encoding ascii -Value @" -# ============================================================================ -# environ.ini - Red Planet 4.12 configuration -# ============================================================================ -# One KEY=VALUE per line, read at game start. Lines starting with # or ; -# are comments; anything without an = is ignored. Delete a line (or -# comment it out) to fall back to the built-in default. -# -# 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) -------------------------------------- - -# Control stack: tokens separated by ; or , processed left to right. -# PAD the virtual RIO (XInput controller + keyboard, -# rebindable via bindings.txt) -# RIO real serial cockpit hardware on COM1 -# RIO:COMn same, on another port (RIO:COM3, ...) -# KEYBOARD the engine keyboard handler -# MOUSE, JOYSTICK, FLIGHTSTICKPRO, THRUSTMASTER, DIJOYSTICK -# legacy pointer/joystick drivers (untested here) -# Unset falls back to KEYBOARD alone. -L4CONTROLS=PAD;KEYBOARD - -# Renderer bring-up argument. Only its presence is checked (the DPL -# resolution parsing it once fed is gone) and the game refuses to start -# without it - any non-empty value works. Leave as shipped. -DPLARG=1 - -# DPL (renderer/scene) configuration file, searched beside the exe. -# Any notation file name; RPDPL.INI is the one that ships. -L4DPLCFG=RPDPL.INI - -# Gauge (MFD/instrument) canvas. Must name a page of GAUGE\L4GAUGE.INI: -# 640x480x8 | 640x480x16 | 800x600x16 -# Unset disables the gauge renderer (and with it all MFDs). -L4GAUGE=640x480x16 - -# Plasma display. -# SCREEN render the pod's plasma glass in-window (currently -# parked off-layout) -# COM1, COM2... drive real plasma glass on that serial port -# (9600 baud, N81) -# Unset = no plasma display. -L4PLASMA=SCREEN - -# 0 = classic separate gauge windows; 1 = the single-window glass -# cockpit (all seven displays composed on a locked 1920x1080 canvas -# around the viewscreen); 2 = exploded diagnostic view (each display -# in its own native-resolution desktop window - MFDs 640x480, map -# 480x640 - decoded exactly as the pod's VDB split them, no downscale). -L4MFDSPLIT=1 - -# The game window - and in the exploded view (L4MFDSPLIT=2) each display -# window - is placed fresh every launch, so moving one somewhere useful -# never survived the menu-race-menu loop. This remembers where you put -# them, in mfd_layout.cfg beside this file: -# off / 0 / unset computed placement only, no file (default) -# load put the windows back where they were saved -# save the same, and re-save on every finished drag -# The game window gets its size back too, so you can size the cockpit to -# suit your monitor once and keep it. The display windows get position -# only: their size follows their content and their button banks, so an -# old one is never restored over them. Arrange everything once with -# save, then leave it on load. -# -# The plasma display window takes part too, under "Plasma Display". -# -# Each line in mfd_layout.cfg reads <title>=<x>,<y>,<w>,<h>, and you can -# append ,noframe to take that window's title bar and border off - a -# cockpit that fills the monitor edge to edge without -fit taking the -# whole screen. Put the window where you want it first: a bare window -# has nothing to drag by. Delete the flag to get the frame back. -#RP412MFDLAYOUT=off - -# Size of the six secondary displays in the glass cockpit, as a -# percentage of their pod size. The pod bolted them down at one size; -# on a big panel there is room to trade viewscreen for instrument, so -# turn these up if you want to actually read the other displays while -# you fly. 100 = as the pod had them. Range 25-200 (out-of-range and -# unreadable values fall back to the group setting, then to 100). -# -# The scaling is applied in canvas units, before the cockpit is fitted -# to your window, so a given number looks the same on every monitor. -# The layout stays legal whatever you ask for - the panes are clamped -# against their actual neighbours, shrinking uniformly so a display -# never comes out stretched. They do overlap the viewscreen, exactly -# as the pod's bezels did, but never each other. -# -# L4MFDSCALE sets all five green MFDs at once. -L4MFDSCALE=100 - -# ...and any single display can override it. Uncomment one to size it -# on its own - useful if you only care about, say, the damage readout. -# UL upper left UC upper center UR upper right -# LL lower left LR lower right -#L4MFDSCALE_UL=100 -#L4MFDSCALE_UC=100 -#L4MFDSCALE_UR=100 -#L4MFDSCALE_LL=100 -#L4MFDSCALE_LR=100 - -# The portrait radar/map, sized on its own (it already sits at 1.35x -# the MFDs by default). It shares the canvas with whichever MFD is -# above it, so at extreme settings one of the two gives way. -L4RADARSCALE=100 - -# Where the radar sits: -# CENTER bottom centre, under the viewscreen, as the pod had it -# (default; BOTTOM and CENTRE mean the same) -# LEFT bottom left corner (or BOTTOMLEFT) -# RIGHT bottom right corner (or BOTTOMRIGHT) -# MIDLEFT left edge, halfway up (or LEFTCENTER / LEFTCENTRE) -# MIDRIGHT right edge, halfway up (or RIGHTCENTER / RIGHTCENTRE) -# Anywhere but CENTER stops it blocking the middle of the road, which -# is worth having on a wide screen. -# -# In a bottom corner it is one of three panes along the bottom, and the -# lower MFD whose corner it takes slides inboard beside it. Halfway up -# a side it leaves the bottom row entirely and sits between that side's -# two MFDs - roomy on a tall radar, but if the MFDs on that side are -# also scaled up, the radar is the one that gives way (it has to clear -# both of them, and it grows from the middle in both directions). -L4RADARPOS=CENTER - -# The Winners Circle: at the end of a race the finishers are stood on -# the award platform in finishing order, with each pilot's callsign on -# the plate beside their spot, and held there for a few seconds before -# the results screen. 1 = show it, 0 = straight to the results. -RP412PODIUM=1 - -# The shot is framed for you, but these move the camera if you want it -# somewhere else. Distances are in game units, measured from the middle -# of the group of finishers. -# STANDOFF how far out in front of the stand the camera sits -# HEIGHT how far above the group -# AIM height of the point it looks at, relative to the group - -# negative tilts down, positive tilts up -# ASPECT the stand was composed for a 4:3 pod monitor, so the shot -# is cropped to that shape with black either side. 0 runs it -# full width instead. -# FADEIN seconds to come up out of the black after the race fades -# CAM 0 watches from your own cockpit rather than off the stand -#RP412PODIUMSTANDOFF=36 -#RP412PODIUMHEIGHT=12 -#RP412PODIUMAIM=2 -#RP412PODIUMASPECT=1.333 -#RP412PODIUMFADEIN=0.45 -#RP412PODIUMCAM=1 - -# Override the game length the menu picked, in seconds. The shortest the -# menu offers is 3:00, which is a long wait when what you are testing is -# what happens at the buzzer. Unset = use the menu's choice. -#RP412MISSIONSECONDS=20 - -# Simulation/render frame rate, integer frames/second. The desktop -# default is 60; the arcade pods shipped at 25. -TARGETFPS=60 - -# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client -# running and steam_appid.txt beside the exe; without them the game -# logs the reason and falls back to plain TCP. 0 = TCP only. -RP412STEAM=1 - -# ---- Optional --------------------------------------------------------------- - -# RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound to -# lamp buttons glow with the panel, flash modes and all. -# Unset or nonzero = on (the default); 0 = off. -#RP412KEYLIGHT=0 - -# Invert the stick on top of whatever bindings.txt produces: -# X = invert X only, Y = invert Y only, XY = both (case-insensitive). -#L4PADFLIP=XY - -# Anti-aliasing sample count, passed straight to Direct3D 9: -# 0 = off, else 2..16 as the GPU supports (1 selects the driver's -# "nonmaskable" mode; unsupported counts fail device creation). -#MULTISAMPLE=0 - -# Particle budget, integer. Default 8192. -#MAXPARTICLES=8192 - -# On-screen plasma glass (L4PLASMA=SCREEN only). SCALE = integer pixel -# size 1..16, default 4 (out-of-range values are ignored). POS = window -# top-left as X,Y screen coordinates; unset = auto, parked below the -# main window. -#L4PLASMASCALE=4 -#L4PLASMAPOS=0,0 - -# Fixed random seed (repeatable runs): any unsigned integer. -# Unset seeds from the clock. -#RANDOM=12345 - -# ---- LAN play without Steam ------------------------------------------------- -# Host a race over plain TCP: list the member pods' console channels -# (members run: rpl4opt.exe -windowed -res 1920 1080 -net 1501). -# RP412HOSTPODS comma-separated IP[:port] list, one entry per member -# pod; port defaults to 1501 per entry -# RP412HOSTPORT this machine's console port, integer > 0 -# (default 1501) -# RP412HOSTADDR this machine's LAN IP as members can reach it -# (default 127.0.0.1) -#RP412HOSTPODS=192.168.1.20:1501,192.168.1.21:1501 -#RP412HOSTPORT=1501 -#RP412HOSTADDR=192.168.1.10 - -# ---- Developer / testing ---------------------------------------------------- - -# Nonzero arms the debug keys: Alt+W wireframe, Alt+V predator vision, -# Alt+F frame dump, Alt+/ perf stats, Alt+E event-queue dump. -# 0 or unset = off. (Alt+Q, the mission abort, is always live.) -#RP412DEVKEYS=1 - -# Console race-length override, integer seconds (short test races). -# Values <= 0 are ignored. -#L4CONSOLELEN=30 - -# Nonzero = Steam transport loopback self-test at boot (logs PASS/FAIL). -#RP412STEAMSELFTEST=1 - -# ---- Arcade heritage (multi-monitor pods; not used on the desktop) ---------- -# PRIMGAUGE / SECGAUGE / MFDGAUGE / MFDGAUGE2 pin a display to a monitor -# by adapter index (0, 1, 2...). SPANDISABLE: 0 = let the MFDs span one -# wide surface, nonzero = separate windows (setting MFDGAUGE2 alone also -# forces spanning off). L4EYES = "x y z xrot yrot zrot [type]" floats -# for a detached camera; a type starting with r offsets it relative to -# the pod. L4INTERCOM enables the crew intercom - only its presence -# matters (traditionally COM2). NOMODES skips the mode/lamp programming; -# presence alone triggers it, even NOMODES=0. LOGSIZE > 0 sizes the -# trace log in dev builds compiled with tracing. -#PRIMGAUGE=1 -#SECGAUGE=2 -#MFDGAUGE=3 -#MFDGAUGE2=4 -#SPANDISABLE=1 -#L4EYES=1 -#L4INTERCOM=COM2 -#NOMODES=1 -#LOGSIZE=1000000 -"@ +# --- desktop configuration ------------------------------------------------- +# environ.ini is NOT shipped. The exe carries the template and writes it on +# first run (RPL4ENVIRON.cpp), the same way it writes bindings.txt - so a +# tester can drop a new build over an old folder and keep every setting they +# have changed. Laying one down here would overwrite their file on every +# unzip, which is the whole problem. Set-Content -Path "$dist\start-windowed.bat" -Encoding ascii -Value @" @echo off