RP412PHYSICSHZ defaults to 50: the simulation advances in fixed 20 ms steps whatever the display does, and every machine plays the same race. The proof preceded the promotion - a scripted lap with a crash, a burn, a tumble and two respawns runs bit-identical at 30, 60 and 144 fps, and identical runs reproduce exactly, neither of which was ever true of this engine at any frame rate. Fifty because it is exact on the engine's millisecond clock (a rate like 60 quietly becomes 17 ms steps wearing the wrong name), and because its settled hover ride height measured closest to the frame-coupled physics the game has always run - the least change of feel for the most change of correctness. The pods' 25 and the smoother 100 stay one line away for the play testers, and 0 keeps the original frame-coupled behaviour for comparison, where the frame rate is part of the simulation. Carried-over environ files do not mention the option, so existing testers get 50 on their next build and rpl4.log names both the option they have not heard of and the mode every launch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
682 lines
28 KiB
C++
682 lines
28 KiB
C++
#include "rpl4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "rpl4environ.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
//########################################################################
|
|
// 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"
|
|
"# Read the keyboard, pad and stick only while the game is the window in\n"
|
|
"# front. The pod was the only thing running on its cabinet, so the\n"
|
|
"# virtual RIO reads the key state directly rather than waiting on the\n"
|
|
"# message pump - which means it reads it whatever is in front, and\n"
|
|
"# switching to another window to type flies the pod around while you\n"
|
|
"# type in it.\n"
|
|
"# 1 controls go neutral when you switch away (default)\n"
|
|
"# 0 read them regardless, as earlier builds did\n"
|
|
"# Any window of the game counts as the game, so clicking an MFD pane or\n"
|
|
"# the plasma glass does not drop your controls. Real RIO cockpit\n"
|
|
"# hardware is unaffected either way - this is the keyboard, pad and\n"
|
|
"# joystick path only.\n"
|
|
"RP412INPUTFOCUS=1\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 <title>=<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"
|
|
"# How long the podium holds before the results screen, in seconds\n"
|
|
"# (1-60). The stand is worth a look but eleven seconds of one parked\n"
|
|
"# pod is a long look in single player. With RP412PODIUM=0 there is no\n"
|
|
"# hold at all - straight to the results, as that option promises.\n"
|
|
"#RP412PODIUMHOLD=11\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"
|
|
"# The physics step, in steps per second. 50 is the default: the\n"
|
|
"# simulation advances in fixed 20 ms steps whatever the display does,\n"
|
|
"# so the SAME race plays out on every machine - measured bit-identical\n"
|
|
"# at 30, 60 and 144 fps, through a scripted lap with a crash, a burn\n"
|
|
"# and two respawns. A pod at 30 fps and a pod at 144 are finally in\n"
|
|
"# the same gravity.\n"
|
|
"#\n"
|
|
"# The alternatives, all exact on the engine's millisecond clock:\n"
|
|
"# 25 the arcade pods' rate - the step the original handling was\n"
|
|
"# tuned against, coarsest contact response\n"
|
|
"# 100 the smoothest contact and terrain response\n"
|
|
"# 0 the original frame-coupled physics, where the frame rate is\n"
|
|
"# part of the simulation - kept for comparison\n"
|
|
"# (Rates that do not divide 1000 evenly - 60, say - quietly run at the\n"
|
|
"# nearest millisecond step instead; the log says so if you try one.)\n"
|
|
"#\n"
|
|
"# What to feel for between rates: hover bounce, wall hits, how the pod\n"
|
|
"# takes the crest of a hill. Report the rate with the verdict.\n"
|
|
"RP412PHYSICSHZ=50\n"
|
|
"\n"
|
|
"# How long one background pass may spend drawing cockpit gauges, in\n"
|
|
"# milliseconds. The gauges and the MFD/map displays are redrawn in the\n"
|
|
"# time left over after the 3D view; on a big, busy map there is none\n"
|
|
"# left, and at the original one-gauge-per-pass the map and the countdown\n"
|
|
"# clock could sit frozen for seconds at a time - until something (a\n"
|
|
"# death, say) lightened the 3D view enough for the backlog to drain.\n"
|
|
"# Working to a slice ties the refresh rate to elapsed time instead. Set\n"
|
|
"# 0 for the old behaviour; raise it to favour the displays over frame\n"
|
|
"# rate.\n"
|
|
"RP412GAUGESLICE=2\n"
|
|
"\n"
|
|
"# How many times the map redraws per turn of the gauge rate wheel, 1 to\n"
|
|
"# 16. The renderer gives each gauge one step of a sixteen-step wheel and\n"
|
|
"# a gauge redraws only on its own step, so a map left on one step waits a\n"
|
|
"# whole turn. 16 = redraw on every step (default); 1 = whatever the gauge\n"
|
|
"# data asks for, which is how it behaved before this existed. Each step\n"
|
|
"# costs one map redraw against a pass that runs ninety gauges.\n"
|
|
"RP412MAPRATE=16\n"
|
|
"\n"
|
|
"# 1 = log how many times a second every cockpit display is actually\n"
|
|
"# refreshed, to rpl4.log. Watching the screen cannot tell a display that\n"
|
|
"# has stopped refreshing from one whose picture simply is not changing.\n"
|
|
"#RP412GAUGEDIAG=1\n"
|
|
"\n"
|
|
"# 0 = light the on-screen cockpit buttons on the same slow cadence the\n"
|
|
"# arcade pod's serial hardware used. The lamp state is filled once per\n"
|
|
"# gauge cycle, so under the load described above the lit buttons froze\n"
|
|
"# and flashing ones stalled while the 3D view stayed perfectly smooth.\n"
|
|
"# On by default: the buttons are refreshed every frame instead. Ignored\n"
|
|
"# when real RIO hardware is selected - the pod keeps its own cadence.\n"
|
|
"#RP412LAMPSWEEP=0\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"
|
|
"# ---- Test harness -----------------------------------------------------------\n"
|
|
"\n"
|
|
"# The knobs that make a run repeatable and measurable. All are off\n"
|
|
"# unless set and cost nothing when off; none belongs in a real race.\n"
|
|
"# They exist so a claim about the game can be tested instead of argued.\n"
|
|
"\n"
|
|
"# Dump the gauge profile to rpl4.log every N seconds: every cockpit\n"
|
|
"# display with its rate mask and tier, how often it ran and what it\n"
|
|
"# cost. This is the engine's own ProfileReport, which was only ever\n"
|
|
"# reachable from the arcade RIO mapper's F11 before.\n"
|
|
"#RP412GAUGEPROFILE=8\n"
|
|
"\n"
|
|
"# 1 = log renderable construction and what each frame is made of, so a\n"
|
|
"# model that never got built can be told from one that is simply out\n"
|
|
"# of shot.\n"
|
|
"#RP412RENDERDIAG=1\n"
|
|
"\n"
|
|
"# 1 = trace the player pod's position to rpl4.log on the SIMULATION's\n"
|
|
"# own clock, stopping the pod dead at the green light so every run\n"
|
|
"# starts from rest. Two runs of the same race then compare sample for\n"
|
|
"# sample - this is the instrument that proved RP412PHYSICSHZ plays the\n"
|
|
"# same race at every frame rate, bit for bit.\n"
|
|
"#RP412PHYSTRACE=1\n"
|
|
"\n"
|
|
"# Try this drop zone first at spawn instead of a random pick. The pick\n"
|
|
"# is seeded by RANDOM=, but a seed only repeats a run if the same\n"
|
|
"# NUMBER of draws comes before the pick, and that count rides on load\n"
|
|
"# timing - so pin the pad too, or two 'identical' runs start over\n"
|
|
"# different ground. Falls back to the random walk if the zone is\n"
|
|
"# taken, so it cannot wedge.\n"
|
|
"#RP412SPAWNZONE=3\n"
|
|
"\n"
|
|
"# Drive the pod from a timeline file instead of the controls - the\n"
|
|
"# same lap, exactly, every run. One row per change, held until the\n"
|
|
"# next row: time-in-seconds throttle stickX stickY pedals, values\n"
|
|
"# 0..1 for throttle and -1..1 elsewhere, # for comments. Times are\n"
|
|
"# SIMULATION seconds from the green light, so with RP412PHYSICSHZ set\n"
|
|
"# the same script is the same race at any frame rate - this is how\n"
|
|
"# driving, not just settling, gets verified bit-identical.\n"
|
|
"#RP412INPUTSCRIPT=testlap.txt\n"
|
|
"\n"
|
|
"# 1 = log the XInput-class controllers the generic-joystick scan skips\n"
|
|
"# (attached DirectInput devices are always logged). For debugging a pad\n"
|
|
"# that answers twice or a stick that does not answer at all.\n"
|
|
"#RP412JOYLOG=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"
|
|
"# The cabinets ran the game at unity and did all their volume and tone\n"
|
|
"# shaping outside it, in an amplifier and a 3-way crossover. You almost\n"
|
|
"# certainly have neither, so these two stand in for them. Both default\n"
|
|
"# to leaving the mix exactly as the pod played it.\n"
|
|
"\n"
|
|
"# Master volume, 0.0 to 2.0, the amplifier's knob. 1.0 is unity. The\n"
|
|
"# sound effects now carry the pitch, layering and dynamics the original\n"
|
|
"# AWE32 soundbanks ask for, which is a good deal livelier than earlier\n"
|
|
"# 4.12 builds - lower this if the whole thing sits too hot.\n"
|
|
"#\n"
|
|
"# PageUp and PageDown change it while you play, in steps of 0.05, and\n"
|
|
"# whatever you leave it on is written to volume.cfg beside the exe and\n"
|
|
"# used from then on - so this line only decides where a machine that has\n"
|
|
"# never been touched starts out. Delete volume.cfg to come back here.\n"
|
|
"#RP412AUDIOVOLUME=0.8\n"
|
|
"\n"
|
|
"# Bass trim, 0.0 to 1.0, the crossover's low band. 1.0 is the low end\n"
|
|
"# exactly as authored. The soundbanks put real weight under collisions,\n"
|
|
"# engines and explosions - deep layers earlier builds played at the\n"
|
|
"# wrong rate, so they barely sounded at all. Lower this to pull that\n"
|
|
"# back; it eases in below 22kHz of playback rate and reaches full cut\n"
|
|
"# on the deepest layers, leaving the mid and top alone.\n"
|
|
"#\n"
|
|
"# Home and End change it while you play, in steps of 0.05, and what you\n"
|
|
"# leave it on is written to bass.cfg beside the exe and used from then\n"
|
|
"# on - so this line only decides where an untouched machine starts.\n"
|
|
"# Delete bass.cfg to come back here.\n"
|
|
"#RP412AUDIOBASS=0.7\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"
|
|
"# Who transforms the vertices: hw hands it to the GPU, sw does it on the\n"
|
|
"# CPU as this engine always has. There was no hardware to hand it to when\n"
|
|
"# it was written; there is now, and it is not close - on a busy track the\n"
|
|
"# 3D foreground drops from about 17ms a frame to under half a\n"
|
|
"# millisecond, and all of that time goes back to the cockpit displays,\n"
|
|
"# which is what makes the map, the clock and the gauges live rather than\n"
|
|
"# updating every few seconds.\n"
|
|
"# Falls back to sw by itself if the adapter has no hardware T&L. sw is\n"
|
|
"# the way back if a driver's fixed-function lighting or fog looks wrong -\n"
|
|
"# the two are not bit-identical.\n"
|
|
"RP412VERTEXPROC=hw\n"
|
|
"\n"
|
|
"# 0 = present without waiting for the panel's retrace. Costs tearing,\n"
|
|
"# buys latency. Measured to make very little difference to the frame\n"
|
|
"# budget here - the frame is full of work, not waiting - so this is a\n"
|
|
"# preference rather than a fix.\n"
|
|
"#RP412VSYNC=0\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;
|
|
}
|