Follow-up to the gos-displays.txt enumeration trace. The expanded log arrived from the Intel tester and settled the failure - and disproved the hypothesis it was written to test. What the log showed ------------------- The consistency check printed "No duplicate device assignments", so the suspected -tmon clash was NOT the cause. The actual evidence was that HSH_EnterFullScreen2 runs TWICE: the first entry brings every panel up DD_OK, the second fails on the same device with DDERR_EXCLUSIVEMODEALREADYSET and then DDERR_NOCOOPERATIVELEVELSET. Two independent causes, one operational and one a real defect. Cause 1 - Windows Display Settings numbers are not DirectDraw indices -------------------------------------------------------------------- The operator set -tmon by reading the numbers off the Display Settings arrangement diagram. On that machine all three numbering systems differ: Settings 3 -> \\.\DISPLAY1 -> device 0 (primary, 800x600, main) Settings 4 -> \\.\DISPLAY4 -> device 3 (USB adapter, radar) Settings 2 -> \\.\DISPLAY2 -> device 1 (mfd1) Settings 1 -> \\.\DISPLAY3 -> device 2 (mfd2) A permutation with two accidental fixed points - no derivable rule, and the Settings ordinal is not exposed by any documented API, so it cannot be translated in code. -tmon 1,4,2,3 worked first try (user-confirmed). This is why Windows ships an Identify button rather than publishing the mapping. Cause 2 - panels re-opened without being released (REAL BUG, all machines) ------------------------------------------------------------------------- EnterFullScreenMode() calls HSH_EnterFullScreen2() on every mode change and every lost-front-buffer recovery, but the teardown HSH_DirectDrawRelease2() was only wired to DirectDrawRelease(), i.e. full shutdown. CHSH_Device::InitFirst() therefore overwrote pDD with a fresh IDirectDraw7 while the previous one still held exclusive fullscreen, leaking it and its exclusive claim for the life of the process. It only bites when a panel sits on the Windows primary: secondary outputs grant exclusive mode again, the primary does not. Every working pod happens to have the main display on the primary, so no panel is ever there - which is the whole reason this looked hardware-specific. FIX: HSH_EnterFullScreen2() now releases first when hsh_initialized || hsh_mrdev_initialized. Tagged [panelreinit]. Reuses the existing teardown, which already restores the display mode, drops the coop level and clears the flags for both the MFD/radar and cameraship paths. New: -tident, the game's own Identify ------------------------------------- MW4.exe -tident [3..120, default 20] fills every display with a distinct colour and prints, huge, the number to type into -tmon, plus its device index and the role currently assigned to it. Then exits. Deliberately uses DDSCL_NORMAL and paints via GDI on the primary surface: no exclusive mode, no display mode change. Taking exclusive fullscreen on several devices at once is the very failure being diagnosed, and a diagnostic that trips over that fault is worthless - this works even on a pod where the MFD modes are broken. It opens the real DirectDraw devices rather than positioning GDI windows by HMONITOR, so it proves the device-index -> physical-output association through the same path the panels use. Positioning by DirectDraw's own reported HMONITOR would be circular. Implemented as IdentifyDisplays() in VideoCard.cpp, called at the end of FindVideoCards() followed by ExitProcess(0). Everything it paints is also written to gos-displays.txt, so the mapping survives even if a monitor is dead. Also added: desktop-order block ------------------------------- gos-displays.txt now prints the monitors sorted left-to-right by desktop position with their device indices, which maps directly onto the picture in Display Settings. For the reporting machine it reads out as -tmon 1,4,2,3 with no derivation required. Documentation ------------- * -help: new -tident entry. The -tmon text now states outright that these are NOT Display Settings numbers and points at -tident. Its old "-tmon 1,2,3,4" example was actively inviting the mistake that caused this report, so that section was rewritten rather than appended to. * Release notes (md + html, both hand-maintained): -tident section with sample output and the reason it has to exist; -tmon warning; new sections for the CLASH report and the re-entry fix in operator terms; switch-table row; expanded log description with the copy-before-relaunch warning; upgrade-checklist step to run -tident once after upgrading. * CLAUDE.md: STEP 12 with the full engineering record, including the three-way numbering table and both causes. * OPTIONS-INI.md: videodriverindex note now points at -tident. Cost ---- -tident is opt-in and exits immediately after. The desktop-order block is a handful of extra startup writes. Nothing added is reachable from the frame loop. Testing ------- -tmon 1,4,2,3 confirmed working on the reporting machine, which validates the diagnosis. The -tident and re-entry changes are NOT yet built or run; both need a rebuild of MW4.exe (Release + Profile) as they touch CoreTech GameOS. Worth checking on the W4100 bench first that CreateSurface(PRIMARYSURFACE) under DDSCL_NORMAL succeeds on secondary devices through dgVoodoo2 - if a panel comes up blank the log names the failing call. Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com> Co-authored-by: GitHub Copilot <copilot@github.com>
15 KiB
options.ini reference
Complete reference for options.ini, the runtime configuration file that sits next to
MW4.exe.
Compiled 2026-07-25 by reading the actual read sites in the source. Every key below was verified against code; keys that are shipped but not read by any code are called out explicitly rather than silently documented as working.
For command-line switches, run MW4.exe -help (writes mw4-help.txt next to the exe).
How the file is loaded
options.ini is a standard INI file read through the engine's own notation-file reader:
Stuff::NotationFile startup_ini("options.ini", NotationFile::Standard, true);
Stuff::Page* page = startup_ini.FindPage("graphics options");
page->GetEntry("bitdepth", &Environment.bitDepth);
Consequences of this design that matter in practice:
- Section and key lookup is by name. A misspelled key is not an error ?
GetEntrysimply returns false and the compiled-in default is kept. There is no warning. This is why several shipped keys silently do nothing (see "Dead settings" below). - The file is read at startup, in
GetGameOSEnvironment/InitializeGameEngine. Changing it while the game runs has no effect. - Parts of it are written back.
[server]in particular is re-saved from the in-game host UI, and[special commands] killgameis cleared on use. Hand edits to those sections can be overwritten. - A few sections are read via the Win32 profile API (
GetPrivateProfileInt/GetPrivateProfileString) instead ? noted per section.
Related but separate files
| File | Purpose |
|---|---|
options.ini |
This document. Main game configuration. |
options-game.ini, options-cam.ini, options-mr.ini |
Per-role variants copied over options.ini by the pod launcher for game pods, cameraships, and mission-review machines. Same format. |
options-*-var.ini |
Site-local variant overlays kept in the dev tree. |
c:\ctcl.ini |
Arcade console configuration (pod IP list, launch command lines, voice macros). Hardcoded path. Not covered here. |
banner.txt |
mw4print bottom-of-sheet banner text. Plain text, first line only. |
[graphics options]
Read in MW4Application.cpp (GetGameOSEnvironment) and MWOptions.cpp.
| Key | Type | Shipped | Effect |
|---|---|---|---|
bitdepth |
int | 16 | Display colour depth. The pods run 16; on modern Windows the DirectDraw 16-bit path additionally needs the DWM8And16BitMitigation compatibility shim. |
antialias |
bool | false | Full-screen antialiasing (Environment.AntiAlias). |
screenwidth |
int | 800 | Horizontal resolution, read by MWOptions. |
screenheight |
int | 600 | Vertical resolution. |
Read but overridden in this build ? editing has no effect:
| Key | Why |
|---|---|
videodriverindex |
The GetEntry call is commented out and Environment.FullScreenDevice is forced to 0. Use the -tmon command-line switch to assign display devices instead — run MW4.exe -tident first to find out which number is which monitor, as Windows Display Settings numbers do not match. |
Present in the shipped file and consumed by renderer subsystem initialisers
(MidLevelRenderer::InitializeClasses, gosFX, ElementRenderer, Compost), which are
handed the same notation file: noblend, shadowmode, mipbias, loadradius,
compositing, lod, maxlights, hidesky, effectlod, detailtexture, multitexture,
lightmaps, culturals, footsteps, fancywater, movietextures, simplelighting,
missionmusic, vertexlighting, fontsmall, fontmedium, fontlarge, fontlarge2,
fontlarge3.
These are stock MechWarrior 4 renderer/quality settings. They are passed through to the engine libraries rather than read in game code, so their exact clamping lives in those libraries. The shipped values above are the tested pod configuration; change them one at a time.
[sound options]
| Key | Type | Shipped | Effect |
|---|---|---|---|
hardwaremixing |
bool | false | Use DirectSound hardware mixing (Environment.soundMixInHardware). The 2016 release build enabled this (true); the current tree ships false. |
lowendsound |
bool | false | Stock low-quality audio path. |
radius |
float | 1.0 | 3D sound attenuation radius multiplier. |
[special commands]
| Key | Type | Shipped | Effect |
|---|---|---|---|
killgame |
bool | false | Self-clearing kill switch. If true at startup, the game sets it back to false, saves options.ini, and immediately calls ExitGameOS(). Intended as a remote "do not start" flag for a pod. |
autotorsocenter |
int | 2 | Torso auto-centring mode (VehicleInterface::perminateTorsoMode). 0 = off, 1 = slow, 2 = fast. |
Read but overridden in this build ? editing has no effect:
| Key | Why |
|---|---|
huddamagemode |
GetEntry commented out; forced to false. |
hudtargetdamagemode |
GetEntry commented out; forced to false. |
[server]
Multiplayer host defaults. This section is written back by the host configuration UI ? do not hand-edit it while the game is running.
Verified read: playerlimit (Adept/Application.cpp, both GetEntry and SetEntry).
The remaining keys form the persisted NetMissionParameters set and correspond
one-to-one with the in-game host options:
defaultservername, dedicated, visibility, joininprogress,
joininprogresscutoff, joininprogresscutofftime, heaton, forcerespawn, splashon,
weaponjamon, ammobayfireon, advancemodeon, armormodeon, splashpercentage,
unlimitedammo, friendlyfirepercentage, allowzoom, allow3rdperson,
allowdeadtochat, reportstats, serverrecycle, recycledelay, playmissionreview,
deadmechcantsee, deadmechcantseeotherteam, ruletype, usemapcycle, maxplayers,
maxbots, allowdecaltransfer, and the allowed*1 / allowed*2 bitmasks
(allowedmech, allowedbeam, allowedmissile, allowedprojectile, allowedsubsystem).
Notes:
allowed*1/allowed*2are 32-bit hex masks covering items 0?31 and 32?63 respectively.ffffffff= everything permitted.maxplayersandmaxbotsare serialised to clients as 5-bit fields, so the maximum usable value is 31. Writing 32 truncates to 0. SeeRAISING-PLAYER-CAP.md.- In arcade (CTCL) mode these are overwritten at mission launch by
CTCL_DefaultHostSetup, so this section mainly affects standalone hosting.
[joystick]
Read and written by Adept/control_mapping.cpp. Also stores control bindings, which is why
the file is saved from the controls UI.
| Key | Type | Shipped | Effect |
|---|---|---|---|
bithrottlecenter |
float | 0.3 | Bidirectional throttle centre dead zone. |
bithrottlelow |
float | 0.25 | Bidirectional throttle low dead zone. |
bithrottlehigh |
float | 0.06 | Bidirectional throttle high dead zone. |
onethrottlecenter |
float | 0.3 | One-way throttle centre dead zone. |
onethrottlehigh |
float | 0.06 | One-way throttle high dead zone. |
onewaythrottle |
bool | false | Treat the throttle axis as forward-only. |
[Cameraship Params]
Read by CamerashipParams::LoadOptions (VehicleInterface.cpp). Controls the automatic
spectator camera on cameraship pods. All distances are world units, all times are seconds,
all offsets are axis offsets applied to the camera or its look-at point.
Behaviour
| Key | Type | Effect |
|---|---|---|
AllowChatDisplay |
bool | Show chat on the cameraship view. |
AllowBOTs |
bool | Allow the camera to follow bots. |
DisplayCameraStates |
bool | Overlay the current camera state (debug aid). |
DegreesPerSec |
float | Camera orbit rate. Accepted range: strictly between -180 and 180. |
Timing
| Key | Effect |
|---|---|
TimeScoringInterval |
Interval between scoring cut-ins. |
TimeScoringDuration |
How long a scoring cut-in is held. |
TimeScoringAtEnd |
Scoring display duration at mission end. |
TimeIdleChange |
Idle time before the camera picks a new subject. |
TimeStandard |
Duration of the standard shot. |
TimeDeathWatching |
Duration of the death-watch shot. |
TimeDeathTransition |
Transition time into the death shot. |
TimeFixedTracking |
Duration of the fixed-tracking shot. |
TimeSideShot |
Duration of the side shot. |
TimeFrontShot |
Duration of the front shot. |
TimeOverShoulder |
Duration of the over-the-shoulder shot. |
Framing
| Key | Effect |
|---|---|
DistGroundLevel, DistGroundLevelShift |
Ground clearance and its shift. |
DistStandard, HeightStandard, HeightStandardLookAt |
Standard shot distance/height/look-at height. |
OffsDeathWatchingX/Y/Z |
Death-watch camera offsets. |
OffsFixedTrackingX/Y/Z |
Fixed-tracking camera offsets. |
DistSideShot, OffsSideShotX, OffsSideShotY, OffsSideShotHeadY |
Side shot framing. |
DistFrontShot, OffsFrontShotHeadY, OffsFrontShotCameraY |
Front shot framing. |
OffsOverShoulderHeroX/Y/Z, OffsOverShoulderEnemyY |
Over-the-shoulder framing. |
[Battle Tech Misc]
Read by CamerashipParams::LoadOptions (VehicleInterface.cpp). Several key names in
this section do not describe what they do ? they appear to be deliberately obscured. The
mapping below is from the source and is the authoritative meaning.
| Key | Type | Range / clamp | Actually sets |
|---|---|---|---|
SecsMissionReplay |
int | <0 or >=100 ¡æ 60 |
g_nSecsMissionReplay ? mission replay duration. |
SecsMissionReport |
float | <0 or >=30 ¡æ 15 |
g_fSecsMissionReport ? mission report screen duration. |
AutoPowerUpLevel |
float | clamped to 0.0?1.0 | g_fAutoPowerUpLevel ? auto power-up threshold. |
NeedFlushLevel |
float | clamped to 0.0?1.0 | g_fNeedFlushLevel ? heat level at which flush is advised. |
TimeMsgSender |
float | clamped to 0.1?5.0 | g_fTimeMsgSender ? how long a message sender name is shown. |
TimeList_Index |
int | <0 ¡æ 0, >=20 ¡æ 3 |
g_nTimeList_Index ? selected entry in the lobby time-limit dropdown. |
TimeList_Value |
int | <0 or >=100 ¡æ 7 |
g_nTimeList_Value ? lobby time limit in minutes. |
RuleBook |
int | 0 or 1 | ?? g_nMechVariant ? mech variant feature flag. Nothing to do with a rule book. |
DawnWar |
int | 0 or 1 | ?? g_nMechLabOp ? mechlab operation flag. |
BiggieSizeIt |
int | 0 or 1 | ?? g_nMechPodNum ? mech pod count flag, used by the console lobby to enable the larger roster (USE_O_MORE_PODS). |
CanYouHearTheFootSteps |
int | 0 or 1 | ?? g_nBlackMech ? black mech flag. |
?? The shipped file misspells two of these
The shipped options.ini contains:
secmissionreplay=30.0
secmissionreport=15.0
but the code reads SecsMissionReplay and SecsMissionReport (note the s in
Secs). The shipped keys therefore do nothing and the compiled defaults are used. If you
want these to take effect, correct the spelling.
Other keys in this section
| Key | Read by | Notes |
|---|---|---|
MinShift |
logreport.cpp via GetPrivateProfileInt, default 0 |
Torso shift minimum used by the match report. |
lrpt |
logreport.cpp via GetPrivateProfileString, and written back |
Opaque report cache/registration value. Do not hand-edit. |
[RookieMission]
Read by CTCL_SetCDSP (MW4Shell.cpp) at startup and exposed to the console lobby as
script globals. Defines what the arcade "Rookie Mission" / Default button loads. The whole
section is optional; omitting it uses the defaults below.
| Key | Type | Default | Effect |
|---|---|---|---|
MissionName |
string | ScarabStronghold - Attrition |
Mission loaded by default. |
GameType |
int | 2 | Game type index (2 = Attrition). |
TimeLimit |
int | -1 | Minutes. -1 = use the server's current time setting (TimeList_Value). |
Visibility |
int | 0 | 0 clear, 1 light, 2 medium, 3 heavy. |
Weather |
int | 0 | 0 off, 1 rain. |
TimeOfDay |
int | 0 | 0 day, 1 night. |
Radar |
int | 0 | 0 novice, 1 off, 2 bars, 3 unlimited. |
HeatOn |
int | 0 | Heat management. |
FriendlyFire |
int | 0 | Percentage; 0 = off, 100 = full. |
SplashDamage |
int | 0 | Splash damage on/off. |
UnlimitedAmmo |
int | 1 | Unlimited ammo on/off. |
WeaponJam |
int | 0 | Weapon jamming on/off. |
AdvanceMode |
int | 0 | Advanced gyro on/off. |
ArmorMode |
int | 0 | Advanced armor on/off. |
NoReturn |
int | 0 | No respawn (respawn limit) on/off. |
[battle tech print]
Read by mw4print.exe, not by the game. Uses GetPrivateProfileInt. All values are in
the printer's logical units; the defaults are in recscore.cpp.
Page layout: PaperMarginL, PaperMarginT, PaperMarginR, PaperMarginB,
TopImageHeight, GapColumn, ImageTitleHeight, GapTopImageTable.
Table layout: CellMarginLR, CellMarginTV, TableTextHeight, GapFFA, GapTableMH.
Mission highlights: MHHeight, MHTextHeight, GapMHText.
Text block: TextMarginL, TextMarginT, TextMarginR, TextMarginB, TextHeight,
InfoTextHeight.
| Key | Notes |
|---|---|
BannerText |
Legacy fallback only. The banner is now read from banner.txt (first line) next to mw4print.exe; this key is only consulted if that file is missing or empty. It was moved because the game rewrites options.ini and dropped unknown keys. |
Dead settings
These appear in the shipped options.ini but are not read by any code in this tree.
They are stock MechWarrior 4 leftovers. Editing them does nothing.
[network options] ? entire section is dead
connectiontype, connectionspeed, packetsize, defaultconnection, playericon,
teamicon.
Verified: no GetEntry/GetPrivateProfile* call anywhere references these key names. The
connection type actually used at runtime comes from conDefault.connectiontype, which is
populated from the multiplayer connection wizard, not from this file.
Individual dead keys
| Key | Section | Why dead |
|---|---|---|
videodriverindex |
[graphics options] |
Read call commented out; device forced to 0. |
huddamagemode |
[special commands] |
Read call commented out; forced false. |
hudtargetdamagemode |
[special commands] |
Read call commented out; forced false. |
secmissionreplay |
[battle tech misc] |
Misspelled; code reads SecsMissionReplay. |
secmissionreport |
[battle tech misc] |
Misspelled; code reads SecsMissionReport. |
Practical notes
- Back up
options.inibefore experimenting. The game rewrites parts of it, and an unknown key is silently ignored rather than reported, so a typo produces no diagnostic ? just default behaviour. - Per-role files. On a pod, the launcher copies
options-game.ini,options-cam.inioroptions-mr.inioveroptions.inidepending on the machine's role. Edit the role file, not the live one, or your change will be overwritten at next launch. - The 2016 release drop shipped
hardwaremixing=true,BiggieSizeIt=1andRuleBook=1, where the current dev tree ships different values. If you are chasing a behavioural difference against that release, compare these first. - Case. Section and key names are matched case-insensitively by the notation reader, but the spelling must otherwise be exact.