WC3 Pathfinding And Harvest Reachability
Contract
WC3 movement keeps target selection, static routing, and interaction behavior separate:
games/warcraft-3/game/skills/s_move.c owns per-tick steering and local block-and-slide. server/sv_routing.c owns static pathmap line tests, connectivity queries, and cached flow fields. Harvest target selection remains in skills/s_harvest_lumber.c; the router never changes a tree target by itself.
Ground Move, Patrol, Attack-move location orders, and Attack chases use collision-size-aware static routing through direct-line checks, waypoint acceleration, and flow generation. Attack still owns its interaction range independently: reaching a collision-safe route endpoint does not complete the attack. Harvest has an explicit collision split: Gold Mine approach and all resource-return legs use collision-sized static-only routing (live units ignored), while tree approach keeps live-unit collision and uses collision-sized resource-worker local avoidance.
Normal movement uses the live unitinfo.PropWindow, initialized from UnitData.propWin / object field uprw and mutable through SetUnitPropWindow. The authored value is a propulsion window in degrees: steering may continue turning toward the avoidance-resolved heading every simulation tick, but unit_moveindirection() does not translate until the remaining facing error is strictly inside that window. This is shared movement behavior, so Attack chase, Follow, Patrol, Harvest, Repair, Way Gate approach, and ordinary Move inherit the same turn-before-propulsion rule rather than implementing Attack-specific steering. A zero runtime value prevents translation until facing exactly matches the resolved heading, so it can be used to hold movement while retaining turning.
Static interaction rectangles
G_ClosestStaticPathablePointInRectForRadiusFlags() is a WC3-owned query for interactions whose legal destination is a world-space rectangle rather than one point or an entity footprint. It searches every pathmap cell intersecting the rectangle, applies the caller's collision radius and blocked-pathing mask to pathmap.original, and returns the closest point inside a legal intersecting cell. It does not stamp live units into the query. Temporary occupancy remains a move-time collision/local-avoidance concern, preventing a crowded interaction area from becoming unavailable at order submission. Way Gates use it for authored Wrp1/Wrp2 entry rectangles; the shared router retains only generic pathing primitives.
Tests cover sub-cell rectangles, blocked intersecting cells, and live occupancy being ignored by the static query.
Movement-class static pathing
WC3 derives static routing policy from authored movetp rather than reducing every non-flyer to ground movement:
| WC3 movement | Shared routing policy | Static cell is blocked when |
|---|---|---|
foot, horse, hover |
CM_PATHING_UNWALKABLE |
nowalk |
fly |
CM_PATHING_UNFLYABLE |
nofly |
float |
CM_PATHING_UNSWIMMABLE |
nowater |
amph |
CM_PATHING_REQUIRE_ALL \| CM_PATHING_UNWALKABLE \| CM_PATHING_UNSWIMMABLE |
both nowalk and nowater |
M_UnitStaticPathingFlags() owns that WC3 mapping. The resulting policy is carried through destination correction, direct/swept line tests, bounded A*, closest-reachable fallback, resumable flow fields, trained-unit exit placement, formation slots, Way Gate/ability movement helpers, and pathing-aware scripted repositioning. The generic CM_*Walkable* entry points intentionally remain UNWALKABLE wrappers so SC2 and existing ground callers do not change semantics; games that need another movement class use the mask-aware CM_*Pathable*Flags forms. Flow-cache identity includes the full pathing policy as well as adjusted target cell and collision radius, so fields for ground, air, sea, and amphibious movers cannot be reused across incompatible policies.
The shared router exposes generic pathing channels and a generic CM_PATHING_REQUIRE_ALL query modifier; it does not define a Warcraft amphibious unit type. WC3 uses the modifier to express the Warsmash rule that an amphibious unit may traverse a cell when either walking or swimming is legal.
Static entity footprints preserve the relevant Warcraft pathing channels when they are baked. A blocked/red pathing-texture pixel contributes both UNWALKABLE and UNSWIMMABLE, while the green channel contributes UNFLYABLE. LoadTGA stores source BGRA bytes in COLOR32, so the authored red/green channels are read through the loader's existing component layout. A walkable bridge deck clears the land-pathing block over its crossing lane without clearing the underlying water-pathing channel, so ground/amphibious movers can use the deck without turning it into a naval surface. A tall model does not automatically block flight; only authored UNFLYABLE pathing does.
Dynamic command-time obstacles use requester-aware WC3 collision domains rather than the static amph predicate directly. Ground movers overlap ground blockers, float overlaps sea blockers, fly overlaps air blockers, and amph overlaps both ground and sea blockers. For an amphibious request, a blocker in either overlapping domain stamps both selected static channels into the temporary query map, so CM_PATHING_REQUIRE_ALL rejects that occupied space. Precise move-time validation uses the same ground/sea/air domain contract. See Naval Movement And Water Pathing for the naval-specific contract.
Harvest Worker Routing
Harvest/resource movement now follows the Warsmash-style collision contract directly: Gold Mine approach and all resource-return movement ignore live units while still respecting static pathing; tree approach keeps ordinary live-unit collision but applies the deterministic resource-worker queue/pass policy when another worker blocks the local approach. Building interaction routes use the worker's real collision radius, mover-owned bounded A* detours while shared fields rebuild, and footprint-aware near-side endpoints for Town Halls/Lumber Mills.
Wisp auto-harvest also routes with the Wisp's collision radius. A NightElf07 trace showed a Wisp oscillating 118-134 units from its assigned tree, outside Awha's 116-unit harvest range, after the route switched from its nearby waypoint to a completed zero-radius flow field. The mover's step validation still used its 16-unit collision radius, so that field could direct it toward cells its body could not occupy. ai_wisp_walktree() now uses unit_changeangle_for_radius_worker() before unit_moveindirection(), keeping the shared flow field, local worker avoidance, and step validation on the same collision contract.
The NightElf07 regression starts at the logged stalled position with the tree-centered collision-sized field already available and includes the four nearby ATtr footprints from the pathing-grid dump. Starting the test at the original order position was insufficient: its early waypoint could reach harvest range and end the test before the problematic completed-field route was exercised. The regression therefore checks the state where the runtime trace actually stopped, and fails if Wisp lumber movement returns to the generic zero-radius route.
The movement validator is split accordingly: ordinary movement calls the full static + swept-unit check, while Harvest building legs exit after the static point/line checks. SetUnitPathing(false) remains the stronger existing override and still bypasses all collision. The Harvest policy does not mutate the shared pathmap, does not make buildings walkable, and does not alter Harvest/Return resource accounting.
Mine/drop-off interaction legs route with the worker's collision radius even though live-unit collision is disabled. This matters when static pathing changes after an order starts: a newly constructed Farm invalidates the shared field, the bounded mover-owned A* immediately seeks a collision-sized detour while the replacement field is built, and the resulting field cannot choose point-only gaps that the Peasant's physical step validator will reject. Return-resource legs do not use the blocked drop-off centre as their normal endpoint: they choose the innermost collision-safe ring around the authored footprint and select the point on that ring nearest the worker's current side. The bounded mover path can detour around a Farm to that point without making a worker coming from the left circle around to the right side of a Town Hall or Lumber Mill. Because the collision-sized path grid is cell-centred, that innermost legal route endpoint can still sit slightly outside the continuous footprint+one-step deposit threshold. Reaching that exact near-side endpoint is therefore an explicit Return Resources handoff, matching the existing flow_goal_reached rasterization escape rather than making the worker bounce across the same staging cell forever.
Flow-generation handles remain monotonic across cache invalidation. Route entities can retain an old generation after CM_BakeStaticObstacles(); recycling generation 1 after every rebuild could otherwise let that stale handle activate an unrelated newly built field.
The implementation reuses OpenRealm's direct-line, bounded-waypoint, and shared resumable flow routing rather than replacing the entire worker pathfinder. Warsmash itself keeps a longer per-unit waypoint path in CBehaviorMove; a full mover-owned path remains a later option if runtime cases still expose shared-field limitations.
Walkable Bridges And Water
The WPM remains authoritative for horizontal movement: water cells with the no-walk bit are impassable, while the passable cells authored through a bridge form the only legal crossing lane. Bridge elevation is a separate game-side contract. M_CheckGround() starts with W3E terrain height, then checks the level's sparse registry of live DestructableData.walkable entities. Ordinary walkable destructables use their authored Z; DTrx/DTrf elevators add their mutable occluder_height, with the elevator's support footprint extended by the unit collision size. Dead, hidden/non-solid, and pathing-texture-less destructables do not supply ground. See Warcraft III elevators for the animation and height lifecycle.
Human01's river bridge confirms the split. Map object LT05 (WoodBridgeLarge45) is walkable=1, onWater=1, has a 32x32 pathing texture, and is placed at (1216, -960, -114). W3E terrain at its centre is -170.8, so terrain-only ground snapping puts a unit 56.8 world units below the deck even though WPM routing correctly accepts the crossing. Walkable surfaces are registered at map/runtime destructable spawn and unregistered on edict free; ground checks iterate bridges rather than every map entity.
Attack range against a building is measured from the attacker's collision edge to the building's authored no-walk footprint when pathtex is available. skills/s_attack.c therefore uses CM_DistanceToPathingFootprint() for building targets instead of requiring the attacker to enter weapon range of the blocked building centre. This is especially important for explicit force-fire on owned/friendly large buildings: centre-distance range checks make a melee unit orbit the footprint forever even though it is already beside a valid attack surface. Non-building targets retain the existing centre-distance attack check.
Lumber's unreachable-interior-tree detection is the narrower exception: unit_changeangle_for_radius() uses the Peasant's collision radius so Harvest can identify when the best legal approach to a blocked tree has genuinely been exhausted outside HARVEST_RANGE. Do not use that route-end signal for building interactions unless the route request also carries the behavior's interaction range.
Plain right-click movement is different from an interaction order: move_selectlocation() already assigns a collision-safe final waypoint, so ai_move_walk() may safely route that order with the mover's real collision radius. A distant temporary block does not cancel a plain move order. The unit keeps the order and retries local movement while the static route remains reachable; the existing near-goal settle rule is retained for an occupied final slot. If a completed collision-sized flow field reports the mover's component unreachable, the move may stop.
Flow-Field Lifecycle
Game routing no longer uses the old lifetime quota of two synchronous whole-map flow-field bakes. That quota avoided repeated handheld stalls, but after it was spent a later uncached move order received generation 0 forever and generic steering fell back toward the raw target. A reachable order behind trees/buildings could therefore stop even though the static router could have found a route.
CM_RequestHeatmapForRadius() is the game-facing shared-cache miss path. A cache hit returns its generation immediately; a miss takes a place in a FIFO of distinct target/radius/pathing-mask requests and returns 0 until its job completes. G_RunFrame() advances the active resumable reverse shortest-path job after entity simulation through CM_ProcessPathJobs(). On completion, the next queued request is promoted before later entity thinks can claim the slot. Repeated requests for the same field share one queue entry, and a moving goal updates its queued destination without losing its place. This prevents early entities from repeatedly taking the slot after each completion and starving later movers. Static-pathing invalidation clears both the active job and pending requests.
The default relaxation budget is 65,536 queue pops per frame and is runtime-tunable with:
The value is clamped to 256-65,536. The doubled Warcraft III default is a trial aimed at shortening the route-wait frames seen when new and moving units add distinct destinations to the shared FIFO. It keeps SPFA relaxation bounded while allowing each queued destination to make more progress per frame. Only one miss is built at a time; later destinations wait in FIFO order. Route-wait diagnostics can report active flood progress and queued destination fields.
For temporary per-mover wait diagnostics, enable wc3_route_wait_debug 1. The log emits one WC3_ROUTE_WAIT begin and matching end line per route-field wait, with mover and goal identities, wait duration, position delta, active job target, and FIFO depth. Set it back to 0 after capturing the behavior.
Nearby detours do not wait for that whole field. CM_FindPathWaypoint() runs a bounded point-to-point A* accelerator for endpoints within 48 pathing cells, expands at most 2,048 nodes, and returns the farthest recovered path point with a collision-sized clear line from the mover. The mover retains that waypoint until it reaches it, the target changes, or static pathing invalidates the segment. A failed or out-of-envelope acceleration request falls back to the shared incremental field, so long routes remain frame-budgeted.
While a resumable request is pending, unit_changeangle*() leaves both movement.flow_generation == 0 and movement.flow_direct == false. unit_moveindirection() treats that pair as "no heading resolved this tick" and does not commit a step. This shared guard is important: a caller must never turn a pending route into movement along the unit's stale facing.
Plain Move also keeps the stand presentation while that pair is clear. The order switches to walk only after direct steering or a completed flow field supplies a heading; starting the walk pose at order submission made the old facing look like an incorrect first turn during a long route build.
Wisps placed at a harvested tree's origin may start inside its baked UNWALKABLE footprint. When a Move order starts from such a blocked position, the Wisp steers toward its destination until it reaches pathable ground, then resumes ordinary collision-sized routing. The wisp_harvest_move_order_leaves_tree movement regression covers this departure.
The stepper rejects a collision-free candidate along a turn-lagged facing when it points more than 90 degrees away from the resolved route heading or increases distance to the active goal. The old stepper accepted the facing candidate first, so a short scripted cinematic move could advance in the wrong direction while the unit was still rotating; Human02Interlude then left Jaina on Antonidas's later ride-off path. Construction displacement uses its temporary exit point as the active progress goal until it is reached, after which the unit resumes its original order. unit_commit_step() and the arrival snap keep the network/render origin synchronized with authoritative origin2 for the same reason.
The route-job fairness regression is wc3_movement.attack_chase_waits_through_competing_route_jobs_then_resumes; it keeps distinct route requests arriving while asserting the attacker gets its earlier queued field and resumes before those later requests drain. wc3_movement.turn_lag_does_not_step_away_from_route_heading covers turn-lag steering. Both live in games/warcraft-3/game/tests/t_movement.c. Run both game variants with:
CM_BuildHeatmapForRadius() remains the synchronous API for tests/tools that explicitly require a completed field. Production movement goes through M_RefreshHeatmap() -> CM_RequestHeatmapForRadius().
Production services the shared incremental build with 32,768 queue pops per 10 Hz server frame. A 256x256 open field therefore completes in at most two frames instead of the previous sixteen-frame (1.6 second) delay. Override wc3_path_work_budget for slower targets. Complete destination-rooted publication remains the long-route fallback; the bounded accelerator is what removes that publication delay from nearby obstacle detours.
CM_BuildHeatmapForRadius() and the resumable request path key cached fields by adjusted target cell and mover collision radius; movement-class-aware requests additionally key by the blocked pathing mask. The flood and flow query use the same radius-expanded static pathability predicate as move-time terrain checks. The zero-radius CM_BuildHeatmap() wrapper remains for callers that intentionally route a point.
Flow vectors only descend to a strictly lower heatmap price for both collision-sized and radius-zero point fields. The adjusted goal cell therefore has a zero vector instead of pointing back out to a higher-cost neighbour. This matters for shared interaction routes such as Gold Mines and resource drop-offs: an outward point-flow at the adjusted cell makes every worker sharing that field orbit the same wrong location. Cached prices retain INT_MAX for cells that the completed field cannot reach. CM_FlowReachedGoal(generation, x, y) identifies the adjusted goal cell, while CM_FlowCanReach(generation, x, y) distinguishes a disconnected cell from a zero produced by interpolation near the goal.
Harvest interactions still own their final range/contact semantics. Gold Mine approach requests a collision-sized static route toward the Mine, ignores live units at steering/move time, and continues toward the real Mine after the adjusted route goal until the Mine footprint/contact check admits the worker. Because construction rebakes static pathing and invalidates cached fields, a Farm built across the lane causes a collision-sized rebuild; while the shared field is pending, the mover-owned bounded A* waypoint provides the local detour.
Return Resources uses a stronger endpoint contract. CM_FindInnerApproachPointToFootprintForRadius() marks pathmap cells near the authored footprint, chooses the innermost collision-safe ring, then uses distance from the worker only to select the near side of that ring. Gold and lumber return route to that point with static-only collision. If grid/radius quantization leaves the innermost legal cell a few world units outside the continuous footprint+step threshold, reaching the exact endpoint is accepted as the deposit handoff instead of bouncing toward the blocked building and back. Longer detours fall back to the shared collision-sized field. CM_PathCellWorldSize() supplies routing-grid scale only; it is not a gameplay range constant.
The approach mask stores a small footprint-proximity rank so the inner ring can be selected without rescanning every authored footprint pixel for every candidate. Do not replace it with a simple footprint bounding box: sparse and irregular pathing textures require distance to the actual blocked pixels.
Retail Move Destination Behavior
Two defects explained the Human01 fence report and units getting stuck behind trees or towers:
CM_LineIsWalkableForRadius()used ordinary Bresenham stepping. A 45-degree step from one cell to the next checked only those two cells, so the direct shortcut accepted anox/xoarrangement even though both cardinal side cells were blocked. The shortcut now requires both side cells to be legal, matching heatmap expansion andcompute_flow_at().- Location steering requested a point-sized route while
move_is_valid()rejected positions using the unit collision radius. The field could therefore direct a unit through a gap that its body could not occupy. Move, Patrol, and Attack-move now useself->collisionfor the direct line and field; interaction behaviors retain radius zero.
When a clicked destination is in another static connected component, the destination-rooted field reports the mover cell unreachable. CM_ClosestReachablePointForRadius(from, target, radius) floods the mover component with the same radius and diagonal rules, then chooses its legal cell nearest the click. The location order retargets its private waypoint once and follows a normal field to that point. This avoids both failure modes of the old behavior: freezing at the order origin and sliding forever along the blocking wall.
Ordinary destination fields remain incremental and frame-budgeted. The mover-component flood is synchronous only after a completed destination field proves the click unreachable, so this exceptional recovery does not add input-time work to reachable orders.
The current router is now deliberately hybrid. Direct collision-sized lines handle open ground, bounded per-mover A* handles nearby static detours, destination-cached integration fields amortize long routes shared by groups, and local avoidance handles live units. This is closer to retail's split between mover-owned route state and a global pathing system without claiming its unrecovered accelerator implementation.
Route-wait diagnostics
Route-wait begin/end records are compiled only with WC3_DEBUG_ROUTING=1. In
that build, set wc3_route_wait_debug 1 to write WC3_ROUTE_WAIT records to
stderr; leave it at 0 to keep the diagnostics quiet.
Retail Game.dll path audit
The ROC demo data/Warcraft3demo/Game.dll (build 4486, SHA-256 286823c37a1083e91f07d040e46a9df7af4c4952e01fcbba460589bd4e297654) retains RTTI for CAbilityMove, NIpse::CLrPathingSys, and NIpse::CLrPathingAcc. CAbilityMove installs its vtable at Game.dll+0x102898. The path constructor at +0x458040 initializes a roughly 0xb0-byte persistent object, including two 32-byte containers at +0x2c and +0x4c, coordinate/state fields, and a pathing-system pointer. Mover setup at +0x466aa0 allocates and stores one such object. Submission at +0x458670 resets route state and copies the requested coordinate into both current and destination fields.
The update at +0x458930 checks flags at +0x80, can return a pending state from a countdown at +0x8c, invokes progression routines at +0x457da0 and +0x457f20, and exposes multiple result states to the movement caller at +0x4661d0. +0x457da0 appends 8-byte coordinate pairs to the object's route container. +0x457f20 consults one of two global indexed arrays through a signed selector and a -2 sentinel before advancing the route. Together with the separate CLrPathingAcc and CLrPathingSys types, this establishes persistent per-mover progress backed by global accelerated pathing data. It does not establish whether the accelerator is A*, hierarchical sectors, a portal graph, or another Blizzard-specific structure.
Group movement at +0x46a130 and +0x46a330 derives per-mover coordinates and path flags before updating each path object. Retail routing is therefore not a point-only line test followed by movement that independently rejects the unit footprint, nor is there evidence that every order waits for a complete destination-rooted map flood.
This supports the direction of commit 4bad783d: using the mover's collision size consistently and resolving an
unreachable click to a legal endpoint are closer to retail's per-mover, adjusted-endpoint architecture than routing a
point toward an impossible destination. The binary does not establish that OpenRealm's bounded A*, nearest-cell flood,
SPFA integration field, four-slot cache policy, or exact ox/xo diagonal test matches Blizzard's algorithm. Treat the
accelerator as a behaviorally supported approximation: it reproduces immediate nearby route output and mover-owned
progress while retaining OpenRealm's group-friendly cache for long routes.
Retail and OpenRealm algorithm outline
| Stage | Retail evidence | OpenRealm |
|---|---|---|
| Open ground | Route setup can retain or reset mover path state by destination and mode | Collision-sized Bresenham line; no search |
| Nearby detour | Persistent mover path object emits coordinate pairs; global accelerator is consulted | Bounded octile A* emits one smoothed, persistent waypoint |
| Long/shared route | Global CLrPathingSys and CLrPathingAcc; exact sharing policy unrecovered |
Four LRU destination/radius integration fields, built backward with SPFA |
| Dynamic units | Per-mover path flags and updates | Swept-circle movement plus deterministic local avoidance; not baked into static routes |
| Scheduling | Countdown/pending and multiple result states prove resumable progress | A* is capped at 2,048 expansions; complete fields get a configurable per-frame queue budget |
The speed difference was primarily work selection. Before the accelerator, one nearby cache miss cleared every route
node, relaxed the complete reachable component, then copied one integer per map cell before movement could start. A
256x256 diagnostic trace printed cells=65536 at both build start and publication. The accelerator touches only nodes
reached by the bounded A* and uses generation stamps instead of clearing its scratch array. Its contiguous node/heap
arrays usually leave a nearby search with a small L1/L2 working set, but no retail evidence identifies an explicit
"L2 cache" technique.
The same corner rule is applied to direct routing and movement steps: a diagonal
segment is rejected when either cardinal side of the crossed cell corner is
blocked. CM_LineIsWalkableForRadius() and M_MoveIsValid() therefore agree
with the flow flood instead of allowing a direct order or a long simulation step
to squeeze through a diagonal wall corner. With no loaded path map, line tests
remain permissive, matching the existing no-map movement behavior.
Retail Lumber Fallback
Retail Warcraft III continues lumber gathering when the explicitly clicked tree is alive but buried inside an unreachable group of trees.
OpenRealm keeps the clicked tree authoritative while routing can still approach it. If the worker reaches the collision-sized flow field's adjusted goal but remains outside HARVEST_RANGE, or the active field has no route from the worker's component, Harvest treats that approach as failed and selects a replacement tree. The replacement search excludes the failed tree and prefers the nearest live tree with a directly reachable legal harvest approach; a tree already within HARVEST_RANGE is immediately eligible.
This fallback is gameplay behavior in s_harvest_lumber.c, not a pathfinder rule. Normal movement, attack, patrol, item pickup, and spells do not acquire a different target when routing fails.
Multiple Peasants may legally harvest the same tree. Harvest now keeps the closest direct legal chop point instead of pre-assigning angular lanes. Dynamic separation is handled by the resource-worker local avoidance policy: a worker blocked by same-direction resource traffic queues briefly, while crossing or persistently pinned traffic uses deterministic right-first bounded passing. Static flow fields remain shared and occupancy-free, and the final tree approach no longer scans all edicts to allocate or reserve lanes. See worker-crowd-routing.md for the Human02 simulation and policy contract.
Confirmed August 2026 Regression
A Human02 handheld trace for a Peasant ordered to an interior tree showed the worker reaching the forest edge and oscillating indefinitely at roughly 352-358 world units from the target. Every tick reported blocked_frames=0 and, after instrumentation was extended, flow=0 flow_goal=0.
Two routing defects originally combined to prevent Harvest from ever receiving a usable failure/exhaustion state:
- The lumber direct-line gate used
CM_LineIsWalkable()as a zero-radius point test while the actual movement step usedCM_PointIsPathableForRadius(..., self->collision). A line could therefore be declared clear even when the Peasant could not physically fit along it. The fix remains scoped to behavior contracts that can safely use a collision-sized route; gold-mine/building interactions complete at their own contact/range boundary rather than at a flow field's adjusted goal. - The legacy generic heatmap build cap could permanently deny later route fields. The current implementation removes that lifetime quota entirely and uses resumable game routing instead, so lumber and ordinary movement do not depend on which route misses happened earlier in the match.
A third defect made a reached flow goal unstable: compute_flow_at() blended reachable neighbours including equal/higher-cost cells, so an adjusted goal beside asymmetric blocked geometry could point outward. All fields now follow only lower prices; radius-zero interaction routing then hands the final approach back to the behavior at the adjusted route end.
Do not reintroduce a distance-only timeout around Harvest to hide these routing failures. Fix and expose the routing state first, then let Harvest decide whether to retarget.
JASS repositioning: SetUnitPosition vs raw X/Y
SetUnitPosition and SetUnitPositionLoc are pathing-aware repositioning natives. Warsmash implements both through
CUnit.setPointAndCheckUnstuck(): it tests the requested point, then checks a deterministic 64-world-unit square spiral for up to
300 candidates against unit collision and movement pathing. If no candidate is legal, the requested point remains the fallback.
OpenRealm mirrors that contract in G_FindUnitUnstuckPosition() (g_spawn.c). The requested point is checked against
the mover's M_UnitStaticPathingFlags() policy (ground, air, sea, or amphibious) and live blockers in the mover's overlapping collision domain(s) before the spiral advances. Authored building/destructable
pathing is already baked into the static pathmap, so a script that requests a point inside a structure is displaced to the first
legal nearby candidate instead of being left occluded inside the structure. SetUnitX and SetUnitY intentionally remain raw
coordinate setters; do not route them through the unstuck search.
This distinction matters for campaign scripts. The Prologue01 Thrall investigation showed Othr alive and renderer-visible at the
scripted destination while a no-depth/white diagnostic exposed his geometry through a nearby structure. The old native assigned X/Y
directly, unlike Warsmash, so blocked scripted destinations could leave a unit inside authored building pathing.
Diagnostics
Runtime Harvest logging is off by default:
prints Harvest transitions and fallback reasons. Level 2 adds per-approach route state and reports when a requested tree field becomes ready:
Lumber routing uses the WC3_HARVEST_PATH prefix. Gold-mine entry uses WC3_GOLD_PATH, and gold return/deposit uses WC3_GOLD_RETURN. For map-specific mine/model mismatches, level 2 also emits WC3_GOLD_GEOMETRY; level 3 adds the pathing-texture rows as WC3_GOLD_FOOTPRINT. Generic resumable routing does not emit per-build debug lines. A healthy interior-tree fallback should progress through a nonzero flow generation and then one of:
fallback ... reason=route_goal_out_of_range
fallback ... reason=route_unreachable
fallback ... reason=movement_blocked
followed by start and reached for the replacement tree.
Verification
Focused tests live in games/warcraft-3/game/tests/t_pathfinding.c and t_movement.c. They cover:
- cache separation by collision radius;
- cache separation between incompatible movement policies, including ground/UNWALKABLE and flying/UNFLYABLE fields;
- WPM UNWALKABLE, UNFLYABLE, and UNSWIMMABLE point/line queries;
- blocked/red static path-texture pixels rejecting both ground and
floatmovement while green-channel pixels contribute UNFLYABLE; floatstatic routing through swimmable cells and around unswimmable land;amphstatic routing accepting cells that are walkable or swimmable and rejecting cells that are neither;- requester-aware dynamic blockers for ground, sea, air, and amphibious movers;
SetUnitPosition/unstuck collision uses the same domains, sofloatignores ground units and avoids sea units;- flyer move validation accepting UNWALKABLE-only cells and rejecting UNFLYABLE cells;
- resumable cache misses serialize without losing a later destination;
- collision-radius-aware line walkability;
- water rejection with an explicitly passable bridge lane;
- walkable-destructable deck elevation with terrain restoration outside its footprint;
- direct-line and flow rejection of diagonal
ox/xocorner cuts; - rejection of a corridor too narrow for the mover;
- collision-sized Move, Patrol, and Attack-move route selection;
- collision-sized Move routing around a long wall;
- exact reachable clicks and closest reachable points across a disconnected wall;
- collision-radius expansion of the closest reachable boundary;
- end-to-end settling at the nearest reachable point for a disconnected click;
- a zero outward vector at the flow goal;
- end-to-end lumber retarget from a buried clicked tree to a reachable edge tree;
- gold-mine entry through an authored blocking mine footprint;
- gold return/deposit at an authored Town Hall footprint corner;
- lumber return to a Town Hall through an authored blocking building footprint;
- a distant temporarily blocked plain move keeps its order alive while near-goal jitter still settles;
SetUnitPosition/SetUnitPositionLocuse the Warsmash-style blocked-point unstuck spiral whileSetUnitX/Yremain raw.- flying
SetUnitPositionunstuck checks ignore UNWALKABLE-only cells and obey UNFLYABLE cells.
Run when validating locally:
make test-wc3-engine WC3_PATTERN='wc3_api.set_unit_position*'
make test-wc3-engine WC3_PATTERN='wc3_pathfinding.*'
make test-wc3-engine WC3_PATTERN='wc3_movement.plain_move_*'
make test-wc3-engine WC3_PATTERN='wc3_movement.blocked_move_*'
make test-wc3-engine WC3_PATTERN='wc3_movement.*'
make test-wc3-engine WC3_PATTERN='wc3_movement.lumber_*'
Interaction-owned route endpoints
Radius-0 point fields remain available to callers whose interaction contract specifically needs point routing. Attack chases instead use the attacker's collision radius so their field cannot route through a gap the move-time validator rejects. Both forms strictly descend to their adjusted legal route endpoint. Once that endpoint is reached, unit_changeangle() exposes flow_goal_reached and steers toward the real entity target; the attack behavior continues to use its own range test. Gold Mine entry/return may hand off immediately at the route goal or after Move's bounded near-goal settle detector proves a crowded worker has stopped making progress at the interaction edge. This keeps routing monotonic without turning a distant blocked route into a successful interaction.
See Also
- Unit Altitude And Support Surfaces — vertical support surfaces share the WPM terrain classification but are independent of horizontal routing.
- Naval Movement And Water Pathing —
float/amphreuse this router with water-aware pathing policies and movement collision domains.
Shared SC2 movement consumers
SC2 and WC3 include the same server/sv_routing.c in their server worlds.
CM_AccelerateRoute retains the mover-owned waypoint; CM_SlideRoute is WC3's
bounded generic left/right deflection loop extracted from unit_desired_heading.
WC3 retains its speed-priority ring limit, resource-worker policy, collision
callbacks, and turn-rate handling. SC2 calls the shared search for blocked static
steps instead of repeatedly rejecting the same flow direction at a corner.
See SC2 selection and control for the
separate snapshot-precision defect that made both ground and cinematic movement
appear to advance in whole cells.