Skip to content

Latest commit

 

History

History
401 lines (353 loc) · 120 KB

MODERN.md

File metadata and controls

401 lines (353 loc) · 120 KB

The modern build

NOLFSRC_MODERN is the project's define for everything that departs from the retail game: bug fixes and new features. It is set only in the modern build (make modern, make modern-debug; see BUILDING.md, "Variant builds"), never in the release build, which stays byte-for-byte the retail game and is the only build make progress scores. Every change goes behind #ifdef NOLFSRC_MODERN with the retail code kept, unchanged, in the #else.

The first goal of the modern build is to bring in the fixes and features of NOLF Modernizer by HeyThereCoffeee (haekb) -- https://github.com/haekb/nolf1-modernizer, https://haekb.itch.io/nolf-modernizer -- version 1.006 Patch 4. This file is the inventory of everything it changes, and the work plan for doing the same here. Credit for finding these problems, and for the design of the features, belongs to the Modernizer; each item below names the Modernizer change it comes from.

A second source is ChthonVII's fork of the Modernizer (https://github.com/ChthonVII/nolf1-modernizer), which adds a crouch toggle (from the "crouch toggle" Modernizer on Mod DB) and Daniel Gibson's fixes (haekb/nolf1-modernizer#55), and which works out what the different releases called "Patch 4" contain. Everything in the original repository's history is covered below, either ported or marked "not ported" with the reason. The fork's five additions are M-P2-15, M-P3-21, M-P5-17, M-P5-18 and M-P5-19.

Rules for this work

  • Reimplement, never copy. The Modernizer's repository carries no licence of its own (its readme says the code remains under Monolith's v1.003 source EULA), so none of its code is copied into this repository. Read it to learn what changes and why; write the implementation yourself, in this project's style. Commit hashes below are for traceability and credit only. Its assets are another matter: a few of its files are used as they are, from the ext/nolf1-modernizer submodule (never committed here), each named where it is used -- so far the two seasonal menu models (M-P3-11).
  • The 2000-era compilers. The modern build uses the same toolchain as the release build (VC6 cl 8447 for the game DLLs' compiler settings, Intel C/C++ 4.0 for d3d.ren). The Modernizer was built with Visual Studio 2019, SDL2, Microsoft Detours and C++11 (std::string, std::vector, unordered_map, std::regex, lambdas, auto, strcpy_s, brace initialisers). None of that is available or wanted here:
    • SDL2 window/mouse/timer use becomes Win32: raw mouse input through WM_INPUT (declare RAWINPUTDEVICE, RAWINPUT, RegisterRawInputDevices and GetRawInputData by hand -- VC6's headers predate them -- and load the two functions with GetProcAddress from user32.dll), window placement with SetWindowPos/MoveWindow/SetWindowLong, timing with QueryPerformanceCounter. SDL_Log becomes the modern build's own log (see M-P2-07).
    • Detours hooks into lithtech.exe become direct changes in our engine source (src/engine/), because we own it.
    • Containers become fixed arrays, CMoArray, or plain C strings.
  • Game data comes from the retail game. No game data is committed. A data change is a recipe in src/modern/data/ (src/modern/data/README.md): a unified diff against the retail file, or a new file we wrote ourselves. Modernizer- authored art, models, fonts and music are not ported; where a retail file can stand in, or an asset can be generated, the item says so.
  • Open middleware. The modern build has no RAD binaries: OpenMiles (ext/openmiles) stands in for Miles and our FFmpeg-based player (src/modern/binkw32/, ext/ffmpeg) for Bink, built by build/ext.sh (BUILDING.md, "The modern build's middleware"). Its movies play (no +DisableMovies).
  • Release unchanged. After every change: the release build's objects for that file still match (build/engine.sh <target>-one <file> for the engine; make progress for everything), and make modern builds and plays (build/playtest.sh out/nolf-modern-release ...).

Status legend

Status Meaning
todo not started
in progress someone is on it (name the branch, e.g. wt/p3)
done implemented behind NOLFSRC_MODERN, release verified unchanged, modern build tested
not ported deliberately left out; the reason is given
decided a question settled rather than code to write; the decision is given

When an item is done, change its status here in the same change, and if it fixes a retail bug, add or update the entry in RETAIL_BUGS.md.

New console / config variables

All are created with VarTrack, so they are console variables, client-side unless noted. Creating one does not save it: autoexec.cfg keeps those an options folder writes (with the + that marks a variable to save), and any typed as +Name value; see VARIABLES.md.

Variable Type Default Meaning Item
FramerateLimit int 240 the client's frames a second at most in single player (0: no limit); multiplayer is always held at 60; suspended while loading; Interface options' "Frame rate limit" (60, 120, 144, 240, unlimited) M-P1-01, M-P1-23, M-P1-25
ShowFramerate int 0 draw an averaged frame-rate counter in the bottom-left corner M-P3-14
OldMouseLook int 0 use the retail mouse-look path (DirectInput axis offsets, CursorCenter) instead of raw input; Mouse options "Legacy mouse look" M-P1-03
NoRawInput int 0 keep the new mouse look but read the cursor's motion (warped back to the window centre) instead of raw input M-P1-03
NoFunMenus int 0 always use the plain main menu (no seasonal variants) M-P3-11
RestrictCinematicsTo4x3 int 0 black side bars on cinematics at resolutions wider than 4:3 M-P3-08
QuickSwitch int 0 next/previous weapon switches immediately M-P5-12
CrouchToggle int 0 the Duck control toggles crouching instead of holding it (not while swimming, on a ladder, on a vehicle or dead); Game options' "Toggle crouch" M-P5-19
UIScale float 0.5 HUD element scale, multiplied by the screen's height ratio (never drawn smaller than retail); Interface options' HUD size M-P3-12
UseGotyMenu int 1 main menu art: Game of the Year (1) or original (0) M-P3-10
ConsoleBackdrop int 0 developer console background: 0 or 1 the retail menus' blank tag, 2 black M-P2-05
BigHeadMode float 0 heads drawn 1.8 x this size (0 = off; read as 0 in multiplayer); also a server variable, set by the client on entering a world and from the console (M-P2-08) M-P2-10
DisplayTriggers int 0 server: draw trigger volumes as yellow boxes (type it into the console: M-P2-08 passes it on) M-P2-11
EnableScreenTinting int 1 0 = draw screen tints with a 2D overlay instead of the camera's light add (Intel fix; Display options "Intel graphics fix" sets both; d3d7.ren only) M-P1-10
EnableLightScale int 1 0 = keep the global light scale at 1: no light scaling at all (Intel fix; d3d7.ren only) M-P1-10
ModPatchNum float 0 last modern-build config version seen (P1_CONFIG_VERSION, now 1.5); drives one-time upgrades M-P1-12
PadLookSpeed float 1 the controller's right stick: turn rate, times 180 degrees a second across and 120 up and down; Controller options' "Look speed" M-P7-03
PadInvertY int 0 the right stick's up looks down; Controller options' "Invert look" M-P7-03
PadLookCurve float 2 the right stick's response: its deflection past the dead zone, raised to this (1 = linear) M-P7-03
PadLookDeadZone float 0.15 the right stick's radial dead zone (0..0.9 of its travel); Controller options M-P7-03
PadMoveDeadZone float 0.2 the left stick's radial dead zone; Controller options M-P7-04
NetJoinPassword string (empty) the password +connect joins a locked server with (the join folders ask for it instead) M-P6-10
MasterServer string (empty = master.openspy.net) GameSpy master server the browser asks and a hosted server reports to (host or host:port; none turns it off); also saved in NetHost.txt, which the dedicated server's -MasterServer sets M-P6-02, M-P6-13
Windowed int (engine) existing engine variable, now set by Display options' window mode (the mode is set again when it changes) M-P1-06, M-P1-22
WindowMode int 1 with Windowed 1: 2 is a borderless window at the desktop's resolution, otherwise a framed window; Display options' "Window mode" M-P1-22
M-P1-23 300 fps in single player feature M-P1-01 holds the game at 60 frames a second because faster, things went wrong. Put them right, and hold single player at 300 for high refresh rate screens. -- (ours)
M-P1-24 32-bit textures by default feature Display options' texture depth starts at 16-bit (the renderers' 32BitTextures defaults to 0, and no config sets it), which bands the game's textures on any card of today. Make 32-bit the default. -- (ours)
M-P1-25 Frame-rate limit choices feature M-P1-23's limit was 300 or none. Offer the common refresh rates instead: 60, 120, 144, 240, or no limit. -- (ours)
M-P1-26 Ultra texture detail feature Texture resolution's High preset leaves the six sliders at 3 of 5 (GroupOffset 0). 357 textures' own files add an offset of 1 (among them the character heads at 256x256 and the skyboxes at 512x512), so High draws them at half size; only sliders above 3 cancel it. Add a preset that puts every slider at its top. -- (ours)
M-P1-27 Movies keep their shape fix The game plays its intro movies with PLAYBACK_FULLSCREEN, which BinkVideoInst::UpdateOnScreen (and SmackVideoInst's twin) Blts over the whole back buffer: on a wide screen the 4:3 movies (foxpc.bik 320x240, LithLogo.bik and LTLogo.bik 640x480) come out wide, and SierraLogo.bik, 800x400, is squeezed to 4:3 even on a 4:3 screen. -- (ours)
VSync int 0 opengl.ren and d3d9.ren: wait for the display's refresh at each flip (read when the renderer starts) M-P0-04
Antialiasing int 0 opengl.ren and d3d9.ren: samples per pixel for multisampled antialiasing (0 off, 2, 4, 8; read when the renderer starts); Advanced Display's "Antialiasing" M-P1-18
M-P1-19 Launcher starts the game on modern Windows fix On some Windows 10/11 machines "Launch" fails with "Could not launch LITHTECH.EXE" although lithtech.exe starts by hand with the same arguments. The retail launcher passes its own STARTUPINFO (GetStartupInfo: whatever Explorer, a shortcut or Steam started it with) to CreateProcess and leaves Windows to search for a bare lithtech.exe. -- (ours)
M-P1-20 No launcher warning popups feature "Display..." first warns that listing the display modes may take some time, and "Advanced..." that tinkering with the options can make the game run worse; both are OK/Cancel boxes in the way of the dialog, and pointless on a machine of today (the Advanced dialog's Help button still explains each option). -- (ours)
M-P1-21 Launcher art keeps its colours when the launcher loses focus fix Under Wine (Proton), once another window takes the focus the launcher's splash art turns to false colours, and stays that way until the launcher is repainted. Retail redraws the splash from WM_ACTIVATEAPP (to select its palette as a background palette); under Wine a draw made there, while the activation is changing, comes out miscoloured, even with no palette in use (drawn with DIB_RGB_COLORS). The same draw from WM_PAINT while inactive is right. Not checked on Windows. -- (ours)
M-P1-22 Window mode feature Display options' "Windowed" yes/no (M-P1-06) made a window as large as the desktop borderless by itself. Make the choice explicit: full screen, a window, or a borderless window. -- (ours)
M-P0-05 Direct3D 9 renderer, the default feature A second render DLL from the same core, d3d9.ren: the Direct3D 9 fixed-function pipeline, which Proton runs on Vulkan (DXVK) and which every Windows has, where d3d.ren is DirectX 7 (DirectDraw and Direct3D 7, which Wine and Proton translate least well). It is the modern build's default renderer; our d3d.ren, with the modern fixes, ships beside it as d3d7.ren, and opengl.ren stays, so the three can be compared. -- (ours)
Anisotropic, Bilinear, Trilinear int (engine) existing d3d.ren variables (retail d3d.ren reads Anisotropic), now set from Advanced Display's "Texture filtering" M-P1-09

Work packages

Eight packages. Each is meant for one worker in its own worktree (tools/agentwt.sh add p<N>, branch wt/p<N>), merged in the order P0, P1, P2, ..., P7.

Package Theme Items Owns (files nobody else edits)
P0 engine 3 src/engine/d3d/w4_1001eb90_os.cpp, src/engine/d3d/big1ef30.cpp, src/engine/lithtech/clientconsole.cpp, any other src/engine/ file
P1 timing, input, window, display options 13 WinUtil.cpp, FolderDisplay.*, FolderAdvDisplay.*, FolderMouse.*, LightScaleMgr.cpp, ScreenTintMgr.*, LoadingScreen.* (timing parts)
P2 developer console, logging, cheats, debug toggles 13 new ConsoleMgr.* (our own), MessageMgr.* (cheats), GameBase.cpp, Attachments.cpp, CharacterHitBox.cpp, BodyFX.*, NodeController.cpp, SpecialFX.h
P3 menus and HUD at any resolution 20 BaseFolder.*, BaseSelectionFolder.cpp, all Folder*.cpp not listed elsewhere, new FolderInterface.* (our name for the HUD/Interface options folder), InterfaceResMgr.*, PlayerStats.*, LayoutMgr.*, ListCtrl.*, PopupText.cpp, ClientInfoMgr.cpp, new fill-rect cache file
P4 jukebox, game data, strings 11 new FolderJukebox.*, new JukeboxButeMgr.*, src/modern/data/**, tools/moddata.py (recipe extensions), ClientRes.rc/ClientRes.h string tables (see the string rule below)
P5 gameplay, AI, physics and crash fixes 16 CMoveMgr.*, AIMovement.cpp, AIPathMgr.cpp, Door.cpp, PlayerObj.cpp, CharacterFX.cpp, ModelButeMgr.cpp, WeaponModel.*, WeaponChooser.cpp, FolderSummary.cpp, FolderPlayer.cpp (crash part), src/lithshared/ButeMgr/butemgr.cpp
P6 multiplayer, the dedicated server 14 FolderJoin.*, FolderHost.cpp, CommonUtilities.*, src/lithshared/gamespy/**, ClientServerShared.h, VersionMgr.*, src/modern/nolfserv/** and the NOLFSRC_MODERN blocks of src/engine/nolfserv/
P7 controller support 8 src/engine/lithtech/clientinput.cpp, src/modern/engine/**, VehicleMgr.cpp, FolderControls.cpp, FolderCustomControls.cpp (capture), cshell/p7_*

Hot-spot files and how to share them

These files are edited by several packages. To keep merges mechanical:

  • Every change in a hot-spot file goes in its own #ifdef NOLFSRC_MODERN block, opened with a one-line comment naming the item (// M-P1-01: frame limiter). Never reformat, reorder or re-indent surrounding retail code.
  • Prefer one call into a package's own file over inline logic in the hot spot (e.g. ModernFrameLimiter_EndFrame(); in GameClientShell.cpp, with the body in a P1-owned file).
  • Merge order is P0 → P6; a later package rebases on the earlier ones.
Hot spot Packages Rule
src/game/ClientShellDLL/GameClientShell.cpp / .h P1, P2, P3, P5, P6 call-outs only; new members grouped at the end of the class in one #ifdef block per package
src/game/ClientShellDLL/InterfaceMgr.cpp / .h P1, P2, P3, P5 as above
src/game/ClientShellDLL/InterfaceResMgr.cpp / .h P1 (borderless window), P3 P3 owns; P1 adds one call
src/game/ClientShellDLL/FolderMgr.cpp / .h P3, P4 new FOLDER_ID_* values appended in this order, before the last id: MAIN_CASUAL, MAIN_WINTER, INTERFACE (P3), JUKEBOX (P4)
src/game/ClientShellDLL/LayoutMgr.cpp P3, P4 the folder-name table gets the same four names in the same order
src/game/ClientShellDLL/FolderOptions.cpp, FolderCommands.h P3, P4 FOLDER_CMD_INTERFACE (P3) then FOLDER_CMD_JUKEBOX (P4)
src/game/ClientShellDLL/ClientUtilities.cpp / .h P2 (console command, key names), P5 (toggle-zoom command) commands appended before the last array entry, console first
src/game/Shared/CommandIDs.h, MsgIDs.h P2, P5, P1, P6 IDs fixed below
src/game/ObjectDLL/GameServerShell.cpp / .h P1, P2, P6 call-outs only
src/game/ClientShellDLL/PlayerStats.cpp P3, P5 P3 owns; P5's one-line fix in its own block
new source files any not in the .dsp files: put them in src/modern/cshell/ or src/modern/object/ (one file per package and topic, e.g. cshell/p1_input.cpp), which the modern build compiles in by itself (src/modern/README.md)
new strings all not in ClientRes.rc: each package has its own src/modern/cres/<package>.rc and .h (e.g. cres/p3.rc, cres/p3.h), which the modern build adds to CRes.dll; IDs from the package's range below

Fixed identifiers (chosen equal to the Modernizer's, so its multiplayer messages mean the same thing):

Identifier Value Item
COMMAND_ID_TOGGLE_CONSOLE 80 M-P2-03
COMMAND_ID_TOGGLE_ZOOM 81 M-P5-13
MID_CONSOLE_COMMAND_CLIENT 238 M-P2-08
MID_DONT_LOCK_SERVER_FPS 250 M-P1-02

New CRes string IDs, by package (30000-30099 is reserved for the Game of the Year strings of M-P4-08, whose IDs are fixed by the retail level data):

Package Range
P1 31000-31099
P2 31100-31199
P3 31200-31299
P4 31300-31399
P5 31400-31499
P6 31500-31599
P7 31600-31699

String text is ours: write new wording rather than copying the Modernizer's.


P0 -- engine

ID Title Kind What and why Modernizer source Our files Data / strings Depends Status
M-P0-01 Widescreen field of view (Hor+) feature The renderer derives the horizontal field of view as if every screen were 4:3, so wide screens get a vertically squashed, horizontally cropped view. The Modernizer's patched d3d.ren (BIN/, 120 bytes) jumps out of two places in the camera setup into a routine that recomputes the field of view from the screen's aspect ratio: fov' = 2*atan(tan(fov/2) * aspect / (4/3)), applied to the field at +0xb4 (from width/height at +0xac/+0xb0) at 0x1001edb0, and to the width/height pair at +0x24/+0x28 at 0x1001f309, with the constant 4/3 (0x3faaaaab) and 0.5. Do the same calculation in C in our source. BIN/d3d.ren (patch sites 0x1001edb0, 0x1001f309; code at 0x10087b86, 0x10087bdf) src/engine/d3d/w4_1001eb90_os.cpp (function 0x1001eb90: the scene's field of view), src/engine/d3d/w43_10017c10_osoiqipni.cpp (function 0x10017c10: models with their own field of view) -- -- done. The scene's horizontal field of view is widened in 0x1001eb90 from the scene rectangle's own width/height (not right/bottom, so an off-origin rectangle works too); at 4:3 nothing changes. The Modernizer's second patch (0x1001f309: the transform's +0x4c replaced by a fixed approximation that ignores the game's field of view) is done differently: +0x4c is read only by the model draw for models with a field of view of their own (the player-view weapons), whose X scale is tan(fovX/2) and so would stretch sideways; there the game's own (4:3) field of view is used and the scale divided by the widening factor, so the weapon looks exactly as at 4:3 whatever the game's FOV (zoom, damage effects). big1ef30.cpp is unchanged. Shown at 960x540 vs 720x540 (build/playtest.sh)
M-P0-02 Console output to the game's console feature The Modernizer's developer console (P2) shows the engine's console text by detouring the engine's console print functions. Here, add a hook in the engine instead: when a console line is printed, also pass it to a callback the game DLL registers. Suggested shape: append one method to the end of ILTClient (both src/LT2/sdk/inc/iltclient.h and the engine's implementation) under NOLFSRC_MODERN, so existing slots keep their layout. DetourMgr.cpp (hooks 0x0041b880, 0x0041b8c0) src/engine/lithtech/clientconsole.cpp (0x0041b880, 0x0041b8c0, 0x0041d380), src/LT2/sdk/inc/iltclient.h, the ILTClient implementation in src/engine/lithtech/ -- -- done: ILTClient::SetConsoleOutputFn appended after the last retail slot (iltclient.h, under the define); the engine calls it from CConsoleView 0x0041d1c0 in clientconsole.cpp, the one place every console line passes (after the level filter, inside the console's lock). The vtable slots come from link/vtables.txt's %ifdef NOLFSRC_MODERN block (tools/mkvtabobj.py --define, passed by build/engine.sh in the modern build); the release vtables.obj is unchanged
M-P0-03 Fast FillRect fix (performance) ILTClient::FillRect locks and uploads a surface on every call, so screens that fill many rectangles each frame (menus, sliders, the scope, letterbox bars) lose a lot of frame rate. The Modernizer works around it in the game DLL with a cache of pre-filled surfaces drawn with blits (M-P3-17). Investigate fixing the engine/renderer path itself; if that is not practical, P3's game-side cache stands. commits 128eae2, 992bd24, OptimizedRenderer.cpp src/engine/lithtech/warpblit.cpp (ci_FillRect, 0x0040ea30), src/engine/d3d/d3dblit.cpp, src/engine/d3d/w26_10020fd0_osoiqip.cpp (GetRenderObject, 0x10020fd0) -- -- done. Fixed in the engine: a FillRect onto the screen no longer locks the whole back buffer; the renderer does a DirectDraw colour-fill Blt (ending/restarting the scene and adding the dirty rectangle, as its DirectDraw blit path does). The engine finds the renderer's fill by name through GetRenderObject("FillScreenRect"), so with a retail d3d.ren (which answers NULL) it keeps the retail path; the colour goes through the same screen-format conversion as the software fill, so the pixels are the same. Fills of off-screen surfaces keep the retail path (a system-memory lock; the re-upload when such a surface is next drawn is unavoidable either way). M-P3-17 is therefore not needed
M-P0-04 OpenGL renderer feature A render DLL on OpenGL, opengl.ren, loadable in place of d3d.ren: the first step to a game that does not need Direct3D, and eventually runs natively on Linux. Written from the Mac release's OpenGL renderer (the same engine version, with every function named: tools/pef.py) and from what the PC engine expects of d3d.ren. -- (ours; the Mac release rez/games/nolf-mac-usa) src/modern/render/ (README.md there), build/ext.sh, build/retailextract.py (installs it) -- -- done. Stage 1 done: the device (WGL on the engine's window, windowed or full screen), the mode list, clear and flip, every 2D path (surfaces, keyed/scaled/warped blits, Optimized-2D colours and blend modes, the screen lock, FillScreenRect, BlitFromScreen), screenshots, texture upload (32-bit and S3TC). Menus' 2D, the console and the HUD's 2D draw. Stage 2 (the world) done but for portal views: visibility through the engine's own PVS query, world models, the sky's world models, fog, lightmaps and detail textures at the retail brightness, matching d3d.ren in T01S01's first room. Stage 3 (objects) under way: models, sprites, particle systems, poly grids, line systems and canvases draw (the main menu and M03S01's cabin match d3d.ren), dynamic lights light the world, models take the sun and the world's static lights, environment maps (chrome, the world's, water's) and model shadows draw, and all 60 single-player worlds match d3d.ren; the movies play (DirectDraw stand-ins for the engine's players). Everything d3d.ren draws in the game is drawn. Since M-P0-05 the renderer is a core over a device backend (rdev.h), gl/ making it opengl.ren

P1 -- timing, input, window, display options

ID Title Kind What and why Modernizer source Our files Data / strings Depends Status
M-P1-01 60 fps frame limiter (client) fix The game's movement, animation and sound timing were tuned for about 60 fps; uncapped, things run too fast. At the end of each client frame, wait (high-resolution counter) until 1/60 s has passed since the last frame. Applies when FramerateLock is 1, always in multiplayer (firing rate otherwise rises with frame rate), and is suspended while the loading screen runs. 690d5ad, 4828519, 1cc5735, 2caf0ba, 995cb20; GameClientShell.cpp (PostUpdate), LoadingScreen.cpp src/game/ClientShellDLL/GameClientShell.cpp/.h, LoadingScreen.cpp strings for the option are in P3 (M-P3-13) -- done: cshell/p1_timing.cpp (sleeps in 1 ms steps, then spins; keeps a 60 Hz cadence); measured about 60 fps in the playtest, where the release build runs up to about 100. FramerateLock is FramerateLimit since M-P1-25
M-P1-02 60 fps limiter on servers fix A dedicated server also runs too fast unlimited. The server shell caps its update rate to 60 in multiplayer, unless the host's own client (which already limits) sends MID_DONT_LOCK_SERVER_FPS; the server only honours that message from a client with a host port. 0f48561, 006b6aa/be04376; GameServerShell.cpp, MsgIDs.h src/game/ObjectDLL/GameServerShell.cpp/.h, src/game/Shared/MsgIDs.h, GameClientShell.cpp (send on connect when hosting) -- M-P1-01 done: object/p1_server.cpp; the message is honoured only from the local client (CIF_LOCAL), not "a client with a host port" as in the Modernizer, which tested the server's own port
M-P1-03 Raw mouse look fix The retail mouse look (DirectInput axis offsets with CursorCenter) stutters on modern systems. Read relative mouse motion from raw input (WM_INPUT on the game window, which GameClientShell already subclasses), scale it by MouseSensitivity (the Modernizer halves the retail scale formula), and use it for yaw/pitch. In menus give the cursor back; in game hide and confine it. OldMouseLook 1 restores the retail path; NoRawInput 1 uses cursor-delta motion instead of raw input. 47b3a1f, 83c8743, fd21e9f, f55c2d8, c4998c8, 6b2ca03, 0ffc916, e8d390d, 3ee3a3e; GameClientShell.cpp (UpdateMouseLook area), InterfaceMgr.cpp (UseCursor) GameClientShell.cpp/.h, InterfaceMgr.cpp, a new P1 input file -- -- done: cshell/p1_input.cpp (WM_INPUT, absolute devices handled; NoRawInput: cursor motion warped back to the centre, also the fallback when raw input is missing); joystick look is not read in the new mode
M-P1-04 Mouse options feature Add a "legacy mouse look" toggle to the Mouse options folder (writes OldMouseLook); rename "Mouse smoothing" to describe what it really is, the mouse wheel input rate. f2ed6f7, ec3deeb; FolderMouse.cpp, ClientRes.rc src/game/ClientShellDLL/FolderMouse.cpp/.h P1 strings: toggle label + help; replacement text for the two retail strings is a CRes change behind the define M-P1-03 done: the slider is relabelled with P1 strings (31006/31007) instead of changing the two retail strings, so ClientRes.rc is untouched
M-P1-05 High-resolution game clock fix CWinUtil::GetTime uses GetTickCount (10-16 ms resolution), which makes frame pacing uneven. Use QueryPerformanceCounter. 8c05855; WinUtil.cpp src/game/ClientShellDLL/WinUtil.cpp -- -- done: seconds since the first call (keeps float precision), GetTickCount if there is no counter
M-P1-06 Windowed mode option and window handling feature Display options get a windowed-mode toggle (writes Windowed, forces a mode reset when it changes); on a mode change the window is resized and centred; when windowed at the desktop's own resolution, make it borderless; re-centre the window when the game regains focus. Retail requires +windowed 1 on the launcher's command line. e6e4fb9, 2a51552, e9f178b, de40512, b65ac1a; FolderDisplay.cpp, InterfaceResMgr.cpp (HandleBorderlessWindowed), GameClientShell.cpp (focus) FolderDisplay.cpp/.h, InterfaceResMgr.cpp, GameClientShell.cpp P1 strings: label + help -- done: cshell/p1_window.cpp, called from ScreenDimsChanged and on render init / focus regained; a correctly placed window is left where the user put it, so "re-centre on focus" only happens when the engine has moved or resized it
M-P1-07 Widescreen resolutions fix The resolution list drops every mode that is not exactly 4:3, so wide screens cannot run at their native resolution. Keep all modes of at least 640x480, and drop 16-bit modes instead (they are redundant and cause problems). Mind the list walk: skipping a mode must still advance to the next one (the Modernizer's first version looped forever, 516ec65). 98369bb, 516ec65; FolderDisplay.cpp (BuildRendererList area) FolderDisplay.cpp -- M-P0-01, P3 (menus must cope with wide modes) done: the 16-bit mode in use is kept in the list, so opening Display options does not change the mode by itself
M-P1-08 Safer fallback mode and surface settings fix When the configured mode fails, the retail game falls back to 640x480x16; fall back to 800x600x32. Force optimizesurfaces 1 on startup (it sometimes ends up off, which makes text draw with black boxes), and CursorCenter 0 after init in case it was left on. 98369bb, bc49fee; GameClientShell.cpp (OnEngineInitialized) GameClientShell.cpp -- -- done
M-P1-09 Texture filtering option (anisotropic) feature Replace the Advanced Display "Trilinear" toggle with a cycle: none / trilinear / 2x, 4x, 8x, 16x anisotropic. It writes Bilinear, Trilinear and Anisotropic (all read by the retail d3d.ren); "none" turns bilinear off too. 527c8ea, 0bee38b; FolderAdvDisplay.cpp src/game/ClientShellDLL/FolderAdvDisplay.cpp/.h P1 strings: label, help, six choices -- done: seven choices -- a "bilinear" one added (the retail default, Bilinear 1 / Trilinear 0, would otherwise show as "none"); the variables are written only when the choice changes
M-P1-10 Intel graphics fix (light scale and screen tint) compat On Intel integrated graphics, interface light scaling makes the screen go black underwater and the engine's screen tints do not show. A Display option ("Intel graphics fix") sets EnableLightScale 0 and EnableScreenTinting 0: CLightScaleMgr then disables light scaling, and CScreenTintMgr draws the tint as a small solid surface scaled over the screen with additive blending instead of using the camera tint. 466df2c, 84b3317, e1ad31f, 628a788, 2e8f6f0; LightScaleMgr.cpp, ScreenTintMgr.cpp, GameClientShell.cpp (draw), FolderDisplay.cpp LightScaleMgr.cpp, ScreenTintMgr.cpp/.h, FolderDisplay.cpp, GameClientShell.cpp P1 strings: label + help -- done: light scale held at 1 (all light scaling, not only the interface's, as in the Modernizer); the overlay is a 4x4 surface added over the scene. Only with d3d7.ren (our d3d.ren): the fault is Intel's DirectX 7 driver, which only it draws through, while d3d9.ren and opengl.ren draw light scaling and tints themselves; the toggle is greyed out for any other renderer selected, and the two variables are ignored with one
M-P1-11 Faster loading fix The loading-screen thread sleeps 100 ms between frames ("not faster than 10 fps"), which also slows the load itself down. Sleep 10 ms. 6fddadd; LoadingScreen.cpp src/game/ClientShellDLL/LoadingScreen.cpp -- -- done
M-P1-12 One-time config upgrade feature Store a config version (ModPatchNum); when it is older than the build's, apply one-time changes: reset the key bindings to the new defaults (M-P4-06) and raise BulletHoles to 100. 94b3171, 3573050, df62e6f; GameClientShell.cpp, VersionMgr.cpp GameClientShell.cpp, src/game/Shared/VersionMgr.cpp/.h -- M-P4-06 done: the config version is P1's own (P1_CONFIG_VERSION 1.0 in cshell/p1_modern.h, not VersionMgr); applies whatever defctrls.cfg holds, so M-P4-06's defaults arrive through it. 1.1: a config naming d3d.ren names d3d9.ren (M-P0-05). 1.2: the controller's default bindings, alone (M-P7-02). 1.3: a window as large as the desktop is a borderless window (M-P1-22). 1.4: 32-bit textures (M-P1-24). 1.5: FramerateLock 0 becomes FramerateLimit 0 (M-P1-25)
M-P1-13 Window procedure guard on exit fix During shutdown the subclassed window procedure can be called after the original procedure pointer is cleared, and crashes; ignore such calls. 5755077; GameClientShell.cpp (HookedWindowProc) GameClientShell.cpp -- -- done: such calls go to DefWindowProc
M-P1-14 Launcher resolutions feature The launcher (NOLF.exe) lists only exact 4:3 modes and starts a new install at 640x480x16, and linux-run.sh (then linux-run-launcher.sh) forces a 640x480x16 window on top of whatever it picks. List what the game's own Display options list (M-P1-07): every mode of at least 640x480, wide ones too, in 32 bits (16 only where a size has no 32-bit mode); start a card the config names no mode for at the largest 4:3 mode that fits the desktop -- the game was made for 4:3, wide modes are there to be picked -- or, if the desktop offers no 4:3 mode, the largest that fits. -- (ours) src/engine/launcher/dlgdisplay.cpp (CRenderer::Init, CDisplayModes::SetDefaultModes), build/game-scripts/linux-run.sh -- M-P1-07 done: a config still naming 640x480x16 (the shipped defaults.cfg) finds that mode gone and gets the new default; linux-run.sh no longer sets the mode in the modern build, nor (since it replaced the launcher-less script) ++windowed 1, which overrode Display options' window mode: the modern build starts as it was last left, the release build in a 640x480 window. Checked under Xvfb at 1600x1200 (picks it) and 1920x1080 (listed; no 4:3 mode, so picked)
M-P1-15 No disc check feature The launcher (NOLF.exe) will not start the game without a CD-ROM drive holding the play disc (CheckCd, \DATA\NOLF.REZ), although the full install has every archive in the game directory; linux-run.sh (then linux-run-launcher.sh) fakes a drive for it. Skip the check when the game's archive was found in the game directory, so the disc is needed only by an install that left the data on it. -- (ours) src/engine/launcher/cdcheck.cpp (CheckCd), build/game-scripts/linux-run.sh -- -- done: linux-run.sh no longer maps the disc drive in the modern build
M-P1-16 Update and GOTY archives without the installer fix The launcher adds nolfu003.rez (1.003) and nolfgoty.rez (Game of the Year) to the command line only from the registry values the retail installers write (Update Command Line, Content Command Line); a copied or unpacked install has none, so it ran without the GOTY content. When a value is missing, take the archive from the game directory; and put -rez custom (the game code built from source) last whatever the registry says, so no archive overrides it. -- (ours) src/engine/launcher/launcher.cpp (WinMain, LaunchGame) -- -- done: checked with no registry values: the command line names both archives, custom last
M-P1-17 Fresh-install and Steam Deck resolution feature The launcher only passes a display mode once its Display dialog has been opened (and a card picked there), so a fresh install ran at autoexec.cfg's mode: the shipped defaults' 640x480, or whatever a copied config named. On a fresh install (the config names no colour depth: the game writes one on its first exit) build the display list at launch, pick the config's renderer and its first hardware card, and pass the default mode (M-P1-14's largest 4:3 that fits the desktop). On a Steam Deck (SteamDeck=1, which Steam sets and Proton passes on) the default is the panel's 1280x800, else the largest mode that fits it. -- (ours) src/engine/launcher/launcher.cpp (LaunchGame), src/engine/launcher/dlgdisplay.cpp (ModernDefaultMode) -- M-P1-14 done: in a 1280x800 Wine desktop a fresh install starts at 1280x800 with SteamDeck=1 and 1024x768 without; a config the game has written keeps its mode
M-P1-18 Antialiasing option feature Multisampled antialiasing in our renderers, and an Advanced Display cycle for it beside "Texture filtering": off, 2x, 4x, 8x. d3d.ren (DirectX 7) has none. -- (ours) src/modern/render/ (gl/, d3d9/), FolderAdvDisplay.cpp/.h P1 strings: label, help, three choices M-P0-05 done: the renderers draw into a multisampled target (a framebuffer object in GL, whose window keeps one pixel format for life; a multisampled render target and depth surface in Direct3D 9) and resolve it into the back buffer before a flip or a read; the most samples the device has up to the setting. The cycle writes Antialiasing and restarts the renderer (RestartRender) when it changes

P2 -- developer console, logging, cheats, debug toggles

ID Title Kind What and why Modernizer source Our files Data / strings Depends Status
M-P2-01 Drop-down developer console feature A console drawn over the top half of the screen (screen width, half height), single-player only: shows engine and game console output, an edit line, scroll-back (Up/Down to recall commands, Page Up/Down to scroll), and pauses the game while open. While it is open, game commands, camera movement and other key handling are ignored. 2fda13a, 910d120, bbbc018, 254a0a9, 52b34e3, cf7f0a5, b64d954, 1f89822; ConsoleMgr.cpp/.h, hooks in GameClientShell.cpp, InterfaceMgr.cpp new P2 console file(s), GameClientShell.cpp, InterfaceMgr.cpp, InterfaceResMgr.cpp (init on mode change) -- M-P0-02 done: src/modern/cshell/p2_console.*; drawn in InterfaceMgr::PostUpdate before the flip, keys/chars/camera in InterfaceMgr.cpp, commands in GameClientShell::OnCommandOn. Sized from the screen every frame, so nothing is needed on a mode change. Pauses only a running game and unpauses only what it paused. Playtest: tools/p2-console.txt
M-P2-02 Console runs commands feature Enter runs the line as a client console command; if it is a cheat code, run the cheat (and play the cheat sound); when hosting or in single player, also send it to the server (M-P2-08) so server variables can be set. 2fda13a, a880a42, cfc1e79; ConsoleMgr.cpp (Send) P2 console file -- M-P2-01, M-P2-08 done: a cheat code runs as a cheat (with the cheat sound); otherwise the client runs it and, in single player or as host, sends it to the server (M-P2-08). The Modernizer's OldMouseLook 0 special case belongs with M-P1-03
M-P2-03 Rebindable console key feature A new ToggleConsole command (id 80), bound by default to the backquote key, listed at the end of the Custom Controls list; the key-name table learns backquote, Scroll Lock, Num Lock, Insert and Delete. The console is locked (cannot open) while editing controls and during loading screens, and never opens in multiplayer. 3739a8f, c9560cf; CommandIDs.h, ClientUtilities.cpp, FolderCustomControls.cpp, LoadingScreen.cpp src/game/Shared/CommandIDs.h, ClientUtilities.cpp, FolderCustomControls.cpp, LoadingScreen.cpp P2 strings: control name + action string; default binding in M-P4-06 M-P2-01 done: COMMAND_ID_TOGGLE_CONSOLE in CommandIDs.h, Developer console / ToggleConsole in src/modern/cres/p2.*, last Custom Controls entry before the unassigned one; key names `, ~, Grave, Scroll/Num Lock (both spellings), Insert, Delete. The console registers AddAction ToggleConsole 80 at start and binds ##41 when the action has no key (so it works before M-P4-06 and with old configs). Locked on the Custom Controls folder (OnFocus) and while a loading screen is up (LoadingScreen.cpp Show/Hide); never in multiplayer
M-P2-04 Console help list feature A Help console program lists every registered console program. The Modernizer collects the names by replacing the client interface's register/unregister function pointers with proxies; with our own engine, the list can be read from the engine directly (or the same proxy approach used). bbbc018; GameClientShell.cpp (proxy functions), ConsoleMgr.cpp P2 console file, possibly src/engine/lithtech/ -- M-P2-01 done: no proxies -- ILTClient::EnumConsoleCommands (appended like M-P0-02) walks the engine's own command list; Help lists game programs and engine commands, sorted
M-P2-05 Console backdrops feature ConsoleBackdrop picks the console's background. The Modernizer's choice 0 is the NOLF demo's console art (interface/console.pcx, not on the retail discs -- not ported); use retail menu/art/blanktag.pcx (its choice 1) and plain black (its choice 2), and make 0 the blank tag. c3f4ffc, e08a4a1; ConsoleMgr.cpp (Draw) P2 console file -- M-P2-01 done: 0 and 1 menu/art/blanktag.pcx (scaled), 2 black; surfaces made once
M-P2-06 Console history dump feature A console program that writes the whole console history to the modern log (M-P2-07). 4c5b89e; ConsoleMgr.cpp P2 console file -- M-P2-01, M-P2-07 done: console program ConsoleHistoryToLog
M-P2-07 Modern log file feature The Modernizer logs to Debug.log (cleared at start) through SDL. Give the modern build a small always-on log (game directory, cleared per run) for messages such as font loading, timeout retries and the defensive fixes' warnings. Note: src/nolfsrc/nolfsrc_debug.h's NOLFSRC_LOG is debug-build only; this one is for the modern build itself. 36449b2, 21fcab4, 73a804f; GameClientShell.cpp (SDLLog) a small P2 file; used by others -- -- done: ModernLog() (src/modern/cshell/p2_log.h, in both DLLs) appends to modern.log in the game directory, emptied by the first line of each run (a process-wide flag, so either DLL can be first)
M-P2-08 Client-to-server console commands (safe) feature Message MID_CONSOLE_COMMAND_CLIENT (238) carries a console string from client to server, which runs it. The Modernizer's server runs it from any client; ours must only accept it from the local/hosting client (single player or the listen-server host), and ignore it otherwise. a880a42; GameServerShell.cpp (OnMessage), MsgIDs.h src/game/ObjectDLL/GameServerShell.cpp, src/game/Shared/MsgIDs.h -- -- done: MsgIDs.h; the server (src/modern/object/p2_server.cpp) runs the string only for a client with CIF_LOCAL (single player, listen-server host); others are ignored and logged. Same HSTRING payload as the Modernizer's
M-P2-09 Cheats in the console, cheat list feature A console program Cheats prints the cheat codes and what they do (descriptions are ours). Four cheats compiled out of the retail final build are enabled: no-clip (mpclip, also the documented way out of stuck spots), remove all AI, unlock all missions, reset player history. cfc1e79; MessageMgr.cpp (CCheatMgr::Process) src/game/ClientShellDLL/MessageMgr.cpp/.h P2 strings if the list is localised M-P2-01 done: console program Cheats; the modern build also runs six cheats _FINAL compiles out: mpclip, mpteleport, mphookmeup (mission equipment), mpboyisuck, mpbeenthere, mpwhoami (the Modernizer's set: its mpclip/mpteleport/mphookmeup included). English list in code, not localised
M-P2-10 Big head mode feature BigHeadMode <scale>: character heads drawn 1.8 x scale larger -- client: a model-node control on the head node (found by name head_node) for characters and bodies; server: head attachments (Head/Eyes/Chin positions) scaled, and the head hit-box radius enlarged to match. Sent to the server when single player/hosting (via M-P2-08); forced off in multiplayer. Known imperfect hit boxes (the Modernizer says so too). a675150, 51ae03f, 95e945e, c4b7a24, e26fe28; SpecialFX.h, BodyFX.cpp, NodeController.cpp, Attachments.cpp, CharacterHitBox.cpp, GameServerShell.cpp, GameClientShell.cpp those files -- M-P2-08 done: client src/modern/cshell/p2_bighead.* (node control in NodeController.cpp, own control for bodies in BodyFX.*); server p2_server.cpp (head attachments in Attachments.cpp, head hit radius in CharacterHitBox.cpp). Multiplayer reads it as 0 without overwriting the setting. Sent to the server on entering a world. Shown in tools/p2-bighead.txt. As in the Modernizer, glasses and hats grow in place and can end up inside the bigger head
M-P2-11 Display triggers feature Server variable DisplayTriggers: draw the bounding boxes of Trigger objects (yellow, nearly opaque), and remove them when turned off again. Also corrects the retail bounding-box debug view: the debug model path (264.abc -> 364.abc) and radius used to visualise hit boxes. dc377e1; GameServerShell.cpp (UpdateBoundingBoxes), GameBase.cpp, CharacterHitBox.cpp GameServerShell.cpp/.h, src/game/ObjectDLL/GameBase.cpp, CharacterHitBox.cpp -- -- done: P2Server_Update from GameServerShell::Update, GameBase.cpp (box forced on, yellow at 0.95); turning it off removes them. 264.abc -> 364.abc and twice the radius in CCharacterHitBox::CreateNodeRadiusModels (retail has no 264.abc; RETAIL_BUGS.md)
M-P2-12 Speed-run helpers feature The Modernizer logs the addresses of the current mission/level and an is-loading flag so speed-run timers can read them. Our addresses differ from the Modernizer's anyway; provide the same three values in one exported/static block and log their addresses at startup. bdc4770, 5edbf15, b8da510, 6a856af; GameClientShell.h, LoadingScreen.* GameClientShell.cpp/.h, LoadingScreen.cpp/.h -- M-P2-07 done: NolfSpeedRunInfo, exported by CShell.dll (src/modern/cshell/p2_client.cpp): signature NOLFSPEEDRUNINFO, version, mission, level, is-loading (32-bit); addresses in modern.log at start. Updated every frame and at each loading screen; no other GameClientShell change
M-P2-13 Debug level off for new players fix The server forces debuglevel 0 when a player is added, because the level could be left on and flood the console. Check whether this happens with our build before porting. (in GameServerShell.cpp CreatePlayer) GameServerShell.cpp -- -- not ported: does not happen here -- DebugLevel is 0 by default (engine) and in defaults.cfg/autoexec.cfg, and the console shows DebugLevel = 0 in play (tools/p2-console.txt)
M-P2-14 SetPos, GetPos feature Console commands to put the player somewhere, for testing a place without playing to it, and to say where they are. -- (ours) cshell/p2_console.cpp, object/p2_server.cpp; MsgIDs.h, GameServerShell.cpp -- -- done: SetPos <x> <y> <z>, single player: the server teleports the player's object there and then the client's, as a respawn does (MID_P2_SETPOS, from the local client only), so nothing between the two places is touched and a move down is not a fall; GetPos prints the player's position in the form SetPos takes, in the console and the modern log. (At first the client moved its own object and the server swept its object along after it: a trigger on the way could kill the player, god mode or not, and a move down counted as a fall.)
M-P2-15 Console on Shift+Esc feature The console key (backquote by default) is not where it is on every keyboard layout. Shift+Esc opens the console as well. Daniel Gibson, 54e9fb3 (ChthonVII's fork); ConsoleMgr.cpp cshell/p2_console.cpp -- M-P2-03 done: Shift+Esc opens it (Esc, or the console key, closes it, as before). Tested in a scripted run

P3 -- menus and HUD at any resolution

The retail interface is laid out for 640x480 and stretched by separate X and Y ratios (or offset to the centre). The Modernizer switches the menus to a centred 4:3 area scaled by the Y ratio only, with the side areas filled (pillarboxing), so nothing is stretched on wide screens.

ID Title Kind What and why Modernizer source Our files Data / strings Depends Status
M-P3-01 4:3-area menu layout feature InterfaceResMgr X offset becomes the 4:3 side margin (width - height*4/3)/2, Y offset 0; menu controls are positioned by pos * yRatio + xOffset, with each fixed control's layout position remembered so it is not re-offset each frame (controls that position themselves -- edit fields, the password dialog -- opt out). Mouse coordinates are then used as-is. Interface 3D sprites (scale FX) stop multiplying their depth by the X ratio. Done: The page is laid out on the screen as the centred 4:3 area scaled by the height ratio: InterfaceResMgr X offset = the 4:3 side margin (width - height*4/3)/2 (negative on screens narrower than 4:3, whose sides are then cropped like the Hor+ 3D view), Y offset 0. Unlike the Modernizer, controls keep their 640x480 layout positions (as in retail) and are put at pos * yRatio + offset only while they draw; a mouse point is matched against where a control is drawn and handed to it in its own frame (P3_PointOnCtrl/P3_PointToCtrl), so slot shuffling, arrow hiding and code that reads control positions work unchanged and nothing needs to opt out; a row of a folder's list answers the mouse over its scaled cell, not just its (unscaled) text. Layout positions below 0 (the layout's way to hide a control) stay off screen. The 3D parts of the menus keep their depth (no X-ratio push-back): they fill the screen's height as the page does; that they line up with the 2D page horizontally needs the Hor+ field of view of M-P0-01. Text keeps its bitmap font's size. 3ed42b1, 09abe0b, 75a3b2e, 4e333b5, 5995bc1, 38ad510, 596ff46, c42d808; BaseFolder.cpp, InterfaceResMgr.cpp, InterfaceMgr.cpp (mouse), ListCtrl.cpp, the Folder*.cpp scale-FX code BaseFolder.cpp/.h, InterfaceResMgr.cpp/.h, InterfaceMgr.cpp, ListCtrl.cpp/.h, BaseSelectionFolder.cpp, FolderGadgets.cpp, FolderGear.cpp, FolderIntel.cpp, FolderInventory.cpp, FolderMods.cpp, FolderPlayer.cpp, FolderViewInventory.cpp, FolderWeaponControls.cpp, FolderWeapons.cpp, LoadingScreen.cpp -- -- done
M-P3-02 Side bars feature One function draws the shade colour into the side margins; used by menus, the splash screen, the loading screen and the scope overlay (which otherwise shows the world at the sides). Done: Side margins in the shade colour, black since M-P3-22 (menus, between the top and bottom bands, which now scale too; splash, failure and pause screens), black beside the scope. The loading screen's retail margin fill works unchanged with the new offsets. c42d808, 327320e; InterfaceResMgr.cpp (DrawFolderBars), PlayerStats.cpp (scope) InterfaceResMgr.cpp, PlayerStats.cpp, InterfaceMgr.cpp -- M-P3-01 done
M-P3-03 Per-folder widths feature Every options folder multiplies its layout widths (gap, column, slider) by the Y ratio so labels and values do not collide at high resolutions: Audio, Controls, Crosshair, Effects, Game, Host Options, JoinLAN, Keyboard, Mouse, Performance, Texture, Weapon Controls, Joystick, Advanced Display, Host, Player. Done: As described, in each folder's Build (slider widths too); also Host and Player (literal widths) and JoinLAN. Widths are fixed when a folder is first built: after a resolution change in-game they keep the old scale until the next start (positions follow at once). 9372bbe, 78e762f and the menu-scaling commits those Folder*.cpp, JoystickAxis.cpp -- M-P3-01 done
M-P3-04 Back/continue arrows that scale feature The menu back/continue arrows are scale FX sprites that do not follow the new layout. The Modernizer replaces them with new 64x64 bitmaps in a new scalable bitmap control (Modernizer art -- not ported). Keep the retail arrow sprites and position/scale them for the 4:3 area instead; if a bitmap control is needed, generate the arrow images. Note the Modernizer's later crash fix in the weapon-hotkey screen (control Param1 checks) was needed only because of its new control; check ours does not collide the same way. Done: The retail back/continue arrows are 3D scale FX, which already follow the page once their depth is no longer pushed back (M-P3-01); the 2D highlight drawn over them when selected is scaled with the page and placed in the 4:3 area. No new control or art, so the weapon-hotkey crash cannot arise. f717cda, 216fc7f, 76d1427; BaseFolder.cpp, ScalableBitmapCtrl.*, FolderWeaponControls.cpp, BaseSelectionFolder.cpp BaseFolder.cpp, possibly a new control -- M-P3-01 done
M-P3-05 Splash, failure and loading screens fix The splash and mission-failure backgrounds are drawn at 640x480 or stretched; draw them scaled into the 4:3 area with side bars. The loading screen scales its text position and mission photo by the Y ratio. Done: Splash, mission failure and pause backgrounds are scaled into the 4:3 area with side bars; the loading screen's title and mission photo are placed in the 4:3 area and the photo scaled. 4e333b5, 04be8a2; InterfaceMgr.cpp (splash, failure), LoadingScreen.cpp InterfaceMgr.cpp, LoadingScreen.cpp -- M-P3-01, M-P3-02 done
M-P3-06 Multiplayer menus fix Join (server list, sort bars, status/option bars, column widths and arrow offset), Host, Host Levels (the frame drawing is disabled), the multiplayer summary, and the team/frag score positions (moved right so they do not overlap the bound-weapons bar) are adjusted for the scaled layout. Done: Join: list heights, row widths, column widths and gaps inside the rows (which also places the list arrows), the header/status bars, the sort underline, the server count and status text; the double-click on the server list gets the point in the list's frame. Host Levels: lists scaled and their frames drawn at the scaled place (not disabled). Multiplayer summary: team banners and the local row at the scaled place. The score positions need no change: the bound-weapon icons stay centred on the right edge (M-P3-12). fc99f6b, 5929056/26855b3/4762b70, f4b9b98, 58ec382; FolderJoin.cpp, FolderHost.cpp, FolderHostLevels.cpp, FolderMultiSummary.cpp, ClientInfoMgr.cpp those files (FolderJoin/FolderHost shared with P6: P3 owns layout lines) -- M-P3-01 done
M-P3-07 Password dialog and save-name field fix The join-password dialog is centred on the real screen instead of at (320,240), and the save-game name field keeps its position; both opt out of the fixed-control re-positioning. Done: The password dialog is centred on the page's centre (= the screen's) with its pixel offsets kept; the save-name field needs nothing, since controls keep layout positions (M-P3-01). 9dc7f61, 8112bbe; FolderJoin.cpp, FolderSave.cpp FolderJoin.cpp, FolderSave.cpp -- M-P3-01 done
M-P3-08 4:3 cinematics feature RestrictCinematicsTo4x3: during letterboxed cinematics also draw black bars in the side margins, hiding scene edges that were never meant to be seen. Done: As described (the letterbox surface, so the bars fade with it). 27bd359; InterfaceMgr.cpp (letterbox drawing) InterfaceMgr.cpp P3 strings (in M-P3-13) M-P3-01 done
M-P3-09 Space-suit overlay fix The space-suit helmet overlay is stretched on wide screens; scale it with the 4:3 ratio instead of the layout's SpaceScale. Done: SpaceScale times the screen's width over a 4:3 width (never below 1): 1 at 4:3 (retail), 4/3 at 16:9 (the Modernizer's constant). 65915bb; LayoutMgr.cpp (OVM_SPACE) src/game/ClientShellDLL/LayoutMgr.cpp -- -- done
M-P3-10 Original or GOTY main menu feature UseGotyMenu: show the GOTY main menu art (retail default) or the original edition's. The original's textures are retail data -- nolf2.rez's MENU/SPRTEX/MAINMENU1.dtx and MENU/SPRTEX/NOLF.dtx, overridden in a GOTY install -- so a build-time recipe copies them to new names (the Modernizer's MAINMENU1b/NOLFb), with two small .spr files we write; the menu swaps the sprite file names of the InterfaceSpr1 and mainnolftext scale FX. Our install always has nolfgoty.rez, so default to GOTY. Done: As described. Recipes: MENU/SPRTEX/MAINMENU1B.dtx.from, MENU/SPRTEX/NOLFB.dtx.from (from nolf2.rez), files/MENU/SPR/MAINMENU1B.spr, files/MENU/SPR/NOLFB.spr (written for this project); the switch is in Interface options (M-P3-13); VersionMgr is not needed (always GOTY here). 2633a19, 3b8fab5, 04bfa92; BaseFolder.cpp (CreateScaleFX), InterfaceMgr.cpp, VersionMgr.cpp BaseFolder.cpp, InterfaceMgr.cpp, recipes in src/modern/data/ (needs M-P4-01) P3 strings: label, help, two choices M-P4-01 done
M-P3-11 Seasonal main menus feature GetMainFolder() replaces every hard-coded FOLDER_ID_MAIN: from 15 to 31 December the main menu shows a winter Cate, on Fridays a casual Cate, otherwise the normal one; NoFunMenus 1 always uses the normal one. The two variants are new folder ids with their own layout sections. The Modernizer's variant models (INTHERO_CASUAL/WINTER.abc) are its own work; their skins are retail (HERO_CASUAL*.dtx, HERO_WINTER*.dtx). (Its "fixed Christmas" commit fixes its own date test: check the range is 15-31 December inclusive.) Done: Not by replacing the ~35 call sites: CFolderMgr::SetCurrentFolder maps FOLDER_ID_MAIN to the day's main menu, which every path goes through. 15-31 December (inclusive) winter, Fridays casual, NoFunMenus 1 plain. The variants are the IntHero model's Casual/Winter styles (no model-bute change): the Modernizer's own models, INTHERO_CASUAL.abc and INTHERO_WINTER.abc (the interface skeleton and its one animation, interface4, with the in-game outfits), linked from src/modern/data/files/CHARS/MODELS/ into the ext/nolf1-modernizer submodule, and the retail skins (HERO_CASUAL, HERO_WINTER and their _HEAD skins, byte-identical to the Modernizer's copies) installed by .from recipes; two layout sections plus two characters in the layout recipe, holding only the P38 as the Modernizer's do. (First the retail in-game models were used, HERO_CASUAL.abc and HERO_WINTER.abc: their first animation is a still pose, and their outfits are the original edition's, so Cate stood frozen.) 2633a19, f35883d, edc7ba3, 7f433c7, 50ec0b5; InterfaceMgr.cpp (GetMainFolder), FolderMgr.*, ~25 call sites InterfaceMgr.cpp/.h, FolderMgr.cpp/.h, LayoutMgr.cpp, every FOLDER_ID_MAIN site; layout + model-bute recipes -- M-P4-01, M-P4-03 done
M-P3-12 HUD scaling feature UIScale (times the Y ratio) scales the health/armour bars and their flash, the ammo bar and icon, the air meter, the damage icons (drawn a little smaller and higher), and the bound-weapon icons (moved to the top right), using TransformSurfaceToSurfaceTransparent. Done: HUD scale = UIScale times the height ratio, never below 1 (the retail size). Every HUD element keeps its retail place (layout position times the screen ratios); its layout offsets and images scale (ScaleSurfaceToSurface*): health/armour bars and flashes, health/armour icons, ammo bar and icon, air bar and icon, damage icons, bound-weapon icons (kept centred on the right edge, so nothing moves into the scores). Text stays at its font's size. 286b806, 5e5ebf1, 45ef789, eda9e43, 11a62f7, 032fea2, b3dd011, a7737fc, 98c661c, 80a8945, e0944c7; PlayerStats.cpp src/game/ClientShellDLL/PlayerStats.cpp/.h -- -- done
M-P3-13 Interface options folder feature A new options folder ("Interface"), reached from Options: HUD scale slider, frame-rate lock (60 fps / unlocked), show frame rate, cinematic aspect ratio (4:3 / wide), and the main-menu style (when GOTY). Also moves "Quick switch" there if wanted. Done: src/modern/cshell/p3_folderinterface.* (CFolderInterface, FOLDER_ID_INTERFACE, FOLDER_CMD_INTERFACE): HUD size (25-100%), frame rate (writes P1's FramerateLock), frame-rate counter, cutscene framing, main-menu art. Quick switch stays in Game options (P5). Strings: src/modern/cres/p3.rc, IDs 31200-31218. Test scripts: tools/p3-menus.txt, tools/p3-hud.txt (build/playtest.sh gained PLAYTEST_RES, PLAYTEST_CFG, world - and the mmove/mclick page commands). 2120b18, d6bd8c7, 798d802, 470c4a1; FolderHUD.cpp, FolderOptions.cpp, FolderMgr.*, LayoutMgr.cpp new FolderInterface.*, FolderOptions.cpp, FolderCommands.h, FolderMgr.*, LayoutMgr.cpp; layout recipe section [FolderInterface] (M-P4-03) P3 strings: title, menu entry + help, each label + help, choice texts M-P3-12, M-P1-01, M-P3-14, M-P3-08, M-P3-10 done. FramerateLock is FramerateLimit since M-P1-25
M-P3-14 Frame-rate counter feature ShowFramerate: average the frame rate over 10 frames and draw it at the bottom left. Done: Averaged over 10 frames from the engine's frame time, drawn bottom left with the HUD (no GameClientShell.cpp change). c22d278, 8798b8d, 36b9b71, b0606a6; PlayerStats.cpp, GameClientShell.cpp PlayerStats.cpp, GameClientShell.cpp -- -- done
M-P3-15 Intel reading feature Reading an intel item pauses the game and shows the cursor; the popup background fills the screen. Done: As described (the game is paused without pausing sounds). 05a1cd4, d97664e; InterfaceMgr.cpp (popup state), PopupText.cpp InterfaceMgr.cpp, PopupText.cpp -- -- done
M-P3-16 Message perf fix On-screen info messages (ammo pickups, deaths) are drawn with colour-keyed transparency every frame, which costs a lot; draw them without it (no visible difference). Done: As described: the message surfaces are optimized with their transparent colour, so the plain blit looks the same. 3fb3d14; MessageMgr.cpp (draw) MessageMgr.cpp (one line; P2 owns the file) -- -- done
M-P3-17 Fill-rect cache fix (performance) If M-P0-03 does not fix FillRect in the engine: a game-side cache of pre-filled surfaces keyed by size and colour, drawn with blits, used in place of every FillRect in the interface (about 60 call sites: letterbox, fades, crosshair, scope, sliders, list frames, team banners, credits, subtitles...); rebuilt on resolution change; never used after shutdown (the Modernizer's subtitle crash). 128eae2, 992bd24, ae11687, c12d011, bf1c1e8, e1bcb1b, a0b321f; OptimizedRenderer.* and its users new P3 file + the call sites -- M-P0-03 not ported: superseded by M-P0-03, which fixes the slow screen fill in the engine for every caller (no call-site changes, no surface cache to rebuild on mode changes or to outlive shutdown)
M-P3-18 Weapon and ammo chooser fix The chooser draws the previous/next weapon and ammo icons beside the current one (retail had the drawing commented out), and centres the ammo row. Done: As described: previous/next weapon icons; the next ammo beside the current one, the pair centred (a single ammo stays centred). 7921cc7; WeaponChooser.cpp src/game/ClientShellDLL/WeaponChooser.cpp (P5 owns; this is one block) -- -- done
M-P3-19 Fonts fix Bitmap fonts are always drawn with blending (the air and chooser fonts were not), which removes black boxes around text. (The Modernizer also prepared an HD large font and edited two HUD font images; its HD font is disabled in its own code -- not ported; see M-P4-10 for the font images.) Done: The air and chooser fonts are set up with blending first; the retail images (made for colour keying) cannot be split up that way, so they fall back to colour keying -- the visible change needs recoloured images (M-P4-10). Without the fallback the interface failed to start (black screen). d6f610e; InterfaceResMgr.cpp (SetupFont calls) InterfaceResMgr.cpp -- -- done (visible with M-P4-10)
M-P3-20 Version string fix The main menu's version text is positioned by the screen ratios rather than fixed 640x480 coordinates. Done: As described (the screen's ratios, i.e. the screen's bottom-right corner). 75a3b2e; FolderMain.cpp src/game/ClientShellDLL/FolderMain.cpp -- M-P3-01 done
M-P3-21 Sunglasses zoom beside the 4:3 area fix Zoomed through the sunglasses' camera, the scope view is drawn without DrawScope, so at a screen wider than 4:3 nothing blacks out the world beside the round view. Daniel Gibson, a72c3b3 (ChthonVII's fork); PlayerStats.cpp PlayerStats.cpp -- M-P3-02 done: the side bars M-P3-02 draws for the scope are drawn for the sunglasses too. Shown at 960x540 in M01S04
M-P3-22 Black side bars feature M-P3-02 shades the wide screen's side margins in the layout's shade colour, the menus' green. Make them black: the margins are beside the 4:3 page, not part of it. -- (ours) cshell/p3_ui.cpp (P3_DrawMenuSideBars), BaseFolder.cpp (Render), LoadingScreen.cpp -- M-P3-02 done: black beside the menu pages (between their top and bottom bands, which still run the screen's width), the splash, pause and failure screens and the loading screen. Checked under Xvfb at 1280x720: the main menu, Options and T01S01's loading screen

P4 -- jukebox, game data, strings

ID Title Kind What and why Modernizer source Our files Data / strings Depends Status
M-P4-01 Recipe kinds: copy from an archive, game-directory files feature (tooling) tools/moddata.py today patches a file or installs a new one into custom/. Two more recipe kinds are needed: install a retail file from a named archive under a new path (the original main-menu textures of M-P3-10, the retail skins of M-P3-11, the cosmonaut model of M-P4-04), and write into the game directory itself rather than custom/ (for defctrls.cfg, M-P4-06, which the engine reads from disk). Document them in src/modern/data/README.md. -- tools/moddata.py, src/modern/data/README.md, build/variant.sh -- -- done
M-P4-02 Jukebox feature Options gets a Jukebox folder: a list of the game's music themes (Ambush, Ba-De-Dum, Bad Guys, Orchestral, Main Theme, and the GOTY theme when present) and, for the selected theme, its intensity levels; choosing one plays that theme at that intensity, with a "now playing" line; leaving restores the previous music state. Themes are listed in a new attribute file we write, ATTRIBUTES/Jukebox.txt (theme name, music directory, control file, needs-GOTY flag) read by a small bute manager. The Modernizer's intensity names are its own invention ("I made up all these song names"): describe the levels ourselves (or use neutral labels such as "Intensity 3"). cc80f1d, 7c517ea, 0f08dac, 6de82fc, 2a4702e, 494747e, dd1d500, 5914926, e400f56; FolderJukebox.*, JukeboxButeMgr.*, ASSETS/Attributes/Jukebox.txt new FolderJukebox.*, JukeboxButeMgr.*, GameClientShell.* (manager), FolderOptions.cpp, FolderMgr.*, LayoutMgr.cpp; recipe files/ATTRIBUTES/Jukebox.txt; layout section [FolderJukebox] (M-P4-03) P4 strings: entry + help, title, "Themes", "Now playing: %s", theme names M-P4-03 done: src/modern/cshell/p4_jukebox.* (CP4FolderJukebox, CP4JukeboxButeMgr; the manager is the folder's own, loaded when it is first built, so GameClientShell is untouched); hooks in FolderMgr.*, FolderCommands.h, FolderOptions.cpp, LayoutMgr.cpp; files/ATTRIBUTES/Jukebox.txt (our piece names, from the control files' intensities; no GOTY flag: a theme is listed when its control file is in the data); strings 31300-31315 in cres/p4.rc. Pieces play at once (LTDMEnactImmediatly); leaving restores the saved music state (theme, intensity, playing/paused). Test: tools/p4-jukebox.txt (needs an audio device in the container, see its header)
M-P4-03 Layout data feature The Modernizer ships a complete modified copy of ATTRIBUTES/LAYOUT.txt (as LayoutNew.txt, with the game reading that name). Instead, patch the retail LAYOUT.txt with a recipe: new sections for the seasonal main menus, the Interface folder and the Jukebox folder, and the characters they use. The other edits (Player setup animation, Join map column) are separate items. d03fb15 and the folder commits; ASSETS/Attributes/LayoutNew.TXT recipe src/modern/data/ATTRIBUTES/LAYOUT.txt.patch (one file; each package adds its hunk through P4) -- M-P4-01 done: ATTRIBUTES/LAYOUT.txt.patch adds [FolderInterface] (for M-P3-13: a plain options-folder layout like [FolderGame]) and [FolderJukebox] (with its own keys PieceRect, NowPlayingPos, NowPlayingWidth), plus M-P4-05's two values. The seasonal main menus' sections (M-P3-11) are P3's to add as further hunks of this file, once it is known whether the retail skins fit
M-P4-04 Multiplayer character selection fix Player setup gains a second cosmonaut style (cosmonaut_style2, blue/red skins and three heads, all retail skins) whose model is the retail COSMONAUT_ACTION_MULTI.abc under a new name, and its helmet attachment requirement is fixed (ATTACHMENTS.txt Requirement3 style "" -> "Action"); a bystander winter skin and a dummy head are added to their models' lists. The PS2 Cate ("Thief" style, hero_thief*) uses models and skins taken from the PlayStation 2 release -- not on these discs, not ported. 110a997, 50fee30; ASSETS/Attributes/MODELBUTES.TXT, ATTACHMENTS.TXT, CHARS/MODELS/MULTI recipes for ATTRIBUTES/MODELBUTES.txt, ATTRIBUTES/ATTACHMENTS.txt, the model copy -- M-P4-01 done: MODELBUTES.txt.patch ([cosmonaut_style2]: Blue/Red skins, three heads; malebystander_style4 Head3 "Dummy"; malebystander_winter Skin1 "Alternate"), ATTACHMENTS.txt.patch (Requirement3 style "Action", so only the action cosmonaut wears the helmet), CHARS/MODELS/MULTI/COSMONAUT_STYLE2_MULTI.abc.from (the retail model, byte-identical to the Modernizer's copy). PS2 Cate not ported. Test: tools/p4-player.txt
M-P4-05 Player-setup and server-list layout values fix Player setup shows the female model with a walking animation that does not suit it (CharAnim "UUW" -> "LWF"); the Join server list's map column is too narrow (MapWidth 128 -> 256). f628f29, 4762b70; ASSETS/Attributes/LayoutNew.TXT layout recipe (M-P4-03) -- M-P4-03 done (in the layout recipe)
M-P4-06 New default controls feature Defaults change: the mouse wheel switches weapons (it zoomed), the middle mouse button toggles zoom (it reloaded), backquote opens the console; defctrls.cfg gains the two new actions. Applied to existing configs once by M-P1-12. 3573050; BIN/defctrls.cfg recipe for the game-directory defctrls.cfg (needs M-P4-01) -- M-P4-01, M-P2-03, M-P5-13 done: gamedir/defctrls.cfg.patch and gamedir/defaults.cfg.patch (the launcher's "restore defaults" and a new install's autoexec.cfg use the latter): AddAction ToggleConsole 80 / ToggleZoom 81, backquote (##41) -> ToggleConsole, wheel down/up -> NextWeapon/PrevWeapon, middle button -> ToggleZoom. The two actions do something once M-P2-03 and M-P5-13 land
M-P4-07 Alternate skydiving SMG name fix Two weapons share the name "Hampton MPL 9mm SMG"; the skydiving variant gets its own name string ("... (Alternate)") so they can be told apart in lists. 6ca6961; ASSETS/Attributes/WEAPONS.TXT (NameId 4012 -> new id) recipe ATTRIBUTES/WEAPONS.txt.patch P4 string: the new weapon name -- done: ATTRIBUTES/WEAPONS.txt.patch ([Weapon32] NameId 4012 -> 31320, "Hampton MPL 9mm SMG (Skydiving)")
M-P4-08 Game of the Year strings fix The GOTY bonus mission ("Rest and Relaxation") and other GOTY content use CRes string IDs 30000-30084 that are not in the v1.003 source's string table, so with our CRes those texts are missing. The Modernizer types them into ClientRes.rc. Here, take them at build time from the retail GOTY CRES.dll inside nolfgoty.rez (its string table) and compile them into CRes. Note: the release build has the same gap (it also uses the v1.003-source CRes) -- see the note at the end. 2143494; ClientRes.rc 30000-30084 build/build.sh (CRes step) or a generated .rc include; tools/ (string-table reader) IDs 30000-30084 as retail -- done (in the release build too: BUILDING.md step 2c)
M-P4-09 Tech-demo music mood lock not ported MusicMgr locks the mood for the tech demo's music control file; only meaningful with M-P4-11. 2d7697d; MusicMgr.cpp -- -- -- not ported (tech demo not available, M-P4-11)
M-P4-10 HUD font images fix INTERFACE/FONTS/HUD_NMRS.PCX differs from retail in 11 bytes (palette) and HUD_CHOOSER.PCX was re-saved; probably the transparent colour, for the blending change of M-P3-19. If they are needed, derive them from the retail files at build time (a palette edit) rather than committing images. ASSETS/INTERFACE/FONTS possibly a derive recipe -- M-P3-19, M-P4-01 done (investigated): both images differ from retail only in palette entry 255, the background, magenta -> black (pixels identical). HUD_NMRS.PCX is named by LAYOUT.txt but loaded by no code: not needed. HUD_CHOOSER.PCX needs the edit exactly when M-P3-19 draws the chooser font with blending (black transparent); without M-P3-19 the edited font blacks out the whole screen (tested), so the recipe was held back until M-P3-19 landed; it is now active as src/modern/data/INTERFACE/FONTS/HUD_CHOOSER.pcx.bytes (-3 ff00ff 000000, the .bytes recipe kind of tools/moddata.py); with M-P3-19 the game draws normally (playtests playtest-basic, p3-hud at 1280x720). Fixed later: on a 16-bit screen the chooser's text drew every character as the one before ("" as ";Gnkrsdq Vdonm="). The glyph scan (LithBitmapFont::FindOffset) looks for columns of the transparent colour, now black, and the glyphs' shadow edge, entry 254 (3,0,0), rounds to black in a 16-bit surface, so one glyph was split in two. The recipe also makes entry 254 (8,0,0) (-6 030000 080000), the lowest red 16 bits keep: 93 boundaries at 32, 16 and 15 bits alike, as at 32 before. The scan's results are cached in FontData.fntby name and width, so the modern build'sLTF_OFFSETS_VERSION is 1.02 (lithbitmapfont.cpp`), which drops a cache written before
M-P4-11 Extras: LithTech tech demo not ported The Modernizer renames "Custom level" to "Extras" and ships Monolith's October 2000 LithTech "Goodman" tech demo level with its models, textures and music (with a strobe-light warning). That content is not on the retail discs and is not ours to ship. The retail custom-level folder stays. 2d7697d; ASSETS/WORLDS/Extras, CHARS/MODELS/GOODMAN.ABC, PARTNER_DANCIN.ABC, TEX/OBJECTS/OBJ05, MUSIC/NOLFDANCE -- -- -- not ported
M-P4-12 Music without installing DirectMusic feature Wine's DirectMusic (Proton's too) cannot play the music, so the game is silent apart from its sound effects until Microsoft's DirectMusic is installed into the prefix with winetricks -- awkward everywhere and worst on a Steam Deck, where the prefix is Proton's. Carry it in the game directory instead, so the music plays with any prefix as it comes. -- (ours) build/retailextract.py (openruntime), src/modern/directmusic/ (README.md, lithtech.exe.manifest), toolchains/directmusic-5.3.1.904/ -- -- done: step 2 installs Microsoft's nine DirectMusic DLLs (DirectX 9.0c, from toolchains/directmusic-5.3.1.904/, checksummed) as ms_<name>.dll, and a registration-free COM manifest for lithtech.exe that maps the 48 classes they register to those files; the ms_ names keep Wine from substituting its builtins. No engine change. gm.dls is not needed: the engine's loader never gives DirectMusic the default GM collection, and the music's instruments are its own DLS banks. Checked under Wine 9.0 at the main menu with +NoMovies 1 (ALSA captured to a file): silent with Wine's DirectMusic, music with ours, no prefix changes
M-P4-13 The briefing room's Union Jack fix The Union Jack behind the desk in UNITY's briefing room (issue #26) shows inverted: the thin red diagonals are on the wrong side of the white. -- (ours) src/modern/data/TEX/OBJECTS/OBJ14/OBJ0689.dtx.flop, tools/moddata.py (.flop) -- -- done: the texture, TEX/OBJECTS/OBJ14/OBJ0689.dtx (256x128, DXT1), is drawn the right way round; every world that shows it maps it mirrored. Read out of the world files (a parser following our server.dll's loader, CServerWorldFull::Load, w_LoadWorldModel): its 22 surfaces, in T01S02, T02S01, T03S01, T06S01..T10S01 and M15S02 (the world models flagleft and flagright in each, and two of the main geometry's in T06S01 and T09S01), all have the same mapping, P along -x on a wall facing -z, so the image runs right to left. No other texture is a Union Jack. The modern build installs the retail texture mirrored left to right (a new moddata.py recipe, .flop: DXT1 blocks reordered and each block's rows reversed, nothing re-encoded, so it is exact; the release build is unchanged), and every one of those rooms then shows the flag the right way round. Checked in T02S01's briefing cinematic

P5 -- gameplay, AI, physics and crash fixes

Several of these were found under the Modernizer's Visual Studio 2019 build, whose floating-point behaviour differs from VC6's x87 code (its own comments say so for the path-finding fix). For each, first check whether the retail game (or our modern build without the fix) shows the problem; record the answer here, and add an entry to RETAIL_BUGS.md for those that are real retail bugs.

ID Title Kind What and why Modernizer source Our files Data / strings Depends Status
M-P5-01 Enemies flying off at "FTL speed" fix GetMovementData interpolates through a table by a time fraction that can fall slightly outside 0..1, indexing past the table; clamp the time to 0..1. 7206431, f4cfdb6; AIMovement.cpp src/game/ObjectDLL/AIMovement.cpp -- -- done -- retail bug: at the animation's last frame the lookup reads one past the table, and in every retail OBJECT.lto the word after s_afPistolCornerLeft is 0xffffffff (a NaN, times a weight of 0 is still NaN), so a pistol AI ending a left corner move is moved by NaN. Fix: P5_GetMovementData (src/modern/object/p5_fixes.cpp); RETAIL_BUGS.md
M-P5-02 AI path-finding intersection fix (verify) The connection-point intersection divides two dot products and can divide 0 by 0; the Modernizer replaces the NaN with 1.0, saying this mimics what VC6's code did. Check our VC6 build's behaviour before changing anything. 3b14d20, f38c666; AIPathMgr.cpp src/game/ObjectDLL/AIPathMgr.cpp -- -- not ported -- VS2019-only: in VC6's code for BuildPath (AIPathMgr.obj) a NaN fT fails the fT > fLength-fRadius test (test $0x41,%ah) but passes fT < fRadius (test $0x1,%ah), so retail clamps it to fRadius, a valid point near the first endpoint; SSE compares fail both and keep the NaN
M-P5-03 Switches and bombs with no move direction fix (verify) A door/switch with a zero move direction normalises a zero vector (0/0), and moving to a target at zero distance does the same; guard both. On loading a save, a door whose saved vectors are NaN (seen on mission 2's bombs) gets them reset. 2221959, 040e499, 8a109a2, 14c214d; Door.cpp src/game/ObjectDLL/Door.cpp -- -- done -- retail bug (IEEE, any compiler): VEC_NORM of a zero MoveDir (Switch's default) is NaN, the open position too, and CalculateNewPos normalises a zero distance; Switch triggers still fire (SetOpening), which hides it. Fix: zero direction kept, zero/NaN distance arrives, NaNs from retail saves repaired on load (src/modern/object/p5_fixes.cpp); RETAIL_BUGS.md. Not reproduced in a playtest
M-P5-04 Zero-vector normalise in player rotation fix (verify) The player's turn-to-target code normalises a direction that can be zero; skip the normalise then. 839e7b2, 14c214d; PlayerObj.cpp src/game/ObjectDLL/PlayerObj.cpp -- -- done -- retail bug (IEEE): FACEOBJECT with an object straight above/below the player sets a NaN rotation; the rotation is now left alone; RETAIL_BUGS.md. Not reproduced in a playtest
M-P5-05 Invisible impassable geometry fix The player is pushed back by gaps between floor polygons because "standing on" is lost for a frame. Carry the last standing-on surface forward for up to 10 frames while nothing is under the player (not while jumping or falling). The Modernizer's earlier doorway-specific workarounds (673be2f, 0754b9f) were superseded by this. 4ba3819, 2b32b27; CMoveMgr.cpp (UpdateOnGround) src/game/ClientShellDLL/CMoveMgr.cpp/.h -- -- done -- not verified against retail (the seam behaviour is the engine's, the same in both builds); harmless: only the main world is carried, for at most 10 frames (a sixth of a second since M-P1-23), never while jumping or falling or after a drop of more than 1 unit (src/modern/cshell/p5_movement.h, p5_gameplay.cpp)
M-P5-06 Invalid model style crash fix CCharacterFX can see an invalid model style and crash looking up its name; fall back to the default style. 62e9f7a; CharacterFX.cpp src/game/ClientShellDLL/CharacterFX.cpp -- -- done -- retail bug: GetStyleName indexes m_aStyles unchecked, so eModelStyleInvalid (0xFF) reads far past the heap array (a crash only when that memory is unmapped); falls back to the default style; RETAIL_BUGS.md
M-P5-07 Lip-sync with a zero-channel sound fix Lip-sync divides by the sound's channel count, which can come back 0 when the sound data cannot be read; return early. b93d68d; NodeController.cpp NodeController.cpp (P2 owns; one block) -- -- not ported -- debug-only: the retail code never divides by the channel count; dwChannels is only ASSERTed (compiled out in release), and with no sound data both buffers stay null and the averaging loops do nothing
M-P5-08 Player setup crash fix CModelButeMgr frees its attribute data at the end of Init, but player setup reads it when the folder gets focus; free it in Term instead. 256862b; ModelButeMgr.cpp src/game/Shared/ModelButeMgr.cpp -- -- not ported -- Modernizer-only: it used NOLF2's bute manager; retail CButeMgr::Term() is an empty inline (src/LT2/lithshared/incs/butemgr.h:208), so Init's m_buteMgr.Term() frees nothing and player setup reads intact data
M-P5-09 Player setup string overflow fix Player setup copies console strings (skin, head, model, name) into 32-byte buffers; long values overflow. Use buffers big enough (or bounded copies). 2a73340; FolderPlayer.cpp src/game/ClientShellDLL/FolderPlayer.cpp -- -- done -- retail bug: NetPlayerSkin/NetPlayerHead ("name,file") are strcpyd into szTemp[32]; in the shipped CFolderPlayer::OnFocus frame (sub $0x228, szTemp at 0x18(%esp)) the overrun only reaches space another branch uses, so retail shows nothing. Fix compares the console values in place; RETAIL_BUGS.md
M-P5-10 Camera disabler ammo HUD fix The camera disabler's "shots per clip" is about 1000, so the ammo HUD surface is created far larger than the screen; recreate it when it exceeds the screen. b6c8cea; PlayerStats.cpp (UpdateAmmo area) PlayerStats.cpp (P3 owns; one block) -- -- done -- retail bug (data + code): the camera disabler's ShotsPerClip is 10000 in the retail WEAPONS.txt, so the ammo HUD surface becomes 10000 icons wide and stays so. Fix: P5_FitAmmoHUD caps it at the screen width and remakes an oversized one (one block in PlayerStats.cpp); RETAIL_BUGS.md
M-P5-11 Mission summary continue button fix In single player the summary's continue button is missing because the data update only happens on a message that is not sent; always update. d23b2b4; FolderSummary.cpp src/game/ClientShellDLL/FolderSummary.cpp -- -- not ported -- not a retail bug: release-build playtest (tools/p5-summary.txt, exit-level cheat on T02S01) shows the summary filled in and the Next button, from the single-player MID_PLAYER_SUMMARY round trip
M-P5-12 Quick switch feature QuickSwitch: next/previous weapon changes the weapon at once (the chooser stays visible and closes 2.5 s later), and firing works while it is open. Game options get a toggle. 0eb8734, a1637ca, 827a88c; InterfaceMgr.cpp, GameClientShell.cpp, WeaponModel.cpp (ChangeWeapon keep-chooser flag), FolderGame.cpp InterfaceMgr.cpp, GameClientShell.cpp, WeaponModel.cpp/.h, FolderGame.cpp/.h P5 strings: label + help -- done -- QuickSwitch (VarTrack, default 0); chooser selection changes the weapon at once (CWeaponChooser::NextWeapon/PrevWeapon, also on key repeat), chooser closes after 2.5 s or when fire is released, fire works while it is up; Game options toggle; strings 31400-31401 (src/modern/cres/p5.rc). Tested: tools/p5-features.txt
M-P5-13 Toggle zoom feature A ToggleZoom command (id 81): each press steps the scope to the next zoom level, and past the last one zooms out. 3573050, e460812; GameClientShell.cpp (OnCommandOn), ClientUtilities.cpp, CommandIDs.h GameClientShell.cpp, ClientUtilities.cpp, CommandIDs.h P5 strings: control name + action string -- done -- COMMAND_ID_TOGGLE_ZOOM 81, control "Step zoom"/action ToggleZoom (31402-31403), registered with AddAction at start-up; zoom in a step per press, out after the last (re-dispatches ZOOM_IN/ZOOM_OUT). Tested: tools/p5-features.txt (AK47 scope 1, 2, out, 1)
M-P5-14 Shadows lost after cinematics and loads fix Model shadows switch off during cinematics and are restored from a value saved when the cinematic started -- which is 0 if two cinematics overlap, and is never restored after loading a save. Restore MaxModelShadows from DrawShadows when a cinematic ends and after a world loads. 38479a9; GameClientShell.cpp GameClientShell.cpp -- -- done -- retail bug: TurnOnAlternativeCamera runs every frame a cinematic camera is live, so it saves its own 0 and the cinematic's end restores 0 (also ExitLevel/LoadGame write the initial 0); RETAIL_BUGS.md. The fix (P5_RESTORE_MODEL_SHADOWS in src/modern/cshell/p5_gameplay.cpp) was off for a while: with shadows left on, our reconstructed d3d.ren crashed in its shadow pass. The cause was its declaration of Direct3D's DrawPrimitive without __stdcall (w54_10048180_osoiqipsf.cpp, and the same in big4e3e4.cpp): the call's six arguments were popped twice and the shadow drawer's caller got its registers back from the wrong slots. With that fixed in the reconstruction (release build too; make progress unchanged), shadows play on T01S01, M01S01, M02S01 and M03S01 and look as with the retail d3d.ren
M-P5-15 Attribute-file save crash fix (verify) Saving an attribute file (e.g. NetHost.txt) with no filename set crashes in the bute manager. The Modernizer's fix is in NOLF2's bute manager source, which it used instead of the retail library; check whether our reconstructed ButeMgr has the same fault before porting. 856a2b7; LT2/lithshared/butemgr/ButeMgr.cpp src/lithshared/ButeMgr/butemgr.cpp -- -- not ported -- Modernizer-only (NOLF2's bute manager): with our ButeMgrNoMFC.lib and VC6's static iostreams, Save("x.txt") after a failed Parse (null filename buffer) returns true and writes the file (test program built and run under Wine)
M-P5-16 Animation parser value fix (verify) The generated animation parser leaves yyval's string uninitialised for empty rules. Check whether it matters for our flex/bison output before porting. 862a96b; AnimationParse.cpp, bison.simple src/game/ObjectDLL/AnimationParse.y (or the bison template) -- -- not ported -- harmless here: the uninitialised yyval is only pushed for empty rules, whose values no action reads (every szString use is $1/$2 of a string, which copies its STRING token's value)
M-P5-17 Closing popups with Activate or the right button fix A popup (the text of a picked-up intelligence item, say) only closes with Escape. Retail meant the Activate key to close it too (the test is commented out, and the Activate command's own close cannot happen: the popup pauses the game, which stops the commands). Daniel Gibson, 7c658d0 (ChthonVII's fork); PopupText.cpp, InterfaceMgr.cpp InterfaceMgr.cpp (OnKeyDown, OnRButtonDown, Update) -- -- done: the key bound to Activate, and the right mouse button (Activate's default), close it, once the press that opened it is let go (retail's g_bLockPopup, which the Modernizer's version does not wait for). So does the Activate command on whatever else it is bound to -- a controller's X button by default (M-P7-02), which sends no key (issue #28): the popup's update closes it when the command turns on, the state retail's lock already reads there. Tested with a popup sent from the console (Trigger Player "popup <id>"), and with the test controller's X (M-P7-01): the popup stays until X, and X closes it
M-P5-18 Each weapon keeps its ammo feature Changing back to a weapon always loads the "best" ammo the player has (phosphorus for the P38), whatever was in it before. Remember the ammo each weapon was last used with and choose it again while there is some left. Daniel Gibson, cd22c3d (ChthonVII's fork); WeaponModel.cpp WeaponModel.cpp, cshell/p5_gameplay.cpp -- -- done: remembered when a weapon is made, its ammo changed, or the game changes it when one runs out; for the session only (not in saved games, so their format is unchanged). Unlike the fork's, a weapon with nothing remembered keeps retail's choice. Tested: P38 switched to FMJ, to the revolver and back: FMJ
M-P5-19 Crouch toggle feature Crouching only lasts while Duck is held. Offer a toggle: one press crouches, the next stands. ChthonVII, 2a27083 (from the "crouch toggle" Modernizer on Mod DB): a CrouchLock action the server's player toggles src/engine/lithtech/demodtx.cpp, clientconsole.cpp, link/vtables.txt, src/LT2/sdk/inc/iltclient.h; cshell/p5_gameplay.cpp, FolderGame.cpp/.h, GameClientShell.cpp P5 strings 31404-31405 -- done, done differently: Game options' "Toggle crouch" (CrouchToggle) makes the Duck control itself toggle (keyboard, mouse or pad alike). The engine changes the frame's input (ILTClient::SetCommandToggle, appended like M-P0-02), so the client's movement and camera, a recorded demo and the server all see the same crouch: the fork's server-side lock left the multiplayer client's own crouch to the held key, and needs a modified server. Duck holds again while swimming (it dives), on a ladder (it climbs down), on a vehicle (it brakes) and when dead; each of these, and a new level, stands the player up. Tested in T01S01: a tap crouches (eye 21.5 units lower) and stays down after release, the next tap stands; off, it is retail's hold
M-P5-20 A stutter at the top of a ramp, and up some terrain fix The engine's step-up lifts the player onto a floor's plane from the box's lowest corner, wherever that corner is: past the top of a ramp or across a ridge it steps the player onto the slope carried on, in mid-air, and backwards along the slope's normal, several times over (RETAIL_BUGS.md). Lift onto the highest point of the floor that is under the feet, straight up. -- (ours) src/engine/server/worldsim.cpp (ws_StairStep, ws_PolyTopOver) -- -- done: a polygon one can walk on (normal y at least the step slope limit, 0.7071) is clipped to the swept box's footprint and the feet are lifted straight up to the highest point left, or not at all when none of it is under them; steeper ones, and a polygon of more than 32 vertices, keep the retail lift. Over the polygon throughout this is the retail lift, straight up instead of along the normal. Measured on two ramps in M01S03 and a ridge in M01S04 (release build: shoves back and up of 3 to 16 units, up to 44 frames in the air on a ramp and most of the climb on the ridge; fixed: none, on the ground every frame up the ridge), the stairs at M02S03's start (16 units a step, as before), and make test-modern. Not changed: walking down a slope, the player still leaves it for a frame now and then and drops a fraction of a unit (retail too)

P6 -- multiplayer and the dedicated server

ID Title Kind What and why Modernizer source Our files Data / strings Depends Status
M-P6-01 Format-string injection in names and chat fix Player names, chat messages and server names reach printf-style formatting, so a % in them can crash clients and servers (a published LithTech format-string vulnerability). Strip % from player names (server, when set), chat text (server, before relaying), the host's own session name and player name, and server names shown in the browser. Done: FilterFormatChars in CommonUtilities.cpp; the server filters names (OnClientEnterWorld, CPlayerObj::SetNetName) and the three chat messages; the client filters what it receives (InterfaceMgr.cpp: chat, MID_PLAYER_CONNECTED/ADDED names, through src/modern/cshell/p6_multiplayer.cpp), so it is safe on a retail server too; the browser bounds and filters server and player names. Tested: a release-build host crashes when a player named Retail%n%s joins and chats %n...; the modern host shows Retailns: nnnnssss (RETAIL_BUGS.md). 8eee245; CommonUtilities.cpp (FilterString), PlayerObj.cpp, GameServerShell.cpp, FolderHost.cpp, FolderJoin.cpp, FolderPlayer.cpp src/game/Shared/CommonUtilities.cpp/.h, PlayerObj.cpp, GameServerShell.cpp, FolderHost.cpp, FolderJoin.cpp, FolderPlayer.cpp -- -- done
M-P6-02 Master server feature GameSpy's master server is gone, so the internet server list is empty and servers cannot advertise. The Modernizer hex-edited Monolith's prebuilt GameSpy libraries to point at another master (now master.openspy.net). Our src/lithshared/gamespy/ is a stand-in that does not talk to a master at all; under NOLFSRC_MODERN, implement the GameSpy master protocol (server heartbeat; client list query) against a configurable master address. Done: src/lithshared/gamespy/gsmaster.h (protocol notes, the validate algorithm), GameSpyMgr.cpp (heartbeat every minute from the game port, \secure\ answered first with \validate\, statechanged 2 on shutdown), GameSpyClientMgr.cpp (TCP list request with list cmp, then \status\ to each server over UDP, 12 at a time, 3 tries; ping, sorting, player keys). Console variable MasterServer (host or host:port; empty = master.openspy.net; none = off), saved by the client and carried to a hosted server through NetHost.txt (Read/WriteNetHostSettings; src/modern/object/p6_master.cpp). Also bounds the option index the browser reads from a server (RETAIL_BUGS.md). Tested against a local master stub: heartbeat, challenge, status, list and join between two modern games in two containers; the validate response was checked once against master.openspy.net (it accepted it and listed 3 NOLF servers). e390d9f, b26446f, 0fbeba9 (binary edits of LT2/lithshared/libs) src/lithshared/gamespy/** -- -- done
M-P6-03 Version filter default fix The server browser's version filter defaults to "all versions"; default it to the current version, so incompatible servers are not shown. 9999997; FolderJoin.cpp FolderJoin.cpp -- -- done
M-P6-04 Handshake version decision Decided: the modern build keeps the 1.003 handshake (minor version 4), so it plays with retail 1.003 players and servers; the Modernizer's two new client-to-server messages (238, 250) are ignored by a retail server. The Modernizer raises it to 8, which only separates the two populations -- no existing message changed format. Optional, later: a modern server could accept 4 and 8 and answer each client with its own value, letting Modernizer players join too. Version strings stay 1.003. Found while testing (P6): the handshake also compares the client's CShell.dll file CRC with the server's (MID_HANDSHAKE_LETMEIN, kick GAME_DISCON_BADCSHELL). Our release build's CShell.dll already differs from retail's (and the modern one from both), so no build of ours can join a retail server or take a retail client, whatever the version number: a release-build client was refused by a modern host with "Invalid cshell.dll file". Playing with retail would need the client to report (and the server to accept) the retail file's CRC; that is a decision for the plan, not made here. 6ec9a51, 7dfafda, a86ff70; ClientServerShared.h, VersionMgr.cpp src/game/Shared/ClientServerShared.h, VersionMgr.cpp -- -- decided
M-P6-05 Reconnect after a level-change timeout fix Between server level changes clients sometimes time out at once. On a timeout disconnect in multiplayer, retry joining the same address up to twice (the retry has to wait for the next update); reset the counter on entering a world. Done: P6_RejoinAfterTimeout/P6_UpdateRejoin/P6_ResetRejoin (src/modern/cshell/p6_multiplayer.cpp), called from GameClientShell.cpp (OnEvent, Update, OnEnterWorld). Tested by freezing the host for ~2 minutes: the client timed out, rejoined and was back in the level. 24e2980; GameClientShell.cpp (OnEvent disconnect, Update) GameClientShell.cpp/.h -- -- done
M-P6-06 Join retry log feature Print each join attempt and its result to the console. Done: GameClientShell::DoJoinGame and CFolderJoin::DoJoinGame print Joining <addr> (attempt n of m) and Joined/Could not join. 24e2980; GameClientShell.cpp (DoJoinGame) GameClientShell.cpp -- -- done
M-P6-07 Multiplayer frame lock fix Covered by M-P1-01/M-P1-02 (always limited in multiplayer). No P6 code: done when that item is. 995cb20 -- -- M-P1-01 done (by M-P1-01/M-P1-02)
M-P6-08 Big head mode in multiplayer fix Covered by M-P2-10 (forced off in multiplayer). No P6 code: done when that item is. c4b7a24 -- -- M-P2-10 done (by M-P2-10)
M-P6-09 PlayStation 2 Cate not ported A multiplayer character using the PS2 release's Cate model, skins and first-person hands. Not on these discs. 50fee30; CHARS/MODELS/MULTI/hero_thief_multi.abc, CHARS/SKINS/hero_thief*, GUNS/SKINS_PV/thiefhands_pv.dtx -- -- -- not ported
M-P6-10 Server-side join password fix A locked server sends its password to anyone who queries it, and only the joining client checks it (RETAIL_BUGS.md). The server now keeps it and checks it in the handshake. -- (ours) GameServerShell.cpp (ProcessHandshake), MyGameSpyMgr.cpp, GameClientShell.cpp (ProcessHandshake, +connect), FolderJoin.cpp, FolderJoinLAN.cpp/.h, InterfaceMgr.cpp, src/game/Shared/ClientServerShared.h, cshell/p6_multiplayer.cpp P6 strings: the LAN password's help, the refusal M-P6-04 done: a locked server answers queries with PasswordRequired 1 in place of Password; the client appends a tag, a length and the password (each byte XORed with the handshake's mask) to MID_HANDSHAKE_LETMEIN, after the three values a retail server reads; a locked server boots a client without it or with the wrong one (GAME_DISCON_BADPASSWORD: "This server is locked, and the password was missing or wrong."). The Internet join folder asks for it when the server says it wants one; the LAN folder gains a Password field under the port (its server list a line lower); +connect reads NetJoinPassword. Tested with a locked host: +connect clients with the right, a missing and a wrong password (the first joins, the others are refused), and a LAN-folder join with the password typed
M-P6-11 Dedicated server log feature NolfServ.exe, the dedicated server (built from source since this project's server work; src/engine/nolfserv/), shows its console only in its window, so a server run without anyone watching -- headless under Xvfb, in a container -- leaves no record. Write every console line to a log file as well. -- (ours) src/modern/nolfserv/p6_server.cpp, ServerDlg.cpp (WriteConsoleString), ServerApp.cpp (InitInstance), src/modern/cshell/p2_log_body.h -- M-P2-07 done: each line goes to modern-server.log in the game directory as server: <line> (M-P2-07's ModernLog, emptied by the first line of each run); the game DLLs the server loads log there too, through a process-wide variable naming the file, so a server and a game started from the same directory keep their own logs. Tested with tools/servertest.sh: the server's start, its master server, the world loaded, the client joining and the stop are all in the log
M-P6-12 Unattended dedicated server fix Starting the server without its setup wizard, from the settings saved in NetHost.txt, is what a dedicated server on a machine of its own needs, and the retail command line half works: -nowiz as the last argument is not seen, and -NetLevel/-NetCALevel write settings nothing reads (RETAIL_BUGS.md). -- (ours) ServerDlg.cpp (ParseCommandLine), src/modern/nolfserv/p6_server.cpp (P6_LevelOption); build/game-scripts/linux-server.sh (both builds) -- -- done: -nowiz counts wherever it is; each -NetLevel <world> (-NetCALevel for H.A.R.M. vs UNITY) adds a world to the game type's list, the first replacing the saved list (at most 50, as the wizard). linux-server.sh (in every game directory) starts the server with Wine, the wizard on the first run or straight from the saved settings, under xvfb-run when there is no display. Tested in the container: -NetSessionName cmdline -NetLevel <two worlds> -MasterServer none -nowiz starts without the wizard and runs the first of the two; linux-server.sh without a display runs the saved game
M-P6-13 The dedicated server's master server feature A dedicated server reports to the master server the game DLL's M-P6-02 heartbeat names (MasterServer, read from NetHost.txt, empty = master.openspy.net), but nothing on the server says which, and the wizard cannot set it. -- (ours) ServerDlg.cpp (the option table, OnShowWindow), src/modern/nolfserv/p6_server.cpp (P6_ShowMasterServer) -- M-P6-02 done: once the game runs, the console (and the log) says where it is hosted (Hosting "<session>" on <address>, UDP port <port>, for up to <n> players.) and Master server: <name>, or that the game is not listed (none); -MasterServer <host[:port]> on the command line sets it in NetHost.txt, where the server's Object.lto reads it when the game starts. Tested with the default and with none (the heartbeat itself is M-P6-02's, tested there)
M-P6-14 Dedicated server fixes fix Four bugs of the retail NolfServ.exe found while rebuilding it (RETAIL_BUGS.md): console text printed as a format (a % in a player's name or a typed command), a console line of 498 characters or more losing its terminator and overrunning the buffer, the game's option table filled without a bound, and the wizard's find handle never closed. -- (ours) ServerDlg.cpp (WriteConsoleString, UpdateUI, OnConsoleSend, OnShellMessage, DoHost), WizardDlg.cpp (CWizardDlg) -- -- done: text goes through "%s"; the line is cut to 497 characters and terminated; at most MAX_GAME_OPTIONS (20) options; _findclose. The console message the game could send (SERVERAPP_CONSOLEMESSAGE, which no retail game DLL sends) is read whole and bounded instead of as one unbounded word

P7 -- controller support

Gamepads: an XInput pad (an Xbox-style controller; what Steam Input presents to Proton on a Steam Deck, and what Wine makes of most pads). The buttons are an engine input device, bound like keys; the sticks, the menus and Start are read by the client itself, so they do not depend on what a button is bound to. Tested with the test pad (build/playtest.sh's pad command, NOLFSRC_PADTEST); not yet on real hardware.

ID Title Kind What and why Modernizer source Our files Data / strings Depends Status
M-P7-01 The ##gamepad device feature The engine reads one DirectInput joystick, with no dead zone, no hot-plug, and an Xbox pad's two triggers on one axis. Add the pad as a device of its own, read through XInput, so every command can be bound to its buttons. -- (ours) src/modern/engine/p7_gamepad.*, p7_padread.h; clientinput.cpp (four hooks); build/build.mk (LITHTECH_SRC) -- -- done: an object behind IDirectInputDevice2A that answers the calls the engine makes of a joystick from XInput's state (the first connected pad of four; empty slots asked about once a second): six absolute axes (Left Stick X/Y, Right Stick X/Y -1000..1000, Y down positive; Left/Right Trigger Axis 0..1000) and sixteen buttons (A/B/X/Y Button, Left/Right Bumper, Back, Start, Left/Right Stick Click, D-Pad Up/Down/Left/Right, Left/Right Trigger past XInput's threshold), buffered in whatever data format the engine sets; acquired only while the game has the foreground. It is the modern build's only joystick: DirectInput's joysticks are left out of the device list (one of them is the same pad under Wine), so the engine's "the joystick" -- "Joystick 1", capture, device name, bindings list -- is the pad; JoystickDisable (the launcher's "Disable joystick") does not apply to it. With NOLFSRC_PADTEST naming a file the pad is that file's state (p7_padread.h), for the playtest
M-P7-02 Default layout feature Bindings for the pad in the default controls, and in existing configs. -- (ours) gamedir/defctrls.cfg.patch, cshell/p1_config.cpp, cshell/p7_pad.cpp -- M-P7-01, M-P1-12 done: right trigger Fire, left trigger ToggleZoom, A Jump, B Duck, X Activate, Y Reload, right/left bumper next/previous weapon, Back Mission, left stick click Speed, right stick click CenterView, d-pad NextAmmo (up), HolsterWeapon (down), FlashLight (left), Score (right); Start opens the menu (M-P7-05). Not Inventory: its screen closes on the keyboard key's state (InterfaceMgr.cpp). Config version 1.2 runs defctrls.cfg's ##gamepad lines alone, so a config from 1.0/1.1 keeps its keyboard and mouse
M-P7-03 Analog look feature The right stick turns the view at a rate set by how far it is pushed, independent of the frame rate. -- (ours) cshell/p7_pad.cpp; GameClientShell.cpp (CalculateCameraRotation, Update) -- M-P1-03 done: radial dead zone (PadLookDeadZone), the rest of the travel rescaled to 0..1 and raised to PadLookCurve, times 180 degrees a second across and 120 up and down, PadLookSpeed, and the frame time; added to the mouse's offsets, so it slows with the zoom like the mouse, and the mouse's invert applies on top of PadInvertY. Tested: a full push turns about 90 degrees in half a second
M-P7-04 Analog movement feature The left stick moves the way it points, at a speed set by how far it is pushed. -- (ours) cshell/p7_pad.cpp; CMoveMgr.cpp (flags, acceleration, GetMaxVelMag), VehicleMgr.cpp -- -- done: radial dead zone (PadMoveDeadZone); the direction sets the movement flags by octant (what the animations and the server see) and the acceleration; past three-quarters of its travel it runs, below that it walks at that share of the walk speed (at least a third); Speed held turns running over, as Shift does on the keyboard (RunLock is not used with the stick). On the motorcycle and snowmobile the stick is throttle, brake and steering
M-P7-05 Menus with the pad feature Menus take only keys. -- (ours) cshell/p7_pad.cpp; FolderCustomControls.cpp (capture); BaseFolder.cpp/.h (StepFixedRow) -- -- done: outside play the d-pad or the left stick is the arrow keys (repeating after 0.4 s, every 0.12 s), A Enter, B and Start Escape; in play Start is Escape, and A is Space in a cut-scene (skips it) and on the death screen (goes on). Through IClientShell::OnKeyDown/OnKeyUp, as the engine delivers a key; not while Customize controls waits for a control. Tested: main menu to Options to Controls to Controller and back, the pause menu and back. Left and right move between a page's fixed controls along a row -- Back and Continue, or a folder's own (the objectives page's Start mission, Skip outfitting and Outfitting) -- where retail moved between them only with up and down, through the whole page (issue #27): when the selected control is a fixed one and does not take left or right itself, the nearest selectable fixed control that way within 40 page units up or down is selected (Back, Main and Continue count as one row wherever they sit: on the mission status page after a mission, Main is 46 above Continue); the keyboard's arrows too. Tested with the keys and the test controller's d-pad and stick on the briefing and objectives pages and the mission status page after a mission
M-P7-06 Controller options feature A folder for the pad's settings. -- (ours) cshell/p7_foldercontroller.*; FolderControls.cpp, FolderMgr.*, FolderCommands.h, LayoutMgr.cpp ATTRIBUTES/LAYOUT.txt.patch ([FolderController]); P7 strings 31600-31613 M-P7-03, M-P7-04 done: Controls > Controller: Look speed (25-300%), Invert look, Look and Move dead zones (0-50%), Restore controller defaults (the pad's bindings only). The pad's buttons are bound in Customize controls like keys (they show as "Joystick A Button" ...). Controls' retail "Use joystick" and "Configure joystick" are gone: there is no DirectInput joystick, and their set-up would bind the pad's sticks a second time
M-P7-07 Rumble feature Vibration on firing, damage and nearby explosions (nothing in the game asks for it: PlayJoystickEffect is never called). -- (ours) M-P7-01 not started
M-P7-08 Button prompts feature Hints that name a key ("Press [key]") name the pad's button when the pad was used last. -- (ours) M-P7-01 not started
M-P7-09 No DirectInput device enumeration fix input_Init asks DirectInput to enumerate every attached device (EnumDevices), which walks every HID device on the machine; a Windows 11 crash report shows an access violation in RtlAllocateHeap inside dinput.dll there, at startup. The modern build reads no DirectInput joystick (M-P7-01), so it needs only the keyboard and the mouse. -- (ours) src/engine/lithtech/clientinput.cpp (input_Init) -- M-P7-01 done: the system keyboard and mouse are created by their GUIDs and described by their own GetDeviceInfo; EnumDevices is not called. Bindings find them by type (##keyboard, ##mouse), as before. Checked under Wine (the scripted playtest walks, turns and fires); not yet confirmed on the Windows machine that crashed, whose later reports point at heap damage from elsewhere

Not ported

What Why
SDL2, Microsoft Detours, the Visual Studio 2019 / CMake / Azure project files, _CRT_SECURE_NO_WARNINGS, "disable Spectre mitigation", the C++ syntax fixes (int operator==, STD iostream prefixes, default-argument edits in iltclient.h/iltserver.h, const casts in Sparam.cpp) build plumbing for a different compiler; the behaviour they enable is reimplemented with Win32 and VC6 (see the rules above)
Strict floating-point exception macros (DebugNew.h) a debugging aid for their Debug build; our debug build is make debug
The bundled lithtech.exe byte-identical to retail
d3dim700.dll Microsoft's Direct3D immediate-mode runtime, redistributed for compatibility: not ours; Wine provides its own
"Movies disabled" (NextMovie skipped, 25bac3d) only needed because SDL2 conflicted with the video player on the shared window; without SDL2 there is no conflict
The demo console backdrop (interface/console.pcx) art from the NOLF demo, not on the retail discs (M-P2-05 uses retail art instead)
The new arrow, solid-colour and HD-font images (INTERFACE/Arrow*.pcx, GroovyGreen/PitchBlack/QuantumWhite.pcx, FONT_LARGE_0_HD.*, FONT_AIR_Indexed.PCX), the BETHHAND/EXPON TrueType fonts and the ExpoFont layout key Modernizer art and third-party fonts; the solid colours and HD font are unused in its final code, the arrows are replaced as described in M-P3-04
INTHERO_ACTION.lta the source of a retail model, which the build does not need
The LithTech tech demo and everything only it uses M-P4-11, M-P4-09
PS2 Cate M-P6-09
The Modernizer's jukebox song names its own invented names (M-P4-02 writes ours)

Note: Game of the Year strings

Monolith's v1.003 source of CRes.dll lacks the Game of the Year content's strings (IDs 30000-30095: the "Rest and Relaxation" mission name, briefing, objectives, intel and subtitles) and its credits, which the retail GOTY CRES.dll in nolfgoty.rez has. The build now extracts both from the retail game and compiles them into our CRes.dll (BUILDING.md, step 2c), in the release build and therefore in the modern one too, so its resources are the retail GOTY ones (411 of 411 byte-identical). M-P4-08 is covered by that.