diff --git a/doc/site/content/changelogs/_index.markdown b/doc/site/content/changelogs/_index.markdown index ef8ee0fd67..d5138d86f0 100644 --- a/doc/site/content/changelogs/_index.markdown +++ b/doc/site/content/changelogs/_index.markdown @@ -7,4 +7,18 @@ title = "Running changelog" This is the bleeding-edge changelog since version 2026.07, for **pre-release 2026.08**. -No changes as of yet. \ No newline at end of file +# Fixes +* Line-of-fire and other synced ground traces (`TraceRay`, `CWeapon::HaveFreeLineOfFire`, +`Spring.GetUnitWeaponHaveFreeLineOfFire`) no longer report a free line when the ray starts +below the terrain. `LineGroundCol` returns a hit distance of 0 for such a ray and `TraceRay` +discarded that as "no hit", so a weapon whose muzzle or aim-from piece was inside a cliff +believed it could shoot through it, stopped, and never fired. Both the base weapon and cannon +line-of-fire checks now reject a source below the interpolated terrain height when ground +avoidance is enabled, even if the target is within explosion range. This matches the existing +pre-fire muzzle check; the base weapon's explosion-range exception remains for surface sources +and ground hits farther along the shot. +* The underground test of `LineGroundCol` (also behind `Spring.TraceRayGround*`) compares the +ray origin against the interpolated terrain height instead of the corner vertex of its +heightmap square. Next to a steep cliff that vertex could sit far above an origin that was +well clear of the ground, so the whole ground trace was skipped. A ray that starts exactly +on the surface is no longer treated as underground. diff --git a/rts/Game/Game.cpp b/rts/Game/Game.cpp index 32f267d175..63701b2449 100644 --- a/rts/Game/Game.cpp +++ b/rts/Game/Game.cpp @@ -387,35 +387,39 @@ void CGame::Load(const std::string& mapFileName) defsParser = &nullDefsParser; defsParser->Execute(); - // we can not (yet) do a clean early exit here because the dtor assumes - // all loading stages proceeded normally; just force automatic shutdown + // Skip later loading stages, which depend on the failed stage. + // Cleanup routines must tolerate components that were never initialized. forcedQuit = true; } - try { - LOG("[Game::%s][2] globalQuit=%d forcedQuit=%d", __func__, globalQuit.load(), forcedQuit); + if (!forcedQuit) { + try { + LOG("[Game::%s][2] globalQuit=%d forcedQuit=%d", __func__, globalQuit.load(), forcedQuit); - PreLoadSimulation(defsParser); - Watchdog::ClearTimer(WDT_LOAD); - PreLoadRendering(); - Watchdog::ClearTimer(WDT_LOAD); - } catch (const content_error& e) { - contentErrors.emplace_back(e.what()); - LOG_L(L_ERROR, "[Game::%s][2] forced quit with exception \"%s\"", __func__, e.what()); - forcedQuit = true; + PreLoadSimulation(defsParser); + Watchdog::ClearTimer(WDT_LOAD); + PreLoadRendering(); + Watchdog::ClearTimer(WDT_LOAD); + } catch (const content_error& e) { + contentErrors.emplace_back(e.what()); + LOG_L(L_ERROR, "[Game::%s][2] forced quit with exception \"%s\"", __func__, e.what()); + forcedQuit = true; + } } - try { - LOG("[Game::%s][3] globalQuit=%d forcedQuit=%d", __func__, globalQuit.load(), forcedQuit); + if (!forcedQuit) { + try { + LOG("[Game::%s][3] globalQuit=%d forcedQuit=%d", __func__, globalQuit.load(), forcedQuit); - PostLoadSimulation(defsParser); - Watchdog::ClearTimer(WDT_LOAD); - PostLoadRendering(); - Watchdog::ClearTimer(WDT_LOAD); - } catch (const content_error& e) { - contentErrors.emplace_back(e.what()); - LOG_L(L_ERROR, "[Game::%s][3] forced quit with exception \"%s\"", __func__, e.what()); - forcedQuit = true; + PostLoadSimulation(defsParser); + Watchdog::ClearTimer(WDT_LOAD); + PostLoadRendering(); + Watchdog::ClearTimer(WDT_LOAD); + } catch (const content_error& e) { + contentErrors.emplace_back(e.what()); + LOG_L(L_ERROR, "[Game::%s][3] forced quit with exception \"%s\"", __func__, e.what()); + forcedQuit = true; + } } if (!forcedQuit) { try { @@ -456,50 +460,52 @@ void CGame::Load(const std::string& mapFileName) } } - try { - LOG("[Game::%s][7] globalQuit=%d forcedQuit=%d", __func__, globalQuit.load(), forcedQuit); - - if (!globalQuit && saveFileHandler != nullptr) { - loadscreen->SetLoadMessage("Loading Saved Game"); - { - auto lock = CLoadLock::GetUniqueLock(); - saveFileHandler->LoadGame(); + if (!forcedQuit) { + try { + LOG("[Game::%s][7] globalQuit=%d forcedQuit=%d", __func__, globalQuit.load(), forcedQuit); + + if (!globalQuit && saveFileHandler != nullptr) { + loadscreen->SetLoadMessage("Loading Saved Game"); + { + auto lock = CLoadLock::GetUniqueLock(); + saveFileHandler->LoadGame(); + Watchdog::ClearTimer(WDT_LOAD); + } + LoadLua(false, true); Watchdog::ClearTimer(WDT_LOAD); + } else { + ENTER_SYNCED_CODE(); + { + auto lock = CLoadLock::GetUniqueLock(); + eventHandler.GamePreload(); + Watchdog::ClearTimer(WDT_LOAD); + eventHandler.CollectGarbage(true); + Watchdog::ClearTimer(WDT_LOAD); + } + LEAVE_SYNCED_CODE(); } - LoadLua(false, true); - Watchdog::ClearTimer(WDT_LOAD); - } else { - ENTER_SYNCED_CODE(); + // Update height bounds and pathing after pregame or a saved game load. { - auto lock = CLoadLock::GetUniqueLock(); - eventHandler.GamePreload(); + ENTER_SYNCED_CODE(); + //needed in case pre-game terraform changed the map + readMap->UpdateHeightBounds(); Watchdog::ClearTimer(WDT_LOAD); - eventHandler.CollectGarbage(true); + pathManager->PostFinalizeRefresh(); Watchdog::ClearTimer(WDT_LOAD); + LEAVE_SYNCED_CODE(); } - LEAVE_SYNCED_CODE(); - } - // Update height bounds and pathing after pregame or a saved game load. - { - ENTER_SYNCED_CODE(); - //needed in case pre-game terraform changed the map - readMap->UpdateHeightBounds(); - Watchdog::ClearTimer(WDT_LOAD); - pathManager->PostFinalizeRefresh(); - Watchdog::ClearTimer(WDT_LOAD); - LEAVE_SYNCED_CODE(); - } - { - char msgBuf[512]; + { + char msgBuf[512]; - SNPRINTF(msgBuf, sizeof(msgBuf), "[Game::%s][lua{Rules,Gaia}={%p,%p}][locale=\"%s\"]", __func__, luaRules, luaGaia, setlocale(LC_ALL, nullptr)); - CLIENT_NETLOG(gu->myPlayerNum, LOG_LEVEL_INFO, msgBuf); + SNPRINTF(msgBuf, sizeof(msgBuf), "[Game::%s][lua{Rules,Gaia}={%p,%p}][locale=\"%s\"]", __func__, luaRules, luaGaia, setlocale(LC_ALL, nullptr)); + CLIENT_NETLOG(gu->myPlayerNum, LOG_LEVEL_INFO, msgBuf); + } + } catch (const content_error& e) { + contentErrors.emplace_back(e.what()); + LOG_L(L_ERROR, "[Game::%s][7] forced quit with exception \"%s\"", __func__, e.what()); + forcedQuit = true; } - } catch (const content_error& e) { - contentErrors.emplace_back(e.what()); - LOG_L(L_ERROR, "[Game::%s][7] forced quit with exception \"%s\"", __func__, e.what()); - forcedQuit = true; } if (!forcedQuit) { diff --git a/rts/Game/LoadScreen.cpp b/rts/Game/LoadScreen.cpp index e2a9b7e6b9..eb9ec9e194 100644 --- a/rts/Game/LoadScreen.cpp +++ b/rts/Game/LoadScreen.cpp @@ -109,7 +109,14 @@ bool CLoadScreen::Init() clientNet->KeepUpdating(true); netHeartbeatThread = spring::thread(Threading::CreateNewThread(std::bind(&CNetProtocol::UpdateLoop, clientNet))); - game = new CGame(mapFileName, modFileName, saveFile); + try { + game = new CGame(mapFileName, modFileName, saveFile); + } catch (...) { + // CGame publishes itself before parsing startup content. Its destructor is + // not called when construction fails, so do not leave a dangling global. + game = nullptr; + throw; + } CglFont::sync.SetThreadSafety(mtLoading); CLoadLock::SetThreadSafety(mtLoading); @@ -348,4 +355,3 @@ void CLoadScreen::SetLoadMessage(const std::string& text, bool replaceLast) Update(); Draw(); } - diff --git a/rts/Game/TraceRay.cpp b/rts/Game/TraceRay.cpp index df64721c08..9f22260cbe 100644 --- a/rts/Game/TraceRay.cpp +++ b/rts/Game/TraceRay.cpp @@ -316,7 +316,9 @@ float TraceRay( // ground intersection const float groundLength = CGround::LineGroundCol(pos, pos + dir * traceLength); - if (traceLength > groundLength && groundLength > 0.0f) { + // a return value of 0 means the ray starts underground (or on the surface pointing into + // it), which blocks it at the origin; only -1 signals that the ground was not hit at all + if (traceLength > groundLength && groundLength >= 0.0f) { traceLength = groundLength; hitUnit = nullptr; diff --git a/rts/Map/Ground.cpp b/rts/Map/Ground.cpp index 736e21b1df..259d46bb87 100644 --- a/rts/Map/Ground.cpp +++ b/rts/Map/Ground.cpp @@ -242,10 +242,11 @@ float CGround::LineGroundCol(float3 from, float3 to, bool synced) if (synced) { // TODO: do this in unsynced too? // check if our start position is underground (assume ground is unpassable for cannons etc.) - const int sx = from.x / SQUARE_SIZE; - const int sz = from.z / SQUARE_SIZE; - - if (from.y <= hm[sz * mapDims.mapxp1 + sx]) + // compare against the interpolated surface rather than the corner vertex of the square: + // next to a steep rise that vertex can sit far above a start point that is well clear of + // the ground, and a start exactly on the surface is not underground (LineGroundSquareCol + // reports a hit at distance 0 for it if the ray points into the ground) + if (from.y < InterpolateCornerHeight(from.x, from.z, hm)) return 0.0f + skippedDist; } diff --git a/rts/Rendering/Env/Particles/ProjectileDrawer.cpp b/rts/Rendering/Env/Particles/ProjectileDrawer.cpp index 822a6c3fa4..9045b99fc5 100644 --- a/rts/Rendering/Env/Particles/ProjectileDrawer.cpp +++ b/rts/Rendering/Env/Particles/ProjectileDrawer.cpp @@ -174,6 +174,9 @@ void CProjectileDrawer::InitStatic() { } void CProjectileDrawer::KillStatic(bool reload) { RECOIL_DETAILED_TRACY_ZONE; + if (projectileDrawer == nullptr) + return; + projectileDrawer->Kill(); if (reload) diff --git a/rts/Rendering/WorldDrawer.cpp b/rts/Rendering/WorldDrawer.cpp index cf77599b40..c8b58bd50d 100644 --- a/rts/Rendering/WorldDrawer.cpp +++ b/rts/Rendering/WorldDrawer.cpp @@ -194,7 +194,8 @@ void CWorldDrawer::Kill() textureHandler3DO.Kill(); textureHandlerS3O.Kill(); - readMap->KillGroundDrawer(); + if (readMap != nullptr) + readMap->KillGroundDrawer(); IGroundDecalDrawer::FreeInstance(); DepthBufferCopy::Kill(); LuaObjectDrawer::Kill(); diff --git a/rts/Sim/Misc/LosHandler.cpp b/rts/Sim/Misc/LosHandler.cpp index 5b92c0d176..010b267b43 100644 --- a/rts/Sim/Misc/LosHandler.cpp +++ b/rts/Sim/Misc/LosHandler.cpp @@ -695,6 +695,9 @@ void CLosHandler::InitStatic() void CLosHandler::KillStatic(bool reload) { RECOIL_DETAILED_TRACY_ZONE; + if (losHandler == nullptr) + return; + losHandler->Kill(); if (reload) diff --git a/rts/Sim/Units/CommandAI/CommandAI.cpp b/rts/Sim/Units/CommandAI/CommandAI.cpp index 1350f598e4..6cbc5487a7 100644 --- a/rts/Sim/Units/CommandAI/CommandAI.cpp +++ b/rts/Sim/Units/CommandAI/CommandAI.cpp @@ -1000,6 +1000,7 @@ void CCommandAI::GiveAllowedCommand(const Command& c, bool fromSynced) ClearTargetLock((commandQue.empty())? Command(CMD_STOP): commandQue.front()); ClearCommandDependencies(); SetOrderTarget(nullptr); + targetDied = false; // if c is an attack command, the actual order-target // gets set via ExecuteAttack (called from SlowUpdate diff --git a/rts/Sim/Units/Scripts/UnitScriptEngine.cpp b/rts/Sim/Units/Scripts/UnitScriptEngine.cpp index f4f5373a6d..261861a650 100644 --- a/rts/Sim/Units/Scripts/UnitScriptEngine.cpp +++ b/rts/Sim/Units/Scripts/UnitScriptEngine.cpp @@ -52,6 +52,9 @@ void CUnitScriptEngine::InitStatic() { void CUnitScriptEngine::KillStatic() { RECOIL_DETAILED_TRACY_ZONE; + if (unitScriptEngine == nullptr) + return; + cobEngine->Kill(); cobFileHandler->Kill(); unitScriptEngine->Kill(); diff --git a/rts/Sim/Weapons/Cannon.cpp b/rts/Sim/Weapons/Cannon.cpp index 8c1543e048..e9b1423a2b 100644 --- a/rts/Sim/Weapons/Cannon.cpp +++ b/rts/Sim/Weapons/Cannon.cpp @@ -60,6 +60,10 @@ void CCannon::UpdateRange(const float val) bool CCannon::HaveFreeLineOfFire(const float3& srcPos, const float3& tgtPos, const SWeaponTarget& trg) const { RECOIL_DETAILED_TRACY_ZONE; + // Use real terrain height: the trajectory scan's approximate height can be above a clear source. + if ((avoidFlags & Collision::NOGROUND) == 0 && srcPos.y < CGround::GetHeightReal(srcPos)) + return false; + // assume we can still fire at partially submerged targets if (!weaponDef->waterweapon && TargetUnderWater(tgtPos, trg)) return false; @@ -94,6 +98,9 @@ bool CCannon::HaveFreeLineOfFire(const float3& srcPos, const float3& tgtPos, con -1.0f; const float angleSpread = (AccuracyExperience() + SprayAngleExperience()) * 0.6f * 0.9f; + // This scan uses approximate cell-center terrain heights, so a zero-distance hit + // can report an above-ground source as blocked. Keep > 0; the GetHeightReal check + // above rejects sources that are actually underground. if (groundDist > 0.0f) return false; @@ -260,4 +267,3 @@ float CCannon::GetStaticRange2D(const float2& baseConsts, const float2& projCons return (CalcRange2D({baseConsts.y, 0.7071067f, 100.0f}, projConsts, {wdRangeBoostFact, wdHeightBoostFact})); } - diff --git a/rts/Sim/Weapons/Weapon.cpp b/rts/Sim/Weapons/Weapon.cpp index 3424e1d5f5..a466db8627 100644 --- a/rts/Sim/Weapons/Weapon.cpp +++ b/rts/Sim/Weapons/Weapon.cpp @@ -1118,6 +1118,10 @@ bool CWeapon::TestRange(const float3& tgtPos, const SWeaponTarget& trg) const bool CWeapon::HaveFreeLineOfFire(const float3& srcPos, const float3& tgtPos, const SWeaponTarget& trg) const { RECOIL_DETAILED_TRACY_ZONE; + // Match the pre-fire muzzle check before considering the ground-hit AoE exception. + if ((avoidFlags & Collision::NOGROUND) == 0 && srcPos.y < CGround::GetHeightReal(srcPos)) + return false; + float3 tgtDir = tgtPos - srcPos; const float length = tgtDir.LengthNormalize(); @@ -1138,7 +1142,9 @@ bool CWeapon::HaveFreeLineOfFire(const float3& srcPos, const float3& tgtPos, con const float tgtDst = tgtPos.SqDistance(srcPos + tgtDir * gndDst); // true iff ground does not block the ray of length from along - if ((gndDst > 0.0f) && (tgtDst > Square(damages->damageAreaOfEffect))) + // A surface source pointing into terrain can hit at distance 0, so keep >= 0 + // and retain the AoE exception. + if ((gndDst >= 0.0f) && (tgtDst > Square(damages->damageAreaOfEffect))) return false; unit = nullptr; diff --git a/rts/System/Platform/byteorder.h b/rts/System/Platform/byteorder.h index b80b446b33..fa379ef148 100644 --- a/rts/System/Platform/byteorder.h +++ b/rts/System/Platform/byteorder.h @@ -27,6 +27,7 @@ #if defined(__linux__) + #include #include // for memcpy #include