Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

//-----------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions docs/WORKLOG/2026-07-DIARY.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
Loading