Fix six non-macOS bug reports (#78, #79, #82, #87, #88, #89)#93
Merged
Conversation
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>
There was a problem hiding this comment.
💡 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".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root-causes and fixes the six open
bugissues 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 calledSys_Quit(), which reportsEXIT_SUCCESSand 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::PurgeAllran beforeInit()linked its list sentinels and handedActuallyFree()aNULLblock, raising a second error while the first was still unwinding. Both entry points now guard.The
glConfig.isInitializedguard at the call site is removed — it is set ~28 lines beforevertexCache.Init()runs, andShutdownOpenGL()clears it without freeing the cache, so it was wrong in both directions (it did not close the hole, and it leaked after avid_restartteardown). The guard now lives insideidVertexCache, keyed off the sentinels.The four startup content checks now print resolved
fs_basepath/fs_savepath/fs_cdpathand every pk4 actually found inq4baseandbaseoq4— 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ÚSdrew asMENA S.idLangDict::Loadnow transcodes a UTF-8 table to the font codepage, folding typographic characters that land on a zero-advance.notdefcell 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.langis byte-identical to thezpak_english.pk4entry (275049 bytes), as isfrench_mappack.langagainstpak013.pk4. This also fixes mojibake apostrophes in English subtitles.#82 — BC7 /
image_usePrecompressedTexturesnot workingImageTools_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 adds/pack was a no-op for every image-program image); theultrapreset 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_langis constrained to the languages that actually ship azpak_<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), ands_showVoicesbuilt a table every frame and discarded it, producing no output.#88 — scripted NPC never moves
Found a real defect:
idAASFileLocal::FinishAreasnever derivedaasArea_t::ceiling, soAreaCeiling()returned 0 for every area on every map. Consumers computeAreaCeiling(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 degenerateareas.Num() < 2guard.Movement failures were also silent, and the two failure modes are different: with no AAS,
MoveToEntity()still returns true and the AI wanders onStepDirection, so the map script waits onscriptedDone()forever. Both now warn, andai_scriptedMoveTimeoutabandons 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.~idEntityfrees 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 LP64size_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-sizedefaultcases fail closed instead of asserting — release builds compile asserts out, leavingunreadSizeat 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=offkeeps ARM64 and x86-64 evaluating the same float math.Reviewer notes
Two deliberate behaviour changes:
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-r2afor openQ4 need re-encoding. Documented indocs/user/texture-replacements.md. Happy to preserve the old layout instead if you would rather.ai_scriptedMoveTimeoutdefaults 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 to0for strict retail behaviour.Companion change already merged. The AI/AAS reporting, the idlib string-table mirror and the matching build flag live in
openQ4-gameand are on that repo'smainasbd7dbf7— CI clonesopenQ4-gamemain, 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 1and say whether they hear silence or a short buzz — a buzz confirms the sample path, silence rules it out entirely.Verification
tools/testssweep shows no regressions: the 14 failures are identical to a cleanHEADworktree 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.game/airdefense1loaded 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.lang_table_encoding.py(string tables stay UTF-8 and CP1252-representable; the transcode stays wired) andsavegame_pointer_width_safety.py(no fixed-width savegame accessor bound to a wider reference).🤖 Generated with Claude Code