#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" "# Line up each remote player's clock with ours, so their vehicle is\n" "# extrapolated from when its update was SENT rather than when it\n" "# arrived. Without it every remote pod sits one network latency behind\n" "# where it should be - invisible on the 1ms arcade LAN the engine was\n" "# written for, a constant 50-150ms of lag over the internet. 0 restores\n" "# the old arrival-time behaviour if you want to compare.\n" "#RP412NETCLOCK=0\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; }