27 Commits
Author SHA1 Message Date
Fabio Pedretti 9e78a3fe92 Cache canAttackTarget per template in DefenseArmy.assignUnit
For a foe which is not capturable, allowCapture has no effect and the
result of canAttackTarget only depends on the template of the unit.
Cache it in the foe info, which is rebuilt each turn.
2026-11-06 13:11:08 +01:00
Fabio Pedretti 97bbe8b49f Create the path StaticConstraint once for all city districts in Jebel Barkal 2026-11-02 11:36:12 +01:00
Fabio Pedretti b3bb4a62ad Create the CityPainter placement constraint once per area
It was recreated for every candidate building.
2026-11-02 11:36:12 +01:00
Fabio Pedretti 28e69c7332 Scan only the bounding box of the area in CityPainter
The whole map was scanned for each district.
2026-11-02 11:36:12 +01:00
Fabio Pedretti 6fb9d7b86b Lobby: Fix broken sender check for connection data replies
iq.from() is empty on a freshly built outgoing IQ, so the sender
check in handleIq() was always skipped. Store iq.to() instead.

Fixes: 3991dd6c38
2026-10-22 09:52:09 +02:00
Fabio Pedretti 88f5cbc922 Compute the data about the foes once per army and turn in DefenseArmy.assignUnit
assignUnit looped over all the foes of the army for every unit assigned
and, for each (unit, foe) pair, fetched the foe entity and its position,
tested its class, read its UnitAI order data (up to four times) and, when
the foe was close to saturation, tested the Hero and siege classes again.
None of this depends on the unit being assigned.

- Add the module function getFoeInfo(), which gathers once for all the
  foes of the army the entity, its position, the unit it is attacking (if
  any) and, lazily, whether it is a hero or a siege unit and its access
  index. The army keeps the result in foeInfo, which is reset when a foe
  is added, removed or renamed, and at the beginning of update(), as
  positions and orders change from one turn to the next.
- assignUnit reads that table instead of querying the entities. The foes
  are still visited in the same order and with the same checks, so the
  chosen foe is unchanged. assignedAgainst is still read live, as it
  changes while units are assigned.
- Read ent.position() only once.
2026-10-21 11:42:19 +02:00
Fabio Pedretti 862bdd5349 UnitMotion: Compute pushing-grid index once in Move()
pos.X/pos.Y divided by PUSHING_GRID_SIZE twice each (ENSURE, then
m_MovingUnits.get()) instead of once.
2026-10-15 16:56:34 +02:00
Fabio Pedretti 43d8446854 UnitMotion: Fix ShouldAlternatePathfinder() using wrong constant
Used MAX_FAILED_MOVEMENTS (a fixed constant) instead of
m_FailedMovements, making the "every N failures" clause always
false at compile time. Alternation only ever fired once per
failure streak instead of periodically as intended.

Introduced in 70ec781.
2026-10-15 16:56:34 +02:00
Fabio Pedretti f10fe18d7a Create the water constraint once
Deduplicate the stayClasses(tileClassWater, 0) constraint by creating
it once before the placement loop, instead of constructing a new
identical one for every placed dock.
2026-10-15 16:55:16 +02:00
Fabio Pedretti f08832b60d Search the closest water point among boundary points only
Area.getClosestPointTo scans every point of the water search area, and
it was called up to count * retryFactor times in placeDocks.

The nearest point of a set to a point outside of it is always a
boundary point, so compute the boundary once and scan only that. The
logic lives in two small helpers, getBoundaryPoints (Uint8Array mask
based) and getClosestPointInList, so it can be reused and read apart
from placeDocks. getBoundaryPoints returns the original point objects,
while getClosestPointInList returns a clone of the point it finds, so
callers cannot mutate the points stored in the list.
2026-10-15 16:55:16 +02:00
Fabio Pedretti a8ba4945e9 Bail out early and guard against an empty start area
Create the water search area first and return before computing the land
search area if there is no water at all, so that work is not wasted.

Also return if the land search area is empty. Previously pickRandom
would have been called on an empty array and returned undefined, making
getClosestPointTo fail on an undefined position.

Cache the start points instead of calling getPoints() on every try.
2026-10-15 16:55:16 +02:00
Fabio Pedretti 79c0a48ce3 Remove unused areaLand and areaWater areas
areaLand and areaWater were created with a MapBoundsPlacer, which
evaluates the tileclass constraint on every tile of the map. However,
createArea only has an effect if either a painter is specified or the
returned Area is used. Here no painter was passed and the returned
values were never read, so these calls did nothing except waste time.
2026-10-15 16:55:16 +02:00
Fabio Pedretti 78ca04310e Precalculate abn in VertexPathfinder
Avoid calculating it in each of the 5 CheckVisibility functions.
2026-10-12 18:22:20 +02:00
Fabio Pedretti 36eb25de3a Return early on an exact match in getClosestPointTo()
If a point at squared distance 0 is found, no closer point can exist, so
return it immediately instead of scanning the rest of the points. The
returned point is the same as before, since the first point with the
smallest distance was already the one kept.
2026-10-12 16:24:13 +02:00
Fabio Pedretti dda1e6c1a3 Use an indexed loop in getClosestPointTo()
Iterate with an indexed loop over a local reference to the points array
instead of for...of over this.points, which avoids iterator and property
lookup overhead in a loop that can run over thousands of points.
2026-10-12 16:24:13 +02:00
Fabio Pedretti 25592e9f5d Avoid Vector2D allocations in the inner loops
Every neighbor sample created a new Vector2D and went through
validHeight() and getHeight(). Use plain integer coordinates, an inline
bounds check and direct access to the height array instead, which also
removes the per-vertex allocation and the setHeight() call.
2026-10-10 16:42:53 +02:00
Fabio Pedretti 2ebb6d5a12 Precompute the neighbor offsets and distances
The distance of each brush point and the check for the center vertex
were recomputed for every vertex. Compute them once per paint() call
into typed arrays instead.
2026-10-10 16:42:53 +02:00
Fabio Pedretti d6d83c00e8 Use a single flat array for the visited vertices
Replace the array of Uint8Arrays, which was rebuilt with fill().map()
on every iteration, by one flat Uint8Array that is allocated once and
cleared at the start of each iteration.
2026-10-10 16:42:53 +02:00
Fabio Pedretti c8cf658d46 Remove unused heightmap clone
The clone of g_Map.height was only used to read its length, while all
neighbor heights are read from g_Map itself. Cloning the whole heightmap
on every iteration was therefore pure overhead.
2026-10-10 16:42:53 +02:00
Fabio Pedretti 5877ed59bf rms: remove misleading "Placing docks" log calls inside maps
The same "Placing docks" log call is already used inside placeDocks
function, which is called just after these log calls.
Remove as it is useless and confusing.
2026-10-02 21:55:04 +02:00
Fabio Pedretti 54bbef308a Pathfinder: Remove unused ICmpPosition lookup in DistributeAround()
cmpPosition was declared but never read anywhere in the function,
likely left over from an earlier refactor — each unit's own position
is already fetched correctly inside the loop via unitPos. CmpPtr
construction is a pure lookup with no side effects, so removing the
unused instance doesn't change behaviour.
2026-09-01 10:54:32 +02:00
Fabio Pedretti 4dfdfc12b2 Fix: Prevent out-of-bounds memory access in CheckBuildingPlacement
In `CCmpPathfinder::CheckBuildingPlacement`, the boundary check for the
vertical grid dimension (`j > m_TerrainOnlyGrid->m_H`) allowed `j` to equal
`m_TerrainOnlyGrid->m_H`. Since grid indices are 0-based (valid row indices
range from `0` to `m_H - 1`), this boundary condition allowed the loop to
execute with an out-of-bounds row index, leading to an invalid memory
read (`m_TerrainOnlyGrid->get(i, j)`) and potential segfaults.

This patch changes the comparison operator to `j >= m_TerrainOnlyGrid->m_H`,
ensuring that spans extending to or past the grid height bound are properly
flagged as out-of-bounds.
2026-09-01 10:54:32 +02:00
Fabio Pedretti e4e50d7711 MapReader: Reject maps with out-of-range terrain tile texture index
m_Tex1Index is unvalidated (raw .pmp bytes / RMS script) and was used
to index m_TerrainTextures with no bounds check.
2026-08-29 16:19:08 +02:00
Fabio Pedretti 4d721fdab2 RangeManager: Fix off-by-one bounds check in GetLosRevealWholeMap() 2026-08-25 17:47:49 +02:00
Fabio Pedretti 3836f4ea8e Lobby: Fix XmppClient.h include guard typo 2026-08-24 21:36:09 +02:00
Fabio Pedretti d5af8a97c1 Lobby: Remove dead if(true)/else in GuiPollNewMessages()
This is dead since 6fbf036ae4
2026-08-24 21:36:09 +02:00
Fabio Pedretti 811ef65126 Fix: Correct control group iteration in ResolveFoundationCollisions
In `CCmpObstruction::ResolveFoundationCollisions`, the nested loop
intended to assign a persistent control group to colliding normal
entities was incorrectly iterating over `normalEnts` instead of
`persistentEnts`.

This caused the function to attempt to clobber default control groups
with other non-persistent groups, completely bypassing the intended
`ControlPersist` logic.

This patch fixes the inner loop to correctly iterate over
`persistentEnts`, ensuring foundations properly inherit persistent
control groups.

Fixes: 9ae084519f
2026-08-16 13:30:42 +02:00