Skip to content

Q4port - #1

Open
King-witcher wants to merge 57 commits into
id-Software:masterfrom
King-witcher:q4port
Open

Q4port#1
King-witcher wants to merge 57 commits into
id-Software:masterfrom
King-witcher:q4port

Conversation

@King-witcher

Copy link
Copy Markdown

No description provided.

King-witcher and others added 30 commits June 13, 2026 03:16
Reconcile idCommon, idCVarSystem, idCmdSystem, idDeclManager and
idNetworkSystem vtables to the retail gamex86.dll's v37 layout so the
game DLL's callbacks land on the right slots. Add idGame 3-arg Init
(RV_UNIFIED_ALLOCATOR) and wire game->Init(alloc,free,msize) in
Common.cpp. Key fixes: idCommon GetLanguageDict re-added as a trailing
engine-only virtual; declType_t DECL_FX/DECL_PARTICLE kept as
engine-private values; RV_SINGLE_DECL_FILE is ON in retail so the 5
Start/Finish/Load/Write/Flush decl slots exist; idList gains filter_t.
Includes temporary [Q4trace] debug markers (gated by g_q4Trace).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the 9 v37 methods after WriteFile (SetIsFileLoadingAllowed,
GetNewFileMemory, asset-log, etc.) and the download/import/demo tail so
the vtable matches; without this the game's script compiler called
SetIsFileLoadingAllowed/GetNewFileMemory at wrong slots and hung at
"Initializing scripts". Fix internal callers (CopyFile->CopyOSFile,
FindFile, HasD3XP/RunningD3XP as non-virtual false). This lets the
Quake 4 game script (84435 statements) compile inside the engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…repo per the SDK EULA)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OSPathToRelativePath strips BASE_GAMEDIR from OS paths; with it still set to
DOOM 3's "base" it found "base" inside "q4base" but the directory-boundary
check failed, producing malformed q4base/<ospath> paths and 'Non-portable'
warning spam during script loading. q4base also makes fs_game redundant
(DOOM3 FS skips fs_game when == BASE_GAMEDIR, so no double pak load).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
typeinfo.vcxproj's idCommonLocal stub no longer matches our v37 idCommon and
would fail; idlib/curllib are built explicitly so doomdll links fine without
it. Makes the 'Quake 4' F5 launch + Release builds work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The retail game creates decl objects (idDeclEntityDef etc.) with the v37
idDecl vtable; our engine holds them as idDecl*/idDeclBase* and calls
decl->self->Parse()/FreeData()/etc. Ours was still v8 (Parse 2-arg; missing
GetCompressedLength/SetReferencedThisLevel/RebuildTextSource/Validate), so
those calls hit the wrong slot and the game hung parsing the first entityDef
during idGameLocal::Init. Port idDecl+idDeclBase to the v37 layout (3-arg
Parse, new methods at SDK slots) and cascade Parse->3-arg across all 12 engine
decl subclasses (idMaterial, idSoundShader, idDeclSkin/Table/EntityDef/AF/FX/
Particle/PDA/Email/Video/Audio) + MaterialDoc.cpp caller. Includes temporary
[Q4trace] markers in FindType. (idMaterial member layout still v8 -> M3.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t() completes

The engine's idDeclEntityDef::Parse calls game->CacheDictionaryMedia via the
idGame vtable, which is only partially ported (just the 3-arg Init), so it hit
a misaligned slot and hung while parsing the first entityDef (aas_types) during
the game's Init. Temporarily guard that call (media just loads lazily). With it
out of the way the FULL Quake 4 game initializes inside our DOOM 3 engine:
84435 script statements compiled, 531 events, 247 classes, all decls+AFs, aas,
'game initialized.', and game->Init() RETURNS. Re-enable CacheDictionaryMedia
after the full idGame v37 port (M3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump GAME_API_VERSION 8 -> 37 and reconcile idGame/idGameEdit vtables
byte-for-byte with the Quake 4 1.4.2 SDK so the retail gamex86.dll's
GetGameAPI hands back interfaces the engine can call:

- idGame: 3-arg Init(alloc,free,msize) only (RV_UNIFIED_ALLOCATOR),
  RunFrame(cmds,activeEditors,lastCatchupFrame,serverGameFrame),
  InitFromNewMap/SaveGame drop idSoundWorld*, SetUserInfo drops canModify,
  inserted Repeater/MenuFrame/effects/banlist methods, dropped
  GetBestGameType/GetMapLoadingGUI/SelectTimeGroup/GetTimeGroupTime.
- idGameEdit: full 122-method v37 layout.
- gameImport_t/gameExport_t carry rvBSEManager* and rvGameLog*.
- Updated all engine call sites (Session, async client/server, tr_light)
  for the new signatures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The retail gamex86.dll crashed every menu frame with STATUS_FLOAT_STACK_CHECK
(0xC0000092). Root cause: our idSys was still the DOOM 3 (v8) layout while
the Quake 4 SDK gives idSys a virtual destructor at slot 0 -- shifting every
slot by one -- plus FPU_SetPrecision (replacing FPU_EnableExceptions) and a
large block of Raven windowing/input/stdio services. The slot shift made the
game call e.g. DebugVPrintf (void) onto our GetClockTicks (returns double),
leaking the x87 register stack until it faulted.

- Rewrote class idSys / idSysLocal to the exact v37 vtable; new RAVEN methods
  map to existing Sys_*/Win32/stdio services (windowing methods are thin
  ::Win32 wrappers; input-queue and GetGUID are safe stubs for now).
- Added a vectored exception handler (win_main.cpp) that logs the faulting
  module+offset and recovers from stray FP faults -- a dev safety net.
- DeviceContext::FindFont now caches failed font lookups by name so a missing
  font no longer re-appends a fresh slot on every lookup (the list had grown
  past 440 entries with the Quake 4 .fontdat fonts the D3 loader can't read).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the font port begun by the RenderSystem.h struct change. A Quake 4
.fontdat file is a raw dump of one fontInfo_t: 256 glyphs of 9 floats, then
pointSize/fontHeight/ascender/descender and a null material pointer (9236
bytes for a 12-point font). Unlike DOOM 3 (a material per glyph) Quake 4 uses
one texture per font with each glyph a sub-rect.

- RegisterFont now reads fonts/<lang>/<name>_<size>.fontdat as a raw blob and
  binds one material (fonts/<lang>/<name>_<size>.tga|dds) per point size.
- DeviceContext text rendering uses the Quake 4 glyph fields (width/height,
  horiAdvance, horiBearingY, s1/t1/s2/t2) and the shared font material; the
  removed glyphScale is derived as 48/pointSize via DC_GlyphScale().

Fonts now load (the hundreds of "couldn't find font" warnings drop to none)
and menu text renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Quake 4 menus schedule timeline events relative to the previous one
("onTime +50 { ... }"); DOOM 3 only understood absolute times. The D3 parser
read the "+" as the time token (atoi -> 0), then ParseScript expected "{" but
found the number, bailed, and left the transition tokens in the stream. The
windowDef loop then parsed those stray tokens ("screen3_n_17::forecolor", ...)
as properties, desyncing the parser until it smashed a stack buffer
(STATUS_STACK_BUFFER_OVERRUN) deep in the credits screens.

idWindow::Parse now keeps a running onTimeBase and resolves "onTime +N" to
base+N. The mainmenu.gui now parses with zero "expected '{'" warnings and the
session reaches the first menu Redraw.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Quake 4 generates many materials from templates: a .guide file declares
"guide <name>( P1, P2, ... ) { body }" and a .mtr instantiates it with
"guide <declName> <templateName> ( arg1, arg2, ... )". DOOM 3 only had a
ParseGuides() stub, so e.g. _color.mtr (the menu's $desktop::cr_green etc.
color materials) failed to parse and rendered as null.

- idDeclManagerLocal::ParseGuides() now precaches every guides/*.guide template
  (rvGuideTemplate: name, parms, braced body) before the materials folder loads.
- idDeclFile::PreprocessGuides() expands each "guide" directive in place into
  "<declName> <body>" with parameter names replaced by the supplied args, then
  LoadAndParse parses the expanded text (verbatim pass-through when a file has
  no guides). 69 guides load; the _color.mtr parse warnings drop to zero.
- idChoiceWindow::Draw() now guards an empty / out-of-range choice list (Quake 4
  settings dropdowns whose localized choices don't resolve) instead of indexing
  off the end of the list -> fixes an access violation; the engine now sits at
  the parsed main menu indefinitely without crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Quake 4 main menu drives its entire screen/state machine through GUI
script commands DOOM 3 never had, so the menu activated to a blank screen and
logged hundreds of "Unknown script call" warnings (namedEvent x254,
stopTransitions x214, consoleCmd x44, resetVideo).

- namedEvent: fire a window's named event ("window::event" targets a specific
  window, bare "event" runs on the calling window and its children) -- this is
  what runs ingameCheck/showMain/hideMain and the rest of the menu logic.
- stopTransitions: halt a window's in-flight transitions (new idWindow::
  ClearTransitions()).
- consoleCmd: queue a console command (menu actions).
- resetVideo: no-op (we don't decode the intro attract videos).

Unknown-script-call warnings drop to zero and the menu logic now executes
(named events fire, the cursor is grabbed). Remaining: the menu's own state
machine still hides the main panel (hideMain) pending the intro/screen flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Quake 4 GUIs address individual components of vec4 and rect winvars -- e.g.
"matcolor_w" (the alpha of matColor), "backcolor_w", "rect_h" -- in set and
transition statements. DOOM 3's GetWinVarByName only matched whole-var names,
so every one of the menu's fade-ins and button reveals silently no-op'd,
leaving the menu black.

- New idWinFloatMember: a float view into one component of an idWinVec4
  (x/y/z/w) or idWinRectangle (x/y/w/h), reading/writing through the owner's
  public interface so the gui dict stays in sync.
- GetWinVarByName synthesizes one on demand for a trailing "_<comp>" suffix
  (cached in definedVars), with recursion guarded.
- Script_Transition and idWindow::Transition now accept an idWinFloatMember
  target and interpolate its single component.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts + skip intro

Three fixes that get the Quake 4 main menu's animation/state machine actually
running (the menu reached a steady state but stayed black):

- WIN_DESKTOP: idUserInterfaceLocal::InitFromFile only flagged the root window
  WIN_DESKTOP when desktop->Parse() returned true. Quake 4 GUIs make Parse
  return false (they trip the D3 parser on a few late constructs), so the menu
  never got the flag -- and RunTimeEvents()/Time() (which drive every onTime
  event, transition and fade) live behind `if (flags & WIN_DESKTOP)` in
  HandleEvent. Set the flag unconditionally: the root window is the desktop
  regardless. With this the menu's per-frame timeline finally ticks.

- idSimpleWindow::GetWinVarByName now does the same vec4/rect component
  synthesis ("matcolor_w" etc.) as idWindow -- simple windows are the bulk of
  the menu, so without it their fade-ins/reveals no-op'd. Added a definedVars
  list to own the synthesized members.

- StartMenu now sends "noIntro" instead of "playIntro": the intro is a sequence
  of logo videos we can't decode yet and it would park the menu on a black
  videoBlack overlay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The retail mainmenu.gui used two constructs our DOOM 3 GUI parser didn't
recognize, so each leaked into the windowDef token stream via ParseRegEntry
and desynced brace matching, ending in cascades of "Unexpected end of file":

- defineicon "name" "material": consume both tokens (Quake 4 named icons,
  used mainly by the multiplayer server-browser list; lookup not wired yet).
- onBackAction { ... }: added ON_BACKACTION to the script-event table so the
  block parses as a script (fired on the menu "back" action).

Engine now boots fully stable to the (active, redrawn) main menu with no
crash. Diagnostic launchers load uncompressed (.tga) fonts via
image_usePrecompressedTextures 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The menu booted, ran its timeline and issued every DrawMaterial/DrawStretchPic
correctly, yet the GL window stayed fully black. Root cause: a *visible*
container window in mainmenu.gui whose rect register evaluates to 0,0,0,0
pushes a zero-size clip rectangle (idWindow::Redraw -> dc->PushClipRect, not
gated on a degenerate rect). idDeviceContext::ClippedCoords then collapsed
EVERY subsequent draw to w=0/h=0 (x > clipRect.x+clipRect.w with both 0) and
returned "fully clipped", so DrawMaterial bailed before drawing -- the whole
menu was clipped away into the black r_clear.

Fix: ClippedCoords now treats a zero-size clip rect as "no constraint" and
skips it, so a 0x0 animation/container window can't nuke the entire menu.

After this the Q4 main menu draws: the logo, LOAD GAME / MULTIPLAYER / DELETE
GAME / MODS / UPDATES, the MP server browser, the save list, fonts and the
amber UI -- all rendering. Boot-to-menu (M3) achieved.

Diagnosed by a parallel root-cause workflow (ruled out the byte-for-byte
unchanged 2D backend) + a _white/q4logo on-screen probe that proved the
pipeline works, then a clip-rect-stack trace that caught the [0,0,0,0] rect.

Tooling: .vscode run/shot scripts now place the engine window on the right
monitor (X>=1920) so it never covers the middle display, and force the
captured window topmost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The menu rendered but showed every screen at once (main + save + load + MP
browser overlaid) and the rect-register-driven container clipped oddly. Root
cause: a GUI-parser token desync.

The idLexer splits "-1" into '-' and '1'. `textspacing` (used ~49x in
mainmenu.gui) is not a registered window var, so idWindow::ParseRegEntry stored
just "-" as its value and leaked the bare "1" back into the windowDef token
stream. That shifted every following token by one, so a windowDef's closing
'}' got read as a property value -> the window never closed and over-read its
siblings to EOF (the whole p_savegame / p_loadgame / p_mp_browse subtree was
absorbed and their `visible 0` was lost -> they all drew at once). 43
"Unexpected end of file" warnings were the tail of this.

Fixes:
- ParseRegEntry re-attaches a leading '-'/'+' sign to the following number, so
  `textspacing -1` parses as the single value -1 (no leak). After this the
  parse is clean (0 desync warnings) and the inactive screens correctly stay
  hidden -- the menu shows only the main screen.
- ParseRegEntry also refuses to consume a structural '{'/'}' as a property
  value (pushes it back) -- a defensive backstop so any *other* future desync
  closes the window at its real brace instead of cascading to EOF.
- GuiScript Script_Transition FixupParms: guard the rect-relative path on
  `destowner` (not just `owner`) -- a transition whose destination var doesn't
  resolve left destowner NULL and the stock code dereferenced it (null crash,
  exposed once the parse was clean).

The Quake 4 main menu now renders cleanly: QUAKE 4 title, the logo, and the
main items with no screen overlap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Quake 4 reorganized the engine's built-in 2D assets off the DOOM 3 paths, so
the cursor rendered as a black square, the console font didn't load, and solid
fills used a defaulted image:
- whiteImage: guis/assets/white -> the built-in procedural "_white" (can't fail)
- cursor/scrollbars: ui/assets/* -> gfx/guis/* (guicursor_arrow, scrollbar*)
- console charset: textures/bigchars -> gfx/2d/bigchars (Console + Common)

The in-game console text now renders, the cursor is the arrow again, and the
"Couldn't load image" spam for these assets is gone. (The console *background*
material "console" still has no Q4 equivalent -- separate item.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The main buttons (NEW GAME / LOAD GAME / SETTINGS / EXIT) were invisible until
hovered, while MULTIPLAYER / MODS / UPDATES / CREDITS showed fine. Cause: the
two button groups are revealed by different events. The secondary group is set
visible by the desktop's "ingameCheck" (which runs on activate and works). The
main group is faded in by a "mainReset" named event that, in retail, is fired
at the END of the id/Raven/Bethesda intro video sequence. We force-skip that
intro (HandleNamedEvent "noIntro"), and the gui's NoIntro handler is empty --
so nothing ever revealed the main buttons (anim_in's onTime 0 had already
hidden them via "hideMain").

Fix: after forcing "noIntro" in idSessionLocal::StartMenu, also fire
"mainReset" so the main buttons fade in -- matching what the intro's end would
have done. The Quake 4 main menu now shows all of NEW GAME / LOAD GAME /
MULTIPLAYER / SETTINGS / MODS / UPDATES / CREDITS / EXIT on first load.

(Window.cpp: restore actualX/actualY that a diagnostic-trace removal had
dropped, plus trailing-whitespace cleanup.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…amples)

The menu was silent: sound/guis.sndshd failed to parse so every menu sound
defaulted (and "_default.wav" doesn't exist either). Two Quake 4 sndshd deltas
made idSoundShader::ParseShader return false (-> MakeDefault):
- `volumeDb <n>` (Raven renamed `volume`; parms.volume is already in dB, so it's
  the same value) and `omnidirectional` (D3 has SSF_OMNIDIRECTIONAL but never
  parsed the keyword) and `shakeData <scale> <pattern>` (screen-shake-from-sound,
  not implemented -- consume its two tokens).
- Quake 4 lists sample files by path with NO extension (e.g.
  "sound/guis/generic/menu_over"); D3 only recognized a sample token if it
  contained ".wav"/".ogg", so the bare path hit the unknown-token error. Now a
  "sound/"-prefixed token is also treated as a sample (the loader resolves the
  extension).

After this the menu sound shaders parse and their samples load (no more
"unknown token" / "Couldn't load sound" warnings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The menu's own text resolves fine -- the engine loads Quake 4's
strings/english_guis.lang (1794 strings, e.g. #str_200001="LOAD GAME"). The
remaining "Unknown string id" warnings are DOOM 3 string ids the ENGINE
hardcodes that Quake 4's .lang doesn't carry: key names (#str_070xx in
KeyInput.cpp's keynames[]) and loading messages (#str_043xx in Common.cpp).

For the visible one -- key names in the controls/bindings UI -- KeyNumToString
now falls back to the raw English name ("TAB", "ESCAPE", "UPARROW", ...) when
the localized #str lookup returns the unresolved id, instead of displaying the
literal "#str_07018". (The loading-message ids only appear during map load, not
the menu, so they're left as-is for now.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- win_main.cpp: enrich the VEH (Q4_LogFault) to dump registers + a
  stack-scan pseudo-callstack and symbolize addresses via the PDB
  (SymFromAddr) so a faulting vtable jump names the caller/module.
- Session.cpp: breadcrumb traces around every game-> call in
  ExecuteMapChange (6a..12) to localize where map load faults.
- .vscode/run-q4-map.ps1: launch helper that boots straight into a
  map (+map <name>) on monitor 3, clearing logs for a fresh run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The retail game DLL only calls these interfaces at map load, so they
were never ported (still D3 layout) -- the menu worked because it is
rendered engine-internally (caller+impl share the same header). At map
load idGameLocal::MapShutdown calls renderSystem->ShutdownSpecialEffects()
and soundSystem->ResetListener() through the wrong slots -> null/garbage.

Mirror the Q4 1.4.2 SDK headers (q4/renderer/RenderSystem.h,
q4/sound/sound.h) under the retail config (Q4SDK_MD5R on;
_RV_MEM_SYS_SUPPORT/_XENON/_CONSOLE/_USE_OPENAL off): insert the Raven
methods at their exact SDK slot positions as stubs, keeping existing D3
bodies (engine-internal callers stay consistent). Render adds DeferredInit,
GetValidModes, RemoveAllModelReferences, the MD5R pair, TrackTextureUsage,
DrawStretchCopy, DrawTiny*, BeginFrame(viewDef), RenderLightFrustum/
LightProjectionMatrix/ToggleSmpFrame, the 3 special-effect slots,
TakeJPGScreenshot, CaptureRenderToMemory, DebugGraph/ShowDebugGraph.
Sound adds the emitter/listener/voice/reverb slots; AllocSoundWorld/
SetPlayingSoundWorld/GetPlayingSoundWorld/IsEAXAvailable kept non-virtual
so they stay off the vtable.

Verified: renderSystem->ShutdownSpecialEffects() now executes (we get
past it in MapShutdown) and the main menu still renders unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
King-witcher and others added 27 commits June 26, 2026 18:41
…er@idx 32)

Disassembling the retail gamex86 MapShutdown showed soundSystem->ResetListener()
is dispatched at vtable offset 0x80 (index 32), not 29 as our SDK-config port
assumed. The +3 is the _USE_OPENAL trio (IsEAXAvailable / GetDeviceName /
GetDefaultDeviceName) that Quake 4 1.4.2 PC ships -- the prior port (and the
interface-deltas.json note) wrongly treated _USE_OPENAL as undefined and omitted
them, shifting every sound slot from PrintMemInfo onward by 3. The game's
ResetListener() call was landing on our PlayShaderDirectly(worldId,name,channel)
with garbage args, corrupting state and crashing later in MapShutdown.

Insert the 3 OpenAL methods as real vtable slots right after PrintMemInfo and
make IsEAXAvailable virtual (was a non-virtual facade). GetDeviceName/
GetDefaultDeviceName are stubbed (D3 backend has no OpenAL device list; only the
sound-options menu uses them).

Verified: ResetListener() now executes, we get past MapShutdown into the real
map load (LoadLoadingGui, Map Initialization, rw->InitFromMap), and the menu
still renders. No more 0xC0000005.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, .map

- win_main.cpp: Q4_LogFault now symbolizes addresses via SymFromAddr and scans
  the full faulting frame (2048 words, /Oy omits frame pointers), keeping only
  DOOM3.exe/gamex86 return addresses -> a usable pseudo call-stack.
- doomdll.vcxproj: GenerateMapFile on Release|Win32 so DOOM3.map resolves any
  RVA offline (named the .data global &sessLocal that cracked the sound bug).
- RenderSystem_init.cpp: keep the ShutdownSpecialEffects slot-verify trace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
idRenderWorldLocal::InitFromMap rejected the Q4 .proc (it starts with
`PROC "<version>" <crc>`, e.g. PROC "4", not D3's single `mapProcFile003`
id token). Accept the Q4 header (read + discard the version string and CRC),
and in ParseModel read the extra int "sky" flag that Q4 emits on the world
area models (_area*) between numSurfaces and the first surface. The rest of
the model/surface format (8-float verts, int indexes, /* */-annotated blocks)
matches D3 once the lexer skips the comments.

Verified: rw->InitFromMap now returns OK for mp/q4dm1; map load advances to
game->SetUserInfo and into game->InitFromNewMap (which next faults in the
still-D3-layout idCollisionModelManager).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The retail game calls collisionModelManager during InitFromNewMap, but our
manager was still the D3 layout: Q4 inserts Init+Shutdown at slots 2-3 (so the
game's LoadMap@idx3 was hitting our LoadModel -> null write), exposes models as
idCollisionModel* objects (with game-callable GetName/GetBounds/... virtuals)
instead of cmHandle_t ints, and contactInfo_t gained separation + materialType.

Mirror q4/cm/CollisionModel.h: add class idCollisionModel, reorder the manager
to the 24-slot Q4 vtable, and bridge to the untouched D3 algorithms via a thin
cmModel_i wrapper (holds a cmHandle_t; the 6 getters delegate to the manager's
renamed *_h handle-based helpers). New Q4-only methods (ExtractCollisionModel/
FreeModel/PurgeModels/CompoundTrmFromModel/PrintMemInfo) are stubbed; LoadMap/
LoadModel/ModelFromTrm/Translation/Rotation/Contents/Contacts/etc. delegate.
contactInfo_t/trace_t updated to the Q4 layout (cross-boundary). dmap tool call
sites adapted; neo/game + neo/d3xp are not compiled so were left alone.

Verified: the map load now gets PAST collision dispatch -- the game's LoadMap
reaches our idCollisionModelManagerLocal::LoadMap -> BuildModels -> ConvertBrush
(next issue is the idMapFile class layout the game hands us, a separate fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The retail game parses the Q4 .map into its own idMapFile and hands the
idMapBrush* objects to collisionModelManager->LoadMap. Our idMapBrush had an
extra unused `int numSides` member (GetNumSides returns sides.Num()), which
shifted `sides` by 4 bytes, so ConvertBrush read the game's brush at the wrong
offset -> AV reading 0xFF0A0A0A (map text as a pointer). Drop numSides to match
the v37 layout (idMapEntity/idMapPatch already match; idMapFile only differs by
trailing Raven fields that don't move `entities`).

Verified: no more crash -- the game's LoadMap builds collision from the map
brushes and reaches "Game Map Init" / entity spawn (then stalls building
collision, diagnosed next). Adds .vscode/shot-q4-map.ps1 (boot-into-map + shot).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Quake 4 1.4.2 reordered idDrawVert and added a second vertex color, growing it
from D3's 60 bytes (our `padding` was #if 0'd out) to 64: v37 is
`xyz, color[4], normal, color2[4], tangents[2], st`. The game hands us idDrawVert
across the boundary (patch control points in idMapPatch, model geometry), so the
60-vs-64 stride made idList<idDrawVert> index garbage and read st/normal at the
wrong offsets -> the world collision build (ConvertPatch over worldspawn's 2419
prims) churned for MINUTES on corrupt data and never returned.

Reorder our idDrawVert to the v37 layout + add color2 + Get/Set accessors
(field-name accesses and the renderer's `&ac->field` GPU vertex pointers
auto-update). The SSE/SSE3 routines hand-code the OLD field adjacency in their
AOS->SOA shuffles, so they're bypassed via com_forceGenericSIMD 1 (set in the run
scripts) until rewritten; their DRAWVERT_* macros are bumped to size 64.

Verified: collision data now builds in 249 msec (was effectively hung); the load
advances past spawn into entity registration. Next blocker: idRenderWorld is
still D3-layout and is hit when the game registers entities for rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The game registers entities for rendering during InitFromNewMap via
gameRenderWorld->AddEntityDef/AddLightDef etc., but our idRenderWorld was the
D3 layout -> the game's Q4-index calls landed on wrong slots (crash resolved to
idRenderWorldLocal::DebugWinding). Mirror q4/renderer/RenderWorld.h (72 effective
slots; Flush/_RV_MEM_SYS and 2 _XENON methods #ifdef-excluded per the SDK config):
insert the 12 Raven methods after GetRenderLight (WriteRenderLight..
RemoveAllModelReferences), HasSkybox/FindVisibleAreas/RenderPortalFades/
WorldToScreen x2 after RenderScene (which gains renderFlags), GetPortals/
GetPortal(out) before by-value GetPortal, ProcessDemoCommand gains
portalSkyRenderView, DebugClearLines->DebugClear, DebugBounds gains depthTest +
MemorySummary/ShowDebug*/DebugFOV; remove CheckAreaForPortalSky + DebugClearPolygons.
Raven additions are stubbed; D3 bodies preserved.

Also make renderEntity_t/renderLight_t/modelTrace_t binary-identical to v37 (the
game fills them and passes them through AddEntityDef/AddLightDef): renderEntity_t
gains referenceSoundHandle (replaces the referenceSound ptr), bool forceUpdate +
weaponDepthHackInViewID, suppressSurfaceMask/overlayShader/shadowLODDistance/
suppressLOD; renderLight_t gains detailLevel/noDynamicShadows/globalLight/
referenceSoundHandle; modelTrace_t gains rvDeclMatType* materialType. Add
renderEffect_t/attachedModel_t/RF_* enum + the engine call-site fixups.

Verified: NO crash in renderWorld -- the load passes entity registration, builds
collision (20ms), into Game Map Init. Next: declType 9 (DECL_EFFECT) unrecognized
(idDeclManager::FindTypeWithoutParsing bad type 9) -- the v37 declType_t enum
divergence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ype)

After idRenderWorld the map load reached entity spawn and aborted with
"idDeclManager::FindTypeWithoutParsing: bad type: 9" -- the game looks up
DECL_EFFECT (9) for an entity's effect, but in Quake 4 the effect / lipSync /
playback / materialType decl types are registered by engine subsystems (the BSE
effect manager, lip-sync, playback) that we stub out, so the type was never
registered (our declType_t enum already matches v37). Register the four types in
idDeclManagerLocal::Init with minimal pass-through rvDecl* stubs (Parse returns
true, default "{ }") so FindType returns a harmless default instead of aborting.
Effects/lipsync/playback stay no-ops until those systems are actually ported.

Verified: no more bad-type abort; the load builds collision (18ms), registers
entities, and advances to model instantiation. Next blocker: idRenderModel
(unported -- Q4 inserts DampenFluidGrid as the first vtable slot, shifting all
slots, so the game's renderModel->IsDynamicModel lands wrong and it calls
InstantiateDynamicModel on a static model 'func_jumppad_20').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The game instantiates entity render models during spawn; our idRenderModel was
D3-layout so its IsDynamicModel/InstantiateDynamicModel calls landed on wrong
slots ("InstantiateDynamicModel called on static model"). Mirror q4/renderer/
Model.h: DampenFluidGrid becomes vtable slot 0 (non-pure, BEFORE the dtor --
shifts every slot), ~idRenderModel goes out-of-line, InitEmptyFromArgs after
InitEmpty, HasCollisionSurface after DepthHack, InstantiateDynamicModel gains
dword surfMask (= ~SURF_COLLISION default), ReadFromDemoFile/WriteToDemoFile
renamed to ReadFromDemo/WriteToDemo, GetSurfaceMask/SetHasSky/GetHasSky/
SetViewEntity appended, Timestamp returns unsigned int; adds modelCallback_t/
fluidImpact_t/modelTag_t types + non-virtual callback/hasSky members. All
idRenderModel subclasses updated (MD5/MD3/liquid/prt/beam/sprite + statics).

idRenderModelManager mirrored to the 31-slot v37 layout: Reset after Shutdown,
the 11 tool methods after CheckModel(const char*) (wired to the existing
R_*TriSurf helpers where signatures align, stubbed where menu-irrelevant),
CheckModel(idRenderModel*) overload, ListModelSummary after PrintMemInfo.

Header-diff verified slot-exact vs the SDK (47 + 31 slots). Runtime verified:
the InstantiateDynamicModel abort is gone; the load now reaches
idGameLocal::SpawnMapEntities (next: 'Unknown entity classname
weapon_lightninggun' -- a decl-parsing/indexing issue, not ABI).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quake4 organizes decls into subfolders (def/weapons/, def/ai/, def/items/,
...) whereas Doom3's tree was flat. RegisterDeclFolder used the non-recursive
ListFiles, so only the ~50 top-level *.def were indexed and every subfoldered
entityDef (weapon_lightninggun, all AI/items) came back "Unknown entity
classname" during SpawnMapEntities. Switch to ListFilesTree (recursive), which
already returns full folder-prefixed relative paths, so index construction uses
GetFile(i) directly. Scan now covers 246 def files (196 in subfolders).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vtable

The retail gamex86.dll sets up entity guis by calling idUserInterface through
Quake4 v37 slot indices (SetStateString@14, SetStateBool@15, StateChanged@30,
FindGui@13 on the manager). Doom3's v8 layout put those at different slots, so
the first entity carrying a gui (the weapon pickups' nailgun_ammo.gui, etc.)
called a shifted/null slot and crashed with AV EXEC at 0 mid-SpawnMapEntities.

Rebuild both interface vtables to the stock SDK v37 order (with _XENON and
_RV_MEM_SYS_SUPPORT undefined, matching how gamex86.dll was compiled):
  idUserInterface: insert SetInteractive@4, SetStateVector/Vec4@18-19,
    ClearState@20, DeleteState@21, GetLightColor@22, GetStateVector/Vec4@27-28,
    GetDesktop@29, GetMaxTextIndex@41; keep ReadFromDemoFile/WriteToDemoFile at
    their (shifted) 33/34 slots.
  idUserInterfaceManager: drop FindDemoGui, insert GuiIndex@14,
    FindGuiByIndex@15, ClearGameGuis@16, RunAlwaysThinkGUIs@19, RegisterIcon@20.
Implement the new slots in idUserInterfaceLocal / idUserInterfaceManagerLocal
(GetDesktop was already present as a non-virtual, now overrides the pure slot).

With this all 351 entities on mp/q4dm1 spawn; the map load now advances past
SpawnMapEntities into post-spawn gui setup. Verified against the retail DLL
disassembly (sub_20027AE0: call [ebp+38h]=SetStateString, [edx+3Ch]=SetStateBool,
[edx+78h]=StateChanged, [edx+34h]=FindGui). See docs/quake4/interface-deltas.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… callstack

0xC0000409 aborts (pure virtual call, CRT invalid parameter, /GS) reach the
kernel via __fastfail and never hit the vectored exception handler, so crashes
in that class produced no q4-crash.txt record. Install _set_purecall_handler and
_set_invalid_parameter_handler that dump the same stack-scan pseudo-callstack
from the current frame before the CRT tears down. This pinpointed the post-spawn
crash as a pure idWinVar::x() call inside idWindow::EvaluateRegisters while the
engine parses Quake4's guis/mpmain.gui.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…P_TYPE_VARFM

idWindow::CleanUp() deleted definedVars, children and the idSimpleWindow pool
but left every list that holds raw pointers INTO those objects: ops (op->a ->
winvar), updateVars (simple windows register &text/&rect/... into the parent's
list, Winvar.cpp registers vars into their window), and transitions (data ->
winvar). Quake4 guis (mpmain.gui) trip the Doom3 parser into re-entering
idWindow::Parse on the same desktop; Parse(rebuild) -> CleanUp() then freed the
winvars while the stale lists survived, so EvaluateRegisters pure-called
idWinVar::x() and UpdateWinVars() pure-called idWinVar::Update() (0xC0000409,
vptr left at the abstract base by the MSVC dtor chain). Clear ops,
expressionRegisters, updateVars and transitions in CleanUp, mirroring the
teardown UpdateFromDictionary already does.

Also port the Quake4Doom fork's WOP_TYPE_VARFM opcode: ParseTerm previously
dropped directly-resolved idWinFloatMember component vars ("matcolor_w") with a
"Var expression not vec4, float or int" warning; now they emit VARFM ops,
EvaluateRegisters evaluates them through the member-safe case, and FixupParms
re-tags deferred names that resolve to members.

With this mp/q4dm1 completes the FULL map load: InitFromNewMap returns, the
player spawns, and 10 settle RunFrames execute (Q4trace 8-12) - 2221 msec load.
Next frontier: an AV in the game DLL after GenerateAllInteractions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in-game render

The retail gamex86.dll drives per-entity sound through idSoundEmitter pointers
using the Quake4 v37 vtable: UpdateEmitter@1(origin, velocity, listenerId,
parms), StartSound@2(shader, channel, diversity, flags), ...,
CurrentlyPlaying@6(channel), CurrentAmplitude@7(flags, factorDistance),
AttachedToWorld@8, Handle@9. Doom3's layout had Free(immediate)@1, so every
game UpdateEmitter call FREED the emitter and later slots smeared into
neighbors, corrupting the emitter list until AllocSoundEmitter walked freed
memory (AV EXEC 0 / AV READ garbage right after map load -- diagnosed by
disassembling the retail DLL: sub_20028DF0 calls soundSystem[slot28]=
EmitterForIndex then emitter[slot1]=UpdateEmitter with 4 args).

Rebuild the vtable in v37 order and move the engine-internal Free/Index to
appended slots past the game-visible block. Also align soundShaderParms_t to
the Q4 40-byte layout (attenuatedVolume after volume; frequencyShift/wetLevel/
dryLevel appended) since the game builds and reads these structs across the
boundary; propagate the new fields in OverrideParms and zero them on the
demo-read paths. Add out-of-range guard + trace to the system EmitterForIndex.

RESULT: mp/q4dm1 now loads, spawns, renders in-game and plays weapon sounds
with the retail Quake 4 game DLL. Known issues: mostly-black screen (lighting/
material path) and garbled movement input (usercmd_t ABI -- next port).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The engine hands usercmd_t straight into the retail game DLL, which reads it
with the Quake4 v37 layout: realTime (int) inserted after gameTime and buttons
widened byte -> short. With the D3 32-byte layout the game read forwardmove
from the middle of angles[] (mouse motion became walking) and impulse from
uninitialized padding (weapons cycled on their own).

Align the struct, add the Q4 button bits (INGAMESTATS/VOICECHAT/TOURNEY/
STRAFE@BIT8), fill realTime in InitCurrent, swap buttons as a short in
ByteSwap, and move the buttons wire format to short in the usercmd delta
read/write (both endpoints are this engine, so the format stays consistent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s 115 -> 1

Doom3's parsers abort the whole decl (MF_DEFAULTED / parse-fail) on any unknown
keyword, which blacked out most Q4 surfaces and dropped weapon sounds:

- Material.cpp ParseMaterial: accept materialType <type>, materialImage <path>
  (consumed; no renderer use without the BSE effect system), projectileClip and
  needCurrentRender (flags). ParseStage/image-flags: accept 'nomips' as an alias
  of nopicmip (Q4 spells it differently; 45 weapon/character skins defaulted on
  it -- the black weapon models).
- Image_program.cpp: implement Q4's downsize( image, factor ) image program by
  loading the inner image at full resolution (the factor is a texture-memory
  hint). Fixes the "unknown token '('" cascade in every material using it.
- snd_shader.cpp / sound.h: add the Q4 SSF_ bits (USEDOPPLER..HILITE) and accept
  frequentlyUsed, voForPlayer, no_shakes, frequencyshift <min>,<max> and
  noRandomStart. "Sound 'weapon_nailgun_fire' not found" class of misses: gone
  (30 -> 0).
- DeclManager.cpp PreprocessGuides: fix the inlineGuide branch. In retail .mtr
  files inlineGuide is an INVOCATION inside a material body (the definitions
  live in guides/*.guide, already collected by ParseGuides); the old code
  assumed a definition and SkipBracedSection()'d, eating the enclosing
  material's closing brace and silently dropping the REST OF THE FILE
  (characters.mtr lost ~1150 of ~1200 lines; same in monsters/vehicles/
  hardwareshaders/test.mtr). Now the template body is expanded in place with
  its outer braces stripped and parms substituted.

Unknown-keyword warnings on mp/q4dm1: 115 -> 1 (a lone glslProgram
displacement material). Runtime note for later: 'idCollisionModelManager
::Contents: invalid model handle' spam during play (non-fatal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…RLD RENDERS LIT

The black world (with only additive stages visible) was the ARB2 interaction
path: Quake 4's paks ship Raven-modified glprogs, and their interaction.vfp
renders black through this engine's ARB2 backend, while r_renderer arb (fixed
function) rendered the map perfectly -- which pinpointed the fragment-program
path. r_showLightCount had already proven geometry, lights and interaction
pairing were all correct.

Ship the stock DOOM3 ARB program set (from the Quake4Doom reference fork, which
solved this the same way) under base/glprogs/ and deploy them as loose files
into the savepath at build time; loose files take precedence over pak contents,
so the engine loads the D3 programs it was written against.

VERIFIED by screenshot: mp/q4dm1 renders fully lit and textured at 63fps on the
default (ARB2) renderer -- walls, floor, items, weapon model, light fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quake 4 guis declare `onInit { ... }` on the desktop (and elsewhere); Doom3's
idWindow::Parse doesn't know the event, so the whole block fell through to
ParseRegEntry as garbage properties until the block's closing brace CLOSED THE
ENCLOSING WINDOWDEF. From there every remaining construct sat at top level:
InitFromFile re-entered desktop->Parse(rebuild) on nested windowDef tokens
(wiping everything parsed so far -- mpmain.gui rebuilt its desktop 5x) and
hud.gui's desktop lost its name/vars/children, which is why every
"window::var" transition destination resolved to nothing (794 warnings) and
the HUD stayed invisible.

Add the Q4 window events to the ON_ enum + ScriptNames (onTabRelease,
onGainFocus, onLoseFocus, onSelChange, onInit), add idWindow::Init() (runs
ON_INIT depth-first, as in the Quake4Doom fork) called from InitFromFile after
FixupParms, and keep g_q4Trace on past game->Init so the [Q4*] diagnostics
cover map load. Diagnostic prints for cross-window var failures ([Q4gui]/
[Q4gui2]) and top-level desync detection ([Q4gui5]) stay, gated on the flag.

Transition warnings on mp/q4dm1: 794 -> 0; gui re-parses: 6 -> 0.
VERIFIED by screenshot: the in-game HUD draws (crosshair, weapon/ammo panel,
health 100 panel) over the fully lit world at 63fps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dlib)

Standing direction: reconstruct Quake 4's source tree inside neo/ and work from
it directly, rather than keeping the SDK as untracked reference. Retail binary
strings pin Raven's real layout: the source root was CodePC/ (neo/'s role),
with the game at CodePC/game (built as SPGamex86.dll -- the retail
q4base/gamex86.dll) and framework/async/... matching id's layout, so our
in-place engine reconstruction under neo/<subsystem> already lives at the
faithful paths. The game-side trees land as q4game/, q4mpgame/ and q4idlib/
(exact internal structure; the original names game/ and mpgame/ are taken by
the DOOM 3 code). Evidence documented in neo/q4game/CODEPC-LAYOUT.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two user-reported gameplay issues:

- Ambient/map sounds played at full volume everywhere: Quake4 sound shaders
  express minDistance/maxDistance in GAME UNITS (393.7 units = 10 m), while
  Doom3's mixer compares them against listener distances in METERS -- so dlen
  (a few meters) never exceeded minDistance (hundreds) and no attenuation ever
  applied. Convert at the read sites: the emitter's maxDistance member ingest
  in Spatialize() and the two falloff computations in snd_world (the s_drawSounds
  debug text keeps printing raw shader values).

- Movement felt much slower than real Q4 MP: Doom3 only set BUTTON_RUN when
  in_alwaysRun was on AND an async network game was active (SP stamina rule),
  so local play always walked. Quake4 multiplayer always runs; drop the async
  gate and set in_alwaysRun 1 in the q4 run scripts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous always-run fix touched InitCurrent, but MakeCurrent->CmdButtons()
zeroes cmd.buttons and rebuilds them, applying its own
`in_alwaysRun && idAsyncNetwork::IsActive()` gate -- so local play still never
ran. Drop the async gate there (and on toggled_run's key state) so
in_alwaysRun/run-key work locally like Quake4 MP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…orld

The game addresses sound worlds by fixed id (SOUNDWORLD_GAME=1,
SOUNDWORLD_MENU=2) no matter which world is currently playing; our shims used
currentSoundWorld, so whenever the menu/loading world was current, game-world
emitter handles resolved against the wrong emitter list ([Q4snd] out-of-range
console flood, misrouted sounds). Register worlds by creation order in
AllocSoundWorld (session allocates game then menu, matching the Q4 ids),
resolve via WorldFromId() in the worldId-taking shims (StopAllSounds,
Alloc/FreeSoundEmitter, EmitterForIndex, CurrentShakeAmplitudeForPosition,
FadeSoundClasses, PlayShaderDirectly), deregister in ~idSoundWorldLocal, and
drop the flooding diagnostics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
idClipModel::GetCollisionModel() returns NULL for render-model-backed clip
models, and the game feeds that straight into collisionModelManager->Contents/
Translation/Rotation/Contacts meaning "nothing to collide with". Our v37
wrappers converted NULL to handle -1 and Doom3's cores printed "invalid model
handle" for every query (console flood during play). Return the empty result
silently from the wrappers instead: Contents/Contacts 0, Translation/Rotation
a fraction-1 no-hit trace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the fs

- The in-game volume slider did nothing: Quake4's settings gui drives cvar
  "s_volume" as a linear 0..2 scale (step 0.1), while Doom3's master volume
  cvar was "s_volume_dB" in decibels. Rename ours to s_volume (default 1) and
  apply it as a clamped linear scale in the mixer.

- Stop deploying the retail SP gamex86.dll beside DOOM3.exe: FindDLL prefers
  the exe dir, which pinned the singleplayer game even under fs_game q4mp.
  Let the filesystem extract gamex86.dll from the active game dir's paks like
  retail does -- with fs_game q4mp the MULTIPLAYER game DLL (Raven's MPGame
  build, own class layouts / gauntlet start / MP physics) now loads, accepts
  spawnServer (isServer=true), spawns all entities and connects client 0.
  (It then hits a null-member AV in actor spawn -- next frontier, under
  investigation.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ER RUNS

Loading Raven's MPGame DLL (fs_game q4mp) as a listen server got through map
load and client connect, then died in two engine-owned gaps the SP game never
exercised:

1. "idDeclManager::FindTypeWithoutParsing: bad type: 18" -- DECL_PLAYER_MODEL.
   The retail ENGINE registers rvDeclPlayerModel (SDK framework/
   DeclPlayerModel.h); the MP game queries it on every player spawn
   (idPlayer::UpdateModelSetup) and dereferences the decl's DATA MEMBERS
   directly, so the reconstruction (neo/framework/DeclPlayerModel.{h,cpp})
   mirrors the SDK field layout exactly and parses the quoted key/value decl
   format from def/player.def (model/def_head/def_head_ui/headOffset/team/
   skin/description + snd_* into the sounds dict).
   (Diagnosed via live capture of commonLocal.errorMessage: the visible AV at
   gamex86+0x5899 was a SECONDARY crash -- idCommonLocal::Error stops the
   session before printing, the server drop path fired addChatLine, and
   GetLocalPlayer()->StartSound ran on the half-spawned player whose
   idEntity::physics was still NULL.)

2. "WriteDecl: effect decl effects/mp/player/spawn ( index 37 ) is implicit" --
   the MP server refuses to write implicit (non-file-backed) decls to the
   network stream. The retail BSE effects system registers the effects folder;
   register it in the decl manager (effects/*.fx, top-level 'effect <name>'
   blocks) so effect decls index as file-backed.

RESULT (verified by screenshot): mp/q4dm1 as a DM listen server reaches WARMUP
-- "WAITING ON ONE MORE PLAYER", scoreboard with the connected player,
machinegun MP loadout, MP HUD, 63fps. Added .vscode/run-q4-mp.ps1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant