Skip to content

Fix six non-macOS bug reports (#78, #79, #82, #87, #88, #89)#93

Merged
themuffinator merged 1 commit into
mainfrom
fix/non-macos-bug-sweep
Jul 26, 2026
Merged

Fix six non-macOS bug reports (#78, #79, #82, #87, #88, #89)#93
themuffinator merged 1 commit into
mainfrom
fix/non-macos-bug-sweep

Conversation

@themuffinator

@themuffinator themuffinator commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Root-causes and fixes the six open bug issues that are not macOS-specific.

Each issue was investigated against the decompiled retail engine, the 1.4.2 SDK and the retail PK4s, then adversarially re-verified before anything was written. Where a root cause could not be established, that is called out below rather than papered over.

What changed

#79 — engine "crashes" on launch (Linux Mint)

Not a crash. idCommonLocal::Error's recursive-fatal branch called Sys_Quit(), which reports EXIT_SUCCESS and prints nothing — so the real message never reached the log, the log simply stopped mid-shutdown, and the shell saw a clean exit. Both recursive-fatal branches now report and exit through the fatal path.

What tripped it: idVertexCache::PurgeAll ran before Init() linked its list sentinels and handed ActuallyFree() a NULL block, raising a second error while the first was still unwinding. Both entry points now guard.

The glConfig.isInitialized guard at the call site is removed — it is set ~28 lines before vertexCache.Init() runs, and ShutdownOpenGL() clears it without freeing the cache, so it was wrong in both directions (it did not close the hole, and it leaked after a vid_restart teardown). The guard now lives inside idVertexCache, keyed off the sentinels.

The four startup content checks now print resolved fs_basepath/fs_savepath/fs_cdpath and every pk4 actually found in q4base and baseoq4 — which is what surfaces the dominant Linux cause, a case-mismatched directory or pk4 name copied from a Windows install.

#89a — accented characters render as two wrong glyphs

The string tables are UTF-8; the stock fonts are a fixed 256-glyph atlas indexed by a raw byte. MENÚS drew as MENA S. idLangDict::Load now transcodes a UTF-8 table to the font codepage, folding typographic characters that land on a zero-advance .notdef cell to ASCII — a non-breaking space would otherwise delete the word gap rather than merely draw nothing.

CP1252 and pure-ASCII tables are left byte-identical, so retail localized packs and mod tables are unaffected. Verified against the retail archives: the re-encoded english_lips.lang is byte-identical to the zpak_english.pk4 entry (275049 bytes), as is french_mappack.lang against pak013.pk4. This also fixes mojibake apostrophes in English subtitles.

#82 — BC7 / image_usePrecompressedTextures not working

ImageTools_SetCompressionCaps() had exactly one caller, and imagetools is a static library linked separately into the engine and into each renderer module — so depending on the active renderer the capability block stayed zero-initialized and every DDS replacement was silently rejected. Caps are now published from both backends and mirrored into the engine's copy.

Also: user replacements are probed ahead of the retail progimg/ tree instead of behind it (previously a dds/ pack was a no-op for every image-program image); the ultra preset no longer zeroes the cvar and discards an archived user setting; and a miss now logs every candidate path with the reason each was rejected, so a pack author can see what a file must be named.

#87 — no character/NPC voice audio

Not root-caused. The leading hypothesis (localized VO path) was refuted during verification: sys_lang is constrained to the languages that actually ship a zpak_<lang> pack, so it cannot be non-English on a stock install. What is fixed here are the confirmed defects on that path — a sample that resolved to nothing was replaced by the default with no warning at all, voice-over had no English fallback for a text-only language pack (retail has one), and s_showVoices built a table every frame and discarded it, producing no output.

#88 — scripted NPC never moves

Found a real defect: idAASFileLocal::FinishAreas never derived aasArea_t::ceiling, so AreaCeiling() returned 0 for every area on every map. Consumers compute AreaCeiling(area) - boundsHeight, so every flying goal was clamped to a below-ground altitude. It is now derived exactly as the original engine does, including the degenerate areas.Num() < 2 guard.

Movement failures were also silent, and the two failure modes are different: with no AAS, MoveToEntity() still returns true and the AI wanders on StepDirection, so the map script waits on scriptedDone() forever. Both now warn, and ai_scriptedMoveTimeout abandons a move that has made no forward progress so the script can continue.

#89b — loading a save without quitting first leaves stale state

UnloadMap() did not stop the game sound world before destroying the entities that own its emitters. ~idEntity frees an emitter without stopping it, and a freed emitter is only recycled once its channel list empties, so every looping sound from the outgoing session survived. Stop() already does this, which is exactly why quitting to the menu first was clean and loading in place was not. Render defs are also released here, matching the original teardown order.

#78 — arm64 savegame / map load

The savegame root cause (ReadInt( (int &)size ) against an LP64 size_t) is already fixed in tree; a regression guard now pins it and sweeps for the same class. Beyond that: the binary lexer's stored-size default cases fail closed instead of asserting — release builds compile asserts out, leaving unreadSize at 0 and desynchronizing the rest of the declaration stream — an unexpected BC7 image degrades to uncompressed instead of ending the session mid-load, and -ffp-contract=off keeps ARM64 and x86-64 evaluating the same float math.

Reviewer notes

Two deliberate behaviour changes:

  1. BC7 normal maps changed convention. They were being forced into the DXT5nm/RXGB alpha layout, which is why the docs told users to encode with bc7enc -r2a. That was a workaround for the bug — BC7 has no such convention and every common encoder writes plain XYZ in RGB. Packs previously encoded with -r2a for openQ4 need re-encoding. Documented in docs/user/texture-replacements.md. Happy to preserve the old layout instead if you would rather.
  2. ai_scriptedMoveTimeout defaults to 30s. A scripted move with no progress for 30s is abandoned so the script continues. Strictly better than a permanent softlock, and the warning names the AI, its target and why it was stuck — but it is a gameplay behaviour change. Set it to 0 for strict retail behaviour.

Companion change already merged. The AI/AAS reporting, the idlib string-table mirror and the matching build flag live in openQ4-game and are on that repo's main as bd7dbf7 — CI clones openQ4-game main, so this branch is validated against it. The engine half also builds and passes without it; the #88 fixes need both.

Not auto-closing #78, #87 or #88. #87 has no confirmed root cause, #88's specific map was never identified (this was worked from code, not the screenshot), and #78's original defect predates this branch. Suggested follow-up for #87: ask the reporter to set s_warnOnMissingSamples 1 and say whether they hear silence or a short buzz — a buzz confirms the sample path, silence rules it out entirely.

Verification

  • Both repos build clean (MSVC x64, engine plus SP/MP game modules).
  • Full tools/tests sweep shows no regressions: the 14 failures are identical to a clean HEAD worktree baseline (SDL3 backend pins referencing removed functions, plus tests requiring native Linux/macOS). One pin this branch invalidated — the vertex-cache shutdown guard — was updated to the new contract.
  • Runtime smoke: game/airdefense1 loaded and ran 75s with no new errors or warnings. The log confirms the Good Performace on Windows Poor on Cachy OS? #89 fix firing live: strings/english_lips.lang: converted UTF-8 string table to the 8-bit font codepage.
  • New CI tests: lang_table_encoding.py (string tables stay UTF-8 and CP1252-representable; the transcode stays wired) and savegame_pointer_width_safety.py (no fixed-width savegame accessor bound to a wider reference).

🤖 Generated with Claude Code

Addresses GitHub issues #78, #79, #82, #87, #88 and #89.

Startup failures exited silently (#79). idCommonLocal::Error's recursive
fatal branch called Sys_Quit(), which reports EXIT_SUCCESS and prints
nothing, so the original message never reached the log and the shell saw
a clean exit. What tripped it: idVertexCache::PurgeAll ran before Init()
linked its list sentinels and handed ActuallyFree() a NULL block, raising
a second error while the first was still unwinding. Both are fixed, and
the glConfig.isInitialized guard at the call site is gone - it was set
before vertexCache.Init() ran and cleared by ShutdownOpenGL() without
freeing the cache, so it was wrong in both directions. The four startup
content checks now print the resolved paths and the pk4 files actually
found, which is what identifies the common Linux cause of a
case-mismatched directory or pk4 name copied from a Windows install.

Accented text rendered as two wrong glyphs (#89). The string tables are
UTF-8 while the stock fonts are a fixed 256-glyph atlas indexed by a raw
byte, so "MENUS" drew as "MENA S". idLangDict::Load now converts a UTF-8
table to the font codepage, folding the typographic characters that land
on a zero-advance .notdef cell to ASCII - a non-breaking space would
otherwise delete the word gap rather than merely draw nothing. CP1252 and
ASCII tables are left byte-identical, so retail localized packs are
unaffected. This also fixes mojibake apostrophes in English subtitles.

DDS replacements were rejected without explanation (#82).
ImageTools_SetCompressionCaps had a single caller, and imagetools is a
static library linked separately into the engine and each renderer
module, so depending on the active renderer the capability block stayed
zero-initialized and every DDS was refused. Caps are now published from
both backends and mirrored into the engine's copy. User replacements are
probed ahead of the retail progimg/ tree instead of behind it, BC7 normal
maps use the ordinary XYZ-in-RGB layout rather than being forced into the
DXT5nm alpha convention, the ultra preset no longer zeroes the cvar and
discards an archived setting, and a miss now logs every candidate path
with the reason it was rejected.

Sound lookup failures were invisible (#87). A sample that resolved to
nothing was replaced by the default with no warning. The engine now
reports the sample and every path probed, voice-over falls back to
English when a language ships text but no voice track as the original
game did, and s_showVoices prints its table instead of building one every
frame and discarding it. The reported symptom is not yet root-caused;
these are the confirmed defects on that path.

Scripted characters could stall a map permanently (#88).
idAASFileLocal::FinishAreas never derived aasArea_t::ceiling, so
AreaCeiling() returned 0 for every area on every map and consumers
computing "ceiling - bounds height" clamped every flying goal below
ground. Movement failures were also silent: with no AAS, MoveToEntity()
still returns true and the AI wanders, so the map script waits on
scriptedDone() forever. Both failure modes now warn, and
ai_scriptedMoveTimeout (30s, 0 to disable) abandons a move that has made
no progress so the script can continue.

Loading a savegame over a live map left stale state (#89). UnloadMap()
did not stop the game sound world before destroying the entities that own
its emitters, and a freed emitter is only recycled once its channel list
empties, so looping sounds from the outgoing session survived. Stop()
already did this, which is why quitting to the menu first was clean.

Hardened the arm64 paths behind #78. The savegame root cause is already
fixed; a regression guard now pins it. The binary lexer's stored-size
defaults fail closed instead of asserting - release builds compile
asserts out, leaving unreadSize at 0 and desynchronizing the rest of the
stream - an unexpected BC7 image degrades to uncompressed instead of
ending the session mid-load, and -ffp-contract=off keeps ARM64 and
x86-64 evaluating the same float math.

Requires the companion change in openQ4-game for the AI and AAS
reporting, the idlib string-table mirror, and the matching build flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@themuffinator
themuffinator merged commit f5ee7dc into main Jul 26, 2026
10 of 12 checks passed
@themuffinator
themuffinator deleted the fix/non-macos-bug-sweep branch July 26, 2026 05:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f6d35c10e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/renderer/OpenGL/gl_Image.cpp
Comment thread src/sound/snd_world.cpp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant