From 8e77748d54380bcc86a566ff5dcef16492fbf60d Mon Sep 17 00:00:00 2001 From: Yaribz Date: Sun, 13 Sep 2026 09:28:29 +0200 Subject: [PATCH 1/4] fix endianness of stats in demos produced on Linux (#3358) On Linux hosts, the game statistics stored in the replay/demo files were incorrectly written using big-endian instead of little-endian. This is due to the "__BYTE_ORDER" and "__BIG_ENDIAN" macros not being defined on Linux under some circumstances. This commit fixes this problem by including the required header just before the macros are used to detect host endianness. --- rts/System/Platform/byteorder.h | 1 + 1 file changed, 1 insertion(+) diff --git a/rts/System/Platform/byteorder.h b/rts/System/Platform/byteorder.h index b80b446b333..fa379ef1482 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 From 8f47fd283985eff93f4949060929d6290b71ddef Mon Sep 17 00:00:00 2001 From: eun-ice Date: Sun, 13 Sep 2026 01:51:09 -0600 Subject: [PATCH 2/4] Clear stale target death state when replacing command queue (#3346) --- rts/Sim/Units/CommandAI/CommandAI.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/rts/Sim/Units/CommandAI/CommandAI.cpp b/rts/Sim/Units/CommandAI/CommandAI.cpp index 1350f598e4f..6cbc5487a70 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 From 70ed4600e3ec113004beb407a79fc4295d5cfd33 Mon Sep 17 00:00:00 2001 From: eun-ice Date: Sun, 13 Sep 2026 03:17:22 -0600 Subject: [PATCH 3/4] Weapons: Fix muzzle below terrain reporting free line of fire (#3328) * TraceRay: block rays that start below the terrain CGround::LineGroundCol returns a hit distance of 0 when the ray origin is underground, but TraceRay only accepted ground hits with a distance > 0 and CWeapon::HaveFreeLineOfFire applied the same filter to the result. A ray from an underground origin was therefore reported as unobstructed, so a weapon whose muzzle (or aim-from piece) sat inside a cliff passed the line-of-fire test, stopped, and could never fire (#3242), and Spring.GetUnitWeaponHaveFreeLineOfFire told game code the same (#3301). Accept 0 as a hit in both places: the ray is blocked at its origin. CCannon::HaveFreeLineOfFire had the same pattern with TrajectoryGroundCol, which also reports 0 for an origin below the terrain, but tests against GetApproximateHeight; on rough ground that can lie above a muzzle that is clear of the interpolated surface. Reject an origin below GetHeightReal explicitly instead, the test the fire-time check already applies to the muzzle, and keep ignoring the coarse 0 from the trajectory scan. The underground test of LineGroundCol itself compared the origin against the corner vertex of its heightmap square. Next to a steep cliff that vertex can be far above an origin that is well clear of the ground, which skipped the whole ground trace. Compare against the interpolated surface instead, and treat an origin exactly on the surface as above ground; LineGroundSquareCol still reports a hit at distance 0 when such a ray points into the ground. Co-Authored-By: Claude Fable 5.1 * Weapons: consistently reject underground line-of-fire sources Reject sources below real terrain height at the start of both the base weapon and cannon line-of-fire checks when ground avoidance is enabled. This matches the existing pre-fire muzzle rejection, including when the target is within explosion range, and covers the early-return cases. Preserve the base weapon AoE exception for surface sources and later ground hits, including zero-distance hits directed into terrain. Validation: Podman engine-headless build and git diff --check passed. In-game validation remains pending. AI assistance: OpenAI Codex prepared the changes and ran the build. * Weapons: explain differing zero-distance ground checks Document why the accurate ray trace accepts zero-distance ground hits while the approximate cannon trajectory scan ignores them. No behavior changes. Validation: git diff --check passed; reviewed comment-only diff. AI assistance: OpenAI Codex wrote the comments and checked the diff. * Weapons: clarify line-of-fire terrain comments --------- Co-authored-by: Claude Fable 5.1 --- doc/site/content/changelogs/_index.markdown | 16 +++++++++++++++- rts/Game/TraceRay.cpp | 4 +++- rts/Map/Ground.cpp | 9 +++++---- rts/Sim/Weapons/Cannon.cpp | 8 +++++++- rts/Sim/Weapons/Weapon.cpp | 8 +++++++- 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/doc/site/content/changelogs/_index.markdown b/doc/site/content/changelogs/_index.markdown index ef8ee0fd676..d5138d86f02 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/TraceRay.cpp b/rts/Game/TraceRay.cpp index df64721c080..9f22260cbeb 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 736e21b1df4..259d46bb878 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/Sim/Weapons/Cannon.cpp b/rts/Sim/Weapons/Cannon.cpp index 8c1543e048a..e9b1423a2b4 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 3424e1d5f5a..a466db86278 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; From 92efda5e60fb6df54a8f17a87e4f4cdb4aa2de31 Mon Sep 17 00:00:00 2001 From: eun-ice Date: Sun, 13 Sep 2026 03:48:02 -0600 Subject: [PATCH 4/4] Fix crash after early game load failures (#3219) * Fix cleanup after early game load failures * Clear game pointer after constructor failure --- rts/Game/Game.cpp | 120 +++++++++--------- rts/Game/LoadScreen.cpp | 10 +- .../Env/Particles/ProjectileDrawer.cpp | 3 + rts/Rendering/WorldDrawer.cpp | 3 +- rts/Sim/Misc/LosHandler.cpp | 3 + rts/Sim/Units/Scripts/UnitScriptEngine.cpp | 3 + 6 files changed, 82 insertions(+), 60 deletions(-) diff --git a/rts/Game/Game.cpp b/rts/Game/Game.cpp index 32f267d1756..63701b24491 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 e2a9b7e6b99..eb9ec9e1943 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/Rendering/Env/Particles/ProjectileDrawer.cpp b/rts/Rendering/Env/Particles/ProjectileDrawer.cpp index 822a6c3fa48..9045b99fc53 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 cf77599b40f..c8b58bd50db 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 5b92c0d1762..010b267b435 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/Scripts/UnitScriptEngine.cpp b/rts/Sim/Units/Scripts/UnitScriptEngine.cpp index f4f5373a6dc..261861a6506 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();