Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion doc/site/content/changelogs/_index.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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.
# 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.
120 changes: 63 additions & 57 deletions rts/Game/Game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 8 additions & 2 deletions rts/Game/LoadScreen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -348,4 +355,3 @@ void CLoadScreen::SetLoadMessage(const std::string& text, bool replaceLast)
Update();
Draw();
}

4 changes: 3 additions & 1 deletion rts/Game/TraceRay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 5 additions & 4 deletions rts/Map/Ground.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
3 changes: 3 additions & 0 deletions rts/Rendering/Env/Particles/ProjectileDrawer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ void CProjectileDrawer::InitStatic() {
}
void CProjectileDrawer::KillStatic(bool reload) {
RECOIL_DETAILED_TRACY_ZONE;
if (projectileDrawer == nullptr)
return;

projectileDrawer->Kill();

if (reload)
Expand Down
3 changes: 2 additions & 1 deletion rts/Rendering/WorldDrawer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ void CWorldDrawer::Kill()
textureHandler3DO.Kill();
textureHandlerS3O.Kill();

readMap->KillGroundDrawer();
if (readMap != nullptr)
readMap->KillGroundDrawer();
IGroundDecalDrawer::FreeInstance();
DepthBufferCopy::Kill();
LuaObjectDrawer::Kill();
Expand Down
3 changes: 3 additions & 0 deletions rts/Sim/Misc/LosHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,9 @@ void CLosHandler::InitStatic()
void CLosHandler::KillStatic(bool reload)
{
RECOIL_DETAILED_TRACY_ZONE;
if (losHandler == nullptr)
return;

losHandler->Kill();

if (reload)
Expand Down
1 change: 1 addition & 0 deletions rts/Sim/Units/CommandAI/CommandAI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions rts/Sim/Units/Scripts/UnitScriptEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ void CUnitScriptEngine::InitStatic() {

void CUnitScriptEngine::KillStatic() {
RECOIL_DETAILED_TRACY_ZONE;
if (unitScriptEngine == nullptr)
return;

cobEngine->Kill();
cobFileHandler->Kill();
unitScriptEngine->Kill();
Expand Down
8 changes: 7 additions & 1 deletion rts/Sim/Weapons/Cannon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -260,4 +267,3 @@ float CCannon::GetStaticRange2D(const float2& baseConsts, const float2& projCons

return (CalcRange2D({baseConsts.y, 0.7071067f, 100.0f}, projConsts, {wdRangeBoostFact, wdHeightBoostFact}));
}

8 changes: 7 additions & 1 deletion rts/Sim/Weapons/Weapon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 <length> from <srcPos> along <tgtDir>
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;
Expand Down
1 change: 1 addition & 0 deletions rts/System/Platform/byteorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

#if defined(__linux__)

#include <endian.h>
#include <string.h> // for memcpy
#include <byteswap.h>

Expand Down
Loading