diff --git a/BUILD.md b/BUILD.md index 5777104..e2eba2b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -144,10 +144,72 @@ textures now log `L4D3D.cpp couldn't load texture …` and render untextured; the game boots to a running window with `-windowed -res 640 480 -egg TEST.EGG` from a working copy like `assets/RP411/`. +**`steam_api.dll` is optional.** It is **delay-loaded** +(`DelayLoadDLLs` in [RP_L4/RP_L4.vcxproj](RP_L4/RP_L4.vcxproj), with +`delayimp.lib` supplying the helper), so a working copy that has never seen +Steam — like `assets/RP411/`, which predates the Steam work — boots and races +normally. Only Steam itself is switched off, logged as +`Steam: steam_api.dll not found beside the exe - Steam features off, staying +on TCP`. + +Delay loading alone would only move the failure: the first call into a +delay-loaded DLL that cannot be found raises the helper's fatal exception +rather than returning an error. So every path that reaches a Steam symbol +first asks `SteamNetTransport_ClientLibraryPresent()` +([MUNGA_L4/L4STEAMTRANSPORT.cpp](MUNGA_L4/L4STEAMTRANSPORT.cpp)) — a cached +`LoadLibrary` probe using the same plain-name lookup the helper does. The +gates are `SteamNetTransport_Install` and the two lobby entries +`RPL4Lobby_Host`/`_Join`; everything else in the transport and lobby is +downstream of one of those. **Adding a new Steam call site means checking +which gate covers it.** + +For Steam features you need the DLL from +[extern/steamworks_sdk_164/sdk/redistributable_bin/steam_api.dll](extern/steamworks_sdk_164/sdk/redistributable_bin/steam_api.dll) +(the 32-bit one, not `win64\`) beside the exe, plus `steam_appid.txt` — with +the DLL but no appid file `SteamAPI_Init` fails and the game falls back to TCP. + +`environ.ini` must be written **without a BOM**. The parser matches key names +from the start of the line, so a leading UTF-8 BOM silently invalidates the +first key in the file — put `L4CONTROLS` there and it reverts to the built-in +`KEYBOARD` default, which then fail-fasts on `0xC0000409` with +`*****VTV has no controls mapping!*****` in the log (there is no keyboard-only +pod mapper). PowerShell's `Set-Content -Encoding utf8` writes a BOM in 5.1; +use `[System.IO.File]::WriteAllText` with an `ASCIIEncoding`. The log line +`Environ: environ.ini does not mention N option(s)` naming a key that is +plainly in the file is the tell. + For runtime debugging the v143 build produces full PDBs — run `cdb -g -G -lines -y Release rpl4opt.exe ...` from the working directory (cdb ships in this machine's Windows Kits). +### Debug keys + +`RP412DEVKEYS=1` arms them, and they arrive through the engine keyboard +handler, so `L4CONTROLS` must include `KEYBOARD`. That handler takes one key +per frame off the front of the message queue and genuinely drops presses, so +press again before concluding a key is broken. + +| Key | State | +|-----|-------| +| **Alt+W** wireframe | **Live.** Reimplemented on D3D9 as a per-frame `D3DRS_FILLMODE` (`gWireframe`, [MUNGA_L4/L4VIDEO.cpp](MUNGA_L4/L4VIDEO.cpp)). The sky dome and the 2D pass (gunsight, cam-ship HUD) are held solid; particles are wireframed with everything else. | +| **Alt+E** event-queue dump | **Live.** Reaches `GeneralEventQueue::DumpEventQueue`. | +| Alt+V predator vision | Inert. | +| Alt+F frame dump | Inert. | +| Alt+/ perf stats | Inert. | +| Alt+K free memory | Inert, silent. | +| Alt+R dither, Alt+P eyepoint | Inert; they log "Function net yet enabled." | +| Alt+Q abort | Always live, no `RP412DEVKEYS` needed. | + +The inert ones call `DPLRenderer` methods whose bodies were commented out +with the rest of the DPL calls in the 2007 port (`STUBBED: DPL RB 1/14/07`). +Reviving one means writing it against D3D9 rather than un-commenting +anything: the `dpl_*` types those bodies used are empty placeholder classes +now ([DPLSTUB.h](DPLSTUB.h)), and `libDPL/` is reference headers that are not +compiled. Alt+V is the worst of them — the DPL renderer implemented predator +vision internally, reached by passing an out-of-band explosion effect type +(`-1` on, `-2` off) with a NULL DCS, and nothing in this tree records what it +actually looked like. + ### Running without the cockpit (Workstream A prototype) Two new environment options remove the hardware dependency entirely: diff --git a/MUNGA_L4/L4APP.cpp b/MUNGA_L4/L4APP.cpp index fabaffd..167b612 100644 --- a/MUNGA_L4/L4APP.cpp +++ b/MUNGA_L4/L4APP.cpp @@ -867,6 +867,13 @@ void // The debug keys are for developers: RP412DEVKEYS=1 arms them. // Players get exactly one chord - Alt+Q, the deliberate abort. // + // Only Alt+W and Alt+E actually do anything. The rest call DPLRenderer + // methods whose bodies were commented out with the rest of the DPL + // calls in the 2007 Direct3D port and have been empty ever since; each + // is marked INERT below. Reviving one means writing it against D3D9, + // not un-commenting anything - the dpl_* types it used are empty + // placeholder classes now (DPLSTUB.h). + // static int dev_keys = -1; if (dev_keys < 0) { @@ -910,6 +917,7 @@ void //-------------------------------------------- // FrameDump from Division card to Targa file + // INERT: DPLFrameDump and dump_frame_buffer are both stubs. //-------------------------------------------- case PCK_ALT_F: { @@ -925,6 +933,7 @@ void //------------------------------------ // Report current free memory in card + // INERT: the body below is commented out, so this key is silent. //------------------------------------ case PCK_ALT_K: { @@ -952,6 +961,7 @@ void //--------------------------------------- // Report performance statistics (Alt-?) + // INERT: DPLReportPerfStats is a stub - it read DPL's own counters. //--------------------------------------- case PCK_ALT_SLASH: { @@ -964,9 +974,10 @@ void } break; } - //-------------------------- - // Toggle Wireframe display - //-------------------------- + //-------------------------------------------------------------- + // Toggle Wireframe display. Live: reimplemented on D3D9 as a + // per-frame D3DRS_FILLMODE (see gWireframe in L4VIDEO.cpp). + //-------------------------------------------------------------- case PCK_ALT_W: { if (!dev_keys) break; @@ -978,9 +989,13 @@ void } break; } - //-------------------------- - // Toggle "Predator-vision" - //-------------------------- + //-------------------------------------------------------------- + // Toggle "Predator-vision" - a global false-colour/thermal mode + // the DPL renderer implemented internally, reached by passing an + // out-of-band explosion effect type (-1 on, -2 off) with a NULL + // DCS. INERT: DPLTogglePVision is a stub, and what it looked + // like is not recorded anywhere in this tree. + //-------------------------------------------------------------- case PCK_ALT_V: { if (!dev_keys) break; diff --git a/MUNGA_L4/L4STEAMTRANSPORT.cpp b/MUNGA_L4/L4STEAMTRANSPORT.cpp index 0d19d3b..ff10c5e 100644 --- a/MUNGA_L4/L4STEAMTRANSPORT.cpp +++ b/MUNGA_L4/L4STEAMTRANSPORT.cpp @@ -3,6 +3,41 @@ #include "l4steamtransport.h" +//######################################################################## +// Is steam_api.dll actually here? +// +// Defined outside the RP412_STEAM guard on purpose: callers ask without +// caring how the build was configured, and a build without the SDK +// truthfully has no client library. See the header for why every Steam +// path has to come through here first. +// +// The answer is cached because it is asked on menu paint, and because a +// DLL that appeared halfway through a session is not a case worth +// supporting - the delay-load helper would have bound the first miss +// anyway. +//######################################################################## +Logical + SteamNetTransport_ClientLibraryPresent() +{ +#ifdef RP412_STEAM + static int present = -1; + + if (present < 0) + { + present = (LoadLibraryA("steam_api.dll") != NULL) ? 1 : 0; + + if (!present) + { + DEBUG_STREAM << "Steam: steam_api.dll not found beside the exe - " + << "Steam features off, staying on TCP\n" << std::flush; + } + } + return present ? True : False; +#else + return False; +#endif +} + #ifdef RP412_STEAM #include "l4nettransport.h" @@ -686,6 +721,16 @@ Logical return True; } + // + // Before ANY Steam symbol: steam_api.dll is delay-loaded, so calling + // into it when it is missing raises the helper's fatal exception + // rather than failing. This is the gate that makes the DLL optional. + // + if (!SteamNetTransport_ClientLibraryPresent()) + { + return False; + } + if (!SteamAPI_Init()) { DEBUG_STREAM << "SteamNetTransport: SteamAPI_Init failed " diff --git a/MUNGA_L4/L4STEAMTRANSPORT.h b/MUNGA_L4/L4STEAMTRANSPORT.h index 0a333bc..48b79f4 100644 --- a/MUNGA_L4/L4STEAMTRANSPORT.h +++ b/MUNGA_L4/L4STEAMTRANSPORT.h @@ -14,7 +14,8 @@ //######################################################################## // SteamNetTransport - the retail wire (l4steamtransport.cpp). Built // only under RP412_STEAM (Steamworks SDK vendored at -// extern\steamworks_sdk_164; steam_api.dll ships beside the exe). +// extern\steamworks_sdk_164; steam_api.dll ships beside the exe, but is +// delay-loaded and optional - see the note further down). // // Method mapping onto ISteamNetworkingSockets: // @@ -55,6 +56,25 @@ // egg is distributed. //######################################################################## +//######################################################################## +// steam_api.dll is DELAY-LOADED (see the DelayLoadDLLs setting in +// RP_L4.vcxproj), so the game runs on a machine that has never seen +// Steam - a plain import of a missing DLL kills the process at load time +// with 0xC0000135, before a window or a single log line. +// +// The catch is that delay loading only MOVES the failure: the first call +// to a delay-loaded function whose DLL cannot be found raises a fatal +// exception instead. So every path that would reach a Steam symbol has +// to ask this first. It is declared outside the RP412_STEAM guard, and +// answers False in a build without the SDK, so callers need no #ifdef. +// +// Cheap and idempotent: one LoadLibrary on first call, cached after. +// Deliberately the same plain-name load the delay-load helper itself +// does, so a yes here means the helper will succeed too. +//######################################################################## +Logical + SteamNetTransport_ClientLibraryPresent(); + #ifdef RP412_STEAM // Bring Steam up and make this the process transport. False (with the diff --git a/MUNGA_L4/L4VIDEO.cpp b/MUNGA_L4/L4VIDEO.cpp index a1aa9a0..f0df866 100644 --- a/MUNGA_L4/L4VIDEO.cpp +++ b/MUNGA_L4/L4VIDEO.cpp @@ -83,6 +83,19 @@ DWORD // (NULL = present to the device window as always). HWND gMainPresentWindow = NULL; +// +// Alt+W wireframe (RP412DEVKEYS). File scope rather than a DPLRenderer +// member because a fresh renderer is built per mission - a member would +// drop the toggle every time the race restarted, which is exactly when +// you are looking at geometry. +// +// DPLToggleWireframe only flips this; the fill mode is applied once per +// frame in ExecuteImplementation. Setting it there rather than in the key +// handler means it re-asserts itself after a device Reset, which reverts +// the fill mode to D3DFILL_SOLID underneath us. +// +static Logical gWireframe = 0; + //STUBBED: DPL RB 1/14/07 // when this is resolved it can be removed #include "..\DPLSTUB.h" @@ -6402,6 +6415,17 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte hr = mDevice->BeginScene(); + // + // Alt+W wireframe. Set every frame from the flag rather than once at + // toggle time - see gWireframe. It is turned back off for the sky pass + // and again for the 2D pass below; everything between here and there + // draws as edges. + // + mDevice->SetRenderState( + D3DRS_FILLMODE, + gWireframe ? D3DFILL_WIREFRAME : D3DFILL_SOLID + ); + mDevice->SetFVF(L4VERTEX_FVF); D3DXMATRIX viewTransform; @@ -6478,6 +6502,17 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte mDevice->SetTransform(D3DTS_PROJECTION, &mProjectionMatrix); + // + // The sky stays solid under Alt+W. Wireframing the dome fills the top + // of the screen with its own tessellation and buries the geometry you + // turned wireframe on to look at; a solid sky gives the edges something + // to read against. + // + if (gWireframe) + { + mDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID); + } + if (!l4_application->IsDead()) { @@ -6492,6 +6527,11 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte for (d3d_OBJECT *obj = mRenderLists[PASS_SKY]; obj != NULL; obj = obj->GetNext(PASS_SKY)) obj->Draw(PASS_SKY, &viewTransform, mTargetRenderTime); + if (gWireframe) + { + mDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); + } + //Reactivate fog mDevice->SetRenderState(D3DRS_FOGSTART, *((DWORD*)(¤tFogNear))); mDevice->SetRenderState(D3DRS_FOGEND, *((DWORD*)(¤tFogFar))); @@ -6535,6 +6575,12 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte // Wrap it up by doing the 2D pass // mDevice->SetFVF(L4VERTEX_2D_FVF); + // + // Always solid from here on: the gunsight and the cam-ship HUD are + // textured quads on this device, and in wireframe they come out as bare + // diagonals. + // + mDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID); mDevice->SetRenderState(D3DRS_ZWRITEENABLE, true); mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); @@ -6818,19 +6864,17 @@ void void DPLRenderer::DPLToggleWireframe() { - //STUBBBED: DPL RB 1/14/07 - //static Logical wireframe_on = 0; + // + // The DPL original set a renderer property here + // (dpl_render_prop_wireframe) and was stubbed out with the rest of the + // DPL calls in the 2007 D3D port. D3D9 has no equivalent global: the + // fill mode is device state, so all this does is record the intent and + // let the frame apply it. See gWireframe at the top of this file. + // + gWireframe = !gWireframe; - //if ((wireframe_on ^= 1) != 0) - //{ - // DEBUG_STREAM << "wireframe ON" << std::endl << std::flush; - // dpl_SetRenderProperty(dpl_render_prop_wireframe, dpl_render_value_on, NULL ); - //} - //else - //{ - // DEBUG_STREAM << "wireframe OFF" << std::endl << std::flush; - // dpl_SetRenderProperty(dpl_render_prop_wireframe, dpl_render_value_off, NULL ); - //} + DEBUG_STREAM << "wireframe " << (gWireframe ? "ON" : "OFF") + << std::endl << std::flush; } // //############################################################################# diff --git a/RP_L4/RPL4ENVIRON.cpp b/RP_L4/RPL4ENVIRON.cpp index 6a2fe08..3974cdc 100644 --- a/RP_L4/RPL4ENVIRON.cpp +++ b/RP_L4/RPL4ENVIRON.cpp @@ -257,9 +257,11 @@ namespace "# when real RIO hardware is selected - the pod keeps its own cadence.\n" "#RP412LAMPSWEEP=0\n" "\n" -"# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client\n" -"# running and steam_appid.txt beside the exe; without them the game\n" -"# logs the reason and falls back to plain TCP. 0 = TCP only.\n" +"# 1 = Steam networking (lobbies, FakeIP mesh). Needs steam_api.dll, the\n" +"# Steam client running, and steam_appid.txt beside the exe; missing any\n" +"# of them logs the reason and falls back to plain TCP - nothing here can\n" +"# stop the game starting, and steam_api.dll being absent altogether is\n" +"# fine. 0 = TCP only.\n" "RP412STEAM=1\n" "\n" "# Line up each remote player's clock with ours, so their vehicle is\n" @@ -408,8 +410,21 @@ namespace "\n" "# ---- Developer / testing ----------------------------------------------------\n" "\n" -"# Nonzero arms the debug keys: Alt+W wireframe, Alt+V predator vision,\n" -"# Alt+F frame dump, Alt+/ perf stats, Alt+E event-queue dump.\n" +"# Nonzero arms the debug keys. Two of them do something in this build:\n" +"# Alt+W wireframe. The sky dome stays solid so the edges have\n" +"# something to read against, and the gunsight and cam-ship\n" +"# HUD stay solid too.\n" +"# Alt+E write the event queue to the log.\n" +"# The others are stubs the 2007 DPL->Direct3D port left behind and do\n" +"# nothing at all. They are named here so a dead key is not mistaken for\n" +"# a broken one: Alt+V predator vision, Alt+F frame dump, Alt+/ perf\n" +"# stats and Alt+K free memory are silent; Alt+R dither pattern and\n" +"# Alt+P eyepoint position at least say so in the log.\n" +"#\n" +"# All of them arrive through the engine keyboard handler, so L4CONTROLS\n" +"# has to include KEYBOARD - the shipped stack does. That handler reads\n" +"# one key per frame off the front of the message queue and so drops\n" +"# presses; if a key seems dead, press it again before believing it.\n" "# 0 or unset = off. (Alt+Q, the mission abort, is always live.)\n" "#RP412DEVKEYS=1\n" "\n" diff --git a/RP_L4/RPL4LOBBY.cpp b/RP_L4/RPL4LOBBY.cpp index 94826ae..01118f2 100644 --- a/RP_L4/RPL4LOBBY.cpp +++ b/RP_L4/RPL4LOBBY.cpp @@ -987,9 +987,21 @@ Logical return gInLobby; } +// +// Host and Join are the two places that reach Steam without the +// transport having got there first, so they carry their own guard. +// steam_api.dll is delay-loaded and calling into it when it is absent +// raises the helper's fatal exception - the menu already greys these on +// Available(), but a dead DLL must not depend on the UI for safety. +// int RPL4Lobby_Host(HINSTANCE instance, HWND main_window) { + if (!SteamNetTransport_ClientLibraryPresent()) + { + return LobbyRoomLeft; + } + gCallDone = False; SteamAPICall_t call = SteamMatchmaking()->CreateLobby(k_ELobbyTypePublic, kMaxLobbyMembers); @@ -1011,6 +1023,11 @@ int int RPL4Lobby_Join(HINSTANCE instance, HWND main_window) { + if (!SteamNetTransport_ClientLibraryPresent()) + { + return LobbyRoomLeft; + } + gCallDone = False; SteamMatchmaking()->AddRequestLobbyListStringFilter( kLobbyTagKey, "1", k_ELobbyComparisonEqual); diff --git a/RP_L4/RP_L4.vcxproj b/RP_L4/RP_L4.vcxproj index fb06fe5..94bd219 100644 --- a/RP_L4/RP_L4.vcxproj +++ b/RP_L4/RP_L4.vcxproj @@ -66,7 +66,16 @@ /FORCE:MULTIPLE %(AdditionalOptions) ..\lib;%(AdditionalLibraryDirectories) - ws2_32.lib;dinput8.lib;dxguid.lib;OpenAL32.lib;libsndfile-1.lib;d3d9.lib;legacy_stdio_definitions.lib;steam_api.lib;%(AdditionalDependencies) + ws2_32.lib;dinput8.lib;dxguid.lib;OpenAL32.lib;libsndfile-1.lib;d3d9.lib;legacy_stdio_definitions.lib;steam_api.lib;delayimp.lib;%(AdditionalDependencies) + + steam_api.dll;%(DelayLoadDLLs) false true diff --git a/docs/RP412-FRONTEND-DESIGN.md b/docs/RP412-FRONTEND-DESIGN.md index a826bbc..c4d3ea5 100644 --- a/docs/RP412-FRONTEND-DESIGN.md +++ b/docs/RP412-FRONTEND-DESIGN.md @@ -128,6 +128,15 @@ by `SteamNetTransport_RegisterPeer`). Runtime-verified on this box: 32257 game)` under AppID 480, graceful TCP fallback without Steam, default boot untouched. `steam_api.dll` ships in the dist. +**The DLL is optional (2026-08-10):** it is delay-loaded, and every Steam +call site is gated on `SteamNetTransport_ClientLibraryPresent()`, so a +machine with no `steam_api.dll` boots and races instead of dying at load +time with `0xC0000135`. Verified all three ways on this box: absent DLL +boots, absent DLL with `RP412STEAM=1` logs `Steam: steam_api.dll not +found beside the exe` and stays on TCP, and DLL present still brings the +transport up as before. See [BUILD.md](../BUILD.md) §4 for which gate +covers what — a new Steam call site has to sit behind one of them. + Remaining for Steam multiplayer: the lobby (front-end UI + `ISteamMatchmaking`): owner collects each member's FakeIP + fake game port + loadout via lobby member data, feeds `RegisterPeer` on every diff --git a/pack-dist.ps1 b/pack-dist.ps1 index c076990..2abed29 100644 --- a/pack-dist.ps1 +++ b/pack-dist.ps1 @@ -89,8 +89,10 @@ Copy-Item $exe $dist $pdb = Join-Path $root 'Release\rpl4opt.pdb' if (Test-Path $pdb) { Copy-Item $pdb $dist } -# steam_api.dll: the exe imports it (RP412_STEAM build). The Steam wire -# only activates with RP412STEAM=1; plain desktop runs never touch it. +# steam_api.dll: DELAY-loaded by the exe (RP412_STEAM build), not a hard +# import - the game runs without it, with Steam features off. It ships +# anyway because the retail build wants them. The Steam wire only +# activates with RP412STEAM=1; plain desktop runs never touch it. Copy-Item (Join-Path $root 'extern\steamworks_sdk_164\sdk\redistributable_bin\steam_api.dll') $dist # steam_appid.txt: until RP412 has its own AppID, Steam testing runs