diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index c8a07539c3..324be8febf 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -191,9 +191,13 @@
+
+
+
+
@@ -260,6 +264,9 @@
+
+
+
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/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/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/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);
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..8704ef1434 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,11 @@ class CellExt
public:
std::vector RadSites {};
std::vector RadLevels { };
+ DynamicVectorClass FoggedObjects;
+ int InCleanFog { 0 };
- ExtData(CellClass* OwnerObject) : Extension(OwnerObject)
+ ExtData(CellClass* OwnerObject) : Extension(OwnerObject),
+ FoggedObjects {}
{ }
virtual ~ExtData() = default;
diff --git a/src/Ext/EBolt/Hooks.cpp b/src/Ext/EBolt/Hooks.cpp
index 0cf793571c..235878fa95 100644
--- a/src/Ext/EBolt/Hooks.cpp
+++ b/src/Ext/EBolt/Hooks.cpp
@@ -6,11 +6,26 @@
#include
#include
#include
+#include
+
+#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;
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,12 +58,132 @@ DWORD _cdecl EBoltExt::_EBolt_Draw_Colors(REGISTERS* R)
{
enum { SkipGameCode = 0x4C1F66 };
+
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;
- for (int idx = 0; idx < 3; ++idx)
- BoltTemp::Color[idx] = Drawing::RGB_To_Int(color[idx]);
+ 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();
+ 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
+
+ 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: cell=%d\n", foggedByCell ? 1 : 0);
+ }
+ #endif
+
+ // Use cell-based method since coord-based is giving wrong results
+ 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
+ }
+ }
+ }
+ }
+
+ // 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;
}
@@ -62,6 +197,7 @@ DEFINE_HOOK(0x4C20BC, EBolt_DrawArcs, 0xB)
{
enum { DoLoop = 0x4C20C7, Break = 0x4C2400 };
+
GET_STACK(int, plotIndex, STACK_OFFSET(0x408, -0x3E0));
const int arcCount = BoltTemp::ExtData->Arcs;
@@ -71,7 +207,23 @@ 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) {
+ 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;
+ 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 +233,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 +243,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]);
@@ -129,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/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/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/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp
index da6be50161..91b46f3b59 100644
--- a/src/Ext/Techno/Body.Update.cpp
+++ b/src/Ext/Techno/Body.Update.cpp
@@ -18,13 +18,66 @@
#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 particles under fog (client-side only) - now includes all technos, not just enemies
+ if (ScenarioClass::Instance
+ && ScenarioClass::Instance->SpecialFlags.FogOfWar
+ && HouseClass::CurrentPlayer && 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 %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();
+ 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/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..cbf4bea96e 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);
@@ -99,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/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/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/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
new file mode 100644
index 0000000000..23e295d6a8
--- /dev/null
+++ b/src/Misc/FogOfWar.cpp
@@ -0,0 +1,1025 @@
+#include
+
+#include
+
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Burst-reveal detection for viewport refresh
+static int g_RevealedCellsThisFrame = 0;
+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;
+ snap.Reserve(FoggedObject::FoggedObjects.Count);
+ for (auto* fo : FoggedObject::FoggedObjects) {
+ if (fo) { snap.AddItem(fo); }
+ }
+ for (auto* fo : snap) {
+ GameDelete(fo); // FoggedObject dtor should thaw real building & restore footprint
+ }
+}
+
+
+
+// ===== 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()
+{
+ auto& live = FoggedObject::FoggedObjects;
+ if (live.Count <= 0) {
+ return;
+ }
+
+ DynamicVectorClass toDelete;
+ toDelete.Reserve(live.Count);
+
+ // 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 - remove objects with invalid coordinates
+ if (fo->Location.X < 0 || fo->Location.Y < 0) {
+ toDelete.AddItem(fo);
+ continue;
+ }
+
+ // Find the cell by coordinates
+ CellStruct cellCoords = CellClass::Coord2Cell(fo->Location);
+ CellClass* cell = MapClass::Instance.TryGetCellAt(cellCoords);
+ if (!cell) {
+ // Cell missing → definitely orphaned
+ toDelete.AddItem(fo);
+ continue;
+ }
+
+ // 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 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;
+ }
+ }
+ }
+
+ // Remove and free collected objects
+ for (int i = 0; i < toDelete.Count; ++i) {
+ FoggedObject* fo = toDelete[i];
+ if (fo) {
+ FoggedObject::FoggedObjects.Remove(fo);
+ GameDelete(fo);
+ }
+ }
+}
+
+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: bypass all fog gating (no ExtMap access, just draw)
+ if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) {
+ return 0x5F4B48; // draw normally
+ }
+
+ switch (pThis->WhatAmI())
+ {
+ case AbstractType::Anim:
+ if (!static_cast(pThis)->Type->ShouldFogRemove)
+ return 0x5F4B48;
+ break;
+
+ case AbstractType::Unit:
+ case AbstractType::Cell:
+ case AbstractType::Building: // Buildings participate in fog gating
+ break;
+
+ default:
+ 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) {
+ // Clear any previous redraw flag to avoid unnecessary redraws
+ if (pThis->NeedsRedraw) {
+ pThis->NeedsRedraw = false;
+ }
+ return 0x5F4B48; // normal draw
+ }
+
+ // 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;
+}
+
+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) {
+ ++g_RevealedCellsThisFrame;
+ // Heuristic: 64 cells ~ a modest burst. Tweak if needed.
+ 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)) {
+ pBuilding->NeedsRedraw = true;
+ }
+ }
+ }
+
+ R->EAX(bRevealed);
+
+ return 0x4A9DC6;
+}
+
+DEFINE_HOOK(0x486C50, CellClass_ClearFoggedObjects, 0x6)
+{
+ // No deletion work here; avoid re-entrancy / bad ECX.
+ // Let our Ext-free GC handle deletions safely from the draw pass.
+ return 0x486D8A;
+}
+
+DEFINE_HOOK(0x486A70, CellClass_FogCell, 0x5)
+{
+ GET(CellClass*, pThis, ECX);
+
+ // SpySat => do not fog cells at all
+ if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive) {
+ return 0x486BE6; // skip fogging loop
+ }
+
+ 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);
+
+ // 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)
+ pCell = pThis->GetCell();
+
+ pThis->Deselect();
+
+ // 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);
+ };
+
+ // owner cell
+ addToCell(pCell);
+
+ // 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;
+ ++pFoundation)
+ {
+ 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;
+}
+
+// 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;
+ if (pExt && pExt->FoggedObjects.Count > 0) {
+ for (auto const pObject : pExt->FoggedObjects)
+ {
+ // Safety check: ensure pObject is valid
+ if (!pObject) continue;
+
+ if (pObject->Visible)
+ {
+ if (pObject->CoveredType == FoggedObject::CoveredType::Overlay)
+ nOvlIdx = pObject->OverlayData.Overlay;
+ else if (pObject->CoveredType == FoggedObject::CoveredType::Building)
+ {
+ // 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 {
+ // 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");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (nOvlIdx != -1)
+ R->Stack(STACK_OFFSET(0x2C, 0x18), OverlayTypeClass::Array.GetItem(nOvlIdx));
+
+ 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
+}
+
+// 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
+
+// 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);
+ 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;
+
+ // Safety check: Ensure pType is valid before accessing its members
+ if (pType && 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);
+
+ // 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);
+
+ // 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();
+ }
+
+ // SpySat edge-triggered thaw
+ const bool spyNow = (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive);
+ if (spyNow && !g_SpySatWasActive) {
+ ForceUnfreezeAllFogProxiesForSpySat();
+ needsFullRedraw = true;
+ }
+ g_SpySatWasActive = spyNow;
+
+ // 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
+ g_RevealedCellsThisFrame = 0;
+
+ // SpySat reveals everything, so don't draw fogged objects when SpySat is active
+ if (HouseClass::CurrentPlayer && HouseClass::CurrentPlayer->SpySatActive)
+ return 0x6D3650;
+
+ // Simplified and more stable render bounds calculation
+ RectangleStruct finalRect = DSurface::ViewBounds;
+
+ // 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);
+
+ // 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);
+ }
+ }
+ }
+
+ // Update tracking
+ lastViewBounds = currentViewBounds;
+
+ 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
+ ++g_RevealedCellsThisFrame;
+ // Heuristic: 64 cells ~ a modest burst. Tweak if needed.
+ 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)) {
+ pBuilding->NeedsRedraw = true;
+ }
+ }
+
+ 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;
+}
+
+
+// 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
+ 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..ec0e66ee19
--- /dev/null
+++ b/src/Misc/FogOfWar.h
@@ -0,0 +1,57 @@
+#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); }
+}
+
+// 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;
+
+ 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;
+
+ return IsUnrevealedForLocal(t->GetCoords());
+ }
+}
+
+// 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/Misc/Hooks.LaserDraw.cpp b/src/Misc/Hooks.LaserDraw.cpp
index 0716a97063..381362e2a6 100644
--- a/src/Misc/Hooks.LaserDraw.cpp
+++ b/src/Misc/Hooks.LaserDraw.cpp
@@ -5,10 +5,45 @@
#include
#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)
@@ -46,3 +81,4 @@ DEFINE_HOOK(0x550F47, LaserDrawClass_DrawInHouseColor_BetterDrawing, 0x0)
return 0x550F9D;
}
+
diff --git a/src/Misc/Hooks.RadBeam.cpp b/src/Misc/Hooks.RadBeam.cpp
new file mode 100644
index 0000000000..b28d09502b
--- /dev/null
+++ b/src/Misc/Hooks.RadBeam.cpp
@@ -0,0 +1,147 @@
+// src/Misc/Hooks.RadBeam.cpp
+#include "Hooks.RadBeam.h"
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define RADBEAM_FOW_DEBUG 0
+
+// "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.
+// 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(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);
+#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
+ 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");
+ }
+}
+
+// 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/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
diff --git a/src/New/Entity/LaserTrailClass.cpp b/src/New/Entity/LaserTrailClass.cpp
index a6fecf9343..281d14cefa 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 (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 &&
+ HouseClass::CurrentPlayer && !HouseClass::CurrentPlayer->SpySatActive) {
+
+ // Check source location using cell-based fog (matches EBolt method)
+ auto sourceCs = CellClass::Coord2Cell(this->LastLocation.Get());
+ auto* sourceCell = MapClass::Instance.GetCellAt(sourceCs);
+ sourceVisible = sourceCell && !sourceCell->IsFogged();
+
+ // Check target location using cell-based fog
+ auto targetCs = CellClass::Coord2Cell(location);
+ auto* targetCell = MapClass::Instance.GetCellAt(targetCs);
+ targetVisible = targetCell && !targetCell->IsFogged();
+ }
+
+ // 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 &&
+ HouseClass::CurrentPlayer && !HouseClass::CurrentPlayer->SpySatActive) {
+
+ // Check source location using cell-based fog (matches EBolt method)
+ auto sourceCs = CellClass::Coord2Cell(this->LastLocation.Get());
+ auto* sourceCell = MapClass::Instance.GetCellAt(sourceCs);
+ sourceVisible = sourceCell && !sourceCell->IsFogged();
+
+ // Check target location using cell-based fog
+ auto targetCs = CellClass::Coord2Cell(location);
+ auto* targetCell = MapClass::Instance.GetCellAt(targetCs);
+ targetVisible = targetCell && !targetCell->IsFogged();
+ }
+
+ // 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 &&
+ HouseClass::CurrentPlayer && !HouseClass::CurrentPlayer->SpySatActive) {
+
+ // Check source location using cell-based fog (matches EBolt method)
+ auto sourceCs = CellClass::Coord2Cell(this->LastLocation.Get());
+ auto* sourceCell = MapClass::Instance.GetCellAt(sourceCs);
+ sourceVisible = sourceCell && !sourceCell->IsFogged();
+
+ // Check target location using cell-based fog
+ auto targetCs = CellClass::Coord2Cell(location);
+ auto* targetCell = MapClass::Instance.GetCellAt(targetCs);
+ targetVisible = targetCell && !targetCell->IsFogged();
+ }
+
+ // 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/Phobos.cpp b/src/Phobos.cpp
index c061b50170..ce04ad9387 100644
--- a/src/Phobos.cpp
+++ b/src/Phobos.cpp
@@ -9,6 +9,12 @@
#include
#include "Utilities/AresHelper.h"
#include "Utilities/Parser.h"
+#include
+#include
+
+#include
+#include
+#include "Misc/Hooks.RadBeam.h"
#ifndef IS_RELEASE_VER
bool HideWarning = false;
@@ -223,15 +229,23 @@ DEFINE_HOOK(0x67E68A, LoadGame_UnsetFlag, 0x5)
{
Phobos::IsLoadingSaveGame = false;
Phobos::ApplyOptimizations();
+
return 0;
}
DEFINE_HOOK(0x683E7F, ScenarioClass_Start_Optimizations, 0x7)
{
Phobos::ApplyOptimizations();
+ RulesExt::ApplyRemoveShroudGlobally();
+
+ // Initialize RadBeam fog gating after other systems are ready
+ Install_RadBeamFogGate();
+
return 0;
}
+
+
#ifndef IS_RELEASE_VER
DEFINE_HOOK(0x4F4583, GScreenClass_DrawText, 0x6)
{
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