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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ This codebase is inspired by **Quake 2** (id Software). The developer is deeply
| WC3 data model (SLK, unit stats, combat) | [docs/wc3-data-model.md](docs/wc3-data-model.md) |
| WC3 attack damage math, runtime modifiers, armor/type multipliers, projectile impact timing | [docs/games/warcraft-3/attack-damage.md](docs/games/warcraft-3/attack-damage.md) |
| WC3 group Attack, target chase, collision range, and remaining combat-movement gaps | [docs/games/warcraft-3/group-attack-chase-gaps.md](docs/games/warcraft-3/group-attack-chase-gaps.md) |
| WC3 naval movement, FLOAT/AMPH water pathing, collision domains, bridge semantics | [docs/games/warcraft-3/naval-movement.md](docs/games/warcraft-3/naval-movement.md) |
| WC3 JASS native coverage, callback contracts, state ownership | [games/warcraft-3/jass-native-coverage.md](games/warcraft-3/jass-native-coverage.md) |
| WC3 AI next-upgrade cost queries and `ShiftTownSpot` construction placement state | [docs/games/warcraft-3/ai-upgrade-costs-and-town-spot.md](docs/games/warcraft-3/ai-upgrade-costs-and-town-spot.md) |
| WC3 event-trigger queueing, synchronous `TriggerExecute`, coroutine context | [docs/games/warcraft-3/trigger-events.md](docs/games/warcraft-3/trigger-events.md) |
Expand Down
16 changes: 10 additions & 6 deletions common/cmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ struct edict_s;

typedef void (*cmLoadYield_t)(void);

/* Warcraft III pathing bits used by movement-class-aware routing. Existing
* walkability APIs keep their historical UNWALKABLE behavior; callers that
* need another movement class pass the appropriate blocked bit explicitly. */
#define CM_PATHING_UNWALKABLE 0x02
#define CM_PATHING_UNFLYABLE 0x04
/* Generic byte-mask pathing channels used by the shared router. Existing
* walkability APIs keep their historical UNWALKABLE behavior; games select
* the channels and match policy required by their movement rules. */
#define CM_PATHING_UNWALKABLE 0x02
#define CM_PATHING_UNFLYABLE 0x04
#define CM_PATHING_UNSWIMMABLE 0x40
/* Generic query modifier: require every selected static blocker bit instead of
* the default any-selected-bit match. This is a routing policy bit, not WPM data. */
#define CM_PATHING_REQUIRE_ALL 0x80

typedef struct {
vec2_t const *from, *target;
float radius;
uint8_t blocked_flags; /* 0 preserves the legacy UNWALKABLE contract */
uint8_t blocked_flags; /* channels + query modifiers; 0 preserves legacy UNWALKABLE */
} pathAccelParams_t;

struct War3MapVertex {
Expand Down
1 change: 1 addition & 0 deletions docs/games/warcraft-3/group-attack-chase-gaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,5 @@ For completed implementation work, run `make test` as required by

- [Attack Damage](attack-damage.md)
- [Pathfinding And Harvest Reachability](pathfinding.md)
- [Naval Movement And Water Pathing](naval-movement.md)
- [Shift Order Queue](order-queue.md)
72 changes: 72 additions & 0 deletions docs/games/warcraft-3/naval-movement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Naval Movement And Water Pathing

## Contract

Warcraft III movement comes from `UnitData.slk` `movetp`. OpenRealm recognizes the retail/Warsmash values `foot`, `horse`, `fly`, `hover`, `float`, and `amph`; unknown authored values remain movement-disabled. Routing must preserve the movement type instead of treating every non-flyer as a ground unit.

The shared WPM cell already stores `nowalk`, `nofly`, and `nowater`. WC3 routing exposes these policies through `M_UnitStaticPathingFlags()`:

| `movetp` | Static route is blocked when |
|---|---|
| `foot`, `horse`, `hover` | `nowalk` |
| `fly` | `nofly` |
| `float` | `nowater` |
| `amph` | `nowalk && nowater` |

The shared router exposes `CM_PATHING_UNSWIMMABLE` for `nowater` and the generic `CM_PATHING_REQUIRE_ALL` query modifier. WC3 represents `amph` as `REQUIRE_ALL | UNWALKABLE | UNSWIMMABLE`, so the route is rejected only when both terrain channels are blocked. The shared router does not contain an amphibious gameplay type. This follows Warsmash `MovementType.AMPHIBIOUS`, whose pathability is `!UNWALKABLE || !UNSWIMABLE`.

## Pathing Textures

WC3 pathing-texture red blocking applies to both land and naval movement. In OpenRealm's decoded `color32_t` representation the existing red-source blocking channel is `COLOR32.b`; stamping a blocked pixel sets both `nowalk` and `nowater`. The green-source channel remains `nofly`.

A live walkable bridge is special: a clear authored deck may clear terrain `nowalk` so ground units can cross, but it must not clear terrain `nowater`. `FLOAT` therefore continues to use the river/water route and never gains a naval route across the bridge deck. Blocked bridge pixels still block both land and water.

## Dynamic Collision

Warsmash separates moving-unit collision into ground, sea, and air domains. OpenRealm keeps its existing precise `BoxEdicts` broad phase but applies the same domain semantics in `skills/s_move.c`:

| Movement | Collision domain |
|---|---|
| `foot`, `horse`, `hover` | ground |
| `float` | sea |
| `amph` | ground + sea |
| `fly` | air |
| disabled | none |

Two ordinary units block one another when their domain masks overlap. Structures remain precise blockers for ground and sea movement in addition to their authored/coarse static footprint; this preserves OpenRealm's existing protection against leaking through coarse 32-unit building pathing cells. Flyers retain their prior separation from structures in this move-time circle test and rely on authored `nofly` pathing where present.

Command-time dynamic obstacle stamping is requester-aware. The shared router passes the requesting pathing policy back to WC3's `M_UnitDynamicPathingFlags()`: ground movers see ground/amphibious blockers, sea movers see sea/amphibious blockers, flyers see air blockers, and amphibious movers see either ground or sea blockers. For an amphibious request, any overlapping ground/sea blocker stamps both selected channels so the static `REQUIRE_ALL` terrain rule cannot incorrectly treat a lone unit as passable. Static terrain semantics stay generic while dynamic collision-domain policy remains game-owned.

## Rendering

Rendering height is separate from simulation pathability. Existing `M_CheckGround()` behavior remains authoritative:

- `float` uses the water surface and ignores walkable bridge height;
- `amph` uses the water surface only where terrain is swimmable and not walkable;
- an amphibious unit on a walkable bridge is not presented as swimming.

Do not use render-water detection to authorize movement.

## Existing Consumers

Movement/pathing consumers already call `M_UnitStaticPathingFlags()` rather than hard-coding ground routing. Fixing that policy therefore propagates to normal Move, Patrol, Attack/chase, attack-move waypoints, formation slots, spawn/unstuck placement, Way Gate placement, Blink-style point correction, construction approaches, and cargo transport movement.

Repair's authored naval range bonus is already implemented separately in `skills/s_repair.c` for `movetp=float` targets.

Cargo passenger placement already asks `G_FindUnitUnstuckPosition()` using the passenger, so the passenger's movement policy is authoritative. The current last-resort unload fallback still places the passenger at the transport position if no legal unstuck point exists. Retail-exact behavior for the no-legal-unload-position case remains unresolved and is deliberately not changed by the naval routing work.

## Verification

Automated coverage belongs in `games/warcraft-3/game/tests/t_pathfinding.c` and `t_movement.c`:

- land rejects `float` while water accepts it;
- `amph` accepts either land or water and rejects cells blocked to both;
- movement type selects the expected router policy;
- command-time amphibious destinations reject either a ground or sea unit while ground/sea movers ignore the opposite domain;
- a `float` route detours through connected water around unswimmable land;
- pathing-texture blocked pixels reject both ground and float routing;
- float move-time collision ignores ground units and collides with sea units;
- amphibious move-time collision collides with both ground and sea units;
- existing water-height/bridge tests continue to cover `FLOAT` presentation.

Per repository policy, build affected targets and run `make test` before committing code changes.
30 changes: 22 additions & 8 deletions docs/games/warcraft-3/pathfinding.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,22 @@ Tests cover sub-cell rectangles, blocked intersecting cells, and live occupancy

### Movement-class static pathing

WC3 `movetp="fly"` units now use the WPM/pathing-texture **UNFLYABLE** bit (`0x04`) for static movement instead of the ground **UNWALKABLE** bit (`0x02`). Ground movers keep the existing UNWALKABLE contract. This selection is carried through destination correction, direct/swept line tests, bounded A*, closest-reachable fallback, resumable flow fields, trained-unit exit placement, and pathing-aware scripted repositioning.
WC3 derives static routing policy from authored `movetp` rather than reducing every non-flyer to ground movement:

The generic `CM_*Walkable*` entry points intentionally remain UNWALKABLE wrappers so SC2 and existing ground callers do not change semantics. WC3 movement calls the mask-aware `CM_*Pathable*Flags` forms when the mover is `AI_FLYING`. Flow-cache identity includes the blocked-pathing mask as well as adjusted target cell and collision radius, so an air field cannot be reused as a ground field (or vice versa).
| 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 both relevant Warcraft pathing channels when they are baked: the TGA file red channel continues to contribute UNWALKABLE and the green channel contributes UNFLYABLE. `LoadTGA` stores source BGRA bytes in `COLOR32`, so these are `.b` and `.g` respectively in the current loader. A tall model does not automatically block flight; only authored UNFLYABLE pathing does. Conversely, ordinary UNWALKABLE ground footprint cells do not stop a flyer. Dynamic collision remains layer-based in `skills/s_move.c`: air blocks air, ground blocks ground, and opposite layers do not block each other.
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.

This patch deliberately does **not** generalize every WC3 movement type yet. `float`/water pathing and full amphibious horizontal routing still need their own movement-class predicates rather than being inferred from the new fly/ground split.
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](naval-movement.md) for the naval-specific contract.

### Harvest Worker Routing

Expand Down Expand Up @@ -190,7 +199,7 @@ Do not reintroduce a distance-only timeout around Harvest to hide these routing
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 movement-class static pathing (`UNFLYABLE` for flyers, `UNWALKABLE` otherwise) and live same-layer collision circles before the spiral advances. Authored building/destructable
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.
Expand Down Expand Up @@ -228,9 +237,13 @@ followed by `start` and `reached` for the replacement tree.
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 ground/UNWALKABLE and flying/UNFLYABLE fields;
- WPM UNWALKABLE versus UNFLYABLE point/line queries;
- green-channel static path textures contributing UNFLYABLE without making ground unwalkable;
- 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 `float` movement while green-channel pixels contribute UNFLYABLE;
- `float` static routing through swimmable cells and around unswimmable land;
- `amph` static 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, so `float` ignores 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;
Expand Down Expand Up @@ -270,6 +283,7 @@ Radius-0 point fields remain available to callers whose interaction contract spe
## See Also

- [Unit Altitude And Support Surfaces](unit-altitude.md) — vertical support surfaces share the WPM terrain classification but are independent of horizontal routing.
- [Naval Movement And Water Pathing](naval-movement.md) — `float`/`amph` reuse this router with water-aware pathing policies and movement collision domains.

### Shared SC2 movement consumers

Expand Down
1 change: 1 addition & 0 deletions docs/games/warcraft-3/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ File formats, renderer notes, UI/FDF behavior, and gameplay coverage work used b
- [Group Attack And Chase Gaps](group-attack-chase-gaps.md): remaining combat-movement parity work and acceptance coverage.
- [Persistent Hero And Idle-Worker Shortcuts](unit-shortcuts.md)
- [Pathfinding And Harvest Reachability](pathfinding.md)
- [Naval Movement And Water Pathing](naval-movement.md): `float`/`amph` routing, water pathing, collision domains, and bridge semantics.
- [Way Gates](way-gates.md)
- [Inventory And World Items](inventory-and-items.md)
- [Map Trigger Strings in Authored Names](trigger-strings.md)
Expand Down
2 changes: 1 addition & 1 deletion games/starcraft-2/game/g_world.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/* SC2 edicts have no dynamic walkable surfaces; the shared router must not inspect WC3-only fields. */
static bool entity_is_live_walkable_surface(edict_t const *ent) { (void)ent; return false; }
static uint8_t entity_dynamic_pathing_flags(edict_t const *ent) { (void)ent; return CM_PATHING_UNWALKABLE; }
static uint8_t entity_dynamic_pathing_flags(edict_t const *ent, uint8_t mover_flags) { (void)ent; (void)mover_flags; return CM_PATHING_UNWALKABLE; }
static bool entity_is_pathing_ignored(edict_t const *ent) { (void)ent; return false; }
/* SC2 has no bridges; identity transform. */
static void entity_pathtex_transform(pathTexTransformParams_t const *params, pathTexTransform_t *transform) {
Expand Down
2 changes: 2 additions & 0 deletions games/warcraft-3/game/g_local.h
Original file line number Diff line number Diff line change
Expand Up @@ -2642,6 +2642,8 @@ float unit_movedistance(edict_t *);
uint32_t M_RefreshHeatmap(edict_t *, float);
uint32_t M_RefreshHeatmapForMover(edict_t const *, edict_t *, float);
uint8_t M_UnitStaticPathingFlags(edict_t const *);
uint8_t M_UnitDynamicPathingFlags(edict_t const *, uint8_t mover_flags);
bool M_UnitsShareCollisionDomain(edict_t const *, edict_t const *);
bool M_UnitMoveDisabled(edict_t const *);
bool M_IsDead(edict_t const *);
void SP_SpawnUnit(edict_t *);
Expand Down
4 changes: 2 additions & 2 deletions games/warcraft-3/game/g_spawn.c
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,7 @@ static bool SP_CanPlaceUnitAt(edict_t *unit, vec2_t const *point) {
if (other == unit || IS_HOLLOW(other) || other->movetype == MOVETYPE_NONE || other->collision <= 0.0f) {
continue;
}
if (!!(other->aiflags & AI_FLYING) != !!(unit->aiflags & AI_FLYING)) {
if (!M_UnitsShareCollisionDomain(other, unit)) {
continue;
}
delta = Vector2_sub(&other->s.origin2, point);
Expand All @@ -1079,7 +1079,7 @@ static bool G_RepositionBlocker(edict_t const *other) {
edict_t *unit = reposition_unit;
if (other == unit || (G_IsItem(unit) && other == unit->item->carrier) ||
IS_HOLLOW(other) || other->collision <= 0.0f ||
!!(other->aiflags & AI_FLYING) != !!(unit->aiflags & AI_FLYING)) return false;
!M_UnitsShareCollisionDomain(other, unit)) return false;
dx = other->s.origin2.x - reposition_point->x;
dy = other->s.origin2.y - reposition_point->y;
reach = unit->collision + other->collision;
Expand Down
4 changes: 2 additions & 2 deletions games/warcraft-3/game/g_world.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ static bool entity_is_live_walkable_surface(edict_t const *ent) {
ent->data.DestructableData && ent->data.DestructableData->walkable;
}

static uint8_t entity_dynamic_pathing_flags(edict_t const *ent) {
return M_UnitStaticPathingFlags(ent);
static uint8_t entity_dynamic_pathing_flags(edict_t const *ent, uint8_t mover_flags) {
return M_UnitDynamicPathingFlags(ent, mover_flags);
}
static bool entity_is_pathing_ignored(edict_t const *ent) {
/* A construction-site indicator is a visible reservation, not a building
Expand Down
Loading
Loading