From 2152ca8dde42fb793d924ff3f1715a34dc871ce6 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Thu, 18 Sep 2025 14:11:45 +1000 Subject: [PATCH 01/20] initial forward port https://github.com/Phobos-developers/Phobos/pull/530, fix buildings and animations not showing behind fog if SpySat=yes --- Phobos.vcxproj | 5 + src/Ext/Cell/Body.cpp | 3 +- src/Ext/Cell/Body.h | 8 +- src/Misc/FogOfWar.cpp | 610 ++++++++++++++++++++++++++++++++ src/Misc/MapRevealer.cpp | 221 ++++++++++++ src/Misc/MapRevealer.h | 133 +++++++ src/New/Entity/FoggedObject.cpp | 545 ++++++++++++++++++++++++++++ src/New/Entity/FoggedObject.h | 98 +++++ 8 files changed, 1621 insertions(+), 2 deletions(-) create mode 100644 src/Misc/FogOfWar.cpp create mode 100644 src/Misc/MapRevealer.cpp create mode 100644 src/Misc/MapRevealer.h create mode 100644 src/New/Entity/FoggedObject.cpp create mode 100644 src/New/Entity/FoggedObject.h diff --git a/Phobos.vcxproj b/Phobos.vcxproj index c8a07539c3..31569a8fae 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -194,6 +194,9 @@ + + + @@ -260,6 +263,8 @@ + + diff --git a/src/Ext/Cell/Body.cpp b/src/Ext/Cell/Body.cpp index 2c04e51bc9..5bf47c52bd 100644 --- a/src/Ext/Cell/Body.cpp +++ b/src/Ext/Cell/Body.cpp @@ -15,6 +15,7 @@ void CellExt::ExtData::Serialize(T& Stm) Stm .Process(this->RadSites) .Process(this->RadLevels) + .Process(this->FoggedObjects) ; } @@ -70,7 +71,7 @@ DEFINE_HOOK(0x47BDA1, CellClass_CTOR, 0x5) { GET(CellClass*, pItem, ESI); - CellExt::ExtMap.Allocate(pItem); + CellExt::ExtMap.FindOrAllocate(pItem); return 0; } diff --git a/src/Ext/Cell/Body.h b/src/Ext/Cell/Body.h index fc1dfae4d8..6b004b562c 100644 --- a/src/Ext/Cell/Body.h +++ b/src/Ext/Cell/Body.h @@ -6,6 +6,10 @@ #include #include +#include + +class FoggedObject; + class CellExt { public: @@ -36,8 +40,10 @@ class CellExt public: std::vector RadSites {}; std::vector RadLevels { }; + DynamicVectorClass FoggedObjects; - ExtData(CellClass* OwnerObject) : Extension(OwnerObject) + ExtData(CellClass* OwnerObject) : Extension(OwnerObject), + FoggedObjects {} { } virtual ~ExtData() = default; diff --git a/src/Misc/FogOfWar.cpp b/src/Misc/FogOfWar.cpp new file mode 100644 index 0000000000..6975d5d419 --- /dev/null +++ b/src/Misc/FogOfWar.cpp @@ -0,0 +1,610 @@ +#include + +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +DEFINE_HOOK(0x6B8E7A, ScenarioClass_LoadSpecialFlags, 0x5) +{ + ScenarioClass::Instance->SpecialFlags.FogOfWar = + RulesClass::Instance->FogOfWar || R->EAX() || GameModeOptionsClass::Instance.FogOfWar; + + R->ECX(R->EDI()); + return 0x6B8E8B; +} + +DEFINE_HOOK(0x686C03, SetScenarioFlags_FogOfWar, 0x5) +{ + GET(ScenarioFlags, SFlags, EAX); + + SFlags.FogOfWar = RulesClass::Instance->FogOfWar || GameModeOptionsClass::Instance.FogOfWar; + + R->EDX(*reinterpret_cast(&SFlags)); + return 0x686C0E; +} + +DEFINE_HOOK(0x5F4B3E, ObjectClass_DrawIfVisible, 0x6) +{ + GET(ObjectClass*, pThis, ESI); + + if (pThis->InLimbo) + return 0x5F4B7F; + + if (!ScenarioClass::Instance->SpecialFlags.FogOfWar) + return 0x5F4B48; + + // SpySat reveals everything, so bypass fog checks when SpySat is active + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) + return 0x5F4B48; + + switch (pThis->WhatAmI()) + { + case AbstractType::Anim: + if (!static_cast(pThis)->Type->ShouldFogRemove) + return 0x5F4B48; + break; + + case AbstractType::Unit: + case AbstractType::Cell: + case AbstractType::Building: // Allow buildings to be processed for fog + break; + + default: + return 0x5F4B48; + } + + if (!MapClass::Instance.IsLocationFogged(pThis->GetCoords())) + return 0x5F4B48; + + pThis->NeedsRedraw = false; + return 0x5F4D06; +} + +DEFINE_HOOK(0x6F5190, TechnoClass_DrawExtras_CheckFog, 0x6) +{ + GET(TechnoClass*, pThis, ECX); + + return MapClass::Instance.IsLocationFogged(pThis->GetCoords()) ? 0x6F5EEC : 0; +} + +DEFINE_HOOK(0x6D6EDA, TacticalClass_Overlay_CheckFog1, 0xA) +{ + GET(CellClass*, pCell, EAX); + + // SpySat reveals entire map, so no cell is fogged when SpySat is active + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) + return pCell->OverlayTypeIndex == -1 ? 0x6D7006 : 0x6D6EE4; + + return pCell->OverlayTypeIndex == -1 || pCell->IsFogged() ? 0x6D7006 : 0x6D6EE4; +} + +DEFINE_HOOK(0x6D70BC, TacticalClass_Overlay_CheckFog2, 0xA) +{ + GET(CellClass*, pCell, EAX); + + // SpySat reveals entire map, so no cell is fogged when SpySat is active + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) + return pCell->OverlayTypeIndex == -1 ? 0x6D71A4 : 0x6D70C6; + + return pCell->OverlayTypeIndex == -1 || pCell->IsFogged() ? 0x6D71A4 : 0x6D70C6; +} + +DEFINE_HOOK(0x71CC8C, TerrainClass_DrawIfVisible, 0x6) +{ + GET(TerrainClass*, pThis, EDI); + pThis->NeedsRedraw = false; + + return pThis->InLimbo || MapClass::Instance.IsLocationFogged(pThis->GetCoords()) ? 0x71CD8D : 0x71CC9A; +} + +DEFINE_HOOK(0x4804A4, CellClass_DrawTile_DrawSmudgeIfVisible, 0x6) +{ + GET(CellClass*, pThis, ESI); + + // SpySat reveals entire map, so no cell is fogged when SpySat is active + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) + return 0; + + return pThis->IsFogged() ? 0x4804FB : 0; +} + +DEFINE_HOOK(0x5865E2, MapClass_IsLocationFogged, 0x3) +{ + GET_STACK(CoordStruct*, pCoord, 0x4); + + // SpySat reveals entire map (both fog and shroud), so no location is fogged when SpySat is active + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) + { + R->EAX(false); + return 0; + } + + MapRevealer revealer(*pCoord); + CellClass* pCell = MapClass::Instance.GetCellAt(revealer.Base()); + + R->EAX(pCell->Flags & CellFlags::EdgeRevealed ? + false : + !(pCell->GetNeighbourCell(static_cast(3))->Flags & CellFlags::EdgeRevealed)); + + return 0; +} + +DEFINE_HOOK(0x6D7A4F, TacticalClass_DrawPixelEffects_FullFogged, 0x6) +{ + GET(CellClass*, pCell, ESI); + + // SpySat reveals entire map, so no cell is fogged when SpySat is active + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) + return 0; + + return pCell->IsFogged() ? 0x6D7BB8 : 0; +} + + +DEFINE_HOOK(0x4ACE3C, MapClass_TryReshroudCell_SetCopyFlag, 0x6) +{ + GET(CellClass*, pCell, EAX); + + bool bNoFog = static_cast(pCell->AltFlags & AltCellFlags::NoFog); + pCell->AltFlags &= ~AltCellFlags::NoFog; + int Index = TacticalClass::Instance->GetOcclusion(pCell->MapCoords, false); + + if ((bNoFog || pCell->Visibility != Index) && Index >= 0 && pCell->Visibility >= -1) + { + pCell->AltFlags |= AltCellFlags::Mapped; + pCell->Visibility = static_cast(Index); + } + + TacticalClass::Instance->RegisterCellAsVisible(pCell); + + return 0x4ACE57; +} + +DEFINE_HOOK(0x4A9CA0, MapClass_RevealFogShroud, 0x8) +{ + GET(MapClass*, pThis, ECX); + GET_STACK(CellStruct*, pMapCoords, 0x4); + GET_STACK(HouseClass*, pHouse, 0x8); + GET_STACK(bool, bIncreaseShroudCounter, 0xC); + + auto const pCell = pThis->GetCellAt(*pMapCoords); + bool bRevealed = false; + bool const bShouldCleanFog = !(pCell->Flags & CellFlags::EdgeRevealed); + + if (bShouldCleanFog || !(pCell->AltFlags & AltCellFlags::Mapped)) + bRevealed = true; + + bool const bWasRevealed = bRevealed; + + pCell->Flags = pCell->Flags & ~CellFlags::IsPlot | CellFlags::EdgeRevealed; + pCell->AltFlags = pCell->AltFlags & ~AltCellFlags::NoFog | AltCellFlags::Mapped; + + if (bIncreaseShroudCounter) + pCell->IncreaseShroudCounter(); + else + pCell->ReduceShroudCounter(); + + char Visibility = TacticalClass::Instance->GetOcclusion(*pMapCoords, false); + if (pCell->Visibility != Visibility) + { + bRevealed = true; + pCell->Visibility = Visibility; + } + if (pCell->Visibility == -1) + pCell->AltFlags |= AltCellFlags::NoFog; + + char Foggness = TacticalClass::Instance->GetOcclusion(*pMapCoords, true); + if (pCell->Foggedness != Foggness) + { + bRevealed = true; + pCell->Foggedness = Foggness; + } + if (pCell->Foggedness == -1) + pCell->Flags |= CellFlags::CenterRevealed; + + if (bRevealed) + { + TacticalClass::Instance->RegisterCellAsVisible(pCell); + pThis->RevealCheck(pCell, pHouse, bWasRevealed); + } + + if (bShouldCleanFog) + pCell->CleanFog(); + + R->EAX(bRevealed); + + return 0x4A9DC6; +} + +// CellClass_CleanFog +DEFINE_HOOK(0x486C50, CellClass_ClearFoggedObjects, 0x6) +{ + GET(CellClass*, pThis, ECX); + + auto pExt = CellExt::ExtMap.Find(pThis); + for (auto const pObject : pExt->FoggedObjects) + { + if (pObject->CoveredType == FoggedObject::CoveredType::Building) + { + CellClass* pRealCell = MapClass::Instance.GetCellAt(pObject->Location); + + for (auto pFoundation = pObject->BuildingData.Type->GetFoundationData(false); + pFoundation->X != 0x7FFF || pFoundation->Y != 0x7FFF; + ++pFoundation) + { + CellStruct mapCoord = + { + pRealCell->MapCoords.X + pFoundation->X, + pRealCell->MapCoords.Y + pFoundation->Y + }; + + CellClass* pCell = MapClass::Instance.GetCellAt(mapCoord); + if (pCell != pThis) + CellExt::ExtMap.Find(pCell)->FoggedObjects.Remove(pObject); + } + + } + GameDelete(pObject); + } + pExt->FoggedObjects.Clear(); + + return 0x486D8A; +} + +DEFINE_HOOK(0x486A70, CellClass_FogCell, 0x5) +{ + GET(CellClass*, pThis, ECX); + + // SpySat reveals everything for the owning player, so actively remove fog instead of adding it + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) + { + // Clear fog from this cell and surrounding area + auto location = pThis->MapCoords; + for (int i = 1; i < 15; i += 2) + { + CellClass* pCell = MapClass::Instance.GetCellAt(location); + if (pCell->Flags & CellFlags::Fogged) + { + pCell->Flags &= ~CellFlags::Fogged; + // Also clear fogged objects from this cell + CellExt::ExtMap.Find(pCell)->FoggedObjects.Clear(); + } + } + return 0x486BE6; // Skip normal fog processing + } + + if (ScenarioClass::Instance->SpecialFlags.FogOfWar) + { + auto location = pThis->MapCoords; + for (int i = 1; i < 15; i += 2) + { + CellClass* pCell = MapClass::Instance.GetCellAt(location); + int nLevel = pCell->Level; + + if (nLevel >= i - 2 && nLevel <= i) + { + if (!(pCell->Flags & CellFlags::Fogged)) + { + pCell->Flags |= CellFlags::Fogged; + + for (ObjectClass* pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) + { + switch (pObject->WhatAmI()) + { + case AbstractType::Unit: + case AbstractType::Infantry: + case AbstractType::Aircraft: + pObject->Deselect(); + break; + + case AbstractType::Building: + if (BuildingClass* pBld = abstract_cast(pObject)) + { + if (pBld->IsAllFogged()) + pBld->FreezeInFog(nullptr, pCell, !pBld->IsStrange() && pBld->Translucency != 15); + } + break; + + case AbstractType::Terrain: + if (TerrainClass* pTerrain = abstract_cast(pObject)) + { + // pTerrain + FoggedObject* pFoggedTer = GameCreate(pTerrain); + CellExt::ExtMap.Find(pCell)->FoggedObjects.AddItem(pFoggedTer); + } + break; + + default: + continue; + } + } + if (pCell->OverlayTypeIndex != -1) + { + FoggedObject* pFoggedOvl = GameCreate(pCell, true); + CellExt::ExtMap.Find(pCell)->FoggedObjects.AddItem(pFoggedOvl); + } + if (pCell->SmudgeTypeIndex != -1) + { + FoggedObject* pFoggedSmu = GameCreate(pCell, false); + CellExt::ExtMap.Find(pCell)->FoggedObjects.AddItem(pFoggedSmu); + } + } + } + + ++location.X; + ++location.Y; + } + } + + return 0x486BE6; +} + +DEFINE_HOOK(0x457AA0, BuildingClass_FreezeInFog, 0x5) +{ + GET(BuildingClass*, pThis, ECX); + GET_STACK(CellClass*, pCell, 0x8); + GET_STACK(bool, IsVisible, 0xC); + + // SpySat reveals everything, so don't freeze buildings in fog when SpySat is active + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) + { + // When SpySat is active, clear all existing fogged buildings (one-time cleanup) + static bool hasUnfrozen = false; + if (!hasUnfrozen) { + hasUnfrozen = true; + + // Iterate through all cells and properly unfreeze buildings using CleanFog + LTRBStruct bounds = MapClass::Instance.MapCoordBounds; + for (int y = bounds.Top; y < bounds.Bottom; y++) { + for (int x = bounds.Left; x < bounds.Right; x++) { + CellStruct cellCoord { static_cast(x), static_cast(y) }; + if (auto pCurrentCell = MapClass::Instance.TryGetCellAt(cellCoord)) { + // Clear fog flag + pCurrentCell->Flags &= ~CellFlags::Fogged; + + // Properly clean fog from this cell (this should restore building animations) + pCurrentCell->CleanFog(); + } + } + } + } + + // Reset flag when SpySat goes offline + static bool wasActive = true; + if (!HouseClass::CurrentPlayer->SpySatActive && wasActive) { + hasUnfrozen = false; + } + wasActive = HouseClass::CurrentPlayer->SpySatActive; + + return 0x457C80; // Skip fog freezing + } + + if (!pCell) + pCell = pThis->GetCell(); + + pThis->Deselect(); + FoggedObject* pFoggedBld = GameCreate(pThis, IsVisible); + CellExt::ExtMap.Find(pCell)->FoggedObjects.AddItem(pFoggedBld); + + auto MapCoords = pThis->GetMapCoords(); + + for (auto pFoundation = pThis->Type->GetFoundationData(false); + pFoundation->X != 0x7FFF || pFoundation->Y != 0x7FFF; + ++pFoundation) + { + CellStruct currentMapCoord { MapCoords.X + pFoundation->X,MapCoords.Y + pFoundation->Y }; + CellClass* pCurrentCell = MapClass::Instance.GetCellAt(currentMapCoord); + if (pCurrentCell != pCell) + CellExt::ExtMap.Find(pCurrentCell)->FoggedObjects.AddItem(pFoggedBld); + } + + return 0x457C80; +} + +// 486C50 = CellClass_ClearFoggedObjects, 6 Dumplcate + +DEFINE_HOOK(0x70076E, TechnoClass_GetCursorOverCell_OverFog, 0x5) +{ + GET(CellClass*, pCell, EBP); + auto const pExt = CellExt::ExtMap.Find(pCell); + + int nOvlIdx = -1; + for (auto const pObject : pExt->FoggedObjects) + { + if (pObject->Visible) + { + if (pObject->CoveredType == FoggedObject::CoveredType::Overlay) + nOvlIdx = pObject->OverlayData.Overlay; + else if (pObject->CoveredType == FoggedObject::CoveredType::Building) + { + if (HouseClass::CurrentPlayer->IsAlliedWith(pObject->BuildingData.Owner) && pObject->BuildingData.Type->LegalTarget) + R->Stack(STACK_OFFSET(0x2C, 0x19), true); + } + } + } + + if (nOvlIdx != -1) + R->Stack(STACK_OFFSET(0x2C, 0x18), OverlayTypeClass::Array.GetItem(nOvlIdx)); + + return 0x700815; +} + +DEFINE_HOOK(0x51F95F, InfantryClass_GetCursorOverCell_OverFog, 0x6) +{ + GET(InfantryClass*, pThis, EDI); + GET(CellClass*, pCell, EAX); + GET_STACK(const bool, bFog, STACK_OFFSET(0x1C, -0x8)); + + BuildingTypeClass* pType = nullptr; + int ret = 0x51FA93; + + do + { + if (!ScenarioClass::Instance->SpecialFlags.FogOfWar || !bFog) + break; + + for (const auto pObject : CellExt::ExtMap.Find(pCell)->FoggedObjects) + { + if (pObject->Visible && pObject->CoveredType == FoggedObject::CoveredType::Building) + { + pType = pObject->BuildingData.Type; + + if (pThis->Type->Engineer && pThis->Owner->IsControlledByCurrentPlayer()) + { + if (pType->BridgeRepairHut) + { + R->EAX(&pObject->Location); + ret = 0x51FA35; + } + else // Ares hook 51FA82 = InfantryClass_GetActionOnCell_EngineerRepairable, 6 + ret = 0x51FA82; + } + break; + } + } + } + while (false); + + R->EBP(pType); + return ret; +} + +DEFINE_HOOK(0x6D3470, TacticalClass_DrawFoggedObject, 0x8) +{ + GET(TacticalClass*, pThis, ECX); + GET_STACK(RectangleStruct*, pRect1, 0x4); + GET_STACK(RectangleStruct*, pRect2, 0x8); + GET_STACK(bool, bForceViewBounds, 0xC); + + // SpySat reveals everything, so don't draw fogged objects when SpySat is active + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) + return 0x6D3650; // Skip fogged object rendering + + RectangleStruct finalRect { 0,0,0,0 }; + if (bForceViewBounds && DSurface::ViewBounds.Width > 0 && DSurface::ViewBounds.Height > 0) + finalRect = std::move(FoggedObject::Union(finalRect, DSurface::ViewBounds)); + else + { + if (pRect1->Width > 0 && pRect1->Height > 0) + finalRect = std::move(FoggedObject::Union(finalRect, *pRect1)); + if (pRect2->Width > 0 && pRect2->Height > 0) + finalRect = std::move(FoggedObject::Union(finalRect, *pRect2)); + + if (const auto nVisibleCellCount = pThis->VisibleCellCount) + { + RectangleStruct buffer; + buffer.Width = buffer.Height = 60; + + for (int i = 0; i < nVisibleCellCount; ++i) + { + auto const pCell = pThis->VisibleCells[i]; + auto location = pCell->GetCoords(); + auto [point, visible] = TacticalClass::Instance->CoordsToClient(location); + buffer.X = DSurface::ViewBounds.X + point.X - 30; + buffer.Y = DSurface::ViewBounds.Y + point.Y; + finalRect = std::move(FoggedObject::Union(finalRect, buffer)); + } + } + + // TODO: Fix Drawing::DirtyAreas() access - may not exist in current Phobos + //for (int i = 0; i < Drawing::DirtyAreas().Count; i++) + //{ + // RectangleStruct buffer = Drawing::DirtyAreas()[i].Rect; + // buffer.Y += DSurface::ViewBounds.Y; + // if (buffer.Width > 0 && buffer.Height > 0) + // finalRect = std::move(FoggedObject::Union(finalRect, buffer)); + //} + } + + if (finalRect.Width > 0 && finalRect.Height > 0) + { + finalRect.X = std::max(finalRect.X, 0); + finalRect.Y = std::max(finalRect.Y, 0); + finalRect.Width = std::min(finalRect.Width, DSurface::ViewBounds.Width - finalRect.X); + finalRect.Height = std::min(finalRect.Height, DSurface::ViewBounds.Height - finalRect.Y); + + for (const auto pObject : FoggedObject::FoggedObjects) + pObject->Render(finalRect); + } + + return 0x6D3650; +} + +DEFINE_HOOK(0x4ADFF0, MapClass_RevealMapShroud, 0x5) +{ + GET_STACK(bool, bHideBuilding, 0x4); + GET_STACK(bool, bFog, 0x8); + + for (int i = 0; i < TechnoClass::Array.Count; i++) + { + auto const pTechno = TechnoClass::Array.GetItem(i); + + if (pTechno->WhatAmI() != AbstractType::Building || !bHideBuilding) + { + if (pTechno->GetTechnoType()->RevealToAll || + pTechno->DiscoveredByCurrentPlayer && pTechno->Owner->IsControlledByCurrentPlayer() || + RulesClass::Instance->AllyReveal && pTechno->Owner->IsAlliedWith(HouseClass::CurrentPlayer)) + { + pTechno->See(0, bFog); + if (pTechno->IsInAir()) + MapClass::Instance.RevealArea3(&pTechno->Location, + pTechno->LastSightRange - 3, pTechno->LastSightRange + 3, false); + } + } + } + + return 0x4AE0A5; +} + +DEFINE_HOOK(0x577EBF, MapClass_Reveal, 0x6) +{ + GET(CellClass*, pCell, EAX); + + // Vanilla YR code + pCell->ShroudCounter = 0; + pCell->GapsCoveringThisCell = 0; + pCell->AltFlags |= AltCellFlags::Clear; + pCell->Flags |= CellFlags::Revealed; + + // Extra process + pCell->CleanFog(); + + return 0x577EE9; +} + +DEFINE_HOOK(0x586683, CellClass_DiscoverTechno, 0x5) +{ + GET(TechnoClass*, pTechno, EAX); + GET(CellClass*, pThis, ESI); + GET_STACK(HouseClass*, pHouse, STACK_OFFSET(0x18, -0x8)); + + if (pTechno) + pTechno->DiscoveredBy(pHouse); + if (pThis->Jumpjet) + pThis->Jumpjet->DiscoveredBy(pHouse); + + // EAX seems not to be used actually, so we needn't to set EAX here + + return 0x586696; +} + +DEFINE_HOOK(0x4FC1FF, HouseClass_PlayerDefeated_MapReveal, 0x6) +{ + GET(HouseClass*, pHouse, ESI); + + *(int*)0xA8B538 = 1; + MapClass::Instance.Reveal(pHouse); + + return 0x4FC214; +} \ No newline at end of file diff --git a/src/Misc/MapRevealer.cpp b/src/Misc/MapRevealer.cpp new file mode 100644 index 0000000000..c3926c34fd --- /dev/null +++ b/src/Misc/MapRevealer.cpp @@ -0,0 +1,221 @@ +#include "MapRevealer.h" + +#include +#include +#include +#include +#include +#include + +#include + +MapRevealer::MapRevealer(const CoordStruct& coords) : + BaseCell(this->TranslateBaseCell(coords)), + CellOffset(this->GetOffset(coords, this->Base())), + RequiredChecks(RequiresExtraChecks()) +{ + auto const& Rect = MapClass::Instance.MapRect; + this->MapWidth = Rect.Width; + this->MapHeight = Rect.Height; + + this->CheckedCells[0] = { 7, static_cast(this->MapWidth + 5) }; + this->CheckedCells[1] = { 13, static_cast(this->MapWidth + 11) }; + this->CheckedCells[2] = { static_cast(this->MapHeight + 13), + static_cast(this->MapHeight + this->MapWidth - 15) }; +} + +MapRevealer::MapRevealer(const CellStruct& cell) : + MapRevealer(MapClass::Instance.GetCellAt(cell)->GetCoordsWithBridge()) +{ +} + +template +void MapRevealer::RevealImpl(const CoordStruct& coords, int const radius, HouseClass* const pHouse, bool const onlyOutline, bool const allowRevealByHeight, T func) const +{ + auto const level = coords.Z / *reinterpret_cast(0xABDE88); + auto const& base = this->Base(); + + if (this->AffectsHouse(pHouse) && this->IsCellAvailable(base) && radius > 0) + { + auto const spread = std::min(static_cast(radius), CellSpreadEnumerator::Max); + auto const spread_limit_sqr = (spread + 1) * (spread + 1); + + auto const start = (!RulesClass::Instance->RevealByHeight && onlyOutline && spread > 2) + ? spread - 3 : 0u; + + auto const checkLevel = allowRevealByHeight && RulesClass::Instance->RevealByHeight; + + for (CellSpreadEnumerator it(spread, start); it; ++it) + { + auto const& offset = *it; + auto const cell = base + offset; + + if (this->IsCellAvailable(cell)) + { + if (std::abs(offset.X) <= static_cast(spread) && offset.MagnitudeSquared() < spread_limit_sqr) + { + if (!checkLevel || this->CheckLevel(offset, level)) + { + auto pCell = MapClass::Instance.GetCellAt(cell); + func(pCell); + } + } + } + } + } +}; + +void MapRevealer::Reveal0(const CoordStruct& coords, int const radius, HouseClass* const pHouse, bool onlyOutline, bool unknown, bool fog, bool allowRevealByHeight, bool add) const +{ + this->RevealImpl(coords, radius, pHouse, onlyOutline, allowRevealByHeight, [=](CellClass* const pCell) + { + this->Process0(pCell, unknown, fog, add); + }); +} + +void MapRevealer::Reveal1(const CoordStruct& coords, int const radius, HouseClass* const pHouse, bool onlyOutline, bool fog, bool allowRevealByHeight, bool add) const +{ + this->RevealImpl(coords, radius, pHouse, onlyOutline, allowRevealByHeight, [=](CellClass* const pCell) + { + this->Process1(pCell, fog, add); + }); +} + +void MapRevealer::UpdateShroud(size_t start, size_t radius, bool fog) const +{ + if (!fog) + { + auto const& base = this->Base(); + radius = std::min(radius, CellSpreadEnumerator::Max); + start = std::min(start, CellSpreadEnumerator::Max - 3); + + for (CellSpreadEnumerator it(radius, start); it; ++it) + { + auto const& offset = *it; + auto const cell = base + offset; + auto const pCell = MapClass::Instance.GetCellAt(cell); + + bool bFlag = false; + if (pCell->Visibility != 0xFF) + { + auto shroudOcculusion = TacticalClass::Instance->GetOcclusion(cell, false); + if (pCell->Visibility != shroudOcculusion) + { + pCell->Visibility = shroudOcculusion; + pCell->VisibilityChanged = true; + bFlag = true; + } + } + + if (!ScenarioClass::Instance->SpecialFlags.FogOfWar && !bFlag) + { + continue; + } + + if (pCell->Foggedness != 0xFF) + { + auto foggedOcclusion = TacticalClass::Instance->GetOcclusion(cell, true); + if (pCell->Foggedness != foggedOcclusion) + { + pCell->Foggedness = foggedOcclusion; + bFlag = true; + } + } + + if (bFlag) + { + TacticalClass::Instance->RegisterCellAsVisible(pCell); + } + + /*auto shroudOcclusion = TacticalClass::Instance->GetOcclusion(cell, false); + if (pCell->Visibility != shroudOcclusion) { + pCell->Visibility = shroudOcclusion; + pCell->VisibilityChanged = true; + TacticalClass::Instance->RegisterCellAsVisible(pCell); + }*/ + } + } +} + +void MapRevealer::Process0(CellClass* const pCell, bool unknown, bool fog, bool add) const +{ + pCell->Flags &= ~CellFlags::IsPlot; + + if (this->IsCellAllowed(pCell->MapCoords)) + { + if (fog) + { + if ((pCell->Flags & CellFlags::Revealed) != CellFlags::Revealed && pCell->AltFlags & AltCellFlags::Mapped) + { + MouseClass::Instance.MapCellFoggedness(&pCell->MapCoords, HouseClass::CurrentPlayer); + } + } + else + { + if ((pCell->AltFlags & AltCellFlags::Clear) != AltCellFlags::Clear || (pCell->Flags & CellFlags::Revealed) != CellFlags::Revealed) + { + if (!unknown) + { + if (add) + { + MouseClass::Instance.RevealFogShroud(&pCell->MapCoords, HouseClass::CurrentPlayer, false); + } + else + { + pCell->Unshroud(); + } + } + } + } + } +} + +void MapRevealer::Process1(CellClass* const pCell, bool fog, bool add) const +{ + pCell->Flags &= ~CellFlags::IsPlot; + + if (fog) + { + if ((pCell->Flags & CellFlags::Revealed) != CellFlags::Revealed && pCell->AltFlags & AltCellFlags::Mapped) + { + MouseClass::Instance.MapCellFoggedness(&pCell->MapCoords, HouseClass::CurrentPlayer); + } + } + else + { + if (this->IsCellAllowed(pCell->MapCoords)) + { + MouseClass::Instance.RevealFogShroud(&pCell->MapCoords, HouseClass::CurrentPlayer, add); + } + } +} + +void __fastcall MapRevealer::MapClass_RevealArea0(MapClass* pThis, void*, CoordStruct* pCoord, + int nRadius, HouseClass* pHouse, int bOutlineOnly, bool bNoShroudUpdate, bool bFog, + bool bAllowRevealByHeight, bool bHideOnRadar) +{ + MapRevealer const revealer(*pCoord); + revealer.Reveal0(*pCoord, nRadius, pHouse, bOutlineOnly, bNoShroudUpdate, bFog, bAllowRevealByHeight, bHideOnRadar); + revealer.UpdateShroud(0, static_cast(std::max(nRadius, 0)), false); +} + +void __fastcall MapRevealer::MapClass_RevealArea1(MapClass* pThis, void*, CoordStruct* pCoord, + int nRadius, HouseClass* pHouse, int bOutlineOnly, bool bNoShroudUpdate, bool bFog, + bool bAllowRevealByHeight, bool bIncreaseShroudCounter) +{ + MapRevealer const revealer(*pCoord); + revealer.Reveal1(*pCoord, nRadius, pHouse, bOutlineOnly, bFog, bAllowRevealByHeight, bIncreaseShroudCounter); +} + +void __fastcall MapRevealer::MapClass_RevealArea2(MapClass* pThis, void*, + CoordStruct* Coords, int Height, int Radius, bool bSkipReveal) +{ + MapRevealer const revealer(*Coords); + revealer.UpdateShroud(static_cast(std::max(Height, 0)), static_cast(std::max(Radius, 0)), bSkipReveal); +} + +DEFINE_FUNCTION_JUMP(LJMP, 0x5673A0, MapRevealer::MapClass_RevealArea0); + +DEFINE_FUNCTION_JUMP(LJMP, 0x5678E0, MapRevealer::MapClass_RevealArea1); + +DEFINE_FUNCTION_JUMP(LJMP, 0x567DA0, MapRevealer::MapClass_RevealArea2); \ No newline at end of file diff --git a/src/Misc/MapRevealer.h b/src/Misc/MapRevealer.h new file mode 100644 index 0000000000..2457ba8894 --- /dev/null +++ b/src/Misc/MapRevealer.h @@ -0,0 +1,133 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class MapRevealer +{ +public: + MapRevealer(const CoordStruct& coords); + + MapRevealer(const CellStruct& cell); + + const CellStruct& Base() const + { + return this->BaseCell; + } + + void Reveal0(const CoordStruct& coords, int const radius, HouseClass* const pHouse, bool onlyOutline, bool unknown, bool fog, bool allowRevealByHeight, bool add) const; + + void Reveal1(const CoordStruct& coords, int const radius, HouseClass* const pHouse, bool onlyOutline, bool fog, bool allowRevealByHeight, bool add) const; + + void UpdateShroud(size_t start, size_t radius, bool fog = false) const; + + void Process0(CellClass* const pCell, bool unknown, bool fog, bool add) const; + + void Process1(CellClass* const pCell, bool fog, bool add) const; + + bool IsCellAllowed(const CellStruct& cell) const + { + if (this->RequiredChecks) + { + for (const auto& checkedCell : CheckedCells) + { + if (checkedCell == cell) + { + return false; + } + } + } + return true; + } + + bool IsCellAvailable(const CellStruct& cell) const + { + auto const sum = cell.X + cell.Y; + + return sum > this->MapWidth + && cell.X - cell.Y < this->MapWidth + && cell.Y - cell.X < this->MapWidth + && sum <= this->MapWidth + 2 * this->MapHeight; + } + + bool CheckLevel(const CellStruct& offset, int level) const + { + auto const cellLevel = this->Base() + offset + GetRelation(offset) - this->CellOffset; + return MapClass::Instance.GetCellAt(cellLevel)->Level < level + CellClass::BridgeLevels; + } + + static bool AffectsHouse(HouseClass* const pHouse) + { + auto const Player = HouseClass::CurrentPlayer; + + if (pHouse == Player) + { + return true; + } + + if (!pHouse || !Player) + { + return false; + } + + return pHouse->RadarVisibleTo.Contains(Player) || + (RulesClass::Instance->AllyReveal && pHouse->IsAlliedWith(Player)); + } + + static bool RequiresExtraChecks() + { + auto const Session = SessionClass::Instance; + return Session.GameMode == GameMode::LAN || Session.GameMode == GameMode::Internet && + Session.MPGameMode && !Session.MPGameMode->vt_entry_04(); + } + + static CellStruct GetRelation(const CellStruct& offset) + { + return{ static_cast(Math::sgn(-offset.X)), + static_cast(Math::sgn(-offset.Y)) }; + } + +private: + CellStruct TranslateBaseCell(const CoordStruct& coords) const + { + auto const adjust = (TacticalClass::AdjustForZ(coords.Z) / -30) << 8; + auto const baseCoords = coords + CoordStruct { adjust, adjust, 0 }; + return CellClass::Coord2Cell(baseCoords); + } + + CellStruct GetOffset(const CoordStruct& coords, const CellStruct& base) const + { + return base - CellClass::Coord2Cell(coords) - CellStruct { 2, 2 }; + } + + template + void RevealImpl(const CoordStruct& coords, int radius, HouseClass* pHouse, bool onlyOutline, bool allowRevealByHeight, T func) const; + + CellStruct BaseCell; + CellStruct CellOffset; + CellStruct CheckedCells[3]; + bool RequiredChecks; + int MapWidth; + int MapHeight; + +public: + // Reveal_Area + static void __fastcall MapClass_RevealArea0(MapClass* pThis, void*, CoordStruct* pCoord, + int nRadius, HouseClass* pHouse, int bOutlineOnly, bool bNoShroudUpdate, bool bFog, + bool bAllowRevealByHeight, bool bHideOnRadar); + + // Sight_From + static void __fastcall MapClass_RevealArea1(MapClass* pThis, void*, CoordStruct* pCoord, + int nRadius, HouseClass* pHouse, int bOutlineOnly, bool bNoShroudUpdate, bool bFog, + bool bAllowRevealByHeight, bool bIncreaseShroudCounter); + + static void __fastcall MapClass_RevealArea2(MapClass* pThis, void*, + CoordStruct* Coords, int Height, int Radius, bool bSkipReveal); +}; \ No newline at end of file diff --git a/src/New/Entity/FoggedObject.cpp b/src/New/Entity/FoggedObject.cpp new file mode 100644 index 0000000000..65313f091a --- /dev/null +++ b/src/New/Entity/FoggedObject.cpp @@ -0,0 +1,545 @@ +#include "FoggedObject.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +DynamicVectorClass FoggedObject::FoggedObjects; +char FoggedObject::BuildingVXLDrawer[sizeof(BuildingClass)]; + +void FoggedObject::SaveGlobal(IStream* pStm) +{ + pStm->Write(&FoggedObjects.Count, sizeof(FoggedObjects.Count), nullptr); + for (auto const pObject : FoggedObjects) + { + pStm->Write(&pObject, sizeof(pObject), nullptr); + pObject->Save(pStm); + } + + pStm->Write(BuildingVXLDrawer, sizeof(BuildingVXLDrawer), nullptr); +} + +void FoggedObject::LoadGlobal(IStream* pStm) +{ + int nCount; + pStm->Read(&nCount, sizeof(nCount), nullptr); + while (nCount--) + { + auto pObject = GameCreate(); + long pOldObject; + pStm->Read(&pOldObject, sizeof(pOldObject), nullptr); + SwizzleManagerClass::Instance.Here_I_Am(pOldObject, pObject); + pObject->Load(pStm); + } + + pStm->Read(BuildingVXLDrawer, sizeof(BuildingVXLDrawer), nullptr); +} + +FoggedObject::FoggedObject() noexcept +{ + FoggedObjects.AddItem(this); +} + +FoggedObject::FoggedObject(BuildingClass* pBld, bool IsVisible) noexcept +{ + CoveredType = CoveredType::Building; + + Location = pBld->Location; + Visible = IsVisible; + BuildingData.Owner = pBld->Owner; + BuildingData.Type = pBld->Type; + BuildingData.ShapeFrame = pBld->GetShapeNumber(); + BuildingData.PrimaryFacing = pBld->PrimaryFacing; + BuildingData.BarrelFacing = pBld->BarrelFacing; + BuildingData.TurretRecoil = pBld->TurretRecoil; + BuildingData.BarrelRecoil = pBld->BarrelRecoil; + BuildingData.IsFirestormWall = pBld->Type->FirestormWall; + BuildingData.TurretAnimFrame = pBld->TurretAnimFrame; + pBld->GetRenderDimensions(&Bound); + + memset(BuildingData.Anims, 0, sizeof(BuildingData.Anims)); + auto pAnimData = BuildingData.Anims; + for (auto pAnim : pBld->Anims) + { + if (pAnim) + { + pAnimData->AnimType = pAnim->Type; + pAnimData->AnimFrame = pAnim->Animation.Value + pAnimData->AnimType->Start; + pAnimData->ZAdjust = pAnim->ZAdjust + pAnimData->AnimType->YDrawOffset - TacticalClass::AdjustForZ(pAnim->Location.Z); + pAnimData->ZAdjust -= pAnimData->AnimType->Flat ? 3 : 2; + ++pAnimData; + + pAnim->IsFogged = true; + RectangleStruct buffer; + pAnim->GetDimensions(&buffer); + Bound = std::move(Drawing::Union(Bound, buffer)); + } + } + + TacticalClass::Instance->RegisterDirtyArea(Bound, false); + Bound.X += TacticalClass::Instance->TacticalPos.X; + Bound.Y += TacticalClass::Instance->TacticalPos.Y; + pBld->IsFogged = true; + + if (!BuildingVXLDrawer[0] && pBld->Type->TurretAnimIsVoxel || pBld->Type->BarrelAnimIsVoxel) + { + memcpy(BuildingVXLDrawer, pBld, sizeof(BuildingClass)); + reinterpret_cast(BuildingVXLDrawer)->BeingWarpedOut = false; + reinterpret_cast(BuildingVXLDrawer)->WarpFactor = 0.0f; + } + + FoggedObjects.AddItem(this); +} + +FoggedObject::FoggedObject(TerrainClass* pTerrain) noexcept +{ + Location = pTerrain->Location; + CoveredType = CoveredType::Terrain; + + pTerrain->GetRenderDimensions(&Bound); + Bound.X += TacticalClass::Instance->TacticalPos.X - DSurface::ViewBounds.X; + Bound.Y += TacticalClass::Instance->TacticalPos.Y - DSurface::ViewBounds.Y; + + TerrainData.Type = pTerrain->Type; + TerrainData.Frame = 0; + if (TerrainData.Type->IsAnimated) + TerrainData.Frame = pTerrain->Animation.Value; + else if (pTerrain->IsCrumbling) + TerrainData.Frame = pTerrain->Animation.Value + 1; + else if (pTerrain->IsBurning) + TerrainData.Frame = 2; + + TerrainData.Flat = TerrainData.Type->IsAnimated || pTerrain->IsCrumbling; + + FoggedObjects.AddItem(this); +} + +FoggedObject::FoggedObject(CellClass* pCell, bool IsOverlay) noexcept +{ + pCell->GetCoords(&Location); + if (IsOverlay) + { + CoveredType = CoveredType::Overlay; + + RectangleStruct containingRect; + RectangleStruct shapeRect; + pCell->ShapeRect(&shapeRect); + pCell->GetContainingRect(&containingRect); + Bound = std::move(Drawing::Union(shapeRect, containingRect)); + Bound.X += TacticalClass::Instance->TacticalPos.X; + Bound.Y += TacticalClass::Instance->TacticalPos.Y; + + OverlayData.Overlay = pCell->OverlayTypeIndex; + OverlayData.OverlayData = pCell->OverlayData; + } + else + { + CoveredType = CoveredType::Smudge; + + Location.Z = pCell->Level * Unsorted::LevelHeight; + auto [position, visible] = TacticalClass::Instance->CoordsToClient(Location); + + Bound = RectangleStruct + { + position.X - 30 + TacticalClass::Instance->TacticalPos.X, + position.Y - 15 + TacticalClass::Instance->TacticalPos.Y, + 60, + 30 + }; + + SmudgeData.Smudge = pCell->SmudgeTypeIndex; + SmudgeData.SmudgeData = pCell->SmudgeData; + SmudgeData.Height = Location.Z; + } + + FoggedObjects.AddItem(this); +} + +void FoggedObject::Load(IStream* pStm) +{ + pStm->Read(this, sizeof(FoggedObject), nullptr); + + if (CoveredType == CoveredType::Building) + { + SWIZZLE(BuildingData.Owner); + SWIZZLE(BuildingData.Type); + for (auto& Anim : BuildingData.Anims) + { + if (!Anim.AnimType) + break; + SWIZZLE(Anim.AnimType); + } + } + else if (CoveredType == CoveredType::Terrain) + { + SWIZZLE(TerrainData.Type); + } +} + +void FoggedObject::Save(IStream* pStm) +{ + pStm->Write(this, sizeof(FoggedObject), nullptr); +} + +FoggedObject::~FoggedObject() +{ + FoggedObjects.Remove(this); + + if (this->CoveredType == CoveredType::Building) + { + auto pCell = MapClass::Instance.GetCellAt(Location); + if (auto pBld = pCell->GetBuilding()) + { + pBld->IsFogged = false; + for (auto pAnim : pBld->Anims) + if (pAnim) + pAnim->IsFogged = false; + } + } +} + +void FoggedObject::Render(const RectangleStruct& viewRect) const +{ + if (!Visible) + return; + + RectangleStruct buffer = Bound; + buffer.X += DSurface::ViewBounds.X - TacticalClass::Instance->TacticalPos.X; + buffer.Y += DSurface::ViewBounds.Y - TacticalClass::Instance->TacticalPos.Y; + RectangleStruct finalRect = Drawing::Intersect(buffer, viewRect); + if (finalRect.Width <= 0 || finalRect.Height <= 0) + return; + + switch (CoveredType) + { + case CoveredType::Building: + RenderAsBuilding(viewRect); + break; + + case CoveredType::Overlay: + RenderAsOverlay(viewRect); + break; + + case CoveredType::Terrain: + RenderAsTerrain(viewRect); + break; + + case CoveredType::Smudge: + RenderAsSmudge(viewRect); + break; + } +} + +RectangleStruct FoggedObject::Union(const RectangleStruct& rect1, const RectangleStruct& rect2) +{ + if (rect1.Width <= 0 || rect1.Height <= 0) + return rect2; + if (rect2.Width <= 0 || rect2.Height <= 0) + return rect1; + + return + { + std::min(rect1.X,rect2.X), + std::min(rect1.Y,rect2.Y), + std::max(rect1.X + rect1.Width,rect2.X + rect2.Width), + std::max(rect1.Y + rect1.Height,rect2.Y + rect2.Height) + }; +} + +int FoggedObject::GetIndexID() const +{ + int x = Location.X / 256; + int y = Location.Y / 256; + return (y - ((x + y) << 9) - x) - static_cast(CoveredType) * 0x80000 + INT_MAX; +} + +void FoggedObject::RenderAsBuilding(const RectangleStruct& viewRect) const +{ + auto const pType = BuildingData.Type; + if (pType->InvisibleInGame) + return; + + auto pScheme = ColorScheme::Array.GetItem(BuildingData.Owner->ColorSchemeIndex); + auto pSHP = pType->GetImage(); + CoordStruct coord = { Location.X - 128,Location.Y - 128,Location.Z }; + auto [point, visible] = TacticalClass::Instance->CoordsToClient(coord); + point.X += DSurface::ViewBounds.X - viewRect.X; + point.Y += DSurface::ViewBounds.Y - viewRect.Y; + + auto pCell = MapClass::Instance.GetCellAt(Location); + ConvertClass* pConvert; + if (!pType->TerrainPalette) + pConvert = pScheme->LightConvert; + else + { + if (!pCell->LightConvert) + pCell->InitLightConvert(); + pConvert = pCell->LightConvert; + } + if (pType->Palette) + pConvert = pType->Palette->GetItem(BuildingData.Owner->ColorSchemeIndex)->LightConvert; + + if (pSHP) + { + RectangleStruct rect = viewRect; + int height = point.Y + pSHP->Height / 2; + if (rect.Height > height) + rect.Height = height; + int ZAdjust = -2 - TacticalClass::AdjustForZ(Location.Z); + int Intensity = pCell->Intensity_Normal + pType->ExtraLight; + if (rect.Height > 0) + { + if (BuildingData.IsFirestormWall) + { + CC_Draw_Shape(DSurface::Temp, pConvert, pSHP, BuildingData.ShapeFrame, &point, &rect, + BlitterFlags::ZReadWrite | BlitterFlags::Alpha | BlitterFlags::bf_400 | BlitterFlags::Centered, + 0, ZAdjust, ZGradient::Ground, Intensity, 0, nullptr, 0, 0, 0); + } + else + { + CC_Draw_Shape(DSurface::Temp, pConvert, pSHP, BuildingData.ShapeFrame, &point, &rect, + BlitterFlags::ZReadWrite | BlitterFlags::Alpha | BlitterFlags::bf_400 | BlitterFlags::Centered, + 0, ZAdjust, ZGradient::Deg90, Intensity, 0, nullptr, 0, 0, 0); + CC_Draw_Shape(DSurface::Temp, pConvert, pSHP, BuildingData.ShapeFrame + pSHP->Frames / 2, &point, &rect, + BlitterFlags::ZReadWrite | BlitterFlags::Alpha | BlitterFlags::bf_400 | BlitterFlags::Centered | BlitterFlags::Darken, + 0, ZAdjust, ZGradient::Ground, 1000, 0, nullptr, 0, 0, 0); + if (pType->BibShape) + { + CC_Draw_Shape(DSurface::Temp, pConvert, pType->BibShape, BuildingData.ShapeFrame, &point, &viewRect, + BlitterFlags::ZReadWrite | BlitterFlags::Alpha | BlitterFlags::bf_400 | BlitterFlags::Centered, + 0, ZAdjust - 1, ZGradient::Deg90, Intensity, 0, nullptr, 0, 0, 0); + } + } + } + Point2D turretPoint + { + point.X + pType->GetBuildingAnim(BuildingAnimSlot::Turret).Position.X, + point.Y + pType->GetBuildingAnim(BuildingAnimSlot::Turret).Position.Y + }; + if (pType->TurretAnimIsVoxel || pType->BarrelAnimIsVoxel) + { + auto pVXLDrawer = reinterpret_cast(BuildingVXLDrawer); + pVXLDrawer->Type = pType; + pVXLDrawer->SecondaryFacing = BuildingData.PrimaryFacing; + pVXLDrawer->BarrelFacing = BuildingData.BarrelFacing; + pVXLDrawer->TurretRecoil = BuildingData.TurretRecoil; + pVXLDrawer->BarrelRecoil = BuildingData.BarrelRecoil; + pVXLDrawer->Owner = BuildingData.Owner; + pVXLDrawer->Location = Location; + pVXLDrawer->TurretAnimFrame = BuildingData.TurretAnimFrame; + + auto const primaryDir = BuildingData.PrimaryFacing.Current(); + int turretFacing = 0; + int barrelFacing = 0; + if (pType->TurretVoxel.HVA) + turretFacing = BuildingData.TurretAnimFrame % pType->TurretVoxel.HVA->FrameCount; + if (pType->BarrelVoxel.HVA) + barrelFacing = BuildingData.TurretAnimFrame % pType->BarrelVoxel.HVA->FrameCount; + int val32 = primaryDir.Raw; + int turretExtra = ((unsigned char)turretFacing << 16) | val32; + int barrelExtra = ((unsigned char)barrelFacing << 16) | val32; + + if (pType->TurretVoxel.VXL) + { + Matrix3D matrixturret; + matrixturret.MakeIdentity(); + matrixturret.RotateZ(static_cast(primaryDir.GetRadian<256>())); + TechnoTypeExt::ApplyTurretOffset(pType, &matrixturret, 0.125); + + Vector3D negativevector = { -matrixturret.Row[0].W ,-matrixturret.Row[1].W,-matrixturret.Row[2].W }; + Vector3D vector = { matrixturret.Row[0].W ,matrixturret.Row[1].W,matrixturret.Row[2].W }; + Matrix3D matrixbarrel = matrixturret; + if (BuildingData.TurretRecoil.State != RecoilData::RecoilState::Inactive) + { + matrixturret.TranslateX(-BuildingData.TurretRecoil.TravelSoFar); + turretExtra = -1; + } + Matrix3D::MatrixMultiply(&matrixturret, &Matrix3D::VoxelDefaultMatrix, &matrixturret); + + bool bDrawBarrel = pType->BarrelVoxel.VXL && pType->BarrelVoxel.HVA; + if (bDrawBarrel) + { + matrixbarrel.Translate(negativevector); + if (BuildingData.BarrelRecoil.State != RecoilData::RecoilState::Inactive) + { + matrixbarrel.TranslateX(-BuildingData.BarrelRecoil.TravelSoFar); + barrelExtra = -1; + } + matrixbarrel.RotateY(-static_cast(BuildingData.BarrelFacing.Current().GetRadian<256>())); + matrixbarrel.Translate(vector); + Matrix3D::MatrixMultiply(&matrixbarrel, &Matrix3D::VoxelDefaultMatrix, &matrixbarrel); + } + + int facetype = (((((*(unsigned int*)&primaryDir) >> 13) + 1) >> 1) & 3); + if (facetype == 0 || facetype == 3) // Draw barrel first + { + // TODO: Fix voxel drawing + // if (bDrawBarrel) + // pVXLDrawer->DrawVoxel(...); + // pVXLDrawer->DrawVoxel(...); + + } + else + { + // TODO: Fix voxel drawing + // pVXLDrawer->DrawVoxel(...); + // if (bDrawBarrel) + // pVXLDrawer->DrawVoxel(...); + } + } + else if (pType->BarrelVoxel.VXL && pType->BarrelVoxel.HVA) + { + Matrix3D matrixbarrel; + matrixbarrel.MakeIdentity(); + Vector3D negativevector = { -matrixbarrel.Row[0].W ,-matrixbarrel.Row[1].W,-matrixbarrel.Row[2].W }; + Vector3D vector = { matrixbarrel.Row[0].W ,matrixbarrel.Row[1].W,matrixbarrel.Row[2].W }; + matrixbarrel.Translate(negativevector); + matrixbarrel.RotateZ(static_cast(BuildingData.PrimaryFacing.Current().GetRadian<256>())); + matrixbarrel.RotateY(-static_cast(BuildingData.BarrelFacing.Current().GetRadian<256>())); + matrixbarrel.Translate(vector); + Matrix3D::MatrixMultiply(&matrixbarrel, &Matrix3D::VoxelDefaultMatrix, &matrixbarrel); + // TODO: Fix voxel drawing + // pVXLDrawer->DrawVoxel(...); + } + } + } + + for (const auto& AnimData : BuildingData.Anims) + { + if (!AnimData.AnimType) + break; + + auto pAnimType = AnimData.AnimType; + if (auto pAnimSHP = pAnimType->GetImage()) + { + ConvertClass* pAnimConvert = pAnimType->ShouldUseCellDrawer ? pScheme->LightConvert : FileSystem::ANIM_PAL; + + CC_Draw_Shape(DSurface::Temp, pAnimConvert, pAnimSHP, AnimData.AnimFrame, &point, &viewRect, + BlitterFlags::ZReadWrite | BlitterFlags::Alpha | BlitterFlags::bf_400 | BlitterFlags::Centered, + 0, AnimData.ZAdjust, pAnimType->Flat ? ZGradient::Ground : ZGradient::Deg90, + pAnimType->UseNormalLight ? 1000 : pCell->Intensity_Normal, 0, nullptr, 0, 0, 0); + if (pAnimType->Shadow) + { + CC_Draw_Shape(DSurface::Temp, pAnimConvert, pAnimSHP, AnimData.AnimFrame + pAnimSHP->Frames / 2, &point, &viewRect, + BlitterFlags::ZReadWrite | BlitterFlags::Alpha | BlitterFlags::bf_400 | BlitterFlags::Centered | BlitterFlags::Darken, + 0, AnimData.ZAdjust, ZGradient::Deg90, 1000, 0, nullptr, 0, 0, 0); + } + } + } +} + +void FoggedObject::RenderAsSmudge(const RectangleStruct& viewRect) const +{ + auto const pSmudge = SmudgeTypeClass::Array.GetItem(SmudgeData.Smudge); + Point2D position + { + this->Bound.X - TacticalClass::Instance->TacticalPos.X - viewRect.X + DSurface::ViewBounds.X + 30, + this->Bound.Y - TacticalClass::Instance->TacticalPos.Y - viewRect.Y + DSurface::ViewBounds.Y + }; + CellStruct MapCoord = CellClass::Coord2Cell(Location); + pSmudge->DrawIt(position, viewRect, SmudgeData.SmudgeData, SmudgeData.Height, MapCoord); +} + +void FoggedObject::RenderAsOverlay(const RectangleStruct& viewRect) const +{ + if (auto pCell = MapClass::Instance.TryGetCellAt(Location)) + { + CoordStruct coords = + { + (((pCell->MapCoords.X << 8) + 128) / 256) << 8, + (((pCell->MapCoords.Y << 8) + 128) / 256) << 8, + 0 + }; + auto [position, visible] = TacticalClass::Instance->CoordsToClient(coords); + position.X -= 30; + + // Temporarily swap overlay data for rendering + auto originalOverlayType = pCell->OverlayTypeIndex; + auto originalOverlayData = pCell->OverlayData; + + pCell->OverlayTypeIndex = this->OverlayData.Overlay; + pCell->OverlayData = this->OverlayData.OverlayData; + + pCell->DrawOverlay(position, viewRect); + pCell->DrawOverlayShadow(position, viewRect); + + // Restore original data + pCell->OverlayTypeIndex = originalOverlayType; + pCell->OverlayData = originalOverlayData; + } +} + +void FoggedObject::RenderAsTerrain(const RectangleStruct& viewRect) const +{ + auto pCell = MapClass::Instance.GetCellAt(Location); + if (auto pSHP = TerrainData.Type->GetImage()) + { + int nZAdjust = -TacticalClass::AdjustForZ(Location.Z); + auto [point, visible] = TacticalClass::Instance->CoordsToClient(Location); + point.X += DSurface::ViewBounds.X - viewRect.X; + point.Y += DSurface::ViewBounds.Y - viewRect.Y; + if (!pCell->LightConvert) + pCell->InitLightConvert(); + + BlitterFlags blitterFlag = BlitterFlags::Centered | BlitterFlags::bf_400 | BlitterFlags::Alpha; + if (TerrainData.Flat) + blitterFlag |= BlitterFlags::Flat; + else + blitterFlag |= BlitterFlags::ZReadWrite; + + ConvertClass* pConvert; + int nIntensity; + + if (TerrainData.Type->SpawnsTiberium) + { + pConvert = FileSystem::GRFTXT_TIBERIUM_PAL; + nIntensity = pCell->Intensity_Normal; + point.Y -= 16; + } + else + { + pConvert = pCell->LightConvert; + nIntensity = pCell->Intensity_Terrain; + } + + CC_Draw_Shape(DSurface::Temp, pConvert, pSHP, TerrainData.Frame, &point, + &viewRect, blitterFlag, 0, nZAdjust - 12, ZGradient::Deg90, nIntensity, + 0, 0, 0, 0, 0); + if (Game::bDrawShadow) + CC_Draw_Shape(DSurface::Temp, pConvert, pSHP, TerrainData.Frame + pSHP->Frames / 2, &point, + &viewRect, blitterFlag | BlitterFlags::Darken, 0, nZAdjust - 3, + ZGradient::Ground, 1000, 0, 0, 0, 0, 0); + } +} + +DEFINE_HOOK(0x67D32C, Put_All_FoggedObjects, 0x5) +{ + GET(IStream*, pStm, ESI); + + FoggedObject::SaveGlobal(pStm); + + return 0; +} + +DEFINE_HOOK(0x67E826, Decode_All_FoggedObjects, 0x6) +{ + GET(IStream*, pStm, ESI); + + FoggedObject::LoadGlobal(pStm); + + return 0; +} + +DEFINE_HOOK(0x685659, Clear_Scenario_FoggedObjects, 0xA) +{ + FoggedObject::FoggedObjects.Clear(); + + return 0; +} \ No newline at end of file diff --git a/src/New/Entity/FoggedObject.h b/src/New/Entity/FoggedObject.h new file mode 100644 index 0000000000..b1f4ab78df --- /dev/null +++ b/src/New/Entity/FoggedObject.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +// Forward declaration to avoid circular include +class CellExt; + +class FoggedObject +{ +public: + static DynamicVectorClass FoggedObjects; + + static void SaveGlobal(IStream* pStm); + static void LoadGlobal(IStream* pStm); + + explicit FoggedObject() noexcept; + explicit FoggedObject(BuildingClass* pBld, bool IsVisible) noexcept; + explicit FoggedObject(TerrainClass* pTerrain) noexcept; + + // Handles Smudge and Overlay Construct + explicit FoggedObject(CellClass* pCell, bool IsOverlay) noexcept; + + void Load(IStream* pStm); + void Save(IStream* pStm); + + //Destructor + virtual ~FoggedObject(); + + void Render(const RectangleStruct& viewRect) const; + + static RectangleStruct Union(const RectangleStruct& rect1, const RectangleStruct& rect2); +protected: + inline int GetIndexID() const; + + void RenderAsBuilding(const RectangleStruct& viewRect) const; + void RenderAsSmudge(const RectangleStruct& viewRect) const; + void RenderAsOverlay(const RectangleStruct& viewRect) const; + void RenderAsTerrain(const RectangleStruct& viewRect) const; + + static char BuildingVXLDrawer[sizeof(BuildingClass)]; +public: + enum class CoveredType : char + { + Building = 1, + Terrain, + Smudge, + Overlay + }; + + CoordStruct Location; + CoveredType CoveredType; + RectangleStruct Bound; + bool Visible { true }; + + union + { + struct + { + int Overlay; + unsigned char OverlayData; + } OverlayData; + struct + { + TerrainTypeClass* Type; + int Frame; + bool Flat; + } TerrainData; + struct + { + HouseClass* Owner; + BuildingTypeClass* Type; + int ShapeFrame; + FacingClass PrimaryFacing; + FacingClass BarrelFacing; + RecoilData TurretRecoil; + RecoilData BarrelRecoil; + bool IsFirestormWall; + int TurretAnimFrame; + struct + { + AnimTypeClass* AnimType; + int AnimFrame; + int ZAdjust; + } Anims[21]; + } BuildingData; + struct + { + int Smudge; + int SmudgeData; + int Height; + } SmudgeData; + }; +}; \ No newline at end of file From 10e3238d5a29a69223d7e008acc08bc3654fca99 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Fri, 19 Sep 2025 15:18:06 +1000 Subject: [PATCH 02/20] Fix: vision crash on SpySat=yes or high Reveal=x WH - still have issues drawing buildings --- src/Ext/Cell/Body.h | 1 + src/Misc/FogOfWar.cpp | 268 +++++++++++++++++++++++++++++++++++------- 2 files changed, 229 insertions(+), 40 deletions(-) diff --git a/src/Ext/Cell/Body.h b/src/Ext/Cell/Body.h index 6b004b562c..8704ef1434 100644 --- a/src/Ext/Cell/Body.h +++ b/src/Ext/Cell/Body.h @@ -41,6 +41,7 @@ class CellExt std::vector RadSites {}; std::vector RadLevels { }; DynamicVectorClass FoggedObjects; + int InCleanFog { 0 }; ExtData(CellClass* OwnerObject) : Extension(OwnerObject), FoggedObjects {} diff --git a/src/Misc/FogOfWar.cpp b/src/Misc/FogOfWar.cpp index 6975d5d419..82c6d4aa62 100644 --- a/src/Misc/FogOfWar.cpp +++ b/src/Misc/FogOfWar.cpp @@ -15,6 +15,153 @@ #include #include +// Safe fog proxy deletion helper - called from reveal sites where pCell is guaranteed valid +static inline void ClearCellFogProxies(CellClass* cell) { + if (auto* ext = CellExt::ExtMap.Find(cell)) { + // steal then clear + DynamicVectorClass local; + local.Reserve(ext->FoggedObjects.Count); + for (auto* fo : ext->FoggedObjects) { + if (fo) local.AddItem(fo); + } + ext->FoggedObjects.Clear(); + + // delete outside of the list + for (auto* fo : local) { + GameDelete(fo); + } + } +} + +// Track buildings we've already dirtied this frame to avoid duplication +static DynamicVectorClass g_DirtiedBldsThisFrame; + +static inline bool WasDirtiedThisFrame(BuildingClass* b) { + for (int i = 0; i < g_DirtiedBldsThisFrame.Count; ++i) { + if (g_DirtiedBldsThisFrame[i] == b) return true; + } + return false; +} + +// Dirty the full building footprint and request an immediate redraw with height adjustment +// No ExtMap usage, proper sprite coverage for tall buildings +static inline void MarkBuildingFootprintDirty(BuildingClass* bld) +{ + if (!bld) return; + + // Force sprite cache rebuild - this is the key + bld->NeedsRedraw = true; + + // Try additional invalidation methods + bld->Mark(MarkType::Up); + + RectangleStruct unionRect{0,0,0,0}; + + CellStruct origin = bld->GetMapCoords(); + for (auto f = bld->Type->GetFoundationData(false); + f->X != 0x7FFF || f->Y != 0x7FFF; ++f) + { + CellStruct cs{ + static_cast(origin.X + f->X), + static_cast(origin.Y + f->Y) + }; + + if (CellClass* c = MapClass::Instance.TryGetCellAt(cs)) { + // keep occlusion tables in sync + TacticalClass::Instance->RegisterCellAsVisible(c); + + CoordStruct world = c->GetCoords(); + auto [pt, visible] = TacticalClass::Instance->CoordsToClient(world); + + RectangleStruct cellRect{ + DSurface::ViewBounds.X + pt.X - 30, + DSurface::ViewBounds.Y + pt.Y, + 60, 60 + }; + unionRect = FoggedObject::Union(unionRect, cellRect); + } + } + + if (unionRect.Width > 0 && unionRect.Height > 0) { + // inflate upward to cover tall building sprites + const int extraTop = 96; // adjust if you still see tops cut off + if (unionRect.Y > extraTop) { + unionRect.Y -= extraTop; + unionRect.Height += extraTop; + } else { + unionRect.Height += unionRect.Y; // snap to top + unionRect.Y = 0; + } + TacticalClass::Instance->RegisterDirtyArea(unionRect, true); + } +} + +static inline void MarkBuildingFootprintDirtyOnce(BuildingClass* bld) { + if (!bld) return; + if (WasDirtiedThisFrame(bld)) return; + g_DirtiedBldsThisFrame.AddItem(bld); + MarkBuildingFootprintDirty(bld); +} + +// ===== FoggedObject GC ===== +// Requirements: +// - NO CellExt::ExtMap usage here. +// - Two-phase: collect pointers to kill, then delete. +// - Called before the fog proxy render loop each frame (or every N frames). + +static void SafeCleanupOrphanedFoggedObjects() +{ + static int frameCounter = 0; + + // Run every 15 frames to avoid performance issues during mass reveals + if ((++frameCounter % 15) != 0) { + return; + } + + auto& live = FoggedObject::FoggedObjects; // global container + if (live.Count <= 0) { + return; + } + + // Collect candidates first to avoid mutating 'live' while scanning it. + DynamicVectorClass toDelete; + // Guard reserve to something sane to avoid wild counts if memory is corrupt. + const int cap = (live.Count > 0 && live.Count < 1000000) ? live.Count : 0; + if (cap > 0) { toDelete.Reserve(cap); } + + for (int i = 0; i < live.Count; ++i) { + FoggedObject* fo = live[i]; + if (!fo) { continue; } + + // Find the cell by coordinates; DO NOT use ExtMap. + CellStruct cellCoords = CellClass::Coord2Cell(fo->Location); + CellClass* cell = MapClass::Instance.TryGetCellAt(cellCoords); + if (!cell) { + // Cell missing → definitely orphaned + toDelete.AddItem(fo); + continue; + } + + // Orphan rule: if the cell is *not covered* anymore (no fog & no shroud), + // keeping a fog proxy makes no sense; schedule for deletion. + const bool shrouded = !(cell->Flags & CellFlags::EdgeRevealed); + const bool fogged = (cell->Foggedness != -1) || (cell->Flags & CellFlags::Fogged); + + if (!shrouded && !fogged) { + toDelete.AddItem(fo); + } + } + + // Now actually remove and free. + // GameDelete() auto-removes from 'live', so we can skip Remove(fo). + for (int i = 0; i < toDelete.Count; ++i) { + FoggedObject* fo = toDelete[i]; + // Ensure it is gone from the render list before dealloc (defensive). + FoggedObject::FoggedObjects.Remove(fo); + GameDelete(fo); + } +} + DEFINE_HOOK(0x6B8E7A, ScenarioClass_LoadSpecialFlags, 0x5) { ScenarioClass::Instance->SpecialFlags.FogOfWar = @@ -219,46 +366,45 @@ DEFINE_HOOK(0x4A9CA0, MapClass_RevealFogShroud, 0x8) pThis->RevealCheck(pCell, pHouse, bWasRevealed); } - if (bShouldCleanFog) + if (bShouldCleanFog) { pCell->CleanFog(); + // ClearCellFogProxies(pCell); // DISABLED - extension system returns invalid pointers during large reveals + + // Mark all objects in this cell for redraw when fog is cleared + bool hadBuilding = false; + for (ObjectClass* obj = pCell->FirstObject; obj; obj = obj->NextObject) { + obj->NeedsRedraw = true; + if (auto bld = abstract_cast(obj)) { + hadBuilding = true; + // Only dirty when it was fully fogged and is transitioning to visible + if (bld->IsAllFogged()) { + MarkBuildingFootprintDirtyOnce(bld); + } + } + } + + // Only register 60x60 cell rect if no building was present (building footprint covers it) + if (!hadBuilding) { + auto loc = pCell->GetCoords(); + auto [pt, visible] = TacticalClass::Instance->CoordsToClient(loc); + RectangleStruct cellRect{ + DSurface::ViewBounds.X + pt.X - 30, + DSurface::ViewBounds.Y + pt.Y, + 60, 60 + }; + TacticalClass::Instance->RegisterDirtyArea(cellRect, true); + } + } R->EAX(bRevealed); return 0x4A9DC6; } -// CellClass_CleanFog DEFINE_HOOK(0x486C50, CellClass_ClearFoggedObjects, 0x6) { - GET(CellClass*, pThis, ECX); - - auto pExt = CellExt::ExtMap.Find(pThis); - for (auto const pObject : pExt->FoggedObjects) - { - if (pObject->CoveredType == FoggedObject::CoveredType::Building) - { - CellClass* pRealCell = MapClass::Instance.GetCellAt(pObject->Location); - - for (auto pFoundation = pObject->BuildingData.Type->GetFoundationData(false); - pFoundation->X != 0x7FFF || pFoundation->Y != 0x7FFF; - ++pFoundation) - { - CellStruct mapCoord = - { - pRealCell->MapCoords.X + pFoundation->X, - pRealCell->MapCoords.Y + pFoundation->Y - }; - - CellClass* pCell = MapClass::Instance.GetCellAt(mapCoord); - if (pCell != pThis) - CellExt::ExtMap.Find(pCell)->FoggedObjects.Remove(pObject); - } - - } - GameDelete(pObject); - } - pExt->FoggedObjects.Clear(); - + // Let the engine continue. We do our FoggedObject deletion + // from reveal hooks where 'pCell' is guaranteed valid. return 0x486D8A; } @@ -373,8 +519,8 @@ DEFINE_HOOK(0x457AA0, BuildingClass_FreezeInFog, 0x5) // Clear fog flag pCurrentCell->Flags &= ~CellFlags::Fogged; - // Properly clean fog from this cell (this should restore building animations) - pCurrentCell->CleanFog(); + // Don't call CleanFog during mass SpySat cleanup - it triggers extension crashes + // pCurrentCell->CleanFog(); } } } @@ -394,19 +540,29 @@ DEFINE_HOOK(0x457AA0, BuildingClass_FreezeInFog, 0x5) pCell = pThis->GetCell(); pThis->Deselect(); - FoggedObject* pFoggedBld = GameCreate(pThis, IsVisible); - CellExt::ExtMap.Find(pCell)->FoggedObjects.AddItem(pFoggedBld); - auto MapCoords = pThis->GetMapCoords(); + // helper to add an independent FoggedObject to a specific cell + auto addToCell = [&](CellClass* dst) { + auto* fo = GameCreate(pThis, IsVisible); + CellExt::ExtMap.Find(dst)->FoggedObjects.AddItem(fo); + }; + + // owner cell + addToCell(pCell); + // every other foundation cell gets its own FoggedObject instance, too + auto MapCoords = pThis->GetMapCoords(); for (auto pFoundation = pThis->Type->GetFoundationData(false); pFoundation->X != 0x7FFF || pFoundation->Y != 0x7FFF; ++pFoundation) { - CellStruct currentMapCoord { MapCoords.X + pFoundation->X,MapCoords.Y + pFoundation->Y }; - CellClass* pCurrentCell = MapClass::Instance.GetCellAt(currentMapCoord); - if (pCurrentCell != pCell) - CellExt::ExtMap.Find(pCurrentCell)->FoggedObjects.AddItem(pFoggedBld); + CellStruct cs { static_cast(MapCoords.X + pFoundation->X), + static_cast(MapCoords.Y + pFoundation->Y) }; + if (CellClass* other = MapClass::Instance.GetCellAt(cs)) { + if (other != pCell) { + addToCell(other); + } + } } return 0x457C80; @@ -487,6 +643,12 @@ DEFINE_HOOK(0x6D3470, TacticalClass_DrawFoggedObject, 0x8) GET_STACK(RectangleStruct*, pRect2, 0x8); GET_STACK(bool, bForceViewBounds, 0xC); + // Clean up orphans BEFORE computing finalRect or rendering + SafeCleanupOrphanedFoggedObjects(); + + // Clear building deduplication list once per frame + g_DirtiedBldsThisFrame.Clear(); + // SpySat reveals everything, so don't draw fogged objects when SpySat is active if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) return 0x6D3650; // Skip fogged object rendering @@ -579,6 +741,32 @@ DEFINE_HOOK(0x577EBF, MapClass_Reveal, 0x6) // Extra process pCell->CleanFog(); + // ClearCellFogProxies(pCell); // DISABLED - extension system returns invalid pointers during large reveals + + // Mark all objects in this cell for redraw when revealed + bool hadBuilding = false; + for (ObjectClass* obj = pCell->FirstObject; obj; obj = obj->NextObject) { + obj->NeedsRedraw = true; + if (auto bld = abstract_cast(obj)) { + hadBuilding = true; + // Only dirty when it was fully fogged and is transitioning to visible + if (bld->IsAllFogged()) { + MarkBuildingFootprintDirtyOnce(bld); + } + } + } + + // Only register 60x60 cell rect if no building was present (building footprint covers it) + if (!hadBuilding) { + auto loc = pCell->GetCoords(); + auto [pt, visible] = TacticalClass::Instance->CoordsToClient(loc); + RectangleStruct cellRect{ + DSurface::ViewBounds.X + pt.X - 30, + DSurface::ViewBounds.Y + pt.Y, + 60, 60 + }; + TacticalClass::Instance->RegisterDirtyArea(cellRect, true); + } return 0x577EE9; } From cd5a380627a5d64d68b87aae634cbb38fb60de65 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Fri, 19 Sep 2025 16:35:09 +1000 Subject: [PATCH 03/20] Fixed SpySat=yes / Reveal=-1 --- src/Misc/FogOfWar.cpp | 290 +++++++++++++++--------------------------- 1 file changed, 101 insertions(+), 189 deletions(-) diff --git a/src/Misc/FogOfWar.cpp b/src/Misc/FogOfWar.cpp index 82c6d4aa62..8eed94cf56 100644 --- a/src/Misc/FogOfWar.cpp +++ b/src/Misc/FogOfWar.cpp @@ -15,93 +15,26 @@ #include #include -// Safe fog proxy deletion helper - called from reveal sites where pCell is guaranteed valid -static inline void ClearCellFogProxies(CellClass* cell) { - if (auto* ext = CellExt::ExtMap.Find(cell)) { - // steal then clear - DynamicVectorClass local; - local.Reserve(ext->FoggedObjects.Count); - for (auto* fo : ext->FoggedObjects) { - if (fo) local.AddItem(fo); - } - ext->FoggedObjects.Clear(); - - // delete outside of the list - for (auto* fo : local) { - GameDelete(fo); - } +// Burst-reveal detection for viewport refresh +static int g_RevealedCellsThisFrame = 0; +static bool g_NeedViewportRefresh = false; + +// SpySat state tracking +static bool g_SpySatWasActive = false; + +static inline void ForceUnfreezeAllFogProxiesForSpySat() { + // snapshot to avoid invalidation while deleting + DynamicVectorClass snap; + snap.Reserve(FoggedObject::FoggedObjects.Count); + for (auto* fo : FoggedObject::FoggedObjects) { + if (fo) { snap.AddItem(fo); } } -} - -// Track buildings we've already dirtied this frame to avoid duplication -static DynamicVectorClass g_DirtiedBldsThisFrame; - -static inline bool WasDirtiedThisFrame(BuildingClass* b) { - for (int i = 0; i < g_DirtiedBldsThisFrame.Count; ++i) { - if (g_DirtiedBldsThisFrame[i] == b) return true; + for (auto* fo : snap) { + GameDelete(fo); // FoggedObject dtor should thaw real building & restore footprint } - return false; } -// Dirty the full building footprint and request an immediate redraw with height adjustment -// No ExtMap usage, proper sprite coverage for tall buildings -static inline void MarkBuildingFootprintDirty(BuildingClass* bld) -{ - if (!bld) return; - - // Force sprite cache rebuild - this is the key - bld->NeedsRedraw = true; - - // Try additional invalidation methods - bld->Mark(MarkType::Up); - - RectangleStruct unionRect{0,0,0,0}; - - CellStruct origin = bld->GetMapCoords(); - for (auto f = bld->Type->GetFoundationData(false); - f->X != 0x7FFF || f->Y != 0x7FFF; ++f) - { - CellStruct cs{ - static_cast(origin.X + f->X), - static_cast(origin.Y + f->Y) - }; - - if (CellClass* c = MapClass::Instance.TryGetCellAt(cs)) { - // keep occlusion tables in sync - TacticalClass::Instance->RegisterCellAsVisible(c); - - CoordStruct world = c->GetCoords(); - auto [pt, visible] = TacticalClass::Instance->CoordsToClient(world); - - RectangleStruct cellRect{ - DSurface::ViewBounds.X + pt.X - 30, - DSurface::ViewBounds.Y + pt.Y, - 60, 60 - }; - unionRect = FoggedObject::Union(unionRect, cellRect); - } - } - - if (unionRect.Width > 0 && unionRect.Height > 0) { - // inflate upward to cover tall building sprites - const int extraTop = 96; // adjust if you still see tops cut off - if (unionRect.Y > extraTop) { - unionRect.Y -= extraTop; - unionRect.Height += extraTop; - } else { - unionRect.Height += unionRect.Y; // snap to top - unionRect.Y = 0; - } - TacticalClass::Instance->RegisterDirtyArea(unionRect, true); - } -} -static inline void MarkBuildingFootprintDirtyOnce(BuildingClass* bld) { - if (!bld) return; - if (WasDirtiedThisFrame(bld)) return; - g_DirtiedBldsThisFrame.AddItem(bld); - MarkBuildingFootprintDirty(bld); -} // ===== FoggedObject GC ===== // Requirements: @@ -191,9 +124,10 @@ DEFINE_HOOK(0x5F4B3E, ObjectClass_DrawIfVisible, 0x6) if (!ScenarioClass::Instance->SpecialFlags.FogOfWar) return 0x5F4B48; - // SpySat reveals everything, so bypass fog checks when SpySat is active - if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) - return 0x5F4B48; + // SpySat: bypass all fog gating (no ExtMap access, just draw) + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) { + return 0x5F4B48; // draw normally + } switch (pThis->WhatAmI()) { @@ -204,16 +138,37 @@ DEFINE_HOOK(0x5F4B3E, ObjectClass_DrawIfVisible, 0x6) case AbstractType::Unit: case AbstractType::Cell: - case AbstractType::Building: // Allow buildings to be processed for fog + case AbstractType::Building: // Buildings participate in fog gating break; default: return 0x5F4B48; } - if (!MapClass::Instance.IsLocationFogged(pThis->GetCoords())) - return 0x5F4B48; + bool fogged = false; + + if (pThis->WhatAmI() == AbstractType::Building) { + auto* bld = static_cast(pThis); + + // Own/aligned buildings: always draw (YR behavior) + if (bld->Owner && + (bld->Owner->IsControlledByCurrentPlayer() || + (RulesClass::Instance->AllyReveal && bld->Owner->IsAlliedWith(HouseClass::CurrentPlayer)))) { + return 0x5F4B48; // draw + } + // Enemy/neutral buildings: hide only if entire footprint is fogged + fogged = bld->IsAllFogged(); + } else { + // Units, anims, terrain, cells, etc. – single-cell check is fine + fogged = MapClass::Instance.IsLocationFogged(pThis->GetCoords()); + } + + if (!fogged) { + return 0x5F4B48; // normal draw + } + + // Fogged: skip draw this frame (don't keep forcing redraw while fogged) pThis->NeedsRedraw = false; return 0x5F4D06; } @@ -367,33 +322,20 @@ DEFINE_HOOK(0x4A9CA0, MapClass_RevealFogShroud, 0x8) } if (bShouldCleanFog) { + ++g_RevealedCellsThisFrame; + // Heuristic: 64 cells ~ a modest burst. Tweak if needed. + if (g_RevealedCellsThisFrame >= 64) { + g_NeedViewportRefresh = true; + } + pCell->CleanFog(); - // ClearCellFogProxies(pCell); // DISABLED - extension system returns invalid pointers during large reveals - // Mark all objects in this cell for redraw when fog is cleared - bool hadBuilding = false; - for (ObjectClass* obj = pCell->FirstObject; obj; obj = obj->NextObject) { - obj->NeedsRedraw = true; - if (auto bld = abstract_cast(obj)) { - hadBuilding = true; - // Only dirty when it was fully fogged and is transitioning to visible - if (bld->IsAllFogged()) { - MarkBuildingFootprintDirtyOnce(bld); - } + // Just set buildings in this cell to redraw - let the fog gate handle visibility + for (ObjectClass* pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) { + if (auto pBuilding = abstract_cast(pObject)) { + pBuilding->NeedsRedraw = true; } } - - // Only register 60x60 cell rect if no building was present (building footprint covers it) - if (!hadBuilding) { - auto loc = pCell->GetCoords(); - auto [pt, visible] = TacticalClass::Instance->CoordsToClient(loc); - RectangleStruct cellRect{ - DSurface::ViewBounds.X + pt.X - 30, - DSurface::ViewBounds.Y + pt.Y, - 60, 60 - }; - TacticalClass::Instance->RegisterDirtyArea(cellRect, true); - } } R->EAX(bRevealed); @@ -403,8 +345,8 @@ DEFINE_HOOK(0x4A9CA0, MapClass_RevealFogShroud, 0x8) DEFINE_HOOK(0x486C50, CellClass_ClearFoggedObjects, 0x6) { - // Let the engine continue. We do our FoggedObject deletion - // from reveal hooks where 'pCell' is guaranteed valid. + // No deletion work here; avoid re-entrancy / bad ECX. + // Let our Ext-free GC handle deletions safely from the draw pass. return 0x486D8A; } @@ -412,22 +354,9 @@ DEFINE_HOOK(0x486A70, CellClass_FogCell, 0x5) { GET(CellClass*, pThis, ECX); - // SpySat reveals everything for the owning player, so actively remove fog instead of adding it - if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) - { - // Clear fog from this cell and surrounding area - auto location = pThis->MapCoords; - for (int i = 1; i < 15; i += 2) - { - CellClass* pCell = MapClass::Instance.GetCellAt(location); - if (pCell->Flags & CellFlags::Fogged) - { - pCell->Flags &= ~CellFlags::Fogged; - // Also clear fogged objects from this cell - CellExt::ExtMap.Find(pCell)->FoggedObjects.Clear(); - } - } - return 0x486BE6; // Skip normal fog processing + // SpySat => do not fog cells at all + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) { + return 0x486BE6; // skip fogging loop } if (ScenarioClass::Instance->SpecialFlags.FogOfWar) @@ -502,38 +431,17 @@ DEFINE_HOOK(0x457AA0, BuildingClass_FreezeInFog, 0x5) GET_STACK(CellClass*, pCell, 0x8); GET_STACK(bool, IsVisible, 0xC); - // SpySat reveals everything, so don't freeze buildings in fog when SpySat is active - if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) - { - // When SpySat is active, clear all existing fogged buildings (one-time cleanup) - static bool hasUnfrozen = false; - if (!hasUnfrozen) { - hasUnfrozen = true; - - // Iterate through all cells and properly unfreeze buildings using CleanFog - LTRBStruct bounds = MapClass::Instance.MapCoordBounds; - for (int y = bounds.Top; y < bounds.Bottom; y++) { - for (int x = bounds.Left; x < bounds.Right; x++) { - CellStruct cellCoord { static_cast(x), static_cast(y) }; - if (auto pCurrentCell = MapClass::Instance.TryGetCellAt(cellCoord)) { - // Clear fog flag - pCurrentCell->Flags &= ~CellFlags::Fogged; - - // Don't call CleanFog during mass SpySat cleanup - it triggers extension crashes - // pCurrentCell->CleanFog(); - } - } - } - } - - // Reset flag when SpySat goes offline - static bool wasActive = true; - if (!HouseClass::CurrentPlayer->SpySatActive && wasActive) { - hasUnfrozen = false; - } - wasActive = HouseClass::CurrentPlayer->SpySatActive; - - return 0x457C80; // Skip fog freezing + // 1) Do not freeze buildings the player should always see + if (pThis->Owner && ( + pThis->Owner->IsControlledByCurrentPlayer() || + (RulesClass::Instance->AllyReveal && pThis->Owner->IsAlliedWith(HouseClass::CurrentPlayer)) + )) { + return 0x457C80; // skip engine freeze + } + + // 2) Do not freeze anything while SpySat is active (global reveal) + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) { + return 0x457C80; // skip engine freeze } if (!pCell) @@ -541,7 +449,7 @@ DEFINE_HOOK(0x457AA0, BuildingClass_FreezeInFog, 0x5) pThis->Deselect(); - // helper to add an independent FoggedObject to a specific cell + // Per-cell fog proxies (one FoggedObject per footprint cell) auto addToCell = [&](CellClass* dst) { auto* fo = GameCreate(pThis, IsVisible); CellExt::ExtMap.Find(dst)->FoggedObjects.AddItem(fo); @@ -550,7 +458,7 @@ DEFINE_HOOK(0x457AA0, BuildingClass_FreezeInFog, 0x5) // owner cell addToCell(pCell); - // every other foundation cell gets its own FoggedObject instance, too + // every other foundation cell gets its own FoggedObject instance auto MapCoords = pThis->GetMapCoords(); for (auto pFoundation = pThis->Type->GetFoundationData(false); pFoundation->X != 0x7FFF || pFoundation->Y != 0x7FFF; @@ -645,9 +553,26 @@ DEFINE_HOOK(0x6D3470, TacticalClass_DrawFoggedObject, 0x8) // Clean up orphans BEFORE computing finalRect or rendering SafeCleanupOrphanedFoggedObjects(); - - // Clear building deduplication list once per frame - g_DirtiedBldsThisFrame.Clear(); + + // SpySat edge-triggered thaw + const bool spyNow = (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive); + if (spyNow && !g_SpySatWasActive) { + ForceUnfreezeAllFogProxiesForSpySat(); + g_NeedViewportRefresh = true; // force same-frame repaint + } + g_SpySatWasActive = spyNow; + + // One-shot viewport repaint if we had a burst + if (g_NeedViewportRefresh) { + // Mark the whole tactical viewport dirty once + RectangleStruct vb = DSurface::ViewBounds; + TacticalClass::Instance->RegisterDirtyArea(vb, /*force*/true); + + g_NeedViewportRefresh = false; + } + + // Reset per-frame counter + g_RevealedCellsThisFrame = 0; // SpySat reveals everything, so don't draw fogged objects when SpySat is active if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) @@ -740,33 +665,20 @@ DEFINE_HOOK(0x577EBF, MapClass_Reveal, 0x6) pCell->Flags |= CellFlags::Revealed; // Extra process + ++g_RevealedCellsThisFrame; + // Heuristic: 64 cells ~ a modest burst. Tweak if needed. + if (g_RevealedCellsThisFrame >= 64) { + g_NeedViewportRefresh = true; + } + pCell->CleanFog(); - // ClearCellFogProxies(pCell); // DISABLED - extension system returns invalid pointers during large reveals - // Mark all objects in this cell for redraw when revealed - bool hadBuilding = false; - for (ObjectClass* obj = pCell->FirstObject; obj; obj = obj->NextObject) { - obj->NeedsRedraw = true; - if (auto bld = abstract_cast(obj)) { - hadBuilding = true; - // Only dirty when it was fully fogged and is transitioning to visible - if (bld->IsAllFogged()) { - MarkBuildingFootprintDirtyOnce(bld); - } + // Just set buildings in this cell to redraw - let the fog gate handle visibility + for (ObjectClass* pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) { + if (auto pBuilding = abstract_cast(pObject)) { + pBuilding->NeedsRedraw = true; } } - - // Only register 60x60 cell rect if no building was present (building footprint covers it) - if (!hadBuilding) { - auto loc = pCell->GetCoords(); - auto [pt, visible] = TacticalClass::Instance->CoordsToClient(loc); - RectangleStruct cellRect{ - DSurface::ViewBounds.X + pt.X - 30, - DSurface::ViewBounds.Y + pt.Y, - 60, 60 - }; - TacticalClass::Instance->RegisterDirtyArea(cellRect, true); - } return 0x577EE9; } From 0e3a19c686b2f1e6eb053690ec62979dcb3779a4 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Fri, 19 Sep 2025 16:43:35 +1000 Subject: [PATCH 04/20] introduce RemoveShroudOnly --- src/Ext/WarheadType/Body.cpp | 2 + src/Ext/WarheadType/Body.h | 1 + src/Ext/WarheadType/Detonate.cpp | 71 ++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/src/Ext/WarheadType/Body.cpp b/src/Ext/WarheadType/Body.cpp index 550b19d1b3..f817935d88 100644 --- a/src/Ext/WarheadType/Body.cpp +++ b/src/Ext/WarheadType/Body.cpp @@ -121,6 +121,7 @@ void WarheadTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) // Miscs this->Reveal.Read(exINI, pSection, "Reveal"); this->CreateGap.Read(exINI, pSection, "CreateGap"); + this->RemoveShroudOnly.Read(exINI, pSection, "RemoveShroudOnly"); this->TransactMoney.Read(exINI, pSection, "TransactMoney"); this->TransactMoney_Display.Read(exINI, pSection, "TransactMoney.Display"); this->TransactMoney_Display_Houses.Read(exINI, pSection, "TransactMoney.Display.Houses"); @@ -400,6 +401,7 @@ void WarheadTypeExt::ExtData::Serialize(T& Stm) Stm .Process(this->Reveal) .Process(this->CreateGap) + .Process(this->RemoveShroudOnly) .Process(this->TransactMoney) .Process(this->TransactMoney_Display) .Process(this->TransactMoney_Display_Houses) diff --git a/src/Ext/WarheadType/Body.h b/src/Ext/WarheadType/Body.h index 4429a4548f..cd54a6d4c5 100644 --- a/src/Ext/WarheadType/Body.h +++ b/src/Ext/WarheadType/Body.h @@ -23,6 +23,7 @@ class WarheadTypeExt Valueable Reveal; Valueable CreateGap; + Valueable RemoveShroudOnly; Valueable TransactMoney; Valueable TransactMoney_Display; Valueable TransactMoney_Display_Houses; diff --git a/src/Ext/WarheadType/Detonate.cpp b/src/Ext/WarheadType/Detonate.cpp index fd169dcef1..4990e42e48 100644 --- a/src/Ext/WarheadType/Detonate.cpp +++ b/src/Ext/WarheadType/Detonate.cpp @@ -92,6 +92,77 @@ void WarheadTypeExt::ExtData::Detonate(TechnoClass* pOwner, HouseClass* pHouse, MapClass::Instance.Reveal(pHouse); } + const int removeShroudOnly = this->RemoveShroudOnly; + + if (removeShroudOnly > 0) + { + // Custom shroud-only removal - doesn't touch fog at all + auto const center = CellClass::Coord2Cell(coords); + + // Remove shroud in radius without affecting fog + CellRangeIterator{}(center, removeShroudOnly + 0.5, [pHouse](CellClass* pCell) + { + if (pCell && pHouse) + { + // Reduce shroud counter to reveal the cell + while (pCell->ShroudCounter > 0) + { + pCell->ReduceShroudCounter(); + } + + // Clear gap coverage + pCell->GapsCoveringThisCell = 0; + + // Update visibility without touching fog + char visibility = TacticalClass::Instance->GetOcclusion(pCell->MapCoords, false); + if (pCell->Visibility != visibility) + { + pCell->Visibility = visibility; + TacticalClass::Instance->RegisterCellAsVisible(pCell); + } + } + return true; + }); + + // Force redraw to show shroud changes + MapClass::Instance.MarkNeedsRedraw(2); + } + else if (removeShroudOnly < 0) + { + // Global shroud removal - remove shroud from entire map without touching fog + auto const& mapRect = MapClass::Instance.MapRect; + + for (int y = mapRect.Y; y < mapRect.Y + mapRect.Height; y++) + { + for (int x = mapRect.X; x < mapRect.X + mapRect.Width; x++) + { + CellStruct cellCoord = { static_cast(x), static_cast(y) }; + if (auto pCell = MapClass::Instance.TryGetCellAt(cellCoord)) + { + // Reduce shroud counter to reveal the cell + while (pCell->ShroudCounter > 0) + { + pCell->ReduceShroudCounter(); + } + + // Clear gap coverage + pCell->GapsCoveringThisCell = 0; + + // Update visibility without touching fog + char visibility = TacticalClass::Instance->GetOcclusion(pCell->MapCoords, false); + if (pCell->Visibility != visibility) + { + pCell->Visibility = visibility; + TacticalClass::Instance->RegisterCellAsVisible(pCell); + } + } + } + } + + // Force full map redraw + MapClass::Instance.MarkNeedsRedraw(2); + } + if (this->TransactMoney) { pHouse->TransactMoney(this->TransactMoney); From 54c95c0c55ce2b7c81b4dd01d5a65a91712f45bc Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Sat, 20 Sep 2025 07:33:31 +1000 Subject: [PATCH 05/20] First iteration of fixing fog items, lasers fixed --- src/Ext/Sidebar/Placement.cpp | 103 +++++++++++++++++++++++++++ src/Ext/Weapon/FogEffects.cpp | 76 ++++++++++++++++++++ src/Misc/FogEffects.h | 66 ++++++++++++++++++ src/Misc/FogOfWar.cpp | 33 +++++++++ src/Misc/FogOfWar.h | 34 +++++++++ src/New/Entity/LaserTrailClass.cpp | 108 +++++++++++++++++++++++------ src/UI/WorldText.cpp | 75 ++++++++++++++++++++ 7 files changed, 474 insertions(+), 21 deletions(-) create mode 100644 src/Ext/Sidebar/Placement.cpp create mode 100644 src/Ext/Weapon/FogEffects.cpp create mode 100644 src/Misc/FogEffects.h create mode 100644 src/Misc/FogOfWar.h create mode 100644 src/UI/WorldText.cpp diff --git a/src/Ext/Sidebar/Placement.cpp b/src/Ext/Sidebar/Placement.cpp new file mode 100644 index 0000000000..a56802f154 --- /dev/null +++ b/src/Ext/Sidebar/Placement.cpp @@ -0,0 +1,103 @@ +#include +#include + +#include +#include +#include +#include + +// Producer-side fog gating for building placement +// No binary patches, just client-side preview validation + +namespace BuildingPlacementFog { + + // Check if building placement is allowed at current mouse position + bool PlacementAllowedHere(const BuildingTypeClass* pType, HouseClass* pHouse) { + if (!pType || !pHouse) + return true; // Let other validation handle null cases + + // Only apply fog restrictions to human players + if (!pHouse->IsControlledByCurrentPlayer()) + return true; // AI can place anywhere + + // Only apply if fog of war is enabled + if (!ScenarioClass::Instance->SpecialFlags.FogOfWar) + return true; // No restrictions if fog disabled + + // Get mouse world coordinates + if (!WWMouseClass::Instance || !TacticalClass::Instance) + return true; // Safety check - allow if instances not ready + + const Point2D mouse = WWMouseClass::Instance->XY1; + const CoordStruct world = TacticalClass::Instance->ClientToCoords(mouse); + + // Use the safe helper function + return CanPlaceBuildingInFog(world, const_cast(pType)); + } + + // Check if specific coordinates allow building placement + bool PlacementAllowedAt(const CoordStruct& coords, const BuildingTypeClass* pType, HouseClass* pHouse) { + if (!pType || !pHouse) + return true; + + // Only apply fog restrictions to human players + if (!pHouse->IsControlledByCurrentPlayer()) + return true; + + // Only apply if fog of war is enabled + if (!ScenarioClass::Instance->SpecialFlags.FogOfWar) + return true; + + // Use the safe helper function + return CanPlaceBuildingInFog(coords, const_cast(pType)); + } + + // Wrapper for AI placement that respects fog (if desired) + bool AIPlacementAllowedAt(const CoordStruct& coords, const BuildingTypeClass* pType) { + if (!pType) + return true; + + // For AI, you might want different rules + // Currently allows AI to place anywhere, but you can customize this + return true; + } + + // Check if deployment (MCV -> Construction Yard) is allowed + bool DeploymentAllowedHere(HouseClass* pHouse) { + if (!pHouse || !pHouse->IsControlledByCurrentPlayer()) + return true; // AI can deploy anywhere + + if (!ScenarioClass::Instance->SpecialFlags.FogOfWar) + return true; // No restrictions if fog disabled + + // Get mouse world coordinates + if (!WWMouseClass::Instance || !TacticalClass::Instance) + return true; + + const Point2D mouse = WWMouseClass::Instance->XY1; + const CoordStruct world = TacticalClass::Instance->ClientToCoords(mouse); + + // For deployment, just check the single cell + return Fog::ShouldShowActiveAt(world); + } +} + +// Usage examples in your UI/sidebar code: +// +// In building placement preview logic: +// if (!BuildingPlacementFog::PlacementAllowedHere(buildingType, currentPlayer)) { +// // Show red preview, block placement +// return false; +// } +// +// In MCV deployment logic: +// if (!BuildingPlacementFog::DeploymentAllowedHere(currentPlayer)) { +// // Block deployment, show message +// return false; +// } +// +// For AI placement validation: +// if (!BuildingPlacementFog::AIPlacementAllowedAt(coords, buildingType)) { +// // AI finds different location +// return false; +// } \ No newline at end of file diff --git a/src/Ext/Weapon/FogEffects.cpp b/src/Ext/Weapon/FogEffects.cpp new file mode 100644 index 0000000000..8dc7ca95b2 --- /dev/null +++ b/src/Ext/Weapon/FogEffects.cpp @@ -0,0 +1,76 @@ +#include +#include + +#include +#include +#include + +// Producer-side fog gating for weapon effects +// No binary patches, no crashes - just don't create effects in fog + +namespace WeaponFog { + + // Lasers - hide if either endpoint is fogged + bool SpawnLaserIfVisible(const CoordStruct& source, const CoordStruct& target, + const ColorStruct& innerColor, const ColorStruct& outerColor, + const ColorStruct& outerSpread, int duration) { + + if (!Fog::ShouldShowActiveAt(source) || !Fog::ShouldShowActiveAt(target)) + return false; // Don't spawn laser in fog + + // Create the laser using existing game mechanics + auto pLaser = GameCreate(source, target, innerColor, outerColor, outerSpread, duration); + return pLaser != nullptr; + } + + // Particles/sparks - hide if spawn location is fogged + bool SpawnParticleSystemIfVisible(const CoordStruct& at, ParticleSystemTypeClass* pType) { + if (!pType || !Fog::ShouldShowActiveAt(at)) + return false; + + // Create particle system at location + auto pSystem = GameCreate(pType, at, nullptr, nullptr, CoordStruct::Empty, nullptr); + return pSystem != nullptr; + } + + // Explosion animations - hide if explosion location is fogged + bool SpawnExplosionAnimIfVisible(const CoordStruct& at, AnimTypeClass* pType) { + if (!pType || !Fog::ShouldShowActiveAt(at)) + return false; + + // Create explosion animation + auto pAnim = GameCreate(pType, at); + return pAnim != nullptr; + } + + // Electric bolts/arcs - hide if either endpoint is fogged + bool SpawnElectricBoltIfVisible(const CoordStruct& source, const CoordStruct& target) { + if (!Fog::ShouldShowActiveAt(source) || !Fog::ShouldShowActiveAt(target)) + return false; + + // Your electric bolt creation logic here + // This is a placeholder - replace with actual implementation + return true; + } + + // Weapon trail effects - hide if weapon location is fogged + bool SpawnWeaponTrailIfVisible(const CoordStruct& at, const CoordStruct& target) { + if (!Fog::ShouldShowActiveAt(at) || !Fog::ShouldShowActiveAt(target)) + return false; + + // Your weapon trail creation logic here + // This is a placeholder - replace with actual implementation + return true; + } +} + +// Usage examples: +// Replace direct laser creation: +// auto laser = GameCreate(source, target, colors...); +// With: +// WeaponFog::SpawnLaserIfVisible(source, target, colors...); +// +// Replace direct particle creation: +// auto particles = GameCreate(type, at, ...); +// With: +// WeaponFog::SpawnParticleSystemIfVisible(at, type); \ No newline at end of file diff --git a/src/Misc/FogEffects.h b/src/Misc/FogEffects.h new file mode 100644 index 0000000000..8726701da6 --- /dev/null +++ b/src/Misc/FogEffects.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include + +// Forward declarations +class LaserDrawClass; +class ParticleSystemTypeClass; +class AnimTypeClass; +class BuildingTypeClass; +class HouseClass; + +// Producer-side fog effect wrappers +// Safe, crash-free alternatives to low-level draw hooks + +// Weapon effects - hide active visuals in fog +namespace WeaponFog { + bool SpawnLaserIfVisible(const CoordStruct& source, const CoordStruct& target, + const ColorStruct& innerColor, const ColorStruct& outerColor, + const ColorStruct& outerSpread, int duration); + + bool SpawnParticleSystemIfVisible(const CoordStruct& at, ParticleSystemTypeClass* pType); + bool SpawnExplosionAnimIfVisible(const CoordStruct& at, AnimTypeClass* pType); + bool SpawnElectricBoltIfVisible(const CoordStruct& source, const CoordStruct& target); + bool SpawnWeaponTrailIfVisible(const CoordStruct& at, const CoordStruct& target); +} + +// World text - hide floating text in fog +namespace WorldTextFog { + void SpawnDamageTextIfVisible(int damage, const CoordStruct& at, ColorSchemeIndex color); + void SpawnBountyTextIfVisible(int credits, const CoordStruct& at, HouseClass* pHouse); + void SpawnCashDisplayTextIfVisible(int credits, const CoordStruct& at, HouseClass* pHouse); + void SpawnWorldTextIfVisible(const wchar_t* text, const CoordStruct& at, ColorSchemeIndex color, HouseClass* pHouse = nullptr); + void SpawnPromotionTextIfVisible(const wchar_t* rankText, const CoordStruct& at, HouseClass* pHouse); +} + +// Building placement - block placement in fog +namespace BuildingPlacementFog { + bool PlacementAllowedHere(const BuildingTypeClass* pType, HouseClass* pHouse); + bool PlacementAllowedAt(const CoordStruct& coords, const BuildingTypeClass* pType, HouseClass* pHouse); + bool AIPlacementAllowedAt(const CoordStruct& coords, const BuildingTypeClass* pType); + bool DeploymentAllowedHere(HouseClass* pHouse); +} + +// Usage Guide: +// +// 1. Include this header: #include +// +// 2. Replace direct effect creation with fog-aware wrappers: +// OLD: auto laser = GameCreate(source, target, colors...); +// NEW: WeaponFog::SpawnLaserIfVisible(source, target, colors...); +// +// 3. Replace direct text creation with fog-aware wrappers: +// OLD: FlyingStrings::AddMoneyString(text, at, color, house, true); +// NEW: WorldTextFog::SpawnDamageTextIfVisible(damage, at, color); +// +// 4. Add placement validation in your UI code: +// if (!BuildingPlacementFog::PlacementAllowedHere(type, house)) { +// // Show red preview, block placement +// } +// +// Benefits: +// - Zero crashes (no binary patches) +// - Engine-friendly (works with existing fog mechanics) +// - Easy to use (drop-in replacements) +// - Maintainable (clear separation of concerns) \ No newline at end of file diff --git a/src/Misc/FogOfWar.cpp b/src/Misc/FogOfWar.cpp index 8eed94cf56..4460fb0dd9 100644 --- a/src/Misc/FogOfWar.cpp +++ b/src/Misc/FogOfWar.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -707,4 +708,36 @@ DEFINE_HOOK(0x4FC1FF, HouseClass_PlayerDefeated_MapReveal, 0x6) MapClass::Instance.Reveal(pHouse); return 0x4FC214; +} + +// Check if building placement should be allowed at location (respects fog like shroud) +bool CanPlaceBuildingInFog(const CoordStruct& coords, BuildingTypeClass* pBuildingType) { + // If fog of war is disabled, allow placement + if (!ScenarioClass::Instance->SpecialFlags.FogOfWar) { + return true; + } + + // Check if any part of the building foundation is fogged + if (pBuildingType) { + CellStruct baseCell = CellClass::Coord2Cell(coords); + + // Check each foundation cell using the new safe helpers + for (int x = 0; x < pBuildingType->GetFoundationWidth(); x++) { + for (int y = 0; y < pBuildingType->GetFoundationHeight(false); y++) { + CellStruct foundationCell = { + static_cast(baseCell.X + x), + static_cast(baseCell.Y + y) + }; + + CoordStruct cellCoords = CellClass::Cell2Coord(foundationCell); + + // If any foundation cell is fogged, disallow placement + if (!Fog::ShouldShowActiveAt(cellCoords)) { + return false; + } + } + } + } + + return true; // All foundation cells are clear } \ No newline at end of file diff --git a/src/Misc/FogOfWar.h b/src/Misc/FogOfWar.h new file mode 100644 index 0000000000..4ee4f03697 --- /dev/null +++ b/src/Misc/FogOfWar.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Central fog of war utility functions - safe, producer-side approach + +// Active = must be visible now; Passive = visible once revealed, hidden only by shroud. +namespace Fog { + inline bool IsFogged(const CoordStruct& c) { + auto cs = CellClass::Coord2Cell(c); + auto* p = (&MapClass::Instance) ? MapClass::Instance.TryGetCellAt(cs) : nullptr; + CellStruct se{ short(cs.X + 1), short(cs.Y + 1) }; + auto* pse = (&MapClass::Instance) ? MapClass::Instance.TryGetCellAt(se) : nullptr; + const bool e1 = p && (p->Flags & CellFlags::EdgeRevealed); + const bool e2 = pse && (pse->Flags & CellFlags::EdgeRevealed); + return !(e1 || e2); + } + + inline bool IsShrouded(const CoordStruct& c) { + // Same test as above — engine treats 'edge revealed' as "ever seen". + return IsFogged(c); + } + + // Policy helpers + inline bool ShouldShowActiveAt(const CoordStruct& c) { return !IsFogged(c); } + inline bool ShouldShowPassiveAt(const CoordStruct& c) { return !IsShrouded(c); } +} + +// Check if building placement should be allowed at location (respects fog like shroud) +bool CanPlaceBuildingInFog(const CoordStruct& coords, BuildingTypeClass* pBuildingType); \ No newline at end of file diff --git a/src/New/Entity/LaserTrailClass.cpp b/src/New/Entity/LaserTrailClass.cpp index a6fecf9343..06c915d6cd 100644 --- a/src/New/Entity/LaserTrailClass.cpp +++ b/src/New/Entity/LaserTrailClass.cpp @@ -23,18 +23,61 @@ bool LaserTrailClass::Update(CoordStruct location) { if (pType->DrawType == LaserTrailDrawType::Laser) { - const auto pLaser = GameCreate( - this->LastLocation.Get(), location, - this->CurrentColor, ColorStruct { 0, 0, 0 }, ColorStruct { 0, 0, 0 }, - pType->FadeDuration.Get(64)); - - pLaser->Thickness = pType->Thickness; - pLaser->IsHouseColor = true; - pLaser->IsSupported = pType->IsIntense; + // FOG CHECK: Only create laser if both endpoints are visible + bool sourceVisible = true; + bool targetVisible = true; + + // Check if fog of war is enabled and we have valid instances + if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && + &MapClass::Instance && TacticalClass::Instance) { + + // Check source location + auto sourceCs = CellClass::Coord2Cell(this->LastLocation.Get()); + auto* sourceCell = MapClass::Instance.TryGetCellAt(sourceCs); + sourceVisible = sourceCell && (sourceCell->Flags & CellFlags::EdgeRevealed); + + // Check target location + auto targetCs = CellClass::Coord2Cell(location); + auto* targetCell = MapClass::Instance.TryGetCellAt(targetCs); + targetVisible = targetCell && (targetCell->Flags & CellFlags::EdgeRevealed); + } + + // Only create laser if both endpoints are visible + if (sourceVisible && targetVisible) { + const auto pLaser = GameCreate( + this->LastLocation.Get(), location, + this->CurrentColor, ColorStruct { 0, 0, 0 }, ColorStruct { 0, 0, 0 }, + pType->FadeDuration.Get(64)); + + pLaser->Thickness = pType->Thickness; + pLaser->IsHouseColor = true; + pLaser->IsSupported = pType->IsIntense; + } } else if (pType->DrawType == LaserTrailDrawType::EBolt) { - const auto pBolt = GameCreate(); + // FOG CHECK: Only create EBolt if both endpoints are visible + bool sourceVisible = true; + bool targetVisible = true; + + // Check if fog of war is enabled and we have valid instances + if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && + &MapClass::Instance && TacticalClass::Instance) { + + // Check source location + auto sourceCs = CellClass::Coord2Cell(this->LastLocation.Get()); + auto* sourceCell = MapClass::Instance.TryGetCellAt(sourceCs); + sourceVisible = sourceCell && (sourceCell->Flags & CellFlags::EdgeRevealed); + + // Check target location + auto targetCs = CellClass::Coord2Cell(location); + auto* targetCell = MapClass::Instance.TryGetCellAt(targetCs); + targetVisible = targetCell && (targetCell->Flags & CellFlags::EdgeRevealed); + } + + // Only create EBolt if both endpoints are visible + if (sourceVisible && targetVisible) { + const auto pBolt = GameCreate(); const auto pBoltExt = EBoltExt::ExtMap.Find(pBolt); const auto& boltDisable = pType->Bolt_Disable; const auto& boltColor = pType->Bolt_Color; @@ -53,22 +96,45 @@ bool LaserTrailClass::Update(CoordStruct location) pBoltExt->Color[idx] = Drawing::Int_To_RGB(idx < 2 ? defaultAlternate : defaultWhite); } - pBoltExt->Arcs = pType->Bolt_Arcs; - pBolt->Lifetime = 1 << (std::clamp(pType->FadeDuration.Get(17), 1, 31) - 1); - pBolt->AlternateColor = pType->IsAlternateColor; + pBoltExt->Arcs = pType->Bolt_Arcs; + pBolt->Lifetime = 1 << (std::clamp(pType->FadeDuration.Get(17), 1, 31) - 1); + pBolt->AlternateColor = pType->IsAlternateColor; - pBolt->Fire(this->LastLocation, location, 0); + pBolt->Fire(this->LastLocation, location, 0); + } } else if (pType->DrawType == LaserTrailDrawType::RadBeam) { - const auto pRadBeam = RadBeam::Allocate(RadBeamType::RadBeam); - pRadBeam->SetCoordsSource(this->LastLocation); - pRadBeam->SetCoordsTarget(location); - pRadBeam->Period = pType->FadeDuration.Get(15); - pRadBeam->Amplitude = pType->Beam_Amplitude; - - const ColorStruct beamColor = pType->Beam_Color.Get(RulesClass::Instance->RadColor); - pRadBeam->SetColor(beamColor); + // FOG CHECK: Only create RadBeam if both endpoints are visible + bool sourceVisible = true; + bool targetVisible = true; + + // Check if fog of war is enabled and we have valid instances + if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && + &MapClass::Instance && TacticalClass::Instance) { + + // Check source location + auto sourceCs = CellClass::Coord2Cell(this->LastLocation.Get()); + auto* sourceCell = MapClass::Instance.TryGetCellAt(sourceCs); + sourceVisible = sourceCell && (sourceCell->Flags & CellFlags::EdgeRevealed); + + // Check target location + auto targetCs = CellClass::Coord2Cell(location); + auto* targetCell = MapClass::Instance.TryGetCellAt(targetCs); + targetVisible = targetCell && (targetCell->Flags & CellFlags::EdgeRevealed); + } + + // Only create RadBeam if both endpoints are visible + if (sourceVisible && targetVisible) { + const auto pRadBeam = RadBeam::Allocate(RadBeamType::RadBeam); + pRadBeam->SetCoordsSource(this->LastLocation); + pRadBeam->SetCoordsTarget(location); + pRadBeam->Period = pType->FadeDuration.Get(15); + pRadBeam->Amplitude = pType->Beam_Amplitude; + + const ColorStruct beamColor = pType->Beam_Color.Get(RulesClass::Instance->RadColor); + pRadBeam->SetColor(beamColor); + } } result = true; diff --git a/src/UI/WorldText.cpp b/src/UI/WorldText.cpp new file mode 100644 index 0000000000..a745809e4d --- /dev/null +++ b/src/UI/WorldText.cpp @@ -0,0 +1,75 @@ +#include +#include + +#include +#include + +// Producer-side fog gating for world-anchored text +// No low-level text hooks, just don't spawn text in fog + +namespace WorldTextFog { + + // Floating damage numbers - hide if unit location is fogged + void SpawnDamageTextIfVisible(int damage, const CoordStruct& at, ColorSchemeIndex color) { + if (!Fog::ShouldShowActiveAt(at)) + return; // Don't spawn damage text in fog + + // Create floating damage text + wchar_t damageStr[32]; + swprintf_s(damageStr, L"-%d", damage); + FlyingStrings::AddMoneyString(damageStr, at, color, nullptr, true); + } + + // Bounty credit display - hide if target location is fogged + void SpawnBountyTextIfVisible(int credits, const CoordStruct& at, HouseClass* pHouse) { + if (!Fog::ShouldShowActiveAt(at)) + return; // Don't spawn bounty text in fog + + // Create floating credit text + wchar_t creditStr[32]; + swprintf_s(creditStr, L"+$%d", credits); + ColorSchemeIndex color = pHouse ? pHouse->ColorSchemeIndex : ColorSchemeIndex::Yellow; + FlyingStrings::AddMoneyString(creditStr, at, color, pHouse, true); + } + + // ProduceCashDisplay credits - hide if building location is fogged + void SpawnCashDisplayTextIfVisible(int credits, const CoordStruct& at, HouseClass* pHouse) { + if (!Fog::ShouldShowActiveAt(at)) + return; // Don't spawn cash display text in fog + + // Create floating cash text + wchar_t cashStr[32]; + swprintf_s(cashStr, L"+$%d", credits); + ColorSchemeIndex color = pHouse ? pHouse->ColorSchemeIndex : ColorSchemeIndex::Green; + FlyingStrings::AddMoneyString(cashStr, at, color, pHouse, true); + } + + // Generic floating text - hide if location is fogged + void SpawnWorldTextIfVisible(const wchar_t* text, const CoordStruct& at, ColorSchemeIndex color, HouseClass* pHouse = nullptr) { + if (!text || !Fog::ShouldShowActiveAt(at)) + return; // Don't spawn text in fog + + // Create floating text + FlyingStrings::AddMoneyString(text, at, color, pHouse, true); + } + + // Experience/promotion text - hide if unit location is fogged + void SpawnPromotionTextIfVisible(const wchar_t* rankText, const CoordStruct& at, HouseClass* pHouse) { + if (!rankText || !Fog::ShouldShowActiveAt(at)) + return; // Don't spawn promotion text in fog + + ColorSchemeIndex color = pHouse ? pHouse->ColorSchemeIndex : ColorSchemeIndex::White; + FlyingStrings::AddMoneyString(rankText, at, color, pHouse, true); + } +} + +// Usage examples: +// Replace direct damage text creation: +// FlyingStrings::AddMoneyString(damageStr, at, color, house, true); +// With: +// WorldTextFog::SpawnDamageTextIfVisible(damage, at, color); +// +// Replace direct bounty text creation: +// FlyingStrings::AddMoneyString(creditStr, at, color, house, true); +// With: +// WorldTextFog::SpawnBountyTextIfVisible(credits, at, house); \ No newline at end of file From 103903eb9e4a47dc30836cf781313c6559009083 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Sun, 21 Sep 2025 00:12:21 +1000 Subject: [PATCH 06/20] Fixed NaturalParticleSystem from showing through fog --- src/Ext/Building/Hooks.cpp | 40 +++++++++++++++++++++++++++++++++++++- src/Misc/FogOfWar.h | 17 ++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index 92af12c2e1..c3f8e9c7d9 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -10,6 +10,33 @@ #include #include #include +#include +#include // swap-erase helper + +// stable, single-pass erase of the first matching pointer; preserves order +template +static __forceinline void stable_erase_first(std::vector& v, T* value) +{ + for (size_t i = 0, n = v.size(); i < n; ++i) + { + if (v[i] == value) { v.erase(v.begin() + i); return; } + } +} + +// 🔧 Optimized: Small deterministic swap-erase. Order is not semantically used for RestrictedFactoryPlants. +template +__forceinline void swap_erase_first(TCont& v, const TValue& value) +{ + for (size_t i = 0, n = v.size(); i < n; ++i) + { + if (v[i] == value) + { + if (i + 1 != n) { std::swap(v[i], v[n - 1]); } + v.pop_back(); + return; + } + } +} #pragma region Update @@ -316,7 +343,18 @@ DEFINE_HOOK(0x440EBB, BuildingClass_Unlimbo_NaturalParticleSystem_CampaignSkip, { enum { DoNotCreateParticle = 0x440F61 }; GET(BuildingClass* const, pThis, ESI); - return BuildingExt::ExtMap.Find(pThis)->IsCreatedFromMapFile ? DoNotCreateParticle : 0; + + // Skip for map-placed buildings + if (BuildingExt::ExtMap.Find(pThis)->IsCreatedFromMapFile) { + return DoNotCreateParticle; + } + + // Skip for enemy buildings under fog to prevent information leaks + if (FoW::EnemyTechnoUnderFog(pThis)) { + return DoNotCreateParticle; + } + + return 0; } DEFINE_HOOK(0x4519A2, BuildingClass_UpdateAnim_SetParentBuilding, 0x6) diff --git a/src/Misc/FogOfWar.h b/src/Misc/FogOfWar.h index 4ee4f03697..8c3f675768 100644 --- a/src/Misc/FogOfWar.h +++ b/src/Misc/FogOfWar.h @@ -30,5 +30,22 @@ namespace Fog { inline bool ShouldShowPassiveAt(const CoordStruct& c) { return !IsShrouded(c); } } +// Helper for creation-time particle gates +namespace FoW { + inline bool EnemyTechnoUnderFog(AbstractClass* owner) { + if (!(ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar)) return false; + if (!HouseClass::CurrentPlayer || !owner) return false; + + TechnoClass* t = abstract_cast(owner); + if (!t) { if (auto* b = abstract_cast(owner)) t = b; } + if (!t || !t->Owner) return false; + if (HouseClass::CurrentPlayer->IsAlliedWith(t->Owner)) return false; + + const auto cs = CellClass::Coord2Cell(t->GetCoords()); + const auto* cell = MapClass::Instance.TryGetCellAt(cs); + return !cell || !(cell->Flags & CellFlags::EdgeRevealed); + } +} + // Check if building placement should be allowed at location (respects fog like shroud) bool CanPlaceBuildingInFog(const CoordStruct& coords, BuildingTypeClass* pBuildingType); \ No newline at end of file From 51dcd1689f9180810b917da8cbb4062420cee009 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Sun, 21 Sep 2025 00:48:45 +1000 Subject: [PATCH 07/20] Potential IsAlliedWith crash fix --- src/Ext/Techno/Body.Update.cpp | 52 +++++++++++++++++++++++++++++++++- src/Misc/FogOfWar.cpp | 3 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index da6be50161..5519274804 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -18,13 +18,63 @@ #include #include #include +#include // TechnoClass_AI_0x6F9E50 // It's not recommended to do anything more here it could have a better place for performance consideration void TechnoExt::ExtData::OnEarlyUpdate() { - auto const pType = this->OwnerObject()->GetTechnoType(); + auto* const pThis = this->OwnerObject(); + auto const pType = pThis->GetTechnoType(); + + // Hide enemy particles under fog (client-side only) + if (ScenarioClass::Instance + && ScenarioClass::Instance->SpecialFlags.FogOfWar + && HouseClass::CurrentPlayer + && pThis && pThis->Owner + && !HouseClass::CurrentPlayer->IsAlliedWith(pThis->Owner)) + { + // Use the helper added for the NaturalParticleSystem gate + if (FoW::EnemyTechnoUnderFog(pThis)) { + // Clean up all particle systems that could leak info + if (auto* ps = pThis->DamageParticleSystem) { + Debug::Log("DEBUG: Destroying DamageParticleSystem for enemy %s under fog\n", pThis->GetTechnoType()->ID); + ps->UnInit(); + pThis->DamageParticleSystem = nullptr; + } + if (auto* ps = pThis->FireParticleSystem) { + Debug::Log("DEBUG: Destroying FireParticleSystem for enemy %s under fog\n", pThis->GetTechnoType()->ID); + ps->UnInit(); + pThis->FireParticleSystem = nullptr; + } + if (auto* ps = pThis->SparkParticleSystem) { + Debug::Log("DEBUG: Destroying SparkParticleSystem for enemy %s under fog\n", pThis->GetTechnoType()->ID); + ps->UnInit(); + pThis->SparkParticleSystem = nullptr; + } + if (auto* ps = pThis->RailgunParticleSystem) { + Debug::Log("DEBUG: Destroying RailgunParticleSystem for enemy %s under fog\n", pThis->GetTechnoType()->ID); + ps->UnInit(); + pThis->RailgunParticleSystem = nullptr; + } + if (auto* ps = pThis->FiringParticleSystem) { + Debug::Log("DEBUG: Destroying FiringParticleSystem for enemy %s under fog\n", pThis->GetTechnoType()->ID); + ps->UnInit(); + pThis->FiringParticleSystem = nullptr; + } + if (auto* ps = pThis->unk1ParticleSystem) { + Debug::Log("DEBUG: Destroying unk1ParticleSystem for enemy %s under fog\n", pThis->GetTechnoType()->ID); + ps->UnInit(); + pThis->unk1ParticleSystem = nullptr; + } + if (auto* ps = pThis->unk2ParticleSystem) { + Debug::Log("DEBUG: Destroying unk2ParticleSystem for enemy %s under fog\n", pThis->GetTechnoType()->ID); + ps->UnInit(); + pThis->unk2ParticleSystem = nullptr; + } + } + } // Set only if unset or type is changed // Notice that Ares may handle type conversion in the same hook here, which is executed right before this one thankfully diff --git a/src/Misc/FogOfWar.cpp b/src/Misc/FogOfWar.cpp index 4460fb0dd9..c01a98a953 100644 --- a/src/Misc/FogOfWar.cpp +++ b/src/Misc/FogOfWar.cpp @@ -181,6 +181,7 @@ DEFINE_HOOK(0x6F5190, TechnoClass_DrawExtras_CheckFog, 0x6) return MapClass::Instance.IsLocationFogged(pThis->GetCoords()) ? 0x6F5EEC : 0; } + DEFINE_HOOK(0x6D6EDA, TacticalClass_Overlay_CheckFog1, 0xA) { GET(CellClass*, pCell, EAX); @@ -493,7 +494,7 @@ DEFINE_HOOK(0x70076E, TechnoClass_GetCursorOverCell_OverFog, 0x5) nOvlIdx = pObject->OverlayData.Overlay; else if (pObject->CoveredType == FoggedObject::CoveredType::Building) { - if (HouseClass::CurrentPlayer->IsAlliedWith(pObject->BuildingData.Owner) && pObject->BuildingData.Type->LegalTarget) + if (pObject->BuildingData.Owner && HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->IsAlliedWith(pObject->BuildingData.Owner) && pObject->BuildingData.Type->LegalTarget) R->Stack(STACK_OFFSET(0x2C, 0x19), true); } } From 67ee7435cce4a5027f6cc062427644b241dc6488 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Sun, 21 Sep 2025 09:37:03 +1000 Subject: [PATCH 08/20] Fixed Mouse pointer crash --- src/Ext/Techno/Body.Update.cpp | 5 +++ src/Misc/FogOfWar.cpp | 63 +++++++++++++++++++++++++++++----- src/Misc/FogOfWar.h | 12 +++++-- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 5519274804..16553e4f5e 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -38,6 +38,11 @@ void TechnoExt::ExtData::OnEarlyUpdate() // Use the helper added for the NaturalParticleSystem gate if (FoW::EnemyTechnoUnderFog(pThis)) { // Clean up all particle systems that could leak info + if (auto* ps = pThis->NaturalParticleSystem) { + Debug::Log("DEBUG: Destroying NaturalParticleSystem for enemy %s under fog\n", pThis->GetTechnoType()->ID); + ps->UnInit(); + pThis->NaturalParticleSystem = nullptr; + } if (auto* ps = pThis->DamageParticleSystem) { Debug::Log("DEBUG: Destroying DamageParticleSystem for enemy %s under fog\n", pThis->GetTechnoType()->ID); ps->UnInit(); diff --git a/src/Misc/FogOfWar.cpp b/src/Misc/FogOfWar.cpp index c01a98a953..0cfb3fa460 100644 --- a/src/Misc/FogOfWar.cpp +++ b/src/Misc/FogOfWar.cpp @@ -486,16 +486,28 @@ DEFINE_HOOK(0x70076E, TechnoClass_GetCursorOverCell_OverFog, 0x5) auto const pExt = CellExt::ExtMap.Find(pCell); int nOvlIdx = -1; - for (auto const pObject : pExt->FoggedObjects) - { - if (pObject->Visible) + if (pExt && pExt->FoggedObjects.Count > 0) { + for (auto const pObject : pExt->FoggedObjects) { - if (pObject->CoveredType == FoggedObject::CoveredType::Overlay) - nOvlIdx = pObject->OverlayData.Overlay; - else if (pObject->CoveredType == FoggedObject::CoveredType::Building) + // Safety check: ensure pObject is valid + if (!pObject) continue; + + if (pObject->Visible) { - if (pObject->BuildingData.Owner && HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->IsAlliedWith(pObject->BuildingData.Owner) && pObject->BuildingData.Type->LegalTarget) - R->Stack(STACK_OFFSET(0x2C, 0x19), true); + if (pObject->CoveredType == FoggedObject::CoveredType::Overlay) + nOvlIdx = pObject->OverlayData.Overlay; + else if (pObject->CoveredType == FoggedObject::CoveredType::Building) + { + // Multiple safety checks before calling IsAlliedWith + if (pObject->BuildingData.Owner && + pObject->BuildingData.Type && + HouseClass::CurrentPlayer && + pObject->BuildingData.Owner->ArrayIndex >= 0 && + pObject->BuildingData.Owner->ArrayIndex < HouseClass::Array.Count && + HouseClass::CurrentPlayer->IsAlliedWith(pObject->BuildingData.Owner) && + pObject->BuildingData.Type->LegalTarget) + R->Stack(STACK_OFFSET(0x2C, 0x19), true); + } } } } @@ -506,6 +518,41 @@ DEFINE_HOOK(0x70076E, TechnoClass_GetCursorOverCell_OverFog, 0x5) return 0x700815; } +// 0x62F310 address causes crashes - disabling particle update hooks +// Falling back to stable two-layer producer-side + OnEarlyUpdate approach + +// TODO: Refinery smoke hook at 0x7E4324 - disabled due to invalid memory access +// Need to investigate correct registers and skip address +/* +DEFINE_HOOK(0x7E4324, RefinerySmoke_FogGate, 0x6) +{ + // SpySat bypass - if SpySat is active, show everything + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) + return 0; + + // Check if we're in fog of war mode + if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && HouseClass::CurrentPlayer) + { + // This will need adjustment based on what registers/stack are available + // Making educated guesses about common refinery smoke function parameters + + // Try to get building/techno from common registers + GET(TechnoClass*, pTechno, ESI); // Common for techno operations + if (!pTechno) { + GET(BuildingClass*, pBuilding, ESI); + pTechno = pBuilding; + } + + if (pTechno && FoW::EnemyTechnoUnderFog(pTechno)) { + // Skip refinery smoke creation/update for enemy under fog + return 0x7E4380; // Estimated skip address - may need adjustment + } + } + + return 0; // Continue normal execution +} +*/ + DEFINE_HOOK(0x51F95F, InfantryClass_GetCursorOverCell_OverFog, 0x6) { GET(InfantryClass*, pThis, EDI); diff --git a/src/Misc/FogOfWar.h b/src/Misc/FogOfWar.h index 8c3f675768..ec0e66ee19 100644 --- a/src/Misc/FogOfWar.h +++ b/src/Misc/FogOfWar.h @@ -32,6 +32,14 @@ namespace Fog { // Helper for creation-time particle gates namespace FoW { + inline bool IsUnrevealedForLocal(const CoordStruct& c) { + if (!(ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && HouseClass::CurrentPlayer)) + return false; + const auto cs = CellClass::Coord2Cell(c); + const auto* cc = MapClass::Instance.TryGetCellAt(cs); + return !cc || !(cc->Flags & CellFlags::EdgeRevealed); + } + inline bool EnemyTechnoUnderFog(AbstractClass* owner) { if (!(ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar)) return false; if (!HouseClass::CurrentPlayer || !owner) return false; @@ -41,9 +49,7 @@ namespace FoW { if (!t || !t->Owner) return false; if (HouseClass::CurrentPlayer->IsAlliedWith(t->Owner)) return false; - const auto cs = CellClass::Coord2Cell(t->GetCoords()); - const auto* cell = MapClass::Instance.TryGetCellAt(cs); - return !cell || !(cell->Flags & CellFlags::EdgeRevealed); + return IsUnrevealedForLocal(t->GetCoords()); } } From 94c1f5a6f9929e9507b5cae9c1053d77ee95ccdc Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Sun, 21 Sep 2025 12:51:21 +1000 Subject: [PATCH 09/20] Fix RefinerySmokeParticleSystem under fog, added debugging log --- src/Misc/FogOfWar.cpp | 158 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 150 insertions(+), 8 deletions(-) diff --git a/src/Misc/FogOfWar.cpp b/src/Misc/FogOfWar.cpp index 0cfb3fa460..645aa20f72 100644 --- a/src/Misc/FogOfWar.cpp +++ b/src/Misc/FogOfWar.cpp @@ -498,15 +498,26 @@ DEFINE_HOOK(0x70076E, TechnoClass_GetCursorOverCell_OverFog, 0x5) nOvlIdx = pObject->OverlayData.Overlay; else if (pObject->CoveredType == FoggedObject::CoveredType::Building) { - // Multiple safety checks before calling IsAlliedWith - if (pObject->BuildingData.Owner && + // Owner-free, visibility-only approach to avoid crashes + if (HouseClass::CurrentPlayer && pObject->BuildingData.Type && - HouseClass::CurrentPlayer && - pObject->BuildingData.Owner->ArrayIndex >= 0 && - pObject->BuildingData.Owner->ArrayIndex < HouseClass::Array.Count && - HouseClass::CurrentPlayer->IsAlliedWith(pObject->BuildingData.Owner) && - pObject->BuildingData.Type->LegalTarget) - R->Stack(STACK_OFFSET(0x2C, 0x19), true); + pObject->BuildingData.Type->LegalTarget) { + + if (HouseClass::CurrentPlayer->SpySatActive) { + R->Stack(STACK_OFFSET(0x2C, 0x19), true); + } else { + // Use current visibility, not "ever seen" + if (!Fog::IsFogged(pObject->Location)) { + R->Stack(STACK_OFFSET(0x2C, 0x19), true); + } else { + R->Stack(STACK_OFFSET(0x2C, 0x19), false); // make intent explicit + static int logCount = 0; + if (logCount++ < 5) { + Debug::Log("DEBUG: Blocked cursor targeting - building under fog\n"); + } + } + } + } } } } @@ -518,6 +529,137 @@ DEFINE_HOOK(0x70076E, TechnoClass_GetCursorOverCell_OverFog, 0x5) return 0x700815; } +// TODO: Clean up invalid fog proxies - disabled due to wrong address/register causing crashes +/* +DEFINE_HOOK(0x486C50, CellClass_ClearFoggedObjects_CleanupInvalid, 0x6) +{ + GET(CellClass*, pCell, ESI); + auto const pExt = CellExt::ExtMap.Find(pCell); + + if (pExt && pExt->FoggedObjects.Count > 0) { + // Clean up invalid building proxies (when real building no longer exists) + for (int i = pExt->FoggedObjects.Count - 1; i >= 0; i--) { + auto* pObject = pExt->FoggedObjects[i]; + if (pObject && pObject->CoveredType == FoggedObject::CoveredType::Building) { + // Check if the real building still exists at this location + auto* pRealBuilding = pCell->GetBuilding(); + if (!pRealBuilding || + pRealBuilding->Type != pObject->BuildingData.Type || + pRealBuilding->Owner != pObject->BuildingData.Owner) { + // Real building is gone or different - delete the proxy + pExt->FoggedObjects.RemoveItem(i); + GameDelete(pObject); + } + } + } + } + + return 0; // Continue normal execution +} +*/ + +// Universal particle gate causing crashes - disabling +/* +DEFINE_HOOK(0x62CEC0, ParticleClass_Draw_FoWGate, 0x5) +{ + GET(ParticleClass*, pThis, ECX); + + // show-all bypass (existing behaviour) + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) { + return 0; // run original draw + } + + if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && HouseClass::CurrentPlayer) { + const auto cs = CellClass::Coord2Cell(pThis->GetCoords()); + const auto* c = MapClass::Instance.TryGetCellAt(cs); + + // If this cell was never revealed, skip drawing this particle + if (!c || !(c->Flags & CellFlags::EdgeRevealed)) { + return 0x62D295; // safe tail of ParticleClass::Draw (from gamemd.json) + } + } + return 0; // run original +} +*/ + +// Disabling all new hooks - back to stable approach only +/* +DEFINE_HOOK(0x41C000, TechnoClass_UpdateRefinerySmokeSystems_FoWGate, 0x5) +{ + GET(TechnoClass*, pThis, ECX); + + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) { + return 0; // allow normal smoke if spysat is on + } + + if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && HouseClass::CurrentPlayer) { + const auto cs = CellClass::Coord2Cell(pThis->GetCoords()); + const auto* c = MapClass::Instance.TryGetCellAt(cs); + if (!c || !(c->Flags & CellFlags::EdgeRevealed)) { + // TODO: Need to find the actual epilogue address for this function + // For now, let's try a minimal skip + return 0x41C001; // Temporary - need actual function end address + } + } + return 0; // run original +} +*/ + +// Gate refinery smoke at call-site - disabling for stability testing +/* +DEFINE_HOOK(0x7E3C94, Skip_RefinerySmoke_Call_UnitUnload, 0x5) +{ + GET(TechnoClass*, pThis, ECX); // Assuming ECX holds the techno at this call site + + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) { + return 0; // allow normal smoke if spysat is on + } + + if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && HouseClass::CurrentPlayer) { + const auto cs = CellClass::Coord2Cell(pThis->GetCoords()); + const auto* c = MapClass::Instance.TryGetCellAt(cs); + if (!c || !(c->Flags & CellFlags::EdgeRevealed)) { + return 0x7E3C99; // Skip past the call (5 bytes = call instruction size) + } + } + return 0; // perform the original call +} +*/ + +// Refinery smoke gate with proper visibility checking +DEFINE_HOOK(0x73E37E, UnitClass_Unload_RefinerySmoke_FoWGate, 0x6) +{ + if (!(ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar)) + return 0; + + if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) + return 0; + + // Best-effort visibility test: use the unit doing the unload. + // (On this path the harvester is at/inside the refinery cell.) +#if defined(REF_SMOKE_USE_EDI) + GET(UnitClass*, pUnit, EDI); +#else + GET(UnitClass*, pUnit, ESI); // Default: try ESI first +#endif + + if (pUnit && Fog::IsFogged(pUnit->GetCoords())) { + static int logCount = 0; + if (logCount++ < 10) { + Debug::Log("DEBUG: Blocking RefinerySmokeParticleSystem for %s under fog\n", pUnit->GetTechnoType()->ID); + } + return 0x73E38E; // skip the virtual smoke update when the unload cell is fogged + } + + // Optional: log allows to prove visibility path works + static int allowLog = 0; + if (pUnit && allowLog++ < 5) { + Debug::Log("DEBUG: Allow RefinerySmokeParticleSystem for %s (visible now)\n", pUnit->GetTechnoType()->ID); + } + + return 0; // run original call +} + // 0x62F310 address causes crashes - disabling particle update hooks // Falling back to stable two-layer producer-side + OnEarlyUpdate approach From 5ff91b54a7e8a5808c08c8b8b126f3f7a8b20d4a Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Sun, 21 Sep 2025 14:02:45 +1000 Subject: [PATCH 10/20] Improved fog particle hiding and add cursor safety - Hide particles under fog for all units, not just enemies - Add exception handling to prevent cursor targeting crashes on fogged buildings - Simplify fog checks using Fog::IsFogged() for consistency --- src/Ext/Techno/Body.Update.cpp | 12 ++++----- src/Misc/FogOfWar.cpp | 48 ++++++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 16553e4f5e..91b46f3b59 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -28,18 +28,16 @@ void TechnoExt::ExtData::OnEarlyUpdate() auto* const pThis = this->OwnerObject(); auto const pType = pThis->GetTechnoType(); - // Hide enemy particles under fog (client-side only) + // Hide particles under fog (client-side only) - now includes all technos, not just enemies if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar - && HouseClass::CurrentPlayer - && pThis && pThis->Owner - && !HouseClass::CurrentPlayer->IsAlliedWith(pThis->Owner)) + && HouseClass::CurrentPlayer && pThis) { - // Use the helper added for the NaturalParticleSystem gate - if (FoW::EnemyTechnoUnderFog(pThis)) { + // Use current visibility check (same as working refinery smoke hooks) + if (Fog::IsFogged(pThis->GetCoords())) { // Clean up all particle systems that could leak info if (auto* ps = pThis->NaturalParticleSystem) { - Debug::Log("DEBUG: Destroying NaturalParticleSystem for enemy %s under fog\n", pThis->GetTechnoType()->ID); + Debug::Log("DEBUG: Destroying NaturalParticleSystem for %s under fog\n", pThis->GetTechnoType()->ID); ps->UnInit(); pThis->NaturalParticleSystem = nullptr; } diff --git a/src/Misc/FogOfWar.cpp b/src/Misc/FogOfWar.cpp index 645aa20f72..6c07c70677 100644 --- a/src/Misc/FogOfWar.cpp +++ b/src/Misc/FogOfWar.cpp @@ -498,26 +498,36 @@ DEFINE_HOOK(0x70076E, TechnoClass_GetCursorOverCell_OverFog, 0x5) nOvlIdx = pObject->OverlayData.Overlay; else if (pObject->CoveredType == FoggedObject::CoveredType::Building) { - // Owner-free, visibility-only approach to avoid crashes - if (HouseClass::CurrentPlayer && - pObject->BuildingData.Type && - pObject->BuildingData.Type->LegalTarget) { - - if (HouseClass::CurrentPlayer->SpySatActive) { - R->Stack(STACK_OFFSET(0x2C, 0x19), true); - } else { - // Use current visibility, not "ever seen" - if (!Fog::IsFogged(pObject->Location)) { + // Enhanced safety: wrap all potentially dangerous accesses + __try { + // Owner-free, visibility-only approach to avoid crashes + if (HouseClass::CurrentPlayer && + pObject->BuildingData.Type && + pObject->BuildingData.Type->LegalTarget) { + + if (HouseClass::CurrentPlayer->SpySatActive) { R->Stack(STACK_OFFSET(0x2C, 0x19), true); } else { - R->Stack(STACK_OFFSET(0x2C, 0x19), false); // make intent explicit - static int logCount = 0; - if (logCount++ < 5) { - Debug::Log("DEBUG: Blocked cursor targeting - building under fog\n"); + // Use current visibility, not "ever seen" + if (!Fog::IsFogged(pObject->Location)) { + R->Stack(STACK_OFFSET(0x2C, 0x19), true); + } else { + R->Stack(STACK_OFFSET(0x2C, 0x19), false); // make intent explicit + static int logCount = 0; + if (logCount++ < 5) { + Debug::Log("DEBUG: Blocked cursor targeting - building under fog\n"); + } } } } } + __except (EXCEPTION_EXECUTE_HANDLER) { + // Any access violation - just skip this fogged object entirely + static int crashLog = 0; + if (crashLog++ < 3) { + Debug::Log("DEBUG: Caught cursor access violation - skipping fogged building\n"); + } + } } } } @@ -660,6 +670,16 @@ DEFINE_HOOK(0x73E37E, UnitClass_Unload_RefinerySmoke_FoWGate, 0x6) return 0; // run original call } +// Infantry refinery smoke: DISABLED - incorrect hook location/parameters +// Leaving this disabled to avoid breaking pathfinding or other infantry behavior +/* +DEFINE_HOOK(0x522D75, InfantryClass_UnloadAt_Building_RefinerySmoke_FoWGate, 0x6) +{ + // This hook was not working correctly - disabling for stability + return 0; +} +*/ + // 0x62F310 address causes crashes - disabling particle update hooks // Falling back to stable two-layer producer-side + OnEarlyUpdate approach From 830ecf61c2a523f7c29298c1819c9b9f03127461 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Sun, 21 Sep 2025 19:50:41 +1000 Subject: [PATCH 11/20] Fix EBolt and LaserTrailClass from showing in fog --- src/Ext/EBolt/Hooks.cpp | 119 +++++++++++++++++++++++++++-- src/Misc/Hooks.LaserDraw.cpp | 3 + src/New/Entity/LaserTrailClass.cpp | 44 +++++------ 3 files changed, 137 insertions(+), 29 deletions(-) diff --git a/src/Ext/EBolt/Hooks.cpp b/src/Ext/EBolt/Hooks.cpp index 0cf793571c..322389615f 100644 --- a/src/Ext/EBolt/Hooks.cpp +++ b/src/Ext/EBolt/Hooks.cpp @@ -6,11 +6,22 @@ #include #include #include +#include + +#define FOW_DEBUG 1 + +// Static initializer to confirm this DLL is loaded +namespace { + struct _EBoltInitPing { + _EBoltInitPing() { Debug::Log("[FOW] EBolt hooks compiled into this build.\n"); } + } _eboltInitPing; +} namespace BoltTemp { EBoltExt::ExtData* ExtData = nullptr; int Color[3]; + bool FogHidden = false; // Flag to track if current bolt should be hidden due to fog } // Skip create particlesystem in EBolt::Fire @@ -43,18 +54,90 @@ DWORD _cdecl EBoltExt::_EBolt_Draw_Colors(REGISTERS* R) { enum { SkipGameCode = 0x4C1F66 }; + // Always log first few calls to confirm hook is running + static int hookCount = 0; + if (hookCount < 3) { + Debug::Log("[FOW] _EBolt_Draw_Colors hook running (call #%d)\n", ++hookCount); + } + GET(EBolt*, pThis, ECX); - const auto pExt = BoltTemp::ExtData = EBoltExt::ExtMap.Find(pThis); - const auto& color = pExt->Color; + if (!pThis || !*(void**)pThis) return SkipGameCode; + + auto* pExt = EBoltExt::ExtMap.Find(pThis); + if (!pExt) return SkipGameCode; + + BoltTemp::ExtData = pExt; + BoltTemp::FogHidden = false; + + // DESYNC SAFETY: Only apply fog gating for local player's view + // This is purely visual and must not affect game simulation or synchronized state + if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar) { + static int fogCheckCount = 0; + if (fogCheckCount++ < 3) { + Debug::Log("[FOW] FogOfWar is enabled, checking conditions (call #%d)\n", fogCheckCount); + } + if (auto* me = HouseClass::CurrentPlayer; me && !me->SpySatActive) { + // Owner-only fog gate: if you can see the shooter, you can see their bolt + if (pThis->Owner) { + // Convert to cell → back to the cell center; clamp Z to 0 so height can't bite us + CoordStruct ownerWorld = pThis->Owner->GetCoords(); + const CellStruct ownerCell = CellClass::Coord2Cell(ownerWorld); + ownerWorld = CellClass::Cell2Coord(ownerCell); + ownerWorld.Z = 0; + + #if FOW_DEBUG + static int debugCount = 0; + if (debugCount++ < 20) { + CoordStruct originalOwner = pThis->Owner->GetCoords(); + Debug::Log("[FOW] EBolt coord compare: original(%d,%d,%d) cell(%d,%d) cellCenter(%d,%d,0) fogged=%d\n", + originalOwner.X, originalOwner.Y, originalOwner.Z, + ownerCell.X, ownerCell.Y, ownerWorld.X, ownerWorld.Y, + Fog::IsFogged(ownerWorld) ? 1 : 0); + + // Also test the original coordinates for comparison + Debug::Log("[FOW] Original coord fogged=%d, cell center fogged=%d\n", + Fog::IsFogged(originalOwner) ? 1 : 0, Fog::IsFogged(ownerWorld) ? 1 : 0); + } + #endif + + // Try different fog checking methods to find what matches visual fog + bool foggedByCoord = Fog::IsFogged(ownerWorld); + bool foggedByCell = false; + + // Try direct cell fog check + auto* cellObj = MapClass::Instance.GetCellAt(ownerCell); + if (cellObj) { + foggedByCell = cellObj->IsFogged(); + } + + #if FOW_DEBUG + if (debugCount <= 20) { + Debug::Log("[FOW] Fog methods: coord=%d cell=%d\n", foggedByCoord ? 1 : 0, foggedByCell ? 1 : 0); + } + #endif + + // Use cell-based method since coord-based is giving wrong results + if (foggedByCell) { + BoltTemp::FogHidden = true; + } + } + } + } - for (int idx = 0; idx < 3; ++idx) - BoltTemp::Color[idx] = Drawing::RGB_To_Int(color[idx]); + // Always populate colors; some code expects them even if we end up hidden + const auto& color = pExt->Color; + for (int i = 0; i < 3; ++i) + BoltTemp::Color[i] = Drawing::RGB_To_Int(color[i]); return SkipGameCode; } DEFINE_HOOK(0x4C1F33, EBolt_Draw_Colors, 0x7) { + static int hookTest = 0; + if (hookTest++ < 3) { + Debug::Log("[FOW] EBolt_Draw_Colors hook entry (call #%d)\n", hookTest); + } return EBoltExt::_EBolt_Draw_Colors(R); } @@ -62,6 +145,13 @@ DEFINE_HOOK(0x4C20BC, EBolt_DrawArcs, 0xB) { enum { DoLoop = 0x4C20C7, Break = 0x4C2400 }; + // Reduced logging to prevent performance issues + static int arcTest = 0; + if (arcTest < 2) { + arcTest++; + Debug::Log("[FOW] EBolt_DrawArcs: FogHidden=%d\n", BoltTemp::FogHidden ? 1 : 0); + } + GET_STACK(int, plotIndex, STACK_OFFSET(0x408, -0x3E0)); const int arcCount = BoltTemp::ExtData->Arcs; @@ -71,7 +161,22 @@ DEFINE_HOOK(0x4C20BC, EBolt_DrawArcs, 0xB) DEFINE_JUMP(LJMP, 0x4C24BE, 0x4C24C3)// Disable Ares's hook EBolt_Draw_Color1 DEFINE_HOOK(0x4C24C3, EBolt_DrawFirst_Color, 0x9) { - if (BoltTemp::ExtData->Disable[0]) + // TRIPWIRE: If this executes you will SEE magenta bolts briefly + static int trip = 0; + if (trip++ < 3) { + R->EAX(Drawing::RGB_To_Int(ColorStruct{255, 0, 255})); // Bright magenta + return 0x4C24E4; // continue with forced color + } + + #if FOW_DEBUG + static int c=0; + if (c++ < 5) { + Debug::Log("[FOW] EBolt_DrawFirst_Color: FogHidden=%d Disable[0]=%d\n", + BoltTemp::FogHidden ? 1 : 0, BoltTemp::ExtData->Disable[0] ? 1 : 0); + } + #endif + + if (BoltTemp::FogHidden || BoltTemp::ExtData->Disable[0]) return 0x4C2515; R->EAX(BoltTemp::Color[0]); @@ -81,7 +186,7 @@ DEFINE_HOOK(0x4C24C3, EBolt_DrawFirst_Color, 0x9) DEFINE_JUMP(LJMP, 0x4C25CB, 0x4C25D0)// Disable Ares's hook EBolt_Draw_Color2 DEFINE_HOOK(0x4C25D0, EBolt_DrawSecond_Color, 0x6) { - if (BoltTemp::ExtData->Disable[1]) + if (BoltTemp::FogHidden || BoltTemp::ExtData->Disable[1]) return 0x4C262A; R->Stack(STACK_OFFSET(0x424, -0x40C), BoltTemp::Color[1]); @@ -91,7 +196,7 @@ DEFINE_HOOK(0x4C25D0, EBolt_DrawSecond_Color, 0x6) DEFINE_JUMP(LJMP, 0x4C26CF, 0x4C26D5)// Disable Ares's hook EBolt_Draw_Color3 DEFINE_HOOK(0x4C26D5, EBolt_DrawThird_Color, 0x6) { - if (BoltTemp::ExtData->Disable[2]) + if (BoltTemp::FogHidden || BoltTemp::ExtData->Disable[2]) return 0x4C2710; R->EAX(BoltTemp::Color[2]); diff --git a/src/Misc/Hooks.LaserDraw.cpp b/src/Misc/Hooks.LaserDraw.cpp index 0716a97063..dd3a710d25 100644 --- a/src/Misc/Hooks.LaserDraw.cpp +++ b/src/Misc/Hooks.LaserDraw.cpp @@ -5,6 +5,9 @@ #include #include #include +#include + +#define LASER_FOW_DEBUG 0 namespace LaserDrawTemp { diff --git a/src/New/Entity/LaserTrailClass.cpp b/src/New/Entity/LaserTrailClass.cpp index 06c915d6cd..281d14cefa 100644 --- a/src/New/Entity/LaserTrailClass.cpp +++ b/src/New/Entity/LaserTrailClass.cpp @@ -23,23 +23,23 @@ bool LaserTrailClass::Update(CoordStruct location) { if (pType->DrawType == LaserTrailDrawType::Laser) { - // FOG CHECK: Only create laser if both endpoints are visible + // FOG CHECK: Only create laser if both endpoints are visible (updated method) bool sourceVisible = true; bool targetVisible = true; // Check if fog of war is enabled and we have valid instances if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && - &MapClass::Instance && TacticalClass::Instance) { + HouseClass::CurrentPlayer && !HouseClass::CurrentPlayer->SpySatActive) { - // Check source location + // Check source location using cell-based fog (matches EBolt method) auto sourceCs = CellClass::Coord2Cell(this->LastLocation.Get()); - auto* sourceCell = MapClass::Instance.TryGetCellAt(sourceCs); - sourceVisible = sourceCell && (sourceCell->Flags & CellFlags::EdgeRevealed); + auto* sourceCell = MapClass::Instance.GetCellAt(sourceCs); + sourceVisible = sourceCell && !sourceCell->IsFogged(); - // Check target location + // Check target location using cell-based fog auto targetCs = CellClass::Coord2Cell(location); - auto* targetCell = MapClass::Instance.TryGetCellAt(targetCs); - targetVisible = targetCell && (targetCell->Flags & CellFlags::EdgeRevealed); + auto* targetCell = MapClass::Instance.GetCellAt(targetCs); + targetVisible = targetCell && !targetCell->IsFogged(); } // Only create laser if both endpoints are visible @@ -62,17 +62,17 @@ bool LaserTrailClass::Update(CoordStruct location) // Check if fog of war is enabled and we have valid instances if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && - &MapClass::Instance && TacticalClass::Instance) { + HouseClass::CurrentPlayer && !HouseClass::CurrentPlayer->SpySatActive) { - // Check source location + // Check source location using cell-based fog (matches EBolt method) auto sourceCs = CellClass::Coord2Cell(this->LastLocation.Get()); - auto* sourceCell = MapClass::Instance.TryGetCellAt(sourceCs); - sourceVisible = sourceCell && (sourceCell->Flags & CellFlags::EdgeRevealed); + auto* sourceCell = MapClass::Instance.GetCellAt(sourceCs); + sourceVisible = sourceCell && !sourceCell->IsFogged(); - // Check target location + // Check target location using cell-based fog auto targetCs = CellClass::Coord2Cell(location); - auto* targetCell = MapClass::Instance.TryGetCellAt(targetCs); - targetVisible = targetCell && (targetCell->Flags & CellFlags::EdgeRevealed); + auto* targetCell = MapClass::Instance.GetCellAt(targetCs); + targetVisible = targetCell && !targetCell->IsFogged(); } // Only create EBolt if both endpoints are visible @@ -111,17 +111,17 @@ bool LaserTrailClass::Update(CoordStruct location) // Check if fog of war is enabled and we have valid instances if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && - &MapClass::Instance && TacticalClass::Instance) { + HouseClass::CurrentPlayer && !HouseClass::CurrentPlayer->SpySatActive) { - // Check source location + // Check source location using cell-based fog (matches EBolt method) auto sourceCs = CellClass::Coord2Cell(this->LastLocation.Get()); - auto* sourceCell = MapClass::Instance.TryGetCellAt(sourceCs); - sourceVisible = sourceCell && (sourceCell->Flags & CellFlags::EdgeRevealed); + auto* sourceCell = MapClass::Instance.GetCellAt(sourceCs); + sourceVisible = sourceCell && !sourceCell->IsFogged(); - // Check target location + // Check target location using cell-based fog auto targetCs = CellClass::Coord2Cell(location); - auto* targetCell = MapClass::Instance.TryGetCellAt(targetCs); - targetVisible = targetCell && (targetCell->Flags & CellFlags::EdgeRevealed); + auto* targetCell = MapClass::Instance.GetCellAt(targetCs); + targetVisible = targetCell && !targetCell->IsFogged(); } // Only create RadBeam if both endpoints are visible From fd39d7e3f7e99f54ef0b0f3aeade1b8ba0eb4a62 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Mon, 22 Sep 2025 09:14:31 +1000 Subject: [PATCH 12/20] Fixed LaserDrawClass from showing under fog (includes DiskLaser), and removed Ebolt debug logging --- src/Ext/EBolt/Hooks.cpp | 35 +---------------------------------- src/Misc/Hooks.LaserDraw.cpp | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Ext/EBolt/Hooks.cpp b/src/Ext/EBolt/Hooks.cpp index 322389615f..b550960205 100644 --- a/src/Ext/EBolt/Hooks.cpp +++ b/src/Ext/EBolt/Hooks.cpp @@ -8,14 +8,8 @@ #include #include -#define FOW_DEBUG 1 +#define FOW_DEBUG 0 -// Static initializer to confirm this DLL is loaded -namespace { - struct _EBoltInitPing { - _EBoltInitPing() { Debug::Log("[FOW] EBolt hooks compiled into this build.\n"); } - } _eboltInitPing; -} namespace BoltTemp { @@ -54,11 +48,6 @@ DWORD _cdecl EBoltExt::_EBolt_Draw_Colors(REGISTERS* R) { enum { SkipGameCode = 0x4C1F66 }; - // Always log first few calls to confirm hook is running - static int hookCount = 0; - if (hookCount < 3) { - Debug::Log("[FOW] _EBolt_Draw_Colors hook running (call #%d)\n", ++hookCount); - } GET(EBolt*, pThis, ECX); if (!pThis || !*(void**)pThis) return SkipGameCode; @@ -72,10 +61,6 @@ DWORD _cdecl EBoltExt::_EBolt_Draw_Colors(REGISTERS* R) // DESYNC SAFETY: Only apply fog gating for local player's view // This is purely visual and must not affect game simulation or synchronized state if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar) { - static int fogCheckCount = 0; - if (fogCheckCount++ < 3) { - Debug::Log("[FOW] FogOfWar is enabled, checking conditions (call #%d)\n", fogCheckCount); - } if (auto* me = HouseClass::CurrentPlayer; me && !me->SpySatActive) { // Owner-only fog gate: if you can see the shooter, you can see their bolt if (pThis->Owner) { @@ -100,8 +85,6 @@ DWORD _cdecl EBoltExt::_EBolt_Draw_Colors(REGISTERS* R) } #endif - // Try different fog checking methods to find what matches visual fog - bool foggedByCoord = Fog::IsFogged(ownerWorld); bool foggedByCell = false; // Try direct cell fog check @@ -134,10 +117,6 @@ DWORD _cdecl EBoltExt::_EBolt_Draw_Colors(REGISTERS* R) DEFINE_HOOK(0x4C1F33, EBolt_Draw_Colors, 0x7) { - static int hookTest = 0; - if (hookTest++ < 3) { - Debug::Log("[FOW] EBolt_Draw_Colors hook entry (call #%d)\n", hookTest); - } return EBoltExt::_EBolt_Draw_Colors(R); } @@ -145,12 +124,6 @@ DEFINE_HOOK(0x4C20BC, EBolt_DrawArcs, 0xB) { enum { DoLoop = 0x4C20C7, Break = 0x4C2400 }; - // Reduced logging to prevent performance issues - static int arcTest = 0; - if (arcTest < 2) { - arcTest++; - Debug::Log("[FOW] EBolt_DrawArcs: FogHidden=%d\n", BoltTemp::FogHidden ? 1 : 0); - } GET_STACK(int, plotIndex, STACK_OFFSET(0x408, -0x3E0)); const int arcCount = BoltTemp::ExtData->Arcs; @@ -161,12 +134,6 @@ DEFINE_HOOK(0x4C20BC, EBolt_DrawArcs, 0xB) DEFINE_JUMP(LJMP, 0x4C24BE, 0x4C24C3)// Disable Ares's hook EBolt_Draw_Color1 DEFINE_HOOK(0x4C24C3, EBolt_DrawFirst_Color, 0x9) { - // TRIPWIRE: If this executes you will SEE magenta bolts briefly - static int trip = 0; - if (trip++ < 3) { - R->EAX(Drawing::RGB_To_Int(ColorStruct{255, 0, 255})); // Bright magenta - return 0x4C24E4; // continue with forced color - } #if FOW_DEBUG static int c=0; diff --git a/src/Misc/Hooks.LaserDraw.cpp b/src/Misc/Hooks.LaserDraw.cpp index dd3a710d25..381362e2a6 100644 --- a/src/Misc/Hooks.LaserDraw.cpp +++ b/src/Misc/Hooks.LaserDraw.cpp @@ -6,12 +6,44 @@ #include #include #include +#include +#include +#include +#include #define LASER_FOW_DEBUG 0 namespace LaserDrawTemp { ColorStruct maxColor; + bool FogHidden = false; // Flag to track if current laser should be hidden due to fog +} + +// Simple fog gate for vanilla lasers - basic working version +DEFINE_HOOK(0x550260, LaserDrawClass_Draw_FogGate, 0x6) +{ + enum { SkipDrawing = 0x5509D2 }; + + GET(LaserDrawClass*, pThis, ECX); + if(!pThis) return 0; + + // Treat (0,0) source as fogged to avoid a single startup frame + if((pThis->Source.X | pThis->Source.Y) == 0) return SkipDrawing; + + // Simple fog check using source coordinates + if(ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar) { + if(HouseClass::CurrentPlayer && !HouseClass::CurrentPlayer->SpySatActive) { + const CoordStruct from = pThis->Source; + const CellStruct cell = CellClass::Coord2Cell(from); + if(auto* c = MapClass::Instance.GetCellAt(cell)) { + if(c->IsFogged()) { + return SkipDrawing; + } + } + } + } + + return 0; } DEFINE_HOOK(0x550D1F, LaserDrawClass_DrawInHouseColor_Context_Set, 0x6) @@ -49,3 +81,4 @@ DEFINE_HOOK(0x550F47, LaserDrawClass_DrawInHouseColor_BetterDrawing, 0x0) return 0x550F9D; } + From 54d20a6b9c77658eb4ff90efc26cbed2b1937d2c Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Mon, 22 Sep 2025 12:52:14 +1000 Subject: [PATCH 13/20] Fix RadBeam showing under fog --- src/Misc/Hooks.RadBeam.cpp | 138 +++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/Misc/Hooks.RadBeam.cpp diff --git a/src/Misc/Hooks.RadBeam.cpp b/src/Misc/Hooks.RadBeam.cpp new file mode 100644 index 0000000000..d42c7a36c8 --- /dev/null +++ b/src/Misc/Hooks.RadBeam.cpp @@ -0,0 +1,138 @@ +// src/Misc/Hooks.RadBeam.cpp +#include +#include + +#include +#include +#include +#include +#include +#include + +#define RADBEAM_FOW_DEBUG 1 + +// "either-endpoint visible" to match Laser feel; fail closed. +static __forceinline bool EitherEndVisible(const CoordStruct& a, const CoordStruct& b) { + // No fog → always visible + if(!(ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar)) { + return true; + } + // Global reveal + if(HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) { + return true; + } + // A endpoint + if(auto* ca = MapClass::Instance.GetCellAt(CellClass::Coord2Cell(a))) { + if(!ca->IsFogged()) { return true; } + } + // B endpoint + if(auto* cb = MapClass::Instance.GetCellAt(CellClass::Coord2Cell(b))) { + if(!cb->IsFogged()) { return true; } + } + return false; +} + +/* + RadBeam fog gating - DISABLED due to Ares conflicts + + Multiple approaches tried but all crash due to Ares interaction: + 1. Post-prologue hooks (0x6596ED/0x659666) - stack corruption + 2. Call-site hook (0x6592BB) - ECX doesn't contain RadBeam pointer + + RadBeam fog gating remains unimplemented until Ares compatibility resolved. + + Working fog gates: EBolt, LaserDraw, LaserTrail +*/ + +namespace RadBeamTemp +{ + bool FogHidden = false; // Flag to track if current beam should be hidden due to fog +} + +#include + +static __forceinline bool CellVisible(const CellStruct& cs) { + if(!(ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar)) return true; + if(HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) return true; + if(auto* c = MapClass::Instance.GetCellAt(cs)) return !c->IsFogged(); + return false; +} + +// Original draw (whatever Ares/others already point the CALL to) +static void (__thiscall* RadBeam_Draw_Orig)(RadBeam*) = nullptr; + +// Called from the *patched CALL* (so we have real CALL/RET semantics). +static void __fastcall RadBeam_Draw_Gate(RadBeam* pThis, void* /*edx*/) { +#if RADBEAM_FOW_DEBUG + static int dbg = 0; + if(dbg++ < 8 && pThis) { + Debug::Log("[RADBEAM_FOW] Gate pThis=%p from=(%d,%d,%d) to=(%d,%d,%d)\n", + pThis, + pThis->SourceLocation.X, pThis->SourceLocation.Y, pThis->SourceLocation.Z, + pThis->TargetLocation.X, pThis->TargetLocation.Y, pThis->TargetLocation.Z); + } +#endif + if(!pThis) return; + if(!(ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar)) { RadBeam_Draw_Orig(pThis); return; } + if(HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) { RadBeam_Draw_Orig(pThis); return; } + if(EitherEndVisible(pThis->SourceLocation, pThis->TargetLocation)) { RadBeam_Draw_Orig(pThis); return; } + +#if RADBEAM_FOW_DEBUG + if(dbg <= 8) Debug::Log("[RADBEAM_FOW] both fogged → skip draw\n"); +#endif + // both fogged → do nothing, return to caller (after CALL) +} + +static void* ResolveCallTarget(uintptr_t callSite) { + if(*reinterpret_cast(callSite) != 0xE8) return nullptr; // must be CALL rel32 + int32_t rel = *reinterpret_cast(callSite + 1); + return reinterpret_cast(callSite + 5 + rel); +} + +static bool PatchCALL(uintptr_t callSite, void* newTarget) { + DWORD oldProt; + if(!VirtualProtect(reinterpret_cast(callSite), 5, PAGE_EXECUTE_READWRITE, &oldProt)) return false; + *reinterpret_cast(callSite) = 0xE8; + int32_t rel = static_cast(reinterpret_cast(newTarget) - (callSite + 5)); + *reinterpret_cast(callSite + 1) = rel; + VirtualProtect(reinterpret_cast(callSite), 5, oldProt, &oldProt); + FlushInstructionCache(GetCurrentProcess(), reinterpret_cast(callSite), 5); + return true; +} + +// Call this once during DLL init, after Ares has done its own patches. +void Install_RadBeamFogGate() { + // Verified CALL site to RadBeam::Draw inside DrawAll: + const uintptr_t site = 0x6592C6; // next instruction is 0x6592CB + + // Must be CALL (0xE8). If a previous JMP hook sits here, bail. + if(*reinterpret_cast(site) != 0xE8) { +#if RADBEAM_FOW_DEBUG + Debug::Log("[RADBEAM_FOW] ERROR: 0x%08X is not CALL (byte=%02X)\n", + site, *reinterpret_cast(site)); +#endif + return; + } + + // Capture original target (this may be Ares' wrapper — that's fine) + void* target = ResolveCallTarget(site); + RadBeam_Draw_Orig = reinterpret_cast(target); +#if RADBEAM_FOW_DEBUG + Debug::Log("[RADBEAM_FOW] Captured original draw target = %p from site 0x%08X\n", + RadBeam_Draw_Orig, site); +#endif + if(!RadBeam_Draw_Orig) return; + + // Redirect that CALL to our gate + PatchCALL(site, reinterpret_cast(&RadBeam_Draw_Gate)); +#if RADBEAM_FOW_DEBUG + Debug::Log("[RADBEAM_FOW] Patched CALL at 0x%08X to gate\n", site); +#endif +} + +// Install the patch during initialization - use a different hook point +DEFINE_HOOK(0x685659, Scenario_ClearClasses_RadBeamInit, 0xA) +{ + Install_RadBeamFogGate(); + return 0; +} \ No newline at end of file From 076117c504414d8668394c487a18e0e100050713 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Tue, 23 Sep 2025 19:26:57 +1000 Subject: [PATCH 14/20] fixed MindControl Link showing under fog --- src/Ext/CaptureManager/Hooks.cpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Ext/CaptureManager/Hooks.cpp b/src/Ext/CaptureManager/Hooks.cpp index e85cbe5dd7..759fdef77f 100644 --- a/src/Ext/CaptureManager/Hooks.cpp +++ b/src/Ext/CaptureManager/Hooks.cpp @@ -71,10 +71,30 @@ DEFINE_HOOK(0x4721E6, CaptureManagerClass_DrawLinkToVictim, 0x6) if (EnumFunctions::CanTargetHouse(pExt->MindControlLink_VisibleToHouse, pAttacker->Owner, HouseClass::CurrentPlayer)) { - auto nVictimCoord = pVictim->Location; - nVictimCoord.Z += pVictim->GetTechnoType()->LeptonMindControlOffset; - auto const nFLH = pAttacker->GetFLH(-1 - nNodeCount % 5, CoordStruct::Empty); - DrawALinkTo(nFLH, nVictimCoord, pAttacker->Owner->Color); + // Fog of war gating: only draw if both attacker and victim are visible + bool attackerVisible = true; + bool victimVisible = true; + + if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar && + HouseClass::CurrentPlayer && !HouseClass::CurrentPlayer->SpySatActive) + { + auto attackerCell = CellClass::Coord2Cell(pAttacker->Location); + auto victimCell = CellClass::Coord2Cell(pVictim->Location); + + if (auto* cell = MapClass::Instance.GetCellAt(attackerCell)) + attackerVisible = !cell->IsFogged(); + + if (auto* cell = MapClass::Instance.GetCellAt(victimCell)) + victimVisible = !cell->IsFogged(); + } + + if (attackerVisible && victimVisible) + { + auto nVictimCoord = pVictim->Location; + nVictimCoord.Z += pVictim->GetTechnoType()->LeptonMindControlOffset; + auto const nFLH = pAttacker->GetFLH(-1 - nNodeCount % 5, CoordStruct::Empty); + DrawALinkTo(nFLH, nVictimCoord, pAttacker->Owner->Color); + } } R->EBP(nNodeCount); From 6876f4fc4dd123fc440242f39dd6f53a21eb2ac7 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Tue, 23 Sep 2025 23:08:38 +1000 Subject: [PATCH 15/20] Fix Phobos FlyingStrings from showing. also fix cursor crash round 2 --- src/Ext/Building/Hooks.Refinery.cpp | 14 ++++++++++- src/Ext/Building/Hooks.Selling.cpp | 11 ++++++++- src/Ext/WarheadType/Detonate.cpp | 10 +++++++- src/Misc/FlyingStrings.cpp | 36 +++++++++++++++++++++-------- src/Misc/FogOfWar.cpp | 3 ++- 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/Ext/Building/Hooks.Refinery.cpp b/src/Ext/Building/Hooks.Refinery.cpp index ce51e77537..52eff41e6d 100644 --- a/src/Ext/Building/Hooks.Refinery.cpp +++ b/src/Ext/Building/Hooks.Refinery.cpp @@ -52,7 +52,19 @@ DEFINE_HOOK(0x522E4F, InfantryClass_SlaveGiveMoney_CheckBalanceAfter, 0x6) else if (auto const pBldTypeExt = BuildingTypeExt::ExtMap.TryFind(slaveMiner->GetTechnoType()->DeploysInto)) { if (pBldTypeExt->DisplayIncome.Get(RulesExt::Global()->DisplayIncome.Get())) - FlyingStrings::AddMoneyString(money, slaveMiner->Owner, RulesExt::Global()->DisplayIncome_Houses.Get(), slaveMiner->Location); + { + // Only show flying strings if slave miner location is visible (not fogged) + if (auto const pCell = MapClass::Instance.TryGetCellAt(slaveMiner->Location)) + { + if (!pCell->IsFogged() && !pCell->IsShrouded()) + { + FlyingStrings::AddMoneyString( + money, slaveMiner->Owner, + RulesExt::Global()->DisplayIncome_Houses.Get(), + slaveMiner->Location); + } + } + } } return 0; diff --git a/src/Ext/Building/Hooks.Selling.cpp b/src/Ext/Building/Hooks.Selling.cpp index eb507b9d86..03c7f4478d 100644 --- a/src/Ext/Building/Hooks.Selling.cpp +++ b/src/Ext/Building/Hooks.Selling.cpp @@ -19,7 +19,16 @@ DEFINE_HOOK(0x4D9F7B, FootClass_Sell, 0x6) } if (RulesExt::Global()->DisplayIncome.Get()) - FlyingStrings::AddMoneyString(money, pOwner, RulesExt::Global()->DisplayIncome_Houses.Get(), pThis->Location); + { + // Only show flying strings if building location is visible (not fogged) + if (auto const pCell = MapClass::Instance.TryGetCellAt(pThis->Location)) + { + if (!pCell->IsFogged() && !pCell->IsShrouded()) + { + FlyingStrings::AddMoneyString(money, pOwner, RulesExt::Global()->DisplayIncome_Houses.Get(), pThis->Location); + } + } + } return ReadyToVanish; } diff --git a/src/Ext/WarheadType/Detonate.cpp b/src/Ext/WarheadType/Detonate.cpp index 4990e42e48..cbf4bea96e 100644 --- a/src/Ext/WarheadType/Detonate.cpp +++ b/src/Ext/WarheadType/Detonate.cpp @@ -170,7 +170,15 @@ void WarheadTypeExt::ExtData::Detonate(TechnoClass* pOwner, HouseClass* pHouse, if (this->TransactMoney_Display) { auto displayCoords = this->TransactMoney_Display_AtFirer ? (pOwner ? pOwner->Location : coords) : coords; - FlyingStrings::AddMoneyString(this->TransactMoney, pHouse, this->TransactMoney_Display_Houses, displayCoords, this->TransactMoney_Display_Offset); + + // Only show flying strings if location is visible (not fogged) + if (auto const pCell = MapClass::Instance.TryGetCellAt(displayCoords)) + { + if (!pCell->IsFogged() && !pCell->IsShrouded()) + { + FlyingStrings::AddMoneyString(this->TransactMoney, pHouse, this->TransactMoney_Display_Houses, displayCoords, this->TransactMoney_Display_Offset); + } + } } } diff --git a/src/Misc/FlyingStrings.cpp b/src/Misc/FlyingStrings.cpp index 81daef4516..12d06fdadc 100644 --- a/src/Misc/FlyingStrings.cpp +++ b/src/Misc/FlyingStrings.cpp @@ -8,6 +8,7 @@ #include #include #include +#include std::vector FlyingStrings::Data; @@ -21,6 +22,10 @@ bool FlyingStrings::DrawAllowed(CoordStruct& nCoords) void FlyingStrings::Add(const wchar_t* text, const CoordStruct& coords, ColorStruct color, Point2D pixelOffset) { + // Debug: Log all flying strings being added + Debug::Log("[PHOBOS_FLYINGSTRINGS] Adding string '%ls' at (%d,%d,%d)\n", + text, coords.X, coords.Y, coords.Z); + Item item {}; item.Location = coords; item.PixelOffset = pixelOffset; @@ -32,6 +37,10 @@ void FlyingStrings::Add(const wchar_t* text, const CoordStruct& coords, ColorStr void FlyingStrings::AddMoneyString(int amount, HouseClass* owner, AffectedHouse displayToHouses, const CoordStruct& coords, Point2D pixelOffset) { + // Debug: Log all money strings being added + Debug::Log("[PHOBOS_FLYINGSTRINGS] AddMoneyString amount=%d at (%d,%d,%d)\n", + amount, coords.X, coords.Y, coords.Z); + if (amount && (displayToHouses == AffectedHouse::All || owner && EnumFunctions::CanTargetHouse(displayToHouses, owner, HouseClass::CurrentPlayer))) @@ -62,17 +71,26 @@ void FlyingStrings::UpdateAll() point += dataItem.PixelOffset; - RectangleStruct bound = DSurface::Temp->GetRect(); - bound.Height -= 32; + // Only draw if location is not fogged/shrouded + bool canDraw = DrawAllowed(dataItem.Location); + Debug::Log("[PHOBOS_FLYINGSTRINGS] UpdateAll: string '%ls' at (%d,%d,%d) canDraw=%s\n", + dataItem.Text, dataItem.Location.X, dataItem.Location.Y, dataItem.Location.Z, + canDraw ? "YES" : "NO"); - if (Unsorted::CurrentFrame > dataItem.CreationFrame + Duration - 70) + if (canDraw) { - point.Y -= (Unsorted::CurrentFrame - dataItem.CreationFrame); - DSurface::Temp->DrawText(dataItem.Text, &bound, &point, dataItem.Color, 0, TextPrintType::NoShadow); - } - else - { - DSurface::Temp->DrawText(dataItem.Text, &bound, &point, dataItem.Color, 0, TextPrintType::NoShadow); + RectangleStruct bound = DSurface::Temp->GetRect(); + bound.Height -= 32; + + if (Unsorted::CurrentFrame > dataItem.CreationFrame + Duration - 70) + { + point.Y -= (Unsorted::CurrentFrame - dataItem.CreationFrame); + DSurface::Temp->DrawText(dataItem.Text, &bound, &point, dataItem.Color, 0, TextPrintType::NoShadow); + } + else + { + DSurface::Temp->DrawText(dataItem.Text, &bound, &point, dataItem.Color, 0, TextPrintType::NoShadow); + } } if (Unsorted::CurrentFrame > dataItem.CreationFrame + Duration || Unsorted::CurrentFrame < dataItem.CreationFrame) diff --git a/src/Misc/FogOfWar.cpp b/src/Misc/FogOfWar.cpp index 6c07c70677..5e64c3abad 100644 --- a/src/Misc/FogOfWar.cpp +++ b/src/Misc/FogOfWar.cpp @@ -735,7 +735,8 @@ DEFINE_HOOK(0x51F95F, InfantryClass_GetCursorOverCell_OverFog, 0x6) { pType = pObject->BuildingData.Type; - if (pThis->Type->Engineer && pThis->Owner->IsControlledByCurrentPlayer()) + // Safety check: Ensure pType is valid before accessing its members + if (pType && pThis->Type->Engineer && pThis->Owner->IsControlledByCurrentPlayer()) { if (pType->BridgeRepairHut) { From 0fdd77288a0d8bd9da35bff66e8bfdb8973cd349 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Tue, 23 Sep 2025 23:57:15 +1000 Subject: [PATCH 16/20] Implemented RemoveShroudGlobally to support FoW without SW hacks, RemoveShroudOnly should still work though --- src/Ext/Rules/Body.cpp | 67 ++++++++++++++++++++++++++++++++++++++++++ src/Ext/Rules/Body.h | 4 +++ src/Phobos.cpp | 5 ++++ 3 files changed, 76 insertions(+) diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index da9557129c..2d659a7192 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -16,6 +16,11 @@ #include #include #include +#include +#include +#include +#include +#include std::unique_ptr RulesExt::Data = nullptr; @@ -157,6 +162,8 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->HeightShadowScaling = false; this->HeightShadowScaling_MinScale.Read(exINI, GameStrings::AudioVisual, "HeightShadowScaling.MinScale"); + this->RemoveShroudGlobally.Read(exINI, GameStrings::General, "RemoveShroudGlobally"); + this->ExtendedAircraftMissions.Read(exINI, GameStrings::General, "ExtendedAircraftMissions"); this->ExtendedAircraftMissions_UnlandDamage.Read(exINI, GameStrings::General, "ExtendedAircraftMissions.UnlandDamage"); this->AmphibiousEnter.Read(exINI, GameStrings::General, "AmphibiousEnter"); @@ -451,6 +458,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->AirShadowBaseScale_log) .Process(this->HeightShadowScaling) .Process(this->HeightShadowScaling_MinScale) + .Process(this->RemoveShroudGlobally) .Process(this->ExtendedAircraftMissions) .Process(this->ExtendedAircraftMissions_UnlandDamage) .Process(this->AmphibiousEnter) @@ -771,3 +779,62 @@ DEFINE_HOOK(0x6744E4, RulesClass_ReadJumpjetControls_Extra, 0x7) // skip vanilla JumpjetControls and make it earlier load // DEFINE_JUMP(LJMP, 0x668EB5, 0x668EBD); // RulesClass_Process_SkipJumpjetControls // Really necessary? won't hurt to read again + +void RulesExt::ApplyRemoveShroudGlobally() +{ + if (!RulesExt::Global() || !RulesExt::Global()->RemoveShroudGlobally) + { + Debug::Log("RemoveShroudGlobally: Not enabled or RulesExt not available\n"); + return; + } + + Debug::Log("RemoveShroudGlobally: Starting shroud removal process\n"); + + // Global shroud removal - use CellRangeIterator like the warhead does + auto const& mapRect = MapClass::Instance.MapRect; + int cellsProcessed = 0; + + // Calculate map center + CellStruct mapCenter = { + static_cast(mapRect.X + mapRect.Width / 2), + static_cast(mapRect.Y + mapRect.Height / 2) + }; + + // Use a very large radius to cover entire map (max map size is around 200x200) + const int radius = 200; + + Debug::Log("RemoveShroudGlobally: Using CellRangeIterator with center (%d,%d) and radius %d\n", + mapCenter.X, mapCenter.Y, radius); + + CellRangeIterator{}(mapCenter, radius + 0.5, [&cellsProcessed](CellClass* pCell) + { + if (pCell) + { + cellsProcessed++; + // Use the exact same logic as the RemoveShroudOnly warhead + while (pCell->ShroudCounter > 0) + { + pCell->ReduceShroudCounter(); + } + + // Clear gap coverage + pCell->GapsCoveringThisCell = 0; + + // Update visibility without touching fog + char visibility = TacticalClass::Instance->GetOcclusion(pCell->MapCoords, false); + if (pCell->Visibility != visibility) + { + pCell->Visibility = visibility; + TacticalClass::Instance->RegisterCellAsVisible(pCell); + } + } + return true; + }); + + Debug::Log("RemoveShroudGlobally: Processed %d cells, forcing redraw\n", cellsProcessed); + + // Force full map redraw + MapClass::Instance.MarkNeedsRedraw(2); + + Debug::Log("RemoveShroudGlobally: Shroud removal completed\n"); +} diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 4889f08a42..eb44475064 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -98,6 +98,8 @@ class RulesExt Valueable HeightShadowScaling_MinScale; double AirShadowBaseScale_log; + Valueable RemoveShroudGlobally; + Valueable ExtendedAircraftMissions; Valueable ExtendedAircraftMissions_UnlandDamage; Valueable AmphibiousEnter; @@ -523,4 +525,6 @@ class RulesExt { Global()->InvalidatePointer(ptr, removed); } + + static void ApplyRemoveShroudGlobally(); }; diff --git a/src/Phobos.cpp b/src/Phobos.cpp index c061b50170..888af74ecf 100644 --- a/src/Phobos.cpp +++ b/src/Phobos.cpp @@ -9,6 +9,8 @@ #include #include "Utilities/AresHelper.h" #include "Utilities/Parser.h" +#include +#include #ifndef IS_RELEASE_VER bool HideWarning = false; @@ -229,9 +231,12 @@ DEFINE_HOOK(0x67E68A, LoadGame_UnsetFlag, 0x5) DEFINE_HOOK(0x683E7F, ScenarioClass_Start_Optimizations, 0x7) { Phobos::ApplyOptimizations(); + RulesExt::ApplyRemoveShroudGlobally(); return 0; } + + #ifndef IS_RELEASE_VER DEFINE_HOOK(0x4F4583, GScreenClass_DrawText, 0x6) { From 0e70f90de46a2d7361c7384d73309b112d60540d Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Wed, 24 Sep 2025 07:46:10 +1000 Subject: [PATCH 17/20] Fixed issue with flickering/vanishing objects e.g. BuildingTypes, OverlayTypes and TerrainTypes. still to do: objects remain on screen unchanged from fog proxy state --- src/Misc/FogOfWar.cpp | 142 ++++++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 68 deletions(-) diff --git a/src/Misc/FogOfWar.cpp b/src/Misc/FogOfWar.cpp index 5e64c3abad..a44403f26b 100644 --- a/src/Misc/FogOfWar.cpp +++ b/src/Misc/FogOfWar.cpp @@ -15,6 +15,7 @@ #include #include #include +#include // Burst-reveal detection for viewport refresh static int g_RevealedCellsThisFrame = 0; @@ -45,27 +46,36 @@ static inline void ForceUnfreezeAllFogProxiesForSpySat() { static void SafeCleanupOrphanedFoggedObjects() { - static int frameCounter = 0; - - // Run every 15 frames to avoid performance issues during mass reveals - if ((++frameCounter % 15) != 0) { + auto& live = FoggedObject::FoggedObjects; + if (live.Count <= 0) { return; } - auto& live = FoggedObject::FoggedObjects; // global container - if (live.Count <= 0) { - return; + // Batch processing to improve performance + const int maxBatchSize = 50; // Process max 50 objects per cleanup call + const int totalObjects = live.Count; + const int batchSize = std::min(maxBatchSize, totalObjects); + + static int nextIndex = 0; + if (nextIndex >= totalObjects) { + nextIndex = 0; // Wrap around } - // Collect candidates first to avoid mutating 'live' while scanning it. DynamicVectorClass toDelete; - // Guard reserve to something sane to avoid wild counts if memory is corrupt. - const int cap = (live.Count > 0 && live.Count < 1000000) ? live.Count : 0; - if (cap > 0) { toDelete.Reserve(cap); } + toDelete.Reserve(batchSize); - for (int i = 0; i < live.Count; ++i) { - FoggedObject* fo = live[i]; - if (!fo) { continue; } + // Process a batch of objects + for (int processed = 0; processed < batchSize && nextIndex < totalObjects; ++processed, ++nextIndex) { + FoggedObject* fo = live[nextIndex]; + if (!fo) { + continue; + } + + // Quick validity check - skip expensive cell lookups if possible + if (fo->Location.X < 0 || fo->Location.Y < 0) { + toDelete.AddItem(fo); + continue; + } // Find the cell by coordinates; DO NOT use ExtMap. CellStruct cellCoords = CellClass::Coord2Cell(fo->Location); @@ -86,13 +96,13 @@ static void SafeCleanupOrphanedFoggedObjects() } } - // Now actually remove and free. - // GameDelete() auto-removes from 'live', so we can skip Remove(fo). + // Remove and free collected objects for (int i = 0; i < toDelete.Count; ++i) { FoggedObject* fo = toDelete[i]; - // Ensure it is gone from the render list before dealloc (defensive). - FoggedObject::FoggedObjects.Remove(fo); - GameDelete(fo); + if (fo) { + FoggedObject::FoggedObjects.Remove(fo); + GameDelete(fo); + } } } @@ -166,10 +176,15 @@ DEFINE_HOOK(0x5F4B3E, ObjectClass_DrawIfVisible, 0x6) } if (!fogged) { + // Clear any previous redraw flag to avoid unnecessary redraws + if (pThis->NeedsRedraw) { + pThis->NeedsRedraw = false; + } return 0x5F4B48; // normal draw } - // Fogged: skip draw this frame (don't keep forcing redraw while fogged) + // Fogged: skip draw this frame and stop redraw cycling + // Only clear NeedsRedraw if we're actually fogged to prevent flashing pThis->NeedsRedraw = false; return 0x5F4D06; } @@ -763,24 +778,37 @@ DEFINE_HOOK(0x6D3470, TacticalClass_DrawFoggedObject, 0x8) GET_STACK(RectangleStruct*, pRect2, 0x8); GET_STACK(bool, bForceViewBounds, 0xC); - // Clean up orphans BEFORE computing finalRect or rendering - SafeCleanupOrphanedFoggedObjects(); + // Performance optimization: track camera movement + static RectangleStruct lastViewBounds = {0, 0, 0, 0}; + static int cleanupFrameCounter = 0; + static bool needsFullRedraw = false; + + const RectangleStruct currentViewBounds = DSurface::ViewBounds; + const bool cameraMoving = (lastViewBounds.X != currentViewBounds.X || + lastViewBounds.Y != currentViewBounds.Y || + lastViewBounds.Width != currentViewBounds.Width || + lastViewBounds.Height != currentViewBounds.Height); + + // Only run expensive cleanup every 30 frames when camera is stationary, or every 5 frames when moving + const int cleanupInterval = cameraMoving ? 5 : 30; + if ((++cleanupFrameCounter % cleanupInterval) == 0) { + SafeCleanupOrphanedFoggedObjects(); + } // SpySat edge-triggered thaw const bool spyNow = (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive); if (spyNow && !g_SpySatWasActive) { ForceUnfreezeAllFogProxiesForSpySat(); - g_NeedViewportRefresh = true; // force same-frame repaint + needsFullRedraw = true; } g_SpySatWasActive = spyNow; - // One-shot viewport repaint if we had a burst - if (g_NeedViewportRefresh) { - // Mark the whole tactical viewport dirty once + // Viewport refresh logic - reduce frequency + if (g_NeedViewportRefresh || needsFullRedraw || cameraMoving) { RectangleStruct vb = DSurface::ViewBounds; TacticalClass::Instance->RegisterDirtyArea(vb, /*force*/true); - g_NeedViewportRefresh = false; + needsFullRedraw = false; } // Reset per-frame counter @@ -788,54 +816,32 @@ DEFINE_HOOK(0x6D3470, TacticalClass_DrawFoggedObject, 0x8) // SpySat reveals everything, so don't draw fogged objects when SpySat is active if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) - return 0x6D3650; // Skip fogged object rendering + return 0x6D3650; - RectangleStruct finalRect { 0,0,0,0 }; - if (bForceViewBounds && DSurface::ViewBounds.Width > 0 && DSurface::ViewBounds.Height > 0) - finalRect = std::move(FoggedObject::Union(finalRect, DSurface::ViewBounds)); - else - { - if (pRect1->Width > 0 && pRect1->Height > 0) - finalRect = std::move(FoggedObject::Union(finalRect, *pRect1)); - if (pRect2->Width > 0 && pRect2->Height > 0) - finalRect = std::move(FoggedObject::Union(finalRect, *pRect2)); + // Simplified and more stable render bounds calculation + RectangleStruct finalRect = DSurface::ViewBounds; - if (const auto nVisibleCellCount = pThis->VisibleCellCount) - { - RectangleStruct buffer; - buffer.Width = buffer.Height = 60; + // Expand bounds slightly to handle edge cases and reduce flickering + const int expandMargin = 128; // leptons + finalRect.X = std::max(finalRect.X - expandMargin, 0); + finalRect.Y = std::max(finalRect.Y - expandMargin, 0); + finalRect.Width = std::min(finalRect.Width + (expandMargin * 2), DSurface::ViewBounds.Width); + finalRect.Height = std::min(finalRect.Height + (expandMargin * 2), DSurface::ViewBounds.Height); - for (int i = 0; i < nVisibleCellCount; ++i) - { - auto const pCell = pThis->VisibleCells[i]; - auto location = pCell->GetCoords(); - auto [point, visible] = TacticalClass::Instance->CoordsToClient(location); - buffer.X = DSurface::ViewBounds.X + point.X - 30; - buffer.Y = DSurface::ViewBounds.Y + point.Y; - finalRect = std::move(FoggedObject::Union(finalRect, buffer)); + // Only render fogged objects if we have a valid rect and objects exist + if (finalRect.Width > 0 && finalRect.Height > 0 && FoggedObject::FoggedObjects.Count > 0) + { + // Performance optimization: only render objects that might be visible + for (const auto pObject : FoggedObject::FoggedObjects) + { + if (pObject && pObject->Visible) { + pObject->Render(finalRect); } } - - // TODO: Fix Drawing::DirtyAreas() access - may not exist in current Phobos - //for (int i = 0; i < Drawing::DirtyAreas().Count; i++) - //{ - // RectangleStruct buffer = Drawing::DirtyAreas()[i].Rect; - // buffer.Y += DSurface::ViewBounds.Y; - // if (buffer.Width > 0 && buffer.Height > 0) - // finalRect = std::move(FoggedObject::Union(finalRect, buffer)); - //} } - if (finalRect.Width > 0 && finalRect.Height > 0) - { - finalRect.X = std::max(finalRect.X, 0); - finalRect.Y = std::max(finalRect.Y, 0); - finalRect.Width = std::min(finalRect.Width, DSurface::ViewBounds.Width - finalRect.X); - finalRect.Height = std::min(finalRect.Height, DSurface::ViewBounds.Height - finalRect.Y); - - for (const auto pObject : FoggedObject::FoggedObjects) - pObject->Render(finalRect); - } + // Update tracking + lastViewBounds = currentViewBounds; return 0x6D3650; } From d324c831dfa05920faab33fd37f41d4c3f2b7a75 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Wed, 24 Sep 2025 08:57:36 +1000 Subject: [PATCH 18/20] Fog proxy system overhaul - comprehensive fixes for flickering, performance, and cleanup issues --- src/Misc/FogOfWar.cpp | 119 ++++++++++++++++++++++++++++++++---------- 1 file changed, 92 insertions(+), 27 deletions(-) diff --git a/src/Misc/FogOfWar.cpp b/src/Misc/FogOfWar.cpp index a44403f26b..23e295d6a8 100644 --- a/src/Misc/FogOfWar.cpp +++ b/src/Misc/FogOfWar.cpp @@ -24,6 +24,42 @@ static bool g_NeedViewportRefresh = false; // SpySat state tracking static bool g_SpySatWasActive = false; +// Forward declaration for force cleanup function +static void ForceCleanupFogProxiesAt(const CoordStruct& coords, BuildingTypeClass* pBuildingType = nullptr); + +// Force cleanup of fog proxies for a specific building type at a location +static void ForceCleanupFogProxiesAt(const CoordStruct& coords, BuildingTypeClass* pBuildingType) +{ + auto& live = FoggedObject::FoggedObjects; + DynamicVectorClass toDelete; + + for (int i = 0; i < live.Count; ++i) { + FoggedObject* fo = live[i]; + if (!fo || fo->CoveredType != FoggedObject::CoveredType::Building) { + continue; + } + + // Check if this proxy is at the specified location + const int tolerance = 256; // leptons tolerance for building footprint + if (abs(fo->Location.X - coords.X) <= tolerance && + abs(fo->Location.Y - coords.Y) <= tolerance) { + // If building type specified, only remove matching proxies + if (!pBuildingType || fo->BuildingData.Type == pBuildingType) { + toDelete.AddItem(fo); + } + } + } + + // Remove matching proxies + for (int i = 0; i < toDelete.Count; ++i) { + FoggedObject* fo = toDelete[i]; + if (fo) { + FoggedObject::FoggedObjects.Remove(fo); + GameDelete(fo); + } + } +} + static inline void ForceUnfreezeAllFogProxiesForSpySat() { // snapshot to avoid invalidation while deleting DynamicVectorClass snap; @@ -51,33 +87,24 @@ static void SafeCleanupOrphanedFoggedObjects() return; } - // Batch processing to improve performance - const int maxBatchSize = 50; // Process max 50 objects per cleanup call - const int totalObjects = live.Count; - const int batchSize = std::min(maxBatchSize, totalObjects); - - static int nextIndex = 0; - if (nextIndex >= totalObjects) { - nextIndex = 0; // Wrap around - } - DynamicVectorClass toDelete; - toDelete.Reserve(batchSize); + toDelete.Reserve(live.Count); - // Process a batch of objects - for (int processed = 0; processed < batchSize && nextIndex < totalObjects; ++processed, ++nextIndex) { - FoggedObject* fo = live[nextIndex]; + // Check all objects for immediate cleanup of revealed areas + for (int i = 0; i < live.Count; ++i) { + FoggedObject* fo = live[i]; if (!fo) { + toDelete.AddItem(fo); continue; } - // Quick validity check - skip expensive cell lookups if possible + // Quick validity check - remove objects with invalid coordinates if (fo->Location.X < 0 || fo->Location.Y < 0) { toDelete.AddItem(fo); continue; } - // Find the cell by coordinates; DO NOT use ExtMap. + // Find the cell by coordinates CellStruct cellCoords = CellClass::Coord2Cell(fo->Location); CellClass* cell = MapClass::Instance.TryGetCellAt(cellCoords); if (!cell) { @@ -86,13 +113,41 @@ static void SafeCleanupOrphanedFoggedObjects() continue; } - // Orphan rule: if the cell is *not covered* anymore (no fog & no shroud), - // keeping a fog proxy makes no sense; schedule for deletion. - const bool shrouded = !(cell->Flags & CellFlags::EdgeRevealed); - const bool fogged = (cell->Foggedness != -1) || (cell->Flags & CellFlags::Fogged); + // Check if the cell is revealed (no longer needs fog proxy) + const bool revealed = !!(cell->Flags & CellFlags::EdgeRevealed); + const bool notFogged = (cell->Foggedness == -1) && !(cell->Flags & CellFlags::Fogged); - if (!shrouded && !fogged) { + // If revealed and not fogged, remove the proxy immediately + if (revealed && notFogged) { toDelete.AddItem(fo); + continue; + } + + // For building proxies, check if the real building still exists + if (fo->CoveredType == FoggedObject::CoveredType::Building) { + // More aggressive cleanup - if cell is revealed, always remove building proxy + if (revealed && notFogged) { + toDelete.AddItem(fo); + continue; + } + + // Check if there's a real building at this location + bool realBuildingExists = false; + for (ObjectClass* pObj = cell->FirstObject; pObj; pObj = pObj->NextObject) { + if (auto pBuilding = abstract_cast(pObj)) { + if (pBuilding->Type == fo->BuildingData.Type && + !pBuilding->InLimbo && pBuilding->IsAlive && pBuilding->Health > 0) { + realBuildingExists = true; + break; + } + } + } + + // If no real building exists, remove proxy + if (!realBuildingExists) { + toDelete.AddItem(fo); + continue; + } } } @@ -344,9 +399,12 @@ DEFINE_HOOK(0x4A9CA0, MapClass_RevealFogShroud, 0x8) if (g_RevealedCellsThisFrame >= 64) { g_NeedViewportRefresh = true; } - + pCell->CleanFog(); - + + // Force immediate cleanup of fog proxies in this revealed cell + ForceCleanupFogProxiesAt(CellClass::Cell2Coord(*pMapCoords), nullptr); + // Just set buildings in this cell to redraw - let the fog gate handle visibility for (ObjectClass* pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) { if (auto pBuilding = abstract_cast(pObject)) { @@ -789,8 +847,8 @@ DEFINE_HOOK(0x6D3470, TacticalClass_DrawFoggedObject, 0x8) lastViewBounds.Width != currentViewBounds.Width || lastViewBounds.Height != currentViewBounds.Height); - // Only run expensive cleanup every 30 frames when camera is stationary, or every 5 frames when moving - const int cleanupInterval = cameraMoving ? 5 : 30; + // More aggressive cleanup when areas are being revealed or camera is moving + const int cleanupInterval = (cameraMoving || g_RevealedCellsThisFrame > 0) ? 1 : 10; if ((++cleanupFrameCounter % cleanupInterval) == 0) { SafeCleanupOrphanedFoggedObjects(); } @@ -888,9 +946,12 @@ DEFINE_HOOK(0x577EBF, MapClass_Reveal, 0x6) if (g_RevealedCellsThisFrame >= 64) { g_NeedViewportRefresh = true; } - + pCell->CleanFog(); - + + // Force immediate cleanup of fog proxies in this revealed cell + ForceCleanupFogProxiesAt(pCell->GetCoords(), nullptr); + // Just set buildings in this cell to redraw - let the fog gate handle visibility for (ObjectClass* pObject = pCell->FirstObject; pObject; pObject = pObject->NextObject) { if (auto pBuilding = abstract_cast(pObject)) { @@ -927,6 +988,10 @@ DEFINE_HOOK(0x4FC1FF, HouseClass_PlayerDefeated_MapReveal, 0x6) return 0x4FC214; } + +// Building destruction cleanup disabled - was causing crashes during destruction +// The regular cleanup process will handle orphaned building proxies + // Check if building placement should be allowed at location (respects fog like shroud) bool CanPlaceBuildingInFog(const CoordStruct& coords, BuildingTypeClass* pBuildingType) { // If fog of war is disabled, allow placement From 54f93dcf5202cc1f0ea4f665da10b32ec78d7a45 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Wed, 24 Sep 2025 23:07:40 +1000 Subject: [PATCH 19/20] Fixed Ebolt, RadBeam and Shrapnel ownerless Ebolts --- Phobos.vcxproj | 15 ++++++ src/Ext/Bullet/Body.cpp | 93 ++++++++++++++++++++++++++++++++++++-- src/Ext/EBolt/Hooks.cpp | 84 +++++++++++++++++++++++++++++++++- src/Misc/Hooks.RadBeam.cpp | 41 ++++++++++------- src/Misc/Hooks.RadBeam.h | 4 ++ src/Phobos.cpp | 10 ++++ 6 files changed, 226 insertions(+), 21 deletions(-) create mode 100644 src/Misc/Hooks.RadBeam.h diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 31569a8fae..d58bda1b90 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -31,6 +31,7 @@ + @@ -43,6 +44,9 @@ + + + @@ -57,6 +61,7 @@ + @@ -93,6 +98,7 @@ + @@ -186,11 +192,14 @@ + + + @@ -228,7 +237,11 @@ + + + + @@ -244,6 +257,7 @@ + @@ -264,6 +278,7 @@ + diff --git a/src/Ext/Bullet/Body.cpp b/src/Ext/Bullet/Body.cpp index 915c914317..2f843db9fb 100644 --- a/src/Ext/Bullet/Body.cpp +++ b/src/Ext/Bullet/Body.cpp @@ -233,16 +233,77 @@ inline void BulletExt::SimulatedFiringLaser(BulletClass* pBullet, HouseClass* pH // Make sure pBullet and pBullet->WeaponType is not empty before call inline void BulletExt::SimulatedFiringElectricBolt(BulletClass* pBullet) { + // Debug logging for shrapnel EBolt creation + static int shrapnelCount = 0; + if (shrapnelCount++ < 5) { + Debug::Log("[FOW] SimulatedFiringElectricBolt called (shrapnel #%d)\n", shrapnelCount); + } + // Can not use 0x6FD460 because the firer may die const auto pWeapon = pBullet->WeaponType; - if (!pWeapon->IsElectricBolt) + if (!pWeapon->IsElectricBolt) { + if (shrapnelCount <= 5) { + Debug::Log("[FOW] Not IsElectricBolt weapon, skipping\n"); + } return; + } + + if (shrapnelCount <= 5) { + Debug::Log("[FOW] IsElectricBolt=true, proceeding with fog check\n"); + } + + const auto targetCoords = pBullet->Type->Inviso ? pBullet->Location : pBullet->TargetCoords; + + // FOG CHECK: Only create EBolt if either source or target is visible + if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar) { + if (shrapnelCount <= 5) { + Debug::Log("[FOW] Fog of war enabled, checking visibility\n"); + } + if (HouseClass::CurrentPlayer && !HouseClass::CurrentPlayer->SpySatActive) { + // Check source location using cell-based fog (consistent with other EBolt fog checks) + bool sourceVisible = true; + bool targetVisible = true; + + auto sourceCs = CellClass::Coord2Cell(pBullet->SourceCoords); + if (auto* sourceCell = MapClass::Instance.GetCellAt(sourceCs)) { + sourceVisible = !sourceCell->IsFogged(); + } + + auto targetCs = CellClass::Coord2Cell(targetCoords); + if (auto* targetCell = MapClass::Instance.GetCellAt(targetCs)) { + targetVisible = !targetCell->IsFogged(); + } + + if (shrapnelCount <= 5) { + Debug::Log("[FOW] Source visible: %d, Target visible: %d\n", sourceVisible ? 1 : 0, targetVisible ? 1 : 0); + } + + // Only create EBolt if either endpoint is visible + if (!sourceVisible && !targetVisible) { + if (shrapnelCount <= 5) { + Debug::Log("[FOW] Both endpoints fogged, SKIPPING EBolt creation\n"); + } + return; // Both fogged, skip creating EBolt entirely + } else { + if (shrapnelCount <= 5) { + Debug::Log("[FOW] At least one endpoint visible, creating EBolt\n"); + } + } + } else { + if (shrapnelCount <= 5) { + Debug::Log("[FOW] SpySat active or no current player, creating EBolt\n"); + } + } + } else { + if (shrapnelCount <= 5) { + Debug::Log("[FOW] No fog of war, creating EBolt\n"); + } + } const auto pBolt = EBoltExt::CreateEBolt(pWeapon); pBolt->AlternateColor = pWeapon->IsAlternateColor; - const auto targetCoords = pBullet->Type->Inviso ? pBullet->Location : pBullet->TargetCoords; pBolt->Fire(pBullet->SourceCoords, targetCoords, 0); if (const auto particle = WeaponTypeExt::ExtMap.Find(pWeapon)->Bolt_ParticleSystem.Get(RulesClass::Instance->DefaultSparkSystem)) @@ -257,12 +318,38 @@ inline void BulletExt::SimulatedFiringRadBeam(BulletClass* pBullet, HouseClass* if (!pWeapon->IsRadBeam) return; + const auto targetCoords = pBullet->Type->Inviso ? pBullet->Location : pBullet->TargetCoords; + + // FOG CHECK: Only create RadBeam if either source or target is visible + if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar) { + if (HouseClass::CurrentPlayer && !HouseClass::CurrentPlayer->SpySatActive) { + // Check source location using cell-based fog (consistent with other fog checks) + bool sourceVisible = true; + bool targetVisible = true; + + auto sourceCs = CellClass::Coord2Cell(pBullet->SourceCoords); + if (auto* sourceCell = MapClass::Instance.GetCellAt(sourceCs)) { + sourceVisible = !sourceCell->IsFogged(); + } + + auto targetCs = CellClass::Coord2Cell(targetCoords); + if (auto* targetCell = MapClass::Instance.GetCellAt(targetCs)) { + targetVisible = !targetCell->IsFogged(); + } + + // Only create RadBeam if either endpoint is visible + if (!sourceVisible && !targetVisible) { + return; // Both fogged, skip creating RadBeam entirely + } + } + } + const auto pWH = pWeapon->Warhead; const bool isTemporal = pWH && pWH->Temporal; const auto pRadBeam = RadBeam::Allocate(isTemporal ? RadBeamType::Temporal : RadBeamType::RadBeam); pRadBeam->SetCoordsSource(pBullet->SourceCoords); - pRadBeam->SetCoordsTarget((pBullet->Type->Inviso ? pBullet->Location : pBullet->TargetCoords)); + pRadBeam->SetCoordsTarget(targetCoords); const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeapon); diff --git a/src/Ext/EBolt/Hooks.cpp b/src/Ext/EBolt/Hooks.cpp index b550960205..235878fa95 100644 --- a/src/Ext/EBolt/Hooks.cpp +++ b/src/Ext/EBolt/Hooks.cpp @@ -11,6 +11,16 @@ #define FOW_DEBUG 0 +// stable, single-pass erase of the first matching pointer; preserves order +template +static __forceinline void stable_erase_first(std::vector& v, T* value) +{ + for (size_t i = 0, n = v.size(); i < n; ++i) + { + if (v[i] == value) { v.erase(v.begin() + i); return; } + } +} + namespace BoltTemp { EBoltExt::ExtData* ExtData = nullptr; @@ -58,11 +68,33 @@ DWORD _cdecl EBoltExt::_EBolt_Draw_Colors(REGISTERS* R) BoltTemp::ExtData = pExt; BoltTemp::FogHidden = false; + #if FOW_DEBUG + static int totalBolts = 0; + if (totalBolts++ < 10) { + Debug::Log("[FOW] EBolt fog check: ScenarioClass=%p FogOfWar=%d Owner=%p\n", + ScenarioClass::Instance, + (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar) ? 1 : 0, + pThis->Owner); + } + #endif + // DESYNC SAFETY: Only apply fog gating for local player's view // This is purely visual and must not affect game simulation or synchronized state if (ScenarioClass::Instance && ScenarioClass::Instance->SpecialFlags.FogOfWar) { + #if FOW_DEBUG + if (totalBolts <= 10) { + Debug::Log("[FOW] EBolt in fog mode: CurrentPlayer=%p SpySat=%d\n", + HouseClass::CurrentPlayer, + HouseClass::CurrentPlayer ? (HouseClass::CurrentPlayer->SpySatActive ? 1 : 0) : -1); + } + #endif if (auto* me = HouseClass::CurrentPlayer; me && !me->SpySatActive) { // Owner-only fog gate: if you can see the shooter, you can see their bolt + #if FOW_DEBUG + if (totalBolts <= 10) { + Debug::Log("[FOW] EBolt owner check: Owner=%p\n", pThis->Owner); + } + #endif if (pThis->Owner) { // Convert to cell → back to the cell center; clamp Z to 0 so height can't bite us CoordStruct ownerWorld = pThis->Owner->GetCoords(); @@ -95,7 +127,7 @@ DWORD _cdecl EBoltExt::_EBolt_Draw_Colors(REGISTERS* R) #if FOW_DEBUG if (debugCount <= 20) { - Debug::Log("[FOW] Fog methods: coord=%d cell=%d\n", foggedByCoord ? 1 : 0, foggedByCell ? 1 : 0); + Debug::Log("[FOW] Fog methods: cell=%d\n", foggedByCell ? 1 : 0); } #endif @@ -103,6 +135,47 @@ DWORD _cdecl EBoltExt::_EBolt_Draw_Colors(REGISTERS* R) if (foggedByCell) { BoltTemp::FogHidden = true; } + } else { + #if FOW_DEBUG + if (totalBolts <= 10) { + Debug::Log("[FOW] EBolt has no owner - using source/target coords for fog check\n"); + } + #endif + + // Fallback for EBolts without owners (like shrapnel) - check source and target + bool sourceVisible = false, targetVisible = false; + + // Check source coordinates (Point1) + CoordStruct sourceCoords = pThis->Point1; + CellStruct sourceCell = CellClass::Coord2Cell(sourceCoords); + if (auto* sourceCellObj = MapClass::Instance.GetCellAt(sourceCell)) { + sourceVisible = !sourceCellObj->IsFogged(); + } + + // Check target coordinates (Point2) + CoordStruct targetCoords = pThis->Point2; + CellStruct targetCell = CellClass::Coord2Cell(targetCoords); + if (auto* targetCellObj = MapClass::Instance.GetCellAt(targetCell)) { + targetVisible = !targetCellObj->IsFogged(); + } + + #if FOW_DEBUG + if (totalBolts <= 10) { + Debug::Log("[FOW] EBolt endpoint fog check: source(%d,%d)=%s target(%d,%d)=%s\n", + sourceCell.X, sourceCell.Y, sourceVisible ? "visible" : "fogged", + targetCell.X, targetCell.Y, targetVisible ? "visible" : "fogged"); + } + #endif + + // Hide EBolt if both endpoints are fogged (either-endpoint visible policy) + if (!sourceVisible && !targetVisible) { + BoltTemp::FogHidden = true; + #if FOW_DEBUG + if (totalBolts <= 10) { + Debug::Log("[FOW] EBolt both endpoints fogged - hiding\n"); + } + #endif + } } } } @@ -134,6 +207,13 @@ DEFINE_HOOK(0x4C20BC, EBolt_DrawArcs, 0xB) DEFINE_JUMP(LJMP, 0x4C24BE, 0x4C24C3)// Disable Ares's hook EBolt_Draw_Color1 DEFINE_HOOK(0x4C24C3, EBolt_DrawFirst_Color, 0x9) { + // TRIPWIRE: If this executes you will SEE magenta bolts briefly + static int trip = 0; + if (trip++ < 3) { + Debug::Log("[FOW] TRIPWIRE: EBolt hook running - you should see magenta bolt!\n"); + R->EAX(Drawing::RGB_To_Int(ColorStruct{255, 0, 255})); // Bright magenta + return 0x4C24E4; // continue with forced color + } #if FOW_DEBUG static int c=0; @@ -201,7 +281,7 @@ void EBoltFake::_RemoveFromOwner() { auto const pExt = TechnoExt::ExtMap.Find(this->Owner); auto& vec = pExt->ElectricBolts; - vec.erase(std::remove(vec.begin(), vec.end(), this), vec.end()); + stable_erase_first(vec, static_cast(this)); // Performance optimization: was erase(remove(...)) this->Owner = nullptr; } diff --git a/src/Misc/Hooks.RadBeam.cpp b/src/Misc/Hooks.RadBeam.cpp index d42c7a36c8..b28d09502b 100644 --- a/src/Misc/Hooks.RadBeam.cpp +++ b/src/Misc/Hooks.RadBeam.cpp @@ -1,4 +1,5 @@ // src/Misc/Hooks.RadBeam.cpp +#include "Hooks.RadBeam.h" #include #include @@ -9,7 +10,7 @@ #include #include -#define RADBEAM_FOW_DEBUG 1 +#define RADBEAM_FOW_DEBUG 0 // "either-endpoint visible" to match Laser feel; fail closed. static __forceinline bool EitherEndVisible(const CoordStruct& a, const CoordStruct& b) { @@ -101,19 +102,27 @@ static bool PatchCALL(uintptr_t callSite, void* newTarget) { } // Call this once during DLL init, after Ares has done its own patches. +// This function is called from Phobos.cpp ScenarioClass_Start_Optimizations hook void Install_RadBeamFogGate() { + Debug::Log("[RADBEAM_FOW] Install_RadBeamFogGate() called\n"); + // Verified CALL site to RadBeam::Draw inside DrawAll: const uintptr_t site = 0x6592C6; // next instruction is 0x6592CB + Debug::Log("[RADBEAM_FOW] Checking address 0x%08X for CALL instruction\n", site); + uint8_t byteAtSite = *reinterpret_cast(site); + Debug::Log("[RADBEAM_FOW] Byte at 0x%08X = 0x%02X (expected 0xE8 for CALL)\n", site, byteAtSite); + // Must be CALL (0xE8). If a previous JMP hook sits here, bail. - if(*reinterpret_cast(site) != 0xE8) { -#if RADBEAM_FOW_DEBUG - Debug::Log("[RADBEAM_FOW] ERROR: 0x%08X is not CALL (byte=%02X)\n", - site, *reinterpret_cast(site)); -#endif + if(byteAtSite != 0xE8) { + Debug::Log("[RADBEAM_FOW] ERROR: Address 0x%08X is not CALL instruction (found 0x%02X instead of 0xE8)\n", + site, byteAtSite); + Debug::Log("[RADBEAM_FOW] RadBeam fog gating will NOT work - hook installation FAILED\n"); return; } + Debug::Log("[RADBEAM_FOW] Found CALL instruction, proceeding with hook installation\n"); + // Capture original target (this may be Ares' wrapper — that's fine) void* target = ResolveCallTarget(site); RadBeam_Draw_Orig = reinterpret_cast(target); @@ -124,15 +133,15 @@ void Install_RadBeamFogGate() { if(!RadBeam_Draw_Orig) return; // Redirect that CALL to our gate - PatchCALL(site, reinterpret_cast(&RadBeam_Draw_Gate)); -#if RADBEAM_FOW_DEBUG - Debug::Log("[RADBEAM_FOW] Patched CALL at 0x%08X to gate\n", site); -#endif + bool patchSuccess = PatchCALL(site, reinterpret_cast(&RadBeam_Draw_Gate)); + if (patchSuccess) { + Debug::Log("[RADBEAM_FOW] SUCCESS: Patched CALL at 0x%08X to gate function\n", site); + Debug::Log("[RADBEAM_FOW] RadBeam fog gating is now ACTIVE - you should see gate messages when RadBeams are drawn\n"); + } else { + Debug::Log("[RADBEAM_FOW] ERROR: Failed to patch CALL at 0x%08X\n", site); + Debug::Log("[RADBEAM_FOW] RadBeam fog gating will NOT work - patch installation FAILED\n"); + } } -// Install the patch during initialization - use a different hook point -DEFINE_HOOK(0x685659, Scenario_ClearClasses_RadBeamInit, 0xA) -{ - Install_RadBeamFogGate(); - return 0; -} \ No newline at end of file +// RadBeam fog installation is called from ScenarioClass_Start_Optimizations hook +// This ensures proper initialization timing after other systems are ready \ No newline at end of file diff --git a/src/Misc/Hooks.RadBeam.h b/src/Misc/Hooks.RadBeam.h new file mode 100644 index 0000000000..2343748941 --- /dev/null +++ b/src/Misc/Hooks.RadBeam.h @@ -0,0 +1,4 @@ +#pragma once + +// RadBeam fog gating installation function +void Install_RadBeamFogGate(); \ No newline at end of file diff --git a/src/Phobos.cpp b/src/Phobos.cpp index 888af74ecf..1361f1c2a6 100644 --- a/src/Phobos.cpp +++ b/src/Phobos.cpp @@ -12,6 +12,10 @@ #include #include +#include +#include +#include "Misc/Hooks.RadBeam.h" + #ifndef IS_RELEASE_VER bool HideWarning = false; #endif @@ -24,6 +28,7 @@ wchar_t Phobos::wideBuffer[Phobos::readLength]; const char* Phobos::AppIconPath = nullptr; bool Phobos::DisplayDamageNumbers = false; +bool Phobos::DisplayTechnoNames = false; bool Phobos::IsLoadingSaveGame = false; bool Phobos::Optimizations::Applied = false; @@ -225,6 +230,7 @@ DEFINE_HOOK(0x67E68A, LoadGame_UnsetFlag, 0x5) { Phobos::IsLoadingSaveGame = false; Phobos::ApplyOptimizations(); + return 0; } @@ -232,6 +238,10 @@ DEFINE_HOOK(0x683E7F, ScenarioClass_Start_Optimizations, 0x7) { Phobos::ApplyOptimizations(); RulesExt::ApplyRemoveShroudGlobally(); + + // Initialize RadBeam fog gating after other systems are ready + Install_RadBeamFogGate(); + return 0; } From c6c81cb62bc3caf4f2a55fd8890b88dc6d5bfe85 Mon Sep 17 00:00:00 2001 From: ayylmaoRotE Date: Wed, 24 Sep 2025 23:19:11 +1000 Subject: [PATCH 20/20] Removed RotE feats from project that were pulled by accident --- Phobos.vcxproj | 13 ------------- src/Phobos.cpp | 1 - 2 files changed, 14 deletions(-) diff --git a/Phobos.vcxproj b/Phobos.vcxproj index d58bda1b90..324be8febf 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -31,7 +31,6 @@ - @@ -44,9 +43,6 @@ - - - @@ -61,7 +57,6 @@ - @@ -98,7 +93,6 @@ - @@ -192,10 +186,8 @@ - - @@ -237,11 +229,7 @@ - - - - @@ -257,7 +245,6 @@ - diff --git a/src/Phobos.cpp b/src/Phobos.cpp index 1361f1c2a6..ce04ad9387 100644 --- a/src/Phobos.cpp +++ b/src/Phobos.cpp @@ -28,7 +28,6 @@ wchar_t Phobos::wideBuffer[Phobos::readLength]; const char* Phobos::AppIconPath = nullptr; bool Phobos::DisplayDamageNumbers = false; -bool Phobos::DisplayTechnoNames = false; bool Phobos::IsLoadingSaveGame = false; bool Phobos::Optimizations::Applied = false;