Development Diary - April 2026¶
2026-04-28: Switch SDL cursor pipeline to SDL_CreateAnimatedCursor¶
Replaced the SDL cursor frame-by-frame selection path with SDL native animated cursors to ensure ANI cursor playback is handled by SDL runtime.
What was done:
- created branch feat/sdl-animated-cursor from main.
- updated both Zero Hour and Generals SDL3 mouse implementations to create one animated SDL_Cursor per cursor/direction using SDL_CreateAnimatedCursor.
- removed per-frame SDL_SetCursor animation stepping logic and dropped obsolete m_inputFrame state from SDL3 mouse headers.
- preserved fallback behavior to NORMAL/default SDL cursor when animated resources are unavailable.
Validation:
- macOS [Platform] Build GeneralsXZH completed successfully after the change.
- no new blocking compile errors introduced by the cursor migration.
2026-04-28: Harden macOS SDL fullscreen transition against intermittent pseudo-borderless state¶
Addressed a new intermittent macOS fullscreen failure where the game occasionally stayed in a borderless-looking state with Dock/menu bar visible and input focus/mouse interaction degraded.
What was done:
- created branch fix/macos-fullscreen-focus-lock from main for isolated fix.
- updated SDL fullscreen transition helper in both Zero Hour and Generals W3DDisplay.cpp paths.
- added a short fullscreen-state verification/retry loop after requesting fullscreen to handle asynchronous SDL/macOS transition timing.
- raised the game window after fullscreen transition to improve foreground/focus consistency.
- removed temporary fullscreen debug instrumentation remnants from the previous session in active game paths.
Validation:
- macOS GeneralsXZH build completed successfully after the change.
- macOS deploy completed successfully to ~/GeneralsX/GeneralsZH/ for manual runtime validation.
2026-04-28: Finalize upstream sync branch, unify platform tasks, and standardize deploy UX¶
Completed the final adjustments on thesuperhackers-sync-04-28-2026 before commit/push/PR.
What was done:
- standardized post-deploy run instructions for Linux and macOS deploy scripts.
- removed legacy Zero Hour fallback run hint (~/GeneralsX/GeneralsMD) from deploy output.
- aligned Linux deploy summary block labels/order with macOS deploy summary format.
- unified VS Code tasks for configure/build/deploy and build+deploy pipelines using platform-specific linux / osx task overrides.
- updated unified pipeline tasks to exclude runtime launch so run remains manual.
Validation:
- shell syntax checks passed for updated deploy scripts (bash -n).
- tasks.json validated as proper JSON after task unification.
2026-04-27: Fix macOS user custom map loading and harden special-character handling¶
Addressed the user-reported issue where custom maps in ~/Library/Application Support/GeneralsX/GeneralsZH/Maps were not being discovered reliably on macOS, including maps with custom assets/string files.
What was done:
- created branch fix/macos-user-custom-maps-loading for isolated investigation/fix.
- fixed map scanning in shared Core path by accepting both \\ and / separators when parsing map file paths and validating map folder layout.
- fixed map display-name extraction paths to handle both separators in map cache/runtime code paths.
- fixed map map.str path derivation to build from the real map file path instead of mixed mapDir + fname composition.
- patched INIMapCache.cpp in both GeneralsMD and Generals so cache parsing of map names without lookup tags also accepts both separators.
- hardened QuotedPrintable ASCII encoding in both GeneralsMD and Generals to use locale-independent ASCII checks (isAsciiAlphaNumeric) for predictable handling of punctuation/special characters.
Stress/validation notes:
- confirmed custom map load success in runtime logs for user-path maps (including accented-name case).
- executed stress dataset generation with 14 map directory names covering spaces, quotes, punctuation, and accented Unicode characters; no new crash/regression observed in successful run (Exiting with code 0).
- cleaned temporary GX_STRESS_* map directories from user data after testing.
Files touched:
- Core/GameEngine/Source/GameClient/MapUtil.cpp
- GeneralsMD/Code/GameEngine/Source/Common/INI/INIMapCache.cpp
- Generals/Code/GameEngine/Source/Common/INI/INIMapCache.cpp
- GeneralsMD/Code/GameEngine/Source/Common/System/QuotedPrintable.cpp
- Generals/Code/GameEngine/Source/Common/System/QuotedPrintable.cpp
2026-04-27: Fix macOS SDL fullscreen sizing after pillarbox regression¶
Tracked a macOS-only fullscreen regression introduced after the pillarbox/ultrawide merge where fullscreen could render as a blurred upscaled image or as a magnified top-left viewport.
What was done:
- created branch fix/macos-fullscreen-scaling from main for isolated investigation and fixes.
- traced the regression to SDL3 fullscreen timing plus DXVK backbuffer sizing on non-Windows builds.
- changed shared dx8wrapper.cpp logic so non-Windows fullscreen uses the current SDL window pixel size for the initial/reset backbuffer, with native display size only as fallback.
- enabled window resize on the SDL path before device creation/reset so the SDL window dimensions match the selected game resolution.
- delayed SDL native fullscreen application until after the render device finishes applying the final resolution, and mirrored the same fix in both Zero Hour and Generals W3DDisplay.cpp paths.
- documented the lesson in docs/WORKDIR/lessons/2026-04-LESSONS.md.
Validation:
- static diagnostics for the edited display/docs files passed.
- macOS GeneralsXZH build completed successfully after the fullscreen ordering fixes.
- runtime visual confirmation passed on the affected macOS machine after the final backbuffer/pillarbox sync adjustment.
- temporary [GX-FSDBG] instrumentation was added for diagnosis and removed after confirming the fix.
2026-04-27: Restore narrator speech after briefing-video audio fix¶
Investigated the regression reported after commit c0ce9f0, where briefing and victory narrator speech stopped playing even though campaign videos regained audio.
Root cause:
- the OpenAL fix in OpenALAudioStream::update() was correct for video streams that already queue audio before calling update(), but generic AT_Streaming speech follows a different lifecycle.
- narrator speech streams entered processPlayingList() with no queued buffers, filled the queue during update(), but were not restarted again in the same frame.
- the manager then still saw the source as stopped and released it immediately, so mission intro narration and victory speech never got a chance to begin.
Fix:
- added a post-refill restart check in OpenALAudioStream::update() so a stream that buffers data during the current update cycle is started before processPlayingList() evaluates it for teardown.
- kept the existing pre-unqueue restart logic intact so the briefing-video audio fix from issue #38 remains preserved.
Validation:
- code inspection confirms the fix is isolated to shared stream lifecycle logic and does not alter the FFmpeg video-specific conversion/reset path introduced by c0ce9f0.
2026-04-24: Revise generalsx.instructions.md to reflect active release state¶
Updated the main AI agent instructions file to remove stale phase/planning artifacts and align with the current cross-platform release workflow.
What was changed:
- Removed the "Modernization Phases" section (phases 0-4 with acceptance checklists) -- project now has active releases, phase framing no longer applies.
- Replaced legacy build commands with correct script paths under scripts/build/, scripts/env/, and scripts/qa/.
- Updated logging convention: removed phase<N>_ prefix from log file names.
- Added concrete VS Code task labels to the build workflow section.
- Corrected Linux section: fixIncludesCase.sh now points to scripts/tooling/cpp/maintenance/.
- Updated macOS status: removed "Phase 1 complete" wording, now reflects active release branch.
- Rewrote Windows section: focus on MinGW-w64 as primary toolchain per issue #29; removed MSVC/TBD ambiguity; branch/status remain TBD/Not yet started as it is a backlog item.
- Updated build preset descriptions: vc6 and win32 are now labelled as maintenance-only.
- Updated Strategy item 7: "Separate branches per platform" replaced with feature-branch wording.
- Removed "(create if missing)" note for docs/ETC/COMMAND_LINE_PARAMETERS.md -- file exists.
- Removed reference to non-existent docs/WORKDIR/phases/ subdirectory from directory structure.
2026-04-23: Fix Linux Docker configure/build cache mismatch handling¶
Committed the Linux Docker script hardening to prevent host/container CMake cache collisions and to fail correctly when configure errors happen inside piped logging.
What was done:
- switched Linux Docker scripts to set -euo pipefail so failures are not masked by tee pipelines.
- added stale-cache detection for build/<preset>/CMakeCache.txt inside container runs.
- when cache origin paths do not match /work mount, scripts now remove build/<preset> and reconfigure cleanly.
- kept behavior aligned across:
- docker-configure-linux.sh
- docker-build-linux-zh.sh
- docker-build-linux-generals.sh
Validation: - shell syntax checks passed for all updated scripts. - Docker configure path now reports incompatible cache cleanup and continues correctly instead of stopping with source/cache mismatch.
2026-04-23: Fix Linux campaign crash in shroud sampling during river-water rendering¶
Closed a Linux campaign crash where entering mission scenes could hit SIGSEGV in W3DShroud::getShroudLevel() from the river-water diffuse path.
What was done:
- added defensive guards in both Zero Hour and Generals W3DShroud::getShroudLevel(Int x, Int y):
- return early when source texture/data pointers are unavailable.
- return early when sampled cell coordinates are negative.
- kept existing upper-bound behavior and shading math unchanged.
Files touched:
- GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShroud.cpp
- Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShroud.cpp
Validation:
- Linux Docker build for GeneralsXZH succeeded after patch.
- campaign launch no longer crashes in the previously reported shroud stack.
2026-04-23: Sync merge from TheSuperHackers main with unify-aware conflict resolution¶
Completed upstream sync on branch thesuperhackers-sync-04-23-2026 by merging thesuperhackers/main into GeneralsX while preserving cross-platform architecture.
What was done:
- merged upstream main and resolved all merge conflicts with per-file analysis (no blanket ours/theirs strategy).
- handled unify move/delete conflicts by converging to upstream Core structure while preserving GeneralsX behavior where needed:
- removed stale Generals/.../Display.h and Generals/.../System/Image.cpp duplicates in favor of unified Core versions.
- merged Generals/.../GlobalData.cpp include conflict additively to keep both cross-platform filesystem support and upstream renderer-option headers.
- resolved Core/.../render2dsentence.cpp conflict by keeping the existing single cross-platform implementation of Update_Current_Buffer (avoids duplicate definition).
- fixed upstream script relocation side effect in scripts/tooling/cpp/maintenance/remove_return.py by correcting repository-root traversal after GeneralsX script tree reorganization.
- documented full conflict plan and decision log in:
- docs/WORKDIR/planning/PLAN-20260423_THESUPERHACKERS_SYNC_CONFLICT_RESOLUTION.md
Validation:
- macOS configure: passed.
- macOS build:
- GeneralsXZH passed.
- GeneralsX passed.
- macOS deploy:
- GeneralsXZH passed.
- GeneralsX passed.
- headless replay smoke (macOS local data install): both products currently exit with SIGSEGV during early subsystem init around Radar creation; this was observed during sync validation and is tracked as runtime risk for follow-up triage.
- Linux configure/build validation intentionally deferred by user because this machine is macOS-only and Linux validation will be executed manually on a dedicated Linux machine.
2026-04-23: Backport briefing-video audio fix to Generals and remove audit instrumentation¶
Finished the issue #38 video-audio fix by carrying the stable OpenAL/FFmpeg changes from Zero Hour into the shared Generals path, then cleaned out the temporary audit probes used during triage.
What was done:
- kept the shared OpenAL stream fix that restarts before unqueueing processed buffers, preventing freshly queued briefing audio from being dropped when the source reaches AL_STOPPED, AL_INITIAL, or AL_PAUSED.
- backported the FFmpeg-side hardening into the shared/Core video path used by Generals:
- force Bink/OpenAL stream gain to 1.0f before playback start.
- reject invalid decoded audio frame sizes early.
- convert FFmpeg float audio output (FLT/FLTP) to PCM16 before queueing into OpenAL.
- kept the Zero Hour FFmpeg path aligned with the same playback and conversion behavior.
- removed all temporary [GX-AUDIT] traces and leftover cstdio includes from the touched files.
Files touched:
- Core/GameEngineDevice/Source/OpenALAudioDevice/OpenALAudioStream.cpp
- Core/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp
- GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp
- Core/GameEngine/Source/GameClient/GUI/LoadScreen.cpp
Validation:
- manual testing confirmed the fix behaves correctly in both Generals and Zero Hour.
- verified no GX-AUDIT, Temporary audit, or Runtime audit markers remain in the touched files.
- verified editor diagnostics report no errors in the modified sources.
2026-04-21 (SESSION 122): Split Linux CI packaging into explicit gzip and AppImage jobs¶
Refined the Linux workflow model so each reusable build invocation produces exactly one package format.
What was done:
- removed the combined gzip + appimage mode from build-linux.yml entirely.
- kept manual workflow dispatch limited to one explicit package choice per run (gzip or appimage).
- split top-level CI Linux packaging into two separate reusable workflow calls:
- build-linux-gzip
- build-linux-appimage
- updated CI summary and failure gates to track the two Linux packaging jobs independently.
- normalized macOS job display naming to use matrix.game.id for consistency with Linux/Flatpak job naming.
Files touched:
- .github/workflows/build-linux.yml
- .github/workflows/build-macos.yml
- .github/workflows/ci.yml
Validation:
- verified YAML diagnostics for build-linux.yml and ci.yml after the split.
- confirmed no remaining references to the removed combined Linux package mode.
2026-04-21: Implement in-game update checker (issue #51)¶
Implemented a non-blocking background update checker using libcurl and the GitHub Releases API. Active only for tagged release builds (non-dirty builds with a non-empty GitTag). Users can opt out via Options.ini with CheckForUpdates = no.
What was done¶
- Added
libcurl[ssl]tovcpkg.json(platform!windows— VC6/win32 builds unaffected) - Created
cmake/curl.cmaketo find libcurl via vcpkg - Added
SAGE_UPDATE_CHECKCMake option inconfig-build.cmake(auto-enabled whenSAGE_USE_SDL3=ON); added toCMakePresets.jsonforlinux64-deploy,macos-vulkan, and flatpak builds - Created
GeneralsMD/Code/GameEngine/Include/Common/UpdateChecker.handSource/Common/UpdateChecker.cpp: UpdateChecker::start()spawns aSDL_CreateThreadbackground thread at main menu init- Thread performs a single
CURLOPT_URLGET tohttps://api.github.com/repos/fbraz3/GeneralsX/releases/latest - Extracts
tag_nameviastrstr; compares with embeddedGitTagfromgitinfo.h - Sets
SDL_AtomicIntflags when done + update available; result is read from main thread viaUpdateChecker::poll() - Added
Bool m_checkForUpdates(defaultTRUE) toGlobalData+ INI parse table asCheckForUpdates - Modified
MainMenu.cpp(ZH) to callUpdateChecker::start()inMainMenuInit(), poll inMainMenuUpdate(), and on update available: unhide + relabelButtonGetUpdatewith latest version tag - Button click replaced
StartDownloadingPatches()withSDL_OpenURL("https://github.com/fbraz3/GeneralsX/releases")underSAGE_UPDATE_CHECKguard - All code behind
#ifdef SAGE_UPDATE_CHECK— zero impact on legacy VC6 builds
Design decisions¶
- Reused existing
ButtonGetUpdategame window button (already present inMainMenu.wnd, just hidden) — avoids OS-level dialog and integrates naturally in the game UI - Once-per-session only:
s_threadset once prevents repeated checks strstr-based JSON extraction — no external JSON library dependency- 8-second timeout + 64KB body cap on libcurl to prevent hangs or memory abuse
2026-04-20 (2): Fix video audio silenced after AL_PAUSED from macOS focus loss¶
Branch: fix/issue-38-briefing-audio-timing
Problem: After the AL_INITIAL fix, intro and campaign videos still played silently on macOS. Log analysis (previous session, 16958-line log) confirmed the OpenAL source starts in AL_PLAYING then permanently transitions to AL_PAUSED ~3 seconds into playback.
Root cause — OpenAL-Soft macOS auto-pause on focus loss:
OpenAL-Soft uses the Core Audio backend on macOS. When the application momentarily loses window focus (e.g., during shell-map → loading-screen transition — SDL_EVENT_WINDOW_FOCUS_LOST fires, Core Audio suspends the session), ALL OpenAL sources are auto-transitioned to AL_PAUSED. OpenALAudioStream::update() only restarted on AL_STOPPED and AL_INITIAL, so AL_PAUSED was never handled and the source stayed silent for the rest of the session.
Evidence from log: state transitions from 4114 (AL_PLAYING) → 4115 (AL_PAUSED) at the exact moment OSDisplaySetBusyState(false, false) was logged (called by GameLogic::setGameMode() during shell-map mode change, which triggers window event processing). 97 AL_PLAYING entries, then 710+ AL_PAUSED entries — never recovered.
Fix: Extended OpenALAudioStream::update() restart condition to also cover AL_PAUSED:
if (sourceState == AL_STOPPED || sourceState == AL_INITIAL || sourceState == AL_PAUSED) {
alSourcePlay() on a paused source resumes from the current position (OpenAL 1.1 spec §4.3.9), which is the correct behavior for continuous video audio streaming.
Also removed temporary diagnostic fprintf(stderr, ...) traces from FFmpegVideoPlayer.cpp and OpenALAudioStream.cpp that were used to identify the root cause.
Files changed:
- Core/GameEngineDevice/Source/OpenALAudioDevice/OpenALAudioStream.cpp — AL_PAUSED handling
- GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp — diagnostic cleanup
2026-04-20 (SESSION CURRENT): Fix issue #95 portable upgrade destruction on Overlord turret rotation¶
Confirmed and fixed the remaining intermittent portable-upgrade loss on Overlord after force-attack/turret rotation scenarios.
What was done:
- re-enabled focused tracing temporarily in OverlordContain to capture portable removal paths.
- validated that end-of-match removal events were normal cleanup (GameLogic::reset / clearGameData) and not gameplay regression.
- identified gameplay root cause from trace backtrace:
- TurretAIAimTurretState::update -> Object::reactToTurretChange -> OverlordContain::containReactToTransformChange
- OpenContain::redeployOccupants -> OpenContain::putObjAtNextFirePoint -> Thing::setTransformMatrix
- portable rider Object::reactToTransformChange -> GameLogic::destroyObject
- implemented OverlordContain::redeployOccupants() override to skip redeploy for KINDOF_PORTABLE_STRUCTURE riders while preserving base redeploy for non-portable occupants.
- kept portable world synchronization through syncPortablePosition().
- removed temporary trace instrumentation after validation.
Validation:
- local macOS z_generals build completed (warnings only, no new errors).
- gameplay retest reported stable behavior with Overlord upgrades during repeated turret turning/force-attack.
2026-04-20: Fix campaign video audio not playing (OpenAL AL_INITIAL state + LoadScreen loop)¶
Branch: fix/issue-38-briefing-audio-timing
Problem: Campaign briefing videos played without audio on Linux/macOS (OpenAL builds). Video image displayed correctly but no sound.
Root cause #1 — AL_INITIAL state not handled:
OpenALAudioStream::update() only restarted playback on AL_STOPPED. After reset() is called on a freshly created OpenALAudioStream, alSourceStop() is a no-op on an AL_INITIAL source, so the source stays in AL_INITIAL (not AL_STOPPED). When audio frames arrived via onFrame() and called update(), the AL_INITIAL ≠ AL_STOPPED condition skipped the play() call.
Root cause #2 — TheVideoPlayer->update() never called in blocking loop:
SinglePlayerLoadScreen::init() runs a blocking video loop that never returns to the main game loop, so TheVideoPlayer->update() (which calls FFmpegVideoStream::update() → play() if not playing) was never invoked during video playback. If the audio source underran or failed to start, there was nothing to restart it.
Fix:
- OpenALAudioStream::update(): added || sourceState == AL_INITIAL to the restart condition
- LoadScreen.cpp video loop: added explicit TheVideoPlayer->update() call after frameNext()
Branch: fix/issue-38-briefing-audio-timing
Problem: On Linux/macOS (OpenAL builds), the campaign mission briefing audio played during the mission itself instead of during the loading screen video playback. Confirmed in both Generals base game and Zero Hour.
Root cause: FFmpegVideoStream::~FFmpegVideoStream() freed FFmpeg resources (m_frame, m_swsContext, m_ffmpegFile) but did NOT stop or clear the OpenAL audio stream (m_binkAudio via getHandleForBink()). Audio buffers already queued in the OpenAL source continued draining after the stream object was destroyed, so the briefing audio played during the early frames of the mission.
Fix: Added audioStream->reset() (which calls alSourceStop() + unqueues all pending OpenAL buffers) inside FFmpegVideoStream::~FFmpegVideoStream() under #ifdef SAGE_USE_OPENAL, with null-checks for TheAudio and audioStream. Applied to both:
- Core/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp
- GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp
Note: OpenALAudioStream::reset() is already called in the constructor when a new stream opens. The destructor was the missing call site.
Related: Issue #31 (long voice lines/OpenAL reliability) may contribute to the Generals m_briefingVoice path, but is a separate problem.
2026-04-15 (SESSION CURRENT): Fix Linux build break in INI::scanType after upstream sync¶
Addressed Linux and Flatpak build failures caused by a bad merge in the shared INI parser template.
What was fixed:
- restored integer parsing declarations in scanType(std::string_view token):
- std::conditional_t<std::is_integral_v<Type>, Int64, Type> result{}
- const auto [ptr, ec] = std::from_chars(...)
Root cause:
Impact:
Validation:
2026-04-15 (SESSION CURRENT): TheSuperHackers upstream sync + macOS build unblocks for Zero Hour and Generals¶
Completed upstream sync from TheSuperHackers main into branch thesuperhackers-sync-04-15-2026 and resolved merge conflicts while preserving GeneralsX cross-platform architecture.
What was merged/resolved:
Build blockers fixed after sync:
Validation outcome:
2026-04-16 (SESSION 116): Fix issue #84 dual-signature EXC_BAD_ACCESS paths (AI guard + OpenAL cache)¶
Re-triaged issue #84 after additional reports and confirmed it contained two different crash signatures, not a single root cause.
What was changed:
- GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AITNGuard.cpp
- Generals/Code/GameEngine/Source/GameLogic/AI/AITNGuard.cpp
- hardened AITNGuardOuterState::update() with lazy m_attackState initialization when missing.
- added safe team/prototype null checks before accessing m_attackCommonTarget.
- Core/GameEngineDevice/Source/OpenALAudioDevice/OpenALAudioCache.cpp
- fixed cache eviction priority path to handle entries without AudioEventInfo (filename-only loads) instead of dereferencing null pointers.
Why:
- Crash signature A (AITNGuardOuterState::update, low address 0x38) matched missing sub-state dereference risk.
- Crash signature B (OpenALAudioFileCache::freeEnoughSpaceForSample, low address 0x3c) matched unconditional priority access on null m_eventInfo.
Validation: - diagnostics check reported no errors in all modified files. - posted technical follow-up in issue comments with explicit source anchors and retest request.
2026-04-16 (SESSION 120): CI workflow matrix consolidation by OS and Linux package format¶
Consolidated CI reusable workflow calls so Linux and macOS are grouped by OS while keeping Linux package outputs explicit.
What was done:
- refactored reusable Linux workflow to run an internal matrix over:
- game targets (GeneralsMD, Generals),
- package formats (appimage, gzip), producing both .AppImage and .tar bundles in CI.
- refactored reusable macOS workflow to run an internal matrix over game targets (GeneralsMD, Generals).
- simplified top-level ci.yml orchestration by removing duplicated per-game Linux/macOS calls and invoking one reusable workflow per OS.
- updated CI summary/failure gates to align with the consolidated job graph.
Files touched:
- .github/workflows/build-linux.yml
- .github/workflows/build-macos.yml
- .github/workflows/ci.yml
Validation:
- verified references to removed inputs (inputs.game, legacy package-format output normalization) were fully cleaned up.
- confirmed artifact naming/path logic now uses matrix metadata consistently.
2026-04-16 (SESSION 119): Replay header triage with recorder instrumentation and headless validator¶
Investigated invalid replay headers (invalid GameInfo len=0) from newly recorded local files and prepared focused diagnostics to isolate write-time vs read-time behavior.
What was done:
- added recorder instrumentation in Zero Hour to log replay start mode and slot-list serialization details:
- accepted/rejected MSG_NEW_GAME game mode,
- network/skirmish pointers used to build GameInfo,
- serialized slot-list content and byte length right before replay header write.
- fixed instrumentation length logging to use strlen(theSlotList.str()) (portable with current AsciiString implementation).
- kept logs on stderr with explicit fflush(stderr) for reliable capture in macOS/Linux runtime logs.
- documented and kept the headless replay batch validator helper under scripts QA debug tooling:
- scripts/qa/debug/validate-replays-headless.sh
- classification buckets: PLAYABLE_CRC_MISMATCH, INVALID_HEADER, INVALID_OPEN.
Validation:
- rebuilt GeneralsXZH on macOS (build/macos-vulkan) successfully after instrumentation updates.
- manual replay check still reports Replay header parse failed: invalid GameInfo (len=0) for the latest 00000000.rep, confirming issue persists and instrumentation is needed for next captured replays.
Next step: - collect one Skirmish replay and one Campaign replay with the instrumented binary, then compare write-path logs against read-path header parse behavior.
2026-04-13 (SESSION 118): macOS replay suite stabilization for real-world filenames and header parsing¶
Worked on replay automation stability in macOS headless mode to support real user filenames (spaces, symbols) and avoid parser crashes.
What was done:
- created working branch fix/macos-replay-headless-instrumentation.
- improved replay simulation diagnostics to print per-replay context in headless mode:
- replay filename,
- worker command/exit details,
- explicit replay open failures.
- fixed non-Windows worker command path formatting and added non-Windows fallback to in-process replay simulation because WorkerProcess remains Windows-only.
- fixed replay header parsing portability:
- accept absolute replay paths passed via -replay,
- harden replay string parsing to avoid stack overflow,
- read replay Unicode strings as UTF-16 (2-byte) to match Windows replay layout on macOS.
- added headless-safe guard in particle/smudge update path to prevent replay-time null dereference in render-only branches.
- removed temporary invalid replay 00000000.rep from active replay support directory and re-ran suite.
Validation summary (macOS):
- replay files with special characters in names now open and simulate.
- no more stack buffer overflow in replay header parsing.
- no replay open crash observed in current suite run.
- suite currently reaches replay simulation and reports CRC mismatches (determinism issue still open).
- one replay file (test_replay.rep) still fails to open and appears to be invalid test input.
Files touched:
- Core/GameEngine/Source/Common/ReplaySimulation.cpp
- Core/GameEngine/Source/GameClient/System/ParticleSys.cpp
- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp
- Generals/Code/GameEngine/Source/Common/Recorder.cpp
Next step: - investigate first-frame CRC divergence in replay simulation on macOS after parser/runtime stabilization.
2026-04-12 (SESSION 117): Added mismatch-path instrumentation and lobby render/prune tracing¶
Follow-up work for #86 after direct-connect began working but cross-platform game start still hit the classic incompatibility path.
What was done:
- added stderr [LAN86] instrumentation for game-start mismatch diagnosis in:
- local CRC validation enable path,
- per-player cached CRC capture,
- CRC quorum failure and CRC mismatch summary output,
- forced game-start timeout send/receive path,
- mismatch UI activation (setSawCRCMismatch).
- added lobby-side stderr tracing for:
- rendered LAN game rows in the listbox,
- final list refresh count,
- timed-out game pruning from the LAN game list.
Current read on the lobby path:
- no additional compatibility filter was found in the LAN lobby UI callback itself.
- the visible list is driven directly from m_games; the useful remaining suspects are parse/state correctness of LANGameInfo and aggressive timeout pruning.
Files touched:
- GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
- GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp
- Core/GameEngine/Source/GameNetwork/Network.cpp
- Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp
- Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp
- Core/GameEngine/Source/GameNetwork/LANAPI.cpp
Validation: - pending static diagnostics after edit.
2026-04-11 (SESSION 116): Issue #86 branch + LAN diagnostics instrumentation (Linux/macOS)¶
Started work for #86 to investigate LAN discovery failures across Linux and macOS.
What was done:
- created working branch issue-86-lan-discovery-instrumentation.
- added LAN startup diagnostics in lobby flow to log:
- whether IP came from auto-selection or configured default,
- selected local bind IP,
- SetLocalIP success/failure,
- initial RequestLocations() dispatch.
- changed critical LAN diagnostics from DEBUG_LOG-only to explicit fprintf(stderr, ...) probes with [LAN86] prefix because manual -logToCon captures did not reliably surface the new traces.
- expanded reception-side instrumentation to log:
- enumerated LAN IP candidates,
- preferred-vs-fallback LAN IP resolution,
- received LAN message type/source/length,
- handler-level processing for lobby announce, game announce, request locations, and request game info,
- periodic LAN resend behavior.
- instrumented LAN send path to log message type + destination mode:
- direct send,
- direct-connect fan-out,
- broadcast target and queue result.
- instrumented LAN rebind path (SetLocalIP) to log old/new local IP, transport init result, and broadcast enable result.
- instrumented transport/UDP layers for socket-level diagnostics:
- requested bind endpoint + bind success,
- source endpoint for unknown inbound packets,
- socket/bind/send/recv failure error codes.
- instrumented POSIX IP enumeration to log interface name/flags and candidate IPv4 addresses considered for LAN selection.
Files touched:
- GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanLobbyMenu.cpp
- Core/GameEngine/Source/GameNetwork/LANAPI.cpp
- Core/GameEngine/Source/GameNetwork/Transport.cpp
- Core/GameEngine/Source/GameNetwork/udp.cpp
- Core/GameEngine/Source/GameNetwork/IPEnumeration.cpp
- docs/WORKDIR/lessons/2026-04-LESSONS.md
Validation:
- get_errors reported no diagnostics in the edited source files.
Goal for next step: - run manual cross-platform tests (Linux + macOS) and capture logs to confirm whether failure is caused by wrong NIC/IP selection, broadcast routing behavior, or packet filtering.
Update after manual two-machine validation:
- analyzed fresh Linux/macOS logs captured after the expanded [LAN86] stderr instrumentation.
- confirmed that both platforms bind the configured LAN IP correctly and that direct-connect traffic reaches the host in both directions.
- identified a real handshake bug in LAN join handling: the host accepted the remote player locally but changed the join response target to broadcast, causing MSG_JOIN_ACCEPT to be sent to 255.255.255.255 instead of unicast to the joining client.
- this explains the observed false-positive where the host lobby showed the second player while the joining machine still timed out.
- fixed handleRequestJoin() to keep MSG_JOIN_ACCEPT/deny responses directed to the requester IP.
- added join-phase diagnostics for:
- direct-connect request start,
- request-join dispatch,
- join accept / deny processing,
- join-action timeout path.
Files touched in the follow-up fix:
- Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
- Core/GameEngine/Source/GameNetwork/LANAPI.cpp
Validation:
- get_errors reported no diagnostics in the edited LAN source files.
Remaining open item for issue #86:
- LAN discovery list still relies on global broadcast 255.255.255.255; the next likely functional fix is interface/subnet-aware broadcast instead of single global broadcast.
Follow-up after next two-machine run:
- direct-connect join now succeeds, but host->client control propagation remained inconsistent (game options, start signal, and room teardown behavior).
- root-cause hypothesis refined: several in-game control messages still used broadcast when no explicit destination IP was provided, which is fragile on mixed macOS/Linux networks.
- updated LANAPI::sendMessage() to prefer directed fan-out to known human slots for in-game control/state packets (GAME_OPTIONS, GAME_START, GAME_START_TIMER, REQUEST_GAME_LEAVE, SET_ACCEPT, MAP_AVAILABILITY, CHAT, INACTIVE), while preserving broadcast fallback when no target slot exists.
- kept lobby/discovery announcements unchanged so existing LAN listing behavior is not silently altered in this patch.
Files touched:
- Core/GameEngine/Source/GameNetwork/LANAPI.cpp
Validation:
- get_errors reported no diagnostics in the edited file.
Follow-up: LAN lobby discovery improvement
- implemented subnet-aware broadcast dispatch in LANAPI::sendMessage() for POSIX builds.
- broadcast path now prefers per-interface subnet broadcast addresses associated with the selected local LAN IP, with fallback to global 255.255.255.255 if none are available.
- this targets the LAN lobby visibility issue (machines not seeing each other) without removing compatibility fallback behavior.
Files touched:
- Core/GameEngine/Source/GameNetwork/LANAPI.cpp
Validation:
- get_errors reported no diagnostics in the edited file.
2026-04-11 (SESSION 115): Triage issue #84 (macOS EXC_BAD_ACCESS in AI guard state)¶
Investigated user report #84 for an intermittent crash during 8-player skirmish on macOS Apple Silicon.
What was done:
- reviewed the full crash report and call stack from the issue.
- mapped stack frames to current source in Zero Hour AI guard/tunnel network logic.
- identified likely null-dereference risk path in AITNGuardOuterState::update() where m_attackState is dereferenced without a guard while onEnter() has early-success exits.
- added technical references back to the GitHub issue with direct source links.
- requested deterministic repro details from reporter (map, factions, settings, time-to-crash, and consistency).
Why:
- crash signature (KERN_INVALID_ADDRESS at 0x38) and stack top indicate gameplay AI state-machine path rather than graphics backend fault.
- machine-to-machine variance (M1 vs newer Apple Silicon) may affect timing and trigger frequency for an existing logic edge case.
Outcome:
- issue now has actionable repro questions + anchored technical references for follow-up.
- a lessons note was recorded in docs/WORKDIR/lessons/2026-04-LESSONS.md for future triage consistency.
2026-04-11 (SESSION 114): Fix Zero Hour animated 3D shadow corruption on dynamic casters¶
Resolved issue #15 where animated shadows (for example flags and rotor meshes) were rendering incorrectly in Zero Hour while static shadows remained correct.
What was changed:
- GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DVolumetricShadow.cpp
- restored dynamic shadow vertex stream rebinding in W3DVolumetricShadow::RenderDynamicMeshVolume.
- added guarded SetStreamSource update when shadowVertexBufferD3D differs from lastActiveVertexBuffer.
- docs/WORKDIR/lessons/2026-04-LESSONS.md
- documented root cause, fix pattern, and prevention note for render-state drift between Generals and Zero Hour paths.
Why:
- The dynamic shadow draw path in Zero Hour could execute with stale vertex stream state, which explains why animated casters broke while static paths looked correct.
- User validation confirmed that disabling 3D shadows removed the artifact, isolating the fault to the volumetric shadow path.
Validation:
- get_errors reported no diagnostics in the edited rendering file.
- Local direct build path was blocked by stale host cache (/work CMake cache mismatch), so runtime validation used existing workflow and manual in-game verification.
- In-game user validation confirmed shadows are now rendering correctly.
2026-04-10 (SESSION CURRENT): Linux build workflow input normalization and AppImage toolchain supply-chain hardening¶
Applied two related CI/script hardening updates to reduce avoidable failures and tighten packaging trust boundaries.
Workflow update (build-linux.yml):
- Removed the standalone validate-package-format job.
- Added in-job normalization step (package-format) to sanitize unexpected package_format values.
- Invalid values now fall back to appimage with warning (fail-safe behavior).
- Packaging conditionals and artifact metadata now use the sanitized output.
AppImage script security update:
- Updated both scripts:
- scripts/build/linux/build-linux-appimage-zh.sh
- scripts/build/linux/build-linux-appimage-generals.sh
- Replaced legacy floating URL defaults with pinned AppImage/appimagetool release (1.9.1).
- Added strict URL validation to reject continuous and non-release URL patterns.
- Enabled mandatory SHA-256 validation for downloaded appimagetool binaries.
- In CI, force use of pinned downloaded binary instead of an arbitrary system appimagetool from PATH.
Validation:
- bash -n syntax checks passed for both updated AppImage scripts.
- YAML diagnostics remain clean for workflow updates.
Takeaway: For release packaging, deterministic input handling and pinned+verified build tools are mandatory to reduce CI flakiness and supply-chain exposure.
2026-04-10 (SESSION CURRENT): AppImage hardened against FFmpeg SONAME mismatch on newer Ubuntu¶
Addressed user-reported runtime failures on Ubuntu 25.10 where host FFmpeg SONAMEs differ from what the game binary requires (libavcodec.so.60, libavformat.so.60, libavutil.so.58, libswscale.so.7).
Root cause:
- AppImage builders did not bundle FFmpeg runtime libs, so binaries resolved host libraries.
- Host symlink workarounds do not satisfy ELF symbol versioning (LIBAVCODEC_60, LIBAVFORMAT_60, etc.).
Fixes:
- Updated both builders:
- scripts/build/linux/build-linux-appimage-zh.sh
- scripts/build/linux/build-linux-appimage-generals.sh
- Added FFmpeg SONAME bundling + codec dependency closure copy.
- Added fail-fast checks ensuring required FFmpeg libs are present in AppDir runtime.
Validation:
- Both AppImage builds succeeded.
- Verified bundled libs in both AppDirs include:
- libavcodec.so.60
- libavformat.so.60
- libavutil.so.58
- libswscale.so.7
- libswresample.so.4
Takeaway: For this project, AppImage compatibility requires shipping version-matched FFmpeg userspace libs; host symlinks are not a reliable solution for symbol-versioned dependencies.
2026-04-10 (SESSION CURRENT): Fixed AppImage task failure caused by removed flatpak icon path¶
Resolved a regression where [Linux] Build AppImage GeneralsXZH failed after flatpak/ removal.
Root cause:
- ZH AppImage builder still referenced icon path under flatpak/.
- appimagetool requires the desktop icon file referenced by the .desktop entry.
Fixes:
- Updated ZH builder to use assets-based icon path and fallback order:
- assets/generalsx-zh_icon.png
- fallback assets/generalsx_icon.png
- Added explicit fail-fast icon checks for both ZH and Generals AppImage builders.
Validation:
- ./scripts/build/linux/build-linux-appimage-zh.sh linux64-deploy succeeded.
- ./scripts/build/linux/build-linux-appimage-generals.sh linux64-deploy succeeded.
- Both AppImage artifacts were generated under build/.
2026-04-09 (SESSION CURRENT): AppImage launcher path resolution fixed for CNC_GENERALS_* env overrides¶
Improved AppImage AppRun to resolve assets deterministically and honor user-provided environment variables before fallback auto-detection.
Changes:
- Updated launcher generation in scripts/build/linux/build-linux-appimage-zh.sh:
- Prioritizes CNC_GENERALS_ZH_PATH when it points to an existing directory
- Prioritizes CNC_GENERALS_PATH (and maps to CNC_GENERALS_INSTALLPATH if needed)
- Keeps fallback auto-detection (AppImage directory, launch directory, common ~/GeneralsX/* paths)
- Switches working directory to resolved ZH asset path when available
- Updated AppImage docs and scripts README with explicit override examples
Validation: - Rebuilt AppImage successfully with updated launcher logic. - Runtime output confirms resolved asset/base path messages and normal startup progression.
2026-04-09 (SESSION CURRENT): AppImage base-game counterpart added and VS Code tasks aligned¶
Expanded the AppImage work on this branch so packaging is no longer Zero Hour-only.
Implemented:
- Added scripts/build/linux/build-linux-appimage-generals.sh for base Generals.
- Updated VS Code tasks to expose Linux AppImage packaging for both variants.
- Extended AppImage support docs and usage examples.
Result: - Branch now carries the AppImage packaging track cleanly for both games.
2026-04-09 (SESSION CURRENT): AppImage PoC implemented and validated (short-term Linux packaging path)¶
Given repeated Linux packaging friction around runtime compatibility, implemented a practical AppImage packaging path for Zero Hour as a short-term distribution strategy.
Implemented:
- New builder script: scripts/build/linux/build-linux-appimage-zh.sh
- Creates AppDir + AppImage for GeneralsXZH
- Bundles DXVK, SDL3, SDL3_image, OpenAL, and GameSpy runtime libs
- Generates AppRun launcher with existing DXVK/OpenAL runtime defaults
- Reuses existing icon asset and desktop metadata
- Auto-downloads appimagetool when not installed globally
- Updated scripts inventory docs in scripts/README.md
- Added active-work note docs/WORKDIR/support/APPIMAGE_POC_PLAN_2026-04.md
Validation result:
- AppImage generated successfully:
- build/GeneralsXZH-linux64-deploy-x86_64.AppImage
- Smoke launch succeeded through Vulkan window creation and early engine initialization:
- Vulkan library loaded
- SDL3 Vulkan window created successfully
- Engine initialization and INI loading started normally
Takeaway: AppImage is currently a viable short-term Linux packaging path for this project state.
2026-04-08 (SESSION 113): Remove stale local DXVK patch-flow narrative¶
Aligned macOS DXVK docs and CMake comments with the current pinned-fork source model and deprecated the old local patch helper script.
What was changed:
- docs/BUILD/MACOS.md
- replaced the old "PATCH_COMMAND applies 13 patches" explanation with the pinned-fork source model (DXVK_REMOTE_REF).
- updated troubleshooting wording to reference stale/mismatched DXVK source revisions instead of missing local patch application.
- updated status and related-files table to mark cmake/dxvk-macos-patches.py as deprecated.
- cmake/dx8.cmake
- removed legacy comments claiming local patch scripts are required during configure/build.
- clarified that macOS fixes are consumed from the pinned fork commit.
- cmake/dxvk-macos-patches.py
- converted to an explicit no-op deprecation stub (kept for backward compatibility and historical references).
Why:
- The active macOS DXVK flow no longer executes a local PATCH_COMMAND.
- Keeping legacy patch-flow wording causes contributor confusion and can lead to wrong debugging assumptions.
Validation:
- get_errors reported no diagnostics for:
- docs/BUILD/MACOS.md
- cmake/dx8.cmake
- cmake/dxvk-macos-patches.py
2026-04-07 (SESSION 112): Make dry-run preview lightweight (notes txt only)¶
Adjusted release workflow dry-run behavior to avoid generating/uploading large preview bundles.
What was changed:
- .github/workflows/release.yml
- In dry-run mode, keep build/download/validation and notes generation, but skip normalized zip creation.
- Added dedicated preview upload for release-assets/<tag>-notes.txt only.
- Kept real release path unchanged (create_release=true and dry_run=false) with full asset attachment.
- Added notes.txt output alongside notes.md for preview-focused runs.
- docs/WORKDIR/support/RELEASE_PIPELINE.md
- documented that dry-run uploads only the release description txt file.
Why: - Preview runs should validate pipeline logic without producing large artifacts. - Requested UX: quick dry-run output focused on release text review.
Validation:
- YAML diagnostics clean (get_errors reports no workflow errors).
- Dry-run now uploads a lightweight text artifact instead of hundreds of MB.
2026-04-07 (SESSION 111): Fix dry-run flow to execute full release preparation¶
Fixed a workflow control issue where prepare-release could be skipped even in dry-run due to dependency on a disabled Windows placeholder job.
What was changed:
- .github/workflows/release.yml
- Updated prepare-release.needs to depend only on active Linux/macOS jobs.
- Kept the Windows reusable workflow placeholder disabled, but removed it from release-preparation blocking path.
Why:
- Dry-run must execute full preparation (artifact collection + notes generation) and skip only GitHub release creation.
- A skipped dependency in needs prevented prepare-release from running as expected.
Validation:
- YAML diagnostics clean (get_errors reports no workflow errors).
- Conditional logic preserved: only Create GitHub release and upload assets is skipped in dry-run.
2026-04-07 (SESSION 110): Rework release pipeline to build bundles and publish optional release¶
Reworked the release workflow to match the expected operator flow: run platform builds, collect bundles, generate fixed-format notes from local PRs, and only publish when explicitly requested.
What was changed:
- Release workflow redesign:
- .github/workflows/release.yml
- Simplified inputs to: release_version, additional_notes, create_release, is_prerelease, dry_run.
- Set defaults to unchecked for create_release and is_prerelease.
- Added job-level build orchestration via reusable workflows:
- Linux Zero Hour build (build-linux.yml, GeneralsMD, linux64-deploy)
- macOS Zero Hour build (build-macos.yml, GeneralsMD, macos-vulkan)
- Downloaded build artifacts from both jobs and normalized release assets to:
- linux-generalsxzh-linux64-bundle.zip
- macos-generalsxzh-app.tar.zip
- Implemented release-note generation with fixed header block plus optional Additional Notes.
- Implemented changelog generation from PRs associated to commits in local repo (fbraz3/GeneralsX) to avoid upstream TheSuperHackers PR leakage.
- Implemented new contributor detection with @username + PR URL format.
- Added preview artifact upload when dry_run=true or create_release=false.
- Release creation now happens only when create_release=true and dry_run=false.
- Documentation update:
- docs/WORKDIR/support/RELEASE_PIPELINE.md
- Rewritten to reflect new inputs, behavior, output asset names, and publishing flow.
Why: - Align workflow UX with expected release process and avoid accidental publish/tag creation. - Ensure release notes and contributor sections follow the exact target markdown format. - Guarantee release artifacts are generated from fresh Linux/macOS build jobs in the same pipeline execution.
Validation:
- get_errors returned no diagnostics for .github/workflows/release.yml.
- Workflow input defaults and conditional paths validated by inspection (create_release=false, is_prerelease=false).
2026-04-07 (SESSION 109): TheSuperHackers upstream sync with cross-platform preservation¶
Performed a full upstream sync merge from thesuperhackers/main into branch thesuperhackers-sync-04-07-2026 with conflict-by-conflict reconciliation to preserve GeneralsX cross-platform behavior.
What was changed:
- Created and documented a subsystem conflict-resolution plan before resolving conflicts:
- docs/WORKDIR/planning/PLAN-2026-04-07_THESUPERHACKERS_SYNC.md
- Resolved merge conflicts in high-risk areas:
- CI workflow routing (.github/workflows/ci.yml)
- INI directory loading path (Core/GameEngine/Source/Common/INI/INI.cpp)
- platform-specific fatal error reporting (Core/GameEngine/Source/Common/System/Debug.cpp)
- camera zoom behavior alignment (Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp)
- cross-platform registry backend includes (Core/Libraries/Source/WWVegas/WWDownload/registry.cpp)
- camera data guards in both game variants (Generals*/.../GlobalData.h)
- DX8 wrapper cleanup alignment (Generals/.../dx8wrapper.cpp)
- GameEngine Unicode symbol conflict (GeneralsMD/.../GameEngine.cpp)
- Imported upstream INI improvements but kept macOS build compatibility:
- disabled std::from_chars parsing path on Apple targets to avoid deployment-target libc++ limitations.
Why: - Upstream and GeneralsX intentionally diverged. This sync needed targeted reconciliation, not blanket ours/theirs resolution. - Preserve SDL3 + DXVK + OpenAL cross-platform runtime while still importing useful upstream bugfixes and refactors. - Keep macOS target buildable after upstream INI parsing changes.
Validation:
- macOS configure succeeded ([macOS] Configure).
- macOS build succeeded for Zero Hour ([macOS] Build GeneralsXZH) after Apple-specific INI parser guard.
- macOS build succeeded for Generals base ([macOS] Build GeneralsX).
- macOS deploy succeeded for both variants ([macOS] Deploy GeneralsXZH, [macOS] Deploy GeneralsX).
- Runtime smoke tests on macOS completed with successful process exit for both Zero Hour and Generals wrappers.
Notes: - Linux configure/build validation was intentionally deferred for manual verification in this session.
2026-04-07 (SESSION 108): Address Copilot findings on release workflow and docs placement¶
Validated Copilot PR feedback for the release pipeline and applied only the comments that were technically correct and actionable.
What was changed:
- Release workflow hardening:
- .github/workflows/release.yml
- Added concurrency group keyed by workflow + release version to avoid race conditions on manual dispatch.
- Replaced generic ref validation (git rev-parse) with explicit tag validation using git show-ref --tags --verify.
- Removed unused download_artifacts input to avoid operator confusion.
- Improved changelog formatting by stripping duplicated trailing (#PR) when PR link is already appended.
- Reworked new-contributor detection to avoid deriving identities from email-like fields and removed incomplete in suffix output.
- Documentation organization fix:
- Moved release pipeline doc from docs/WORKDIR/RELEASE_PIPELINE.md to docs/WORKDIR/support/RELEASE_PIPELINE.md following WORKDIR subfolder policy.
- Updated content to match current behavior for new contributor formatting.
Why: - Prevent duplicate/competing release executions. - Keep tag checks precise and release-safe. - Avoid exposing potentially sensitive identity data in generated notes. - Keep WORKDIR documentation aligned with repository documentation structure rules.
Validation:
- get_errors returned no diagnostics for .github/workflows/release.yml after changes.
- git status confirms clean rename/move path for release documentation.
2026-04-02 (SESSION 107): Issue #52 save/load stabilization on macOS + clean backport to Generals¶
Closed the macOS save/load regression tracked in issue #52 and completed a clean backport to Generals base game.
What was changed:
- Save/load path semantics hardening (ZH + Generals):
- GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp
- Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp
- Updated map-path helpers to accept both \\ and / separators.
- Preserved real path casing on non-Windows in portableMapPathToRealMapPath (keep lowercase normalization only on Windows).
- Non-Windows save-file iteration parity:
- Added robust std::filesystem save iteration path to Generals iterateSaveFiles, matching Zero Hour behavior on macOS/Linux.
- Guarded directory switch/restore with exception-safe handling.
- Save/load menu safety:
- Added null guard in both PopupSaveLoad implementations to avoid dereference on inconsistent selection states.
Why: - macOS runs on case-sensitive/Unix-style path semantics where Windows-only assumptions can break snapshot/map path restoration during load. - Generals still had a Win32-only save iteration path, causing incomplete parity with the fixed Zero Hour flow.
Validation:
- Manual macOS Zero Hour test passed: create save + load save without crash or load error.
- Manual macOS Generals base test passed after backport.
- Build GeneralsXZH and Build GeneralsX completed in this session; deploy tasks also completed.
2026-04-01 (SESSION 106): Address Copilot findings on path-selection parity and docs layout¶
Validated Copilot review findings for issue #53 and applied only the changes that were technically sound and low risk.
What was changed:
- Deploy/runtime selection parity:
- scripts/build/linux/deploy-linux-zh.sh
- scripts/build/macos/deploy-macos-zh.sh
- Updated deploy destination selection to prefer the directory that actually contains .big assets (matching run script behavior), preventing binary/assets split when GeneralsZH exists but is empty and GeneralsMD contains assets.
- macOS bundle launcher default asset path:
- scripts/build/macos/bundle-macos-zh.sh
- Updated default CNC_GENERALS_ZH_PATH resolution to prefer GeneralsZH only when it has .big assets; fallback to legacy GeneralsMD when it contains assets.
- Docs organization and heading consistency:
- Moved runtime guide from docs/HOW_TO_RUN.md to docs/BUILD/HOW_TO_RUN.md to comply with documentation layout rules.
- Added proper H1 title (# How to Run GeneralsX) to the moved file.
- Updated README link to the new location.
Validation:
- bash -n passed for all modified scripts.
- get_errors returned no diagnostics in modified script/doc files.
2026-04-01 (SESSION 105): Issue #53 - Hide internal GeneralsMD from user-facing Zero Hour paths¶
Implemented user-facing path migration for Zero Hour to prefer GeneralsZH while preserving compatibility with existing GeneralsMD installs.
What was changed:
- Updated Zero Hour launch/deploy/bundle scripts on Linux and macOS to use ~/GeneralsX/GeneralsZH as the preferred runtime path.
- Added automatic legacy fallback to ~/GeneralsX/GeneralsMD when existing installs are detected.
- Updated user-facing script messages and guidance to show GeneralsZH first and GeneralsMD as migration fallback.
- Added visible README section "Running the game" after "How to download" linking to docs/BUILD/HOW_TO_RUN.md.
- Reviewed and updated runtime guide (docs/BUILD/HOW_TO_RUN.md) to recommend GeneralsZH and document GeneralsMD fallback.
- Updated build/runtime docs where relevant (docs/BUILD/MACOS.md, docs/ETC/COMMAND_LINE_PARAMETERS.md).
- Recorded migration lesson in docs/WORKDIR/lessons/2026-04-LESSONS.md.
Why:
- GeneralsMD is an internal source-layout detail and should not be the primary user-facing Zero Hour path.
- Migration must be low-risk and non-breaking for users with existing local installs.
Validation:
- Shell syntax checks passed for all edited launcher/deploy/bundle scripts (bash -n).
- Workspace diagnostics (get_errors) returned no errors for modified script/doc files.
2026-04-01 (SESSION 104): Address Copilot Review on Smudge, OpenContain, and WWMath¶
Validated Copilot review comments and applied only the fixes that were technically sound and low risk.
What was changed:
- Smudge update path hardening:
- Core/GameEngine/Source/GameClient/System/ParticleSys.cpp
- Added explicit smudge reset when heat effects are disabled to avoid stale smudge carry-over.
- Contain list safety in retail-compatible path:
- Generals/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp
- GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp
- Replaced m_containListSize usage in small fixed-array processing with the actual copied count (m_containList.size()) to prevent invalid reads in non-assert builds.
- WWMath Inverse_Lerp robustness:
- Core/Libraries/Source/WWVegas/WWMath/wwmath.h
- Added divide-by-zero guards for degenerate ranges (a == b).
- Added precise double overload Inverse_Lerp(double, double, double) and made float overload forward to it.
Why:
- Prevent stale smudge artifacts when runtime smudge drawing is toggled off.
- Ensure contain processing cannot read beyond initialized copied elements if debug assertions are disabled.
- Avoid NaN/Inf propagation from zero denominator and preserve precision for double inputs.
Validation:
- get_errors reported no diagnostics in all touched files.
- Changes were scoped to the reviewed lines only.
2026-04-01 (SESSION 103): Copilot Review Follow-up on ASCII Stability and Escape Strictness¶
Validated additional Copilot findings on quoted-printable handling and applied targeted hardening in both game variants.
What was changed:
- Updated byte-classification in quoted-printable encoder to be ASCII-only and locale-independent.
- replaced locale-sensitive isalnum(...) usage in appendQuotedPrintableByte(...) with explicit ASCII alphanumeric checks.
- Tightened quoted-printable escape decoding in decodeQuotedPrintableByte(...):
- require exactly two hex digits after MAGIC_CHAR (_).
- validate both digits as hexadecimal before decoding.
- fail fast on malformed/truncated escape sequences instead of partially consuming input.
- Applied identically in both files:
- GeneralsMD/Code/GameEngine/Source/Common/System/QuotedPrintable.cpp
- Generals/Code/GameEngine/Source/Common/System/QuotedPrintable.cpp
Why: - Keep the wire format deterministic across locales/platforms. - Prevent silent corruption when decoding malformed or odd/truncated escape input.
Validation:
- get_errors returned no diagnostics for both touched source files.
- Manual diff inspection confirmed only intended parser/encoder hardening changes.
2026-04-01 (SESSION 102): Copilot Review Follow-up on Quoted-Printable and DXVK Fetch¶
Reviewed Copilot inline comments and applied only the fixes that were technically valid.
What was changed:
- Hardened UTF-16 quoted-printable encode/decode in both game variants:
- GeneralsMD/Code/GameEngine/Source/Common/System/QuotedPrintable.cpp
- Generals/Code/GameEngine/Source/Common/System/QuotedPrintable.cpp
- Encode path now rolls back the write index if low/high byte pair cannot be fully appended, avoiding partial trailing code units.
- Decode path now requires both serialized bytes for each UTF-16 code unit and stops on odd/truncated trailing input.
- Updated macOS DXVK external fetch in cmake/dx8.cmake:
- GIT_SHALLOW changed from TRUE to FALSE for the pinned commit path to improve reliability on clean CI/build machines.
Why: - Prevent malformed trailing characters when output/input is truncated at code-unit boundaries. - Avoid shallow-fetch edge cases when consuming immutable commit pins in CMake ExternalProject.
Validation:
- get_errors reported no diagnostics in all modified files.
- Manual diff check confirmed only the targeted review-follow-up changes.
2026-04-01 (SESSION 101): Work Branch DXVK SHA Bump After Fork Push¶
Updated the GeneralsX work branch to consume the freshly pushed DXVK fix commit via immutable SHA pinning.
What was done:
- Pushed DXVK fork branch fix/macos-size_t-cstddef with commit:
- 46a3bc018bcae408d49d3c500e4e536a11f6789a
- Updated cmake/dx8.cmake in this work branch to pin macOS remote DXVK source to that commit SHA.
Why: - Keep the work branch aligned with the reproducible DXVK orchestration model. - Ensure this branch builds against the exact DXVK fix commit (no moving branch head risk).
Validation:
- cmake --preset macos-vulkan
- [macOS] Build GeneralsXZH
Both steps completed successfully in this session.
2026-04-01 (SESSION 2): Address Copilot review on SDL3 text input path¶
Objective: Evaluate Copilot PR comments for SDL3 text input forwarding and apply only behavior-safe fixes in both Generals and Zero Hour.
Review Decisions:
1. Accepted: clamp forwarded text input to BMP and reject UTF-16 surrogate range before GWM_IME_CHAR dispatch.
2. Accepted: use tracked m_TextInputFocusWindow in forwardTextInputEvent() instead of re-querying focus at dispatch time.
3. Deferred (separate refactor): move duplicated UTF-8 decoder helper to shared code to avoid scope expansion in a bugfix review pass.
Fixes Applied:
1. Generals/Code/GameEngineDevice/Source/SDL3GameEngine.cpp
2. GeneralsMD/Code/GameEngineDevice/Source/SDL3GameEngine.cpp
Validation: 1. File-level diagnostics report no new errors on both updated files.
Outcome: Copilot review concerns for text routing and Unicode filtering were addressed with minimal-risk changes on branch fix/issue-58-sdl3-text-entry.
2026-04-01 (SESSION 1): Issue #58 - SDL3 text entry fixes (ZH + Generals)¶
Objective: Fix keyboard text entry behavior on SDL3 builds where editable fields accepted printable text but had broken editing/navigation behavior.
Root Causes:
1. SDL3 backend forwarded key up/down but did not bridge UTF-8 text input to the GUI GWM_IME_CHAR path.
2. SDL3 keyboard mapping missed editing/navigation scancodes (Backspace, Delete, Home/End, PageUp/PageDown, KP Enter).
3. Backspace could delete twice because SDL key-repeat and engine autorepeat were both active.
Fixes Applied:
1. Added SDL text input bridge in SDL3 GameEngine (Zero Hour and Generals):
- Enable SDL_StartTextInput only when focus is on GWS_ENTRY_FIELD.
- Forward SDL_EVENT_TEXT_INPUT UTF-8 codepoints as GWM_IME_CHAR.
2. Extended SDL scancode mapping in SDL3 keyboard backends:
- Added SDL_SCANCODE_BACKSPACE, DELETE, HOME, END, PAGEUP, PAGEDOWN, KP_ENTER.
3. Removed duplicate repeat source in SDL keyboard ingestion:
- Ignore repeated SDL keydown events (event->key.repeat).
- Use timeGetTime() domain for keyDownTimeMsec to match engine repeat timing logic.
Validation:
1. Incremental macOS build passed (cmake --build build/macos-vulkan --target z_generals -j4, EXIT:0).
2. Runtime verification from manual test: behavior works in both Generals and Zero Hour.
Outcome: Issue #58 resolved on branch fix/issue-58-sdl3-text-entry and ready for PR.