diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp index 9e0bccf2319..21383479791 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp @@ -4773,7 +4773,63 @@ void PartitionManager::xfer( Xfer *xfer ) // ------------------------------------------------------------------------------------------------ void PartitionManager::loadPostProcess() { + // GeneralsX @bugfix coolswood 22/07/2026 Fix fog-of-war after save/load. + // After a save load, the per-cell shroud counters and the pending undo-queue are + // restored from the file, but nothing makes objects re-apply their looks. When the + // restored undo-queue drains (~m_unlookPersistDuration frames later), removeLooker + // fires at every queued position. Static structures never re-look, so their cells + // drop to FOGGED: the player's own base becomes gray "previously seen" ghosts that + // can no longer be selected. The restored m_partitionLastLook and the cell counters + // also disagree (counters reflect looks that no live object owns), so any later + // handleShroud() would double-count. The fix mirrors the established + // TerrainLogic::setActiveBoundary rebuild: drain the queue, snapshot the explored + // (FOGGED) and permanently-revealed (CLEAR) cells, tear the cell grid down to the + // all-shrouded default, then make every object re-look fresh onto the clean grid. + // After this the undo-queue is empty, every live object owns its looker, and static + // structures stay CLEAR permanently. + ShroudStatusStoreRestore partitionStore; + + // Flush the restored undo-queue so lingering undoes do not fire after the rebuild. + processEntirePendingUndoShroudRevealQueue(); + + // Remember cells that are explored-but-not-visible (FOGGED). + storeFoggedCells(partitionStore, TRUE); + + // Release ghost-object partition data before the cell grid is torn down. + TheGhostObjectManager->releasePartitionData(); + + // Invalidate every object's sighting state so the later unlook()/unshroud() inside + // handleShroud() are no-ops instead of queueing double-undoes or double-counting. + for (Object *obj = TheGameLogic->getFirstObject(); obj; obj = obj->getNextObject()) + obj->friend_prepareForMapBoundaryAdjust(); + + // Remember cells that are CLEAR (permanently revealed) after the queue drain. + storeFoggedCells(partitionStore, FALSE); + + // Rebuild the cell grid: shutdown deletes m_cells, init re-allocates every cell at + // the fully-shrouded default (m_currentShroud = 1). + reset(); + init(); + + // Restore permanently-revealed cells with extra lookers. + restoreFoggedCells(partitionStore, FALSE); + + // Prevent new ghost objects from being created while the existing ones are restored. + TheGhostObjectManager->lockGhostObjects(TRUE); + + // Re-register and re-look every object onto the clean grid. handleShroud() now does + // unlook (no-op, SightingInfo is invalid) + look (fresh doShroudReveal), so each + // object owns exactly one looker and the undo-queue stays empty. + for (Object *obj = TheGameLogic->getFirstObject(); obj; obj = obj->getNextObject()) + obj->friend_notifyOfNewMapBoundary(); + + // Restore explored-but-not-visible (FOGGED) cells and ghost-object partition data. + restoreFoggedCells(partitionStore, TRUE); + TheGhostObjectManager->restorePartitionData(); + TheGhostObjectManager->lockGhostObjects(FALSE); + // Redraw the display/radar from the rebuilt cell state. + refreshShroudForLocalPlayer(); } //----------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 484df3aa51f..79bc436b0cf 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -2226,7 +2226,15 @@ void GameLogic::tryStartNewGame( Bool loadingSaveGame ) if( loadingSaveGame == FALSE ) ThePlayerList->newMap(); - if ( isChallengeCampaign ) + // GeneralsX @bugfix coolswood 22/07/2026 Fix fog-of-war after save/load in Generals Challenge. + // The challenge-mode relationship setup must not run when loading a save game: the + // player relationships were already serialized into the save via CHUNK_Players (which + // loads before CHUNK_GameLogic). Running this block on load overwrites the restored + // ALLY relationships (e.g. between the human player slot and their chosen general) with + // fresh ENEMIES derived from the map's "ThePlayer" placeholder. That causes every + // structure of the local general to stop revealing shroud for the human player, so the + // player's own base becomes covered by fog of war within ~30 frames of loading. + if( loadingSaveGame == FALSE && isChallengeCampaign ) { // Establish local player relationships with other teams as // they're set up for "ThePlayer" in challenge mode map. diff --git a/docs/WORKLOG/2026-07-DIARY.md b/docs/WORKLOG/2026-07-DIARY.md index d67ff299356..ae70f7c14d7 100644 --- a/docs/WORKLOG/2026-07-DIARY.md +++ b/docs/WORKLOG/2026-07-DIARY.md @@ -1,5 +1,23 @@ # July 2026 +## 22/07/2026 +### Fix Fog-of-War Over Own Base After Save/Load (Generals Challenge) +- **Symptom**: In Generals Challenge, loading a save caused fog of war to creep over the player's own base within ~5-10 seconds. The player's own buildings became gray "previously seen" ghosts that could no longer be selected or controlled. +- **Disproved hypothesis (player relationships)**: First suspected a lost ALLY relationship between the human player slot and their chosen general after load. Diagnostics showed the relationship was already ENEMY at save time (by design — the alliance is held in team-relations, evaluated before player-relations), and brute-forcing all players to mutual ALLIES on load did **not** fix it. This proved the fog was independent of relationships. +- **Root cause (partition/shroud load path)**: After a save load, `PartitionManager` restores the per-cell shroud counters (`m_currentShroud`) and the pending undo-queue from the file, but nothing makes objects re-apply their lookers — `PartitionManager::loadPostProcess()` was an empty stub. When the restored undo-queue drained (~`m_unlookPersistDuration` = 30 frames later), `removeLooker` fired at each queued position. Static structures never re-look, so their cells dropped to FOGGED: the player's own base became gray, unselectable ghosts. The restored `m_partitionLastLook` and the cell counters also disagreed, so any later `handleShroud()` would double-count. +- **Fix** (`PartitionManager.cpp::loadPostProcess`): Mirrored the established `TerrainLogic::setActiveBoundary` rebuild pattern — drain the queue, snapshot explored (FOGGED) and permanently-revealed (CLEAR) cells, release ghost-object partition data, invalidate every object's sighting state (`friend_prepareForMapBoundaryAdjust`), tear the cell grid down (`reset()`+`init()` to all-shrouded default), restore permanently-revealed cells, re-look every object (`friend_notifyOfNewMapBoundary` → each object owns exactly one live looker), then restore explored cells + ghost objects + `refreshShroudForLocalPlayer()`. After this the undo-queue is empty, each object owns its looker, and static structures stay CLEAR permanently. +- **Defensive guard** (`GameLogic.cpp::tryStartNewGame`): Guarded the challenge-mode relationship-setup block with `loadingSaveGame == FALSE` so it cannot overwrite serialized relationships on load. + +## 22/07/2026 +### Fix "Failed to initialize WSI" Launch Regression (macOS) +- **Symptom**: Game stopped launching after ~18 July with `ERROR: TheDisplay->init() threw exception: Failed to initialize WSI.` (crash before main menu). The generic message hid two distinct, layered root causes. +- **Root cause #1 — DXVK compiled without SDL3 WSI backend**: DXVK's `meson.build` auto-detects the WSI backend via `dependency('SDL3')`/`('SDL2')` and only defines `-DDXVK_WSI_SDL3` (registering the runtime "SDL3" bootstrap) when that pkg-config dependency resolves. The game *unconditionally* sets `DXVK_WSI_DRIVER=SDL3` at runtime (`GeneralsMD/Code/Main/SDL3Main.cpp:288`). The FetchContent SDL3 build (`cmake/sdl3.cmake`) does NOT emit a `.pc` file, so Meson only sees a system SDL3. Without one (and with Homebrew `sdl2-compat` installed), Meson silently compiled DXVK with **only** the SDL2 WSI — so the SDL3 bootstrap was absent and `wsi::init()` threw. The regression was triggered by a clean `meson --reconfigure` losing a previously-available system SDL3. +- **Root cause #2 — d3d8 rpath breaks in flat deploys**: Meson bakes `@loader_path/../d3d9` into `libdxvk_d3d8.0.dylib` (matches the `src/d3d8 -> src/d3d9` build layout). Flat runtime dirs (deploy dir, `.app` bundle `Libraries/`) place d3d8 and d3d9 side-by-side, so `../d3d9` points at a non-existent subdir and `dlopen(libdxvk_d3d8)` fails with `Library not loaded: libdxvk_d3d9.0.dylib`. This was previously masked because the main binary's rpath included `build/macos-vulkan`, where the `dxvk_d3d8_install` custom target placed a flat d3d9 copy — but only when that ALL target ran (a `--target z_generals` rebuild skips it). +- **Fix #1** (`cmake/dx8.cmake`): Added a configure-time guard that checks the Homebrew pkg-config (`/opt/homebrew/bin/pkg-config`, the one Meson's ExternalProject subprocess actually invokes — not the vcpkg-shimmed `PKG_CONFIG_EXECUTABLE`) for SDL3 and fails fast with an actionable `brew install sdl3` message instead of producing a broken DXVK. +- **Fix #2** (`cmake/dx8.cmake` + `cmake/fixup_d3d8_rpath.cmake.in` + deploy/bundle scripts): Added an idempotent `@loader_path` rpath to d3d8 after the build-dir copy step (via a generated CMake script to avoid ninja shell-escaping issues; re-signs adhoc because `install_name_tool` invalidates the signature), and mirrored the same guard in `deploy-macos-{zh,generals}.sh` and `bundle-macos-{zh,generals}.sh` as belt-and-suspenders for non-`dxvk_d3d8_install` build paths. +- **Verification**: Clean `rm -rf build/macos-vulkan` + rebuild + deploy; game now reaches main menu (`Shell::push() filename='Menus/MainMenu.wnd'`) with `Vulkan: Found vkGetInstanceProcAddr` + `Platform WSI` + `VK_EXT_metal_surface` + `Driver : MoltenVK 0.2.2209` — matching the working 18 July log signature. +- **Prereq update**: Documented `brew install sdl3` as required in `build-macos-zh.sh` header. + ## 17/07/2026 ### Fix Non-Deterministic Math in WWMath Matrix Classes - Traced the cross-platform desync at frame 100 to the `TrainCabUngarrisonable` object's translation matrix diverging between macOS (ARM64) and Linux (x86_64).