From 88a740d0dc14c67366016e36fdb13690b0c6d967 Mon Sep 17 00:00:00 2001 From: coolswood Date: Thu, 23 Jul 2026 00:10:20 +0300 Subject: [PATCH] bugfix(savegame): Rebuild shroud grid on load so structures stay visible Loading a save left the partition/shroud state inconsistent. The per-cell shroud counters and the pending undo-queue were restored from the file, but nothing re-applied the object looks those counters represent. When the restored undo-queue drained (~m_unlookPersistDuration frames after load) removeLooker fired at every queued position. Static structures never re-look, so their cells dropped to FOGGED and the player's own base became gray, unselectable ghosts within a few seconds of loading a save. PartitionManager::loadPostProcess was an empty stub, so the grid was never rebuilt after load. Fill it by mirroring the established TerrainLogic::setActiveBoundary rebuild: drain the queue, snapshot the explored (FOGGED) and permanently-revealed (CLEAR) cells, release ghost partition data, invalidate every object's sighting state so the later unlook()/unshroud() are no-ops, tear the cell grid down to the all-shrouded default, then re-look every object so each owns exactly one live looker. Finally restore the explored cells and ghost objects and refresh the display/radar. After this the undo-queue is empty and static structures stay CLEAR permanently. Applied to Zero Hour first, then replicated for Generals. --- .../GameLogic/Object/PartitionManager.cpp | 54 +++++++++++++++++++ .../GameLogic/Object/PartitionManager.cpp | 54 +++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp index f9b464c58bd..7b1a032874b 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp @@ -4766,7 +4766,61 @@ void PartitionManager::xfer( Xfer *xfer ) // ------------------------------------------------------------------------------------------------ void PartitionManager::loadPostProcess() { + // TheSuperHackers @bugfix coolswood 23/07/2026 Rebuild the shroud grid on save load so + // objects re-apply their lookers. After load the per-cell shroud counters and the pending + // undo-queue are restored from the file, but nothing re-applied the object looks that those + // counters represent. 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 and the player's own base becomes gray, unselectable ghosts. + // The restored m_partitionLastLook and the cell counters also disagree, 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. + 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/Object/PartitionManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp index a5f0a55128c..cb9c1e6cd0a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp @@ -4776,7 +4776,61 @@ void PartitionManager::xfer( Xfer *xfer ) // ------------------------------------------------------------------------------------------------ void PartitionManager::loadPostProcess() { + // TheSuperHackers @bugfix coolswood 23/07/2026 Rebuild the shroud grid on save load so + // objects re-apply their lookers. After load the per-cell shroud counters and the pending + // undo-queue are restored from the file, but nothing re-applied the object looks that those + // counters represent. 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 and the player's own base becomes gray, unselectable ghosts. + // The restored m_partitionLastLook and the cell counters also disagree, 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. + 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(); } //-----------------------------------------------------------------------------