diff --git a/AGENTS.md b/AGENTS.md index 23d246904..3e7abaafa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) | diff --git a/common/cmodel.h b/common/cmodel.h index 79331ae61..d13c170b0 100644 --- a/common/cmodel.h +++ b/common/cmodel.h @@ -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 { diff --git a/docs/games/warcraft-3/group-attack-chase-gaps.md b/docs/games/warcraft-3/group-attack-chase-gaps.md index b1a125f2d..20a7c8845 100644 --- a/docs/games/warcraft-3/group-attack-chase-gaps.md +++ b/docs/games/warcraft-3/group-attack-chase-gaps.md @@ -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) diff --git a/docs/games/warcraft-3/naval-movement.md b/docs/games/warcraft-3/naval-movement.md new file mode 100644 index 000000000..23c061d6b --- /dev/null +++ b/docs/games/warcraft-3/naval-movement.md @@ -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. diff --git a/docs/games/warcraft-3/pathfinding.md b/docs/games/warcraft-3/pathfinding.md index d5ddc4249..1a0ff008d 100644 --- a/docs/games/warcraft-3/pathfinding.md +++ b/docs/games/warcraft-3/pathfinding.md @@ -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 @@ -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. @@ -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; @@ -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 diff --git a/docs/games/warcraft-3/readme.md b/docs/games/warcraft-3/readme.md index 18ba187ce..402ffba3f 100644 --- a/docs/games/warcraft-3/readme.md +++ b/docs/games/warcraft-3/readme.md @@ -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) diff --git a/games/starcraft-2/game/g_world.c b/games/starcraft-2/game/g_world.c index 18776ed83..c886272c4 100644 --- a/games/starcraft-2/game/g_world.c +++ b/games/starcraft-2/game/g_world.c @@ -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) { diff --git a/games/warcraft-3/game/g_local.h b/games/warcraft-3/game/g_local.h index c5d4071c2..dd3ae888f 100644 --- a/games/warcraft-3/game/g_local.h +++ b/games/warcraft-3/game/g_local.h @@ -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 *); diff --git a/games/warcraft-3/game/g_spawn.c b/games/warcraft-3/game/g_spawn.c index 8f0c0f2fa..66485d48e 100644 --- a/games/warcraft-3/game/g_spawn.c +++ b/games/warcraft-3/game/g_spawn.c @@ -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); @@ -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; diff --git a/games/warcraft-3/game/g_world.c b/games/warcraft-3/game/g_world.c index f05b754c6..0d93f07e9 100644 --- a/games/warcraft-3/game/g_world.c +++ b/games/warcraft-3/game/g_world.c @@ -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 diff --git a/games/warcraft-3/game/skills/s_move.c b/games/warcraft-3/game/skills/s_move.c index 5103bf01c..246fdf236 100644 --- a/games/warcraft-3/game/skills/s_move.c +++ b/games/warcraft-3/game/skills/s_move.c @@ -1,5 +1,5 @@ /* - * s_move.c — Move ability: ground movement orders for units. + * s_move.c — Move ability: movement orders for units. * * When a player right-clicks on empty ground, move_selectlocation() is called * on the server. It creates a waypoint entity at the target position and @@ -53,8 +53,21 @@ typedef enum { MOVE_IGNORE_UNITS, } moveCollisionPolicy_t; -/* UnitData movetp values that name a movement type; anything else (retail authors "_") is movement-disabled. */ -static cstring_t const move_type_names[] = { "foot", "horse", "fly", "hover", "float", "amph" }; +typedef enum { + WC3_MOVE_GROUND, + WC3_MOVE_AIR, + WC3_MOVE_SEA, + WC3_MOVE_AMPHIBIOUS, + WC3_MOVE_DISABLED, +} wc3MovementClass_t; + +typedef struct { cstring_t name; wc3MovementClass_t movement_class; } wc3MovementTypeMap_t; +/* UnitData movetp values; unknown authored values (retail uses "_") are disabled. */ +static wc3MovementTypeMap_t const movement_types[] = { + { "foot", WC3_MOVE_GROUND }, { "horse", WC3_MOVE_GROUND }, + { "hover", WC3_MOVE_GROUND }, { "fly", WC3_MOVE_AIR }, + { "float", WC3_MOVE_SEA }, { "amph", WC3_MOVE_AMPHIBIOUS }, +}; static edict_t *trymove_self = NULL; static edict_t *trymove_blocker = NULL; /* unit that rejected the last candidate (NULL = clear or terrain) */ static edict_t *trymove_colliders[MAX_MOVE_COLLIDERS]; @@ -238,7 +251,7 @@ float unit_movedistance(edict_t *self) { /* --- Collision-aware movement (block-and-slide) --------------------------- * - * A unit only commits a step into a position that is free of walkable terrain + * A unit only commits a step into a position that is valid for its movement class * and of other units' collision circles. When the steered heading is blocked * it tries progressively larger left/right deflections ("sliding"), so units * flow around obstacles instead of plowing through them. Idle units are hard, @@ -249,8 +262,74 @@ static bool unit_is_flying(edict_t const *ent) { return ent && (ent->aiflags & AI_FLYING) != 0; } +static cstring_t unit_movement_type_name(edict_t const *ent) { + return ent && ent->data.UnitData ? ent->data.UnitData->moveTypeName : NULL; +} + +static wc3MovementClass_t unit_movement_class(edict_t const *ent) { + cstring_t const movetp = unit_movement_type_name(ent); + + if (unit_is_flying(ent)) return WC3_MOVE_AIR; + if (!movetp || !*movetp) return WC3_MOVE_GROUND; + FOR_LOOP(i, sizeof(movement_types) / sizeof(*movement_types)) + if (!strcmp(movetp, movement_types[i].name)) return movement_types[i].movement_class; + return WC3_MOVE_DISABLED; +} + uint8_t M_UnitStaticPathingFlags(edict_t const *ent) { - return unit_is_flying(ent) ? CM_PATHING_UNFLYABLE : CM_PATHING_UNWALKABLE; + switch (unit_movement_class(ent)) { + case WC3_MOVE_AIR: return CM_PATHING_UNFLYABLE; + case WC3_MOVE_SEA: return CM_PATHING_UNSWIMMABLE; + case WC3_MOVE_AMPHIBIOUS: + return CM_PATHING_REQUIRE_ALL | CM_PATHING_UNWALKABLE | CM_PATHING_UNSWIMMABLE; + default: return CM_PATHING_UNWALKABLE; + } +} + +typedef enum { + MOVE_DOMAIN_NONE = 0, + MOVE_DOMAIN_GROUND = 1 << 0, + MOVE_DOMAIN_SEA = 1 << 1, + MOVE_DOMAIN_AIR = 1 << 2, +} moveCollisionDomain_t; + +static uint8_t unit_collision_domains(edict_t const *ent) { + if (!ent) return MOVE_DOMAIN_NONE; + /* Structures remain precise ground/sea blockers in addition to their + * authored footprint, avoiding leakage through coarse 32u pathing cells. */ + if (G_UnitIsStructure(ent)) return MOVE_DOMAIN_GROUND | MOVE_DOMAIN_SEA; + switch (unit_movement_class(ent)) { + case WC3_MOVE_AIR: return MOVE_DOMAIN_AIR; + case WC3_MOVE_SEA: return MOVE_DOMAIN_SEA; + case WC3_MOVE_AMPHIBIOUS: return MOVE_DOMAIN_GROUND | MOVE_DOMAIN_SEA; + case WC3_MOVE_DISABLED: return MOVE_DOMAIN_NONE; + default: return MOVE_DOMAIN_GROUND; + } +} + +static uint8_t pathing_collision_domains(uint8_t mover_flags) { + uint8_t domains = 0; + if (mover_flags & CM_PATHING_UNWALKABLE) domains |= MOVE_DOMAIN_GROUND; + if (mover_flags & CM_PATHING_UNSWIMMABLE) domains |= MOVE_DOMAIN_SEA; + if (mover_flags & CM_PATHING_UNFLYABLE) domains |= MOVE_DOMAIN_AIR; + return domains ? domains : MOVE_DOMAIN_GROUND; +} + +bool M_UnitsShareCollisionDomain(edict_t const *a, edict_t const *b) { + return (unit_collision_domains(a) & unit_collision_domains(b)) != 0; +} + +/* Command-time obstacle stamping is game-owned: the shared router asks which + * pathing channels this blocker occupies for the requesting mover policy. */ +uint8_t M_UnitDynamicPathingFlags(edict_t const *ent, uint8_t mover_flags) { + uint8_t const overlap = unit_collision_domains(ent) & pathing_collision_domains(mover_flags); + + if (!overlap) return 0; + if (mover_flags & CM_PATHING_REQUIRE_ALL) + return mover_flags & (CM_PATHING_UNWALKABLE | CM_PATHING_UNFLYABLE | CM_PATHING_UNSWIMMABLE); + if (overlap & MOVE_DOMAIN_AIR) return CM_PATHING_UNFLYABLE; + if (overlap & MOVE_DOMAIN_SEA) return CM_PATHING_UNSWIMMABLE; + return CM_PATHING_UNWALKABLE; } /* Warsmash MovementType.DISABLED: a unit row whose movetp names no movement type is pathable anywhere and @@ -259,11 +338,7 @@ uint8_t M_UnitStaticPathingFlags(edict_t const *ent) { * A row with no movetp cell at all stays mobile: retail always authors the column, so absence is a * partial row rather than a statement about movement. */ bool M_UnitMoveDisabled(edict_t const *ent) { - cstring_t const movetp = ent && ent->data.UnitData ? ent->data.UnitData->moveTypeName : NULL; - if (!movetp || !*movetp) return false; - FOR_LOOP(i, sizeof(move_type_names) / sizeof(*move_type_names)) - if (!strcmp(movetp, move_type_names[i])) return false; - return true; + return unit_movement_class(ent) == WC3_MOVE_DISABLED; } /* BoxEdicts predicate: solid units/buildings sharing this mover's collision @@ -280,10 +355,10 @@ static bool filter_blockers(edict_t const *ent) { ent->data.DestructableData && ent->data.DestructableData->walkable) return false; /* Trees have collisionSize 0 (they block only via their baked footprint) so * they are already excluded above; buildings keep a real collision circle - * and ARE counted here — relying on the terrain footprint alone let units - * walk through buildings (coarse 32u cells, runtime-spawned statics not yet - * baked). Flyers and ground units are on separate layers. */ - return unit_is_flying(ent) == unit_is_flying(trymove_self); + * and ARE counted here — relying on the terrain footprint alone lets units + * leak through coarse 32u cells. Ground, sea and air movers otherwise only + * collide when their Warsmash-style movement domains overlap. */ + return M_UnitsShareCollisionDomain(ent, trymove_self); } /* Distance from point p to the segment [a,b]. */ diff --git a/games/warcraft-3/game/tests/t_api.c b/games/warcraft-3/game/tests/t_api.c index f1b20e0d8..458b14db0 100644 --- a/games/warcraft-3/game/tests/t_api.c +++ b/games/warcraft-3/game/tests/t_api.c @@ -3095,6 +3095,39 @@ TEST(wc3_api, flyer_unstuck_search_uses_unflyable_instead_of_unwalkable) { T_FEQ(out.y, 192.0f, 0.001f); } +TEST(wc3_api, float_unstuck_collision_uses_sea_domain) { + enum { CELLS = 16 }; + static UnitData_t const foot_data = { .moveTypeName = "foot" }; + static UnitData_t const float_data = { .moveTypeName = "float" }; + uint8_t pathmap[CELLS * CELLS]; + vec2_t const requested = {256.0f, 256.0f}; + vec2_t out; + edict_t *blocker, *mover; + + memset(pathmap, CM_PATHING_UNWALKABLE, sizeof(pathmap)); /* swimmable water */ + reset_entities(); + setup_test_world(); + CM_SetupTestPathmap(CELLS, CELLS, pathmap); + CM_SetupTestWorldBounds(&MAKE(box2_t, + .min = {0.0f, 0.0f}, .max = {512.0f, 512.0f})); + blocker = alloc_test_unit(MAKEFOURCC('h','f','o','o'), requested.x, requested.y); + mover = alloc_test_unit(MAKEFOURCC('h','p','e','a'), 64.0f, 64.0f); + blocker->s.model = mover->s.model = 1; + blocker->collision = mover->collision = 16.0f; + blocker->data.UnitData = &foot_data; + mover->data.UnitData = &float_data; + gi.LinkEntity(blocker); + + T_ASSERT(G_FindUnitUnstuckPosition(mover, &requested, &out)); + T_FEQ(out.x, requested.x, 0.001f); + T_FEQ(out.y, requested.y, 0.001f); + + blocker->data.UnitData = &float_data; + T_ASSERT(G_FindUnitUnstuckPosition(mover, &requested, &out)); + T_FEQ(out.x, requested.x, 0.001f); + T_FEQ(out.y, requested.y - 64.0f, 0.001f); +} + TEST(wc3_api, unit_unstuck_search_skips_live_unit_collision) { vec2_t const requested = {256.0f, 256.0f}; vec2_t out; diff --git a/games/warcraft-3/game/tests/t_pathfinding.c b/games/warcraft-3/game/tests/t_pathfinding.c index 0c2c9917b..317861b76 100644 --- a/games/warcraft-3/game/tests/t_pathfinding.c +++ b/games/warcraft-3/game/tests/t_pathfinding.c @@ -103,6 +103,7 @@ void unit_stand(edict_t *self); #define MAP_W 10 #define MAP_H 10 +#define AMPH_PATHING_FLAGS (CM_PATHING_REQUIRE_ALL | CM_PATHING_UNWALKABLE | CM_PATHING_UNSWIMMABLE) /* 0 = open, 2 = nowalk (bit 1 set, matching pathMapCell_t.nowalk). */ static uint8_t open_map[MAP_W * MAP_H]; /* all open */ @@ -231,6 +232,31 @@ TEST(wc3_pathfinding, static_path_texture_green_channel_marks_unflyable) { gi.MemFree(pathtex); } +TEST(wc3_pathfinding, pathtex_red_channel_blocks_ground_and_float) { + uint8_t cells[8 * 8] = { 0 }; + vec2_t const center = { 4.5f, 4.5f }; + edict_t *building; + pathTex_t *pathtex; + + setup_test_pathmap(8, 8, cells); + reset_entities(); + building = alloc_test_unit(MAKEFOURCC('h','b','a','r'), center.x, center.y); + pathtex = gi.MemAlloc(sizeof(*pathtex) + sizeof(color32_t)); + T_NOT_NULL(pathtex); + pathtex->width = 1; + pathtex->height = 1; + pathtex->map[0] = (color32_t){ .b = 255, .a = 255 }; + building->pathtex = pathtex; + + CM_BakeStaticObstacles(); + + T_ASSERT(!CM_PointIsPathableForRadiusFlags(¢er, 0.0f, CM_PATHING_UNWALKABLE)); + T_ASSERT(!CM_PointIsPathableForRadiusFlags(¢er, 0.0f, CM_PATHING_UNSWIMMABLE)); + + building->pathtex = NULL; + gi.MemFree(pathtex); +} + TEST(wc3_pathfinding, flyer_move_validation_uses_unflyable_static_pathing) { uint8_t cells[8 * 8] = { 0 }; vec2_t target = { 4.5f, 4.5f }; @@ -249,6 +275,176 @@ TEST(wc3_pathfinding, flyer_move_validation_uses_unflyable_static_pathing) { T_ASSERT(!M_MoveIsValid(flyer, &target)); } +TEST(wc3_pathfinding, float_and_amphibious_static_pathing_match_warsmash) { + uint8_t cells[3] = { + CM_PATHING_UNSWIMMABLE, /* walkable land */ + CM_PATHING_UNWALKABLE, /* swimmable water */ + CM_PATHING_UNWALKABLE | CM_PATHING_UNSWIMMABLE /* neither */ + }; + vec2_t const land = { 0.5f, 0.5f }; + vec2_t const water = { 1.5f, 0.5f }; + vec2_t const blocked = { 2.5f, 0.5f }; + + setup_test_pathmap(3, 1, cells); + + T_ASSERT(CM_PointIsPathableForRadiusFlags(&land, 0.0f, CM_PATHING_UNWALKABLE)); + T_ASSERT(!CM_PointIsPathableForRadiusFlags(&land, 0.0f, CM_PATHING_UNSWIMMABLE)); + T_ASSERT(CM_PointIsPathableForRadiusFlags(&land, 0.0f, AMPH_PATHING_FLAGS)); + + T_ASSERT(!CM_PointIsPathableForRadiusFlags(&water, 0.0f, CM_PATHING_UNWALKABLE)); + T_ASSERT(CM_PointIsPathableForRadiusFlags(&water, 0.0f, CM_PATHING_UNSWIMMABLE)); + T_ASSERT(CM_PointIsPathableForRadiusFlags(&water, 0.0f, AMPH_PATHING_FLAGS)); + + T_ASSERT(!CM_PointIsPathableForRadiusFlags(&blocked, 0.0f, AMPH_PATHING_FLAGS)); +} + +TEST(wc3_pathfinding, unit_movement_type_selects_static_pathing_policy) { + static UnitData_t const foot_data = { .moveTypeName = "foot" }; + static UnitData_t const float_data = { .moveTypeName = "float" }; + static UnitData_t const amph_data = { .moveTypeName = "amph" }; + static UnitData_t const fly_data = { .moveTypeName = "fly" }; + edict_t *unit; + + reset_entities(); + unit = make_unit_at(0.0f, 0.0f); + + unit->data.UnitData = &foot_data; + T_EQ(M_UnitStaticPathingFlags(unit), CM_PATHING_UNWALKABLE); + unit->data.UnitData = &float_data; + T_EQ(M_UnitStaticPathingFlags(unit), CM_PATHING_UNSWIMMABLE); + unit->data.UnitData = &h_data; + T_EQ(M_UnitStaticPathingFlags(unit), AMPH_PATHING_FLAGS); + unit->data.UnitData = &fly_data; + T_EQ(M_UnitStaticPathingFlags(unit), CM_PATHING_UNFLYABLE); +} + +TEST(wc3_pathfinding, dynamic_pathing_domains_follow_requesting_mover) { + static UnitData_t const foot_data = { .moveTypeName = "foot" }; + static UnitData_t const float_data = { .moveTypeName = "float" }; + static UnitData_t const amph_data = { .moveTypeName = "amph" }; + static UnitData_t const fly_data = { .moveTypeName = "fly" }; + edict_t *unit; + + reset_entities(); + unit = make_unit_at(0.0f, 0.0f); + + unit->data.UnitData = &foot_data; + T_EQ(M_UnitDynamicPathingFlags(unit, CM_PATHING_UNWALKABLE), CM_PATHING_UNWALKABLE); + T_EQ(M_UnitDynamicPathingFlags(unit, CM_PATHING_UNSWIMMABLE), 0); + T_EQ(M_UnitDynamicPathingFlags(unit, AMPH_PATHING_FLAGS), + CM_PATHING_UNWALKABLE | CM_PATHING_UNSWIMMABLE); + + unit->data.UnitData = &float_data; + T_EQ(M_UnitDynamicPathingFlags(unit, CM_PATHING_UNWALKABLE), 0); + T_EQ(M_UnitDynamicPathingFlags(unit, CM_PATHING_UNSWIMMABLE), CM_PATHING_UNSWIMMABLE); + T_EQ(M_UnitDynamicPathingFlags(unit, AMPH_PATHING_FLAGS), + CM_PATHING_UNWALKABLE | CM_PATHING_UNSWIMMABLE); + + unit->data.UnitData = &h_data; + T_EQ(M_UnitDynamicPathingFlags(unit, CM_PATHING_UNWALKABLE), CM_PATHING_UNWALKABLE); + T_EQ(M_UnitDynamicPathingFlags(unit, CM_PATHING_UNSWIMMABLE), CM_PATHING_UNSWIMMABLE); + + unit->data.UnitData = &fly_data; + T_EQ(M_UnitDynamicPathingFlags(unit, CM_PATHING_UNFLYABLE), CM_PATHING_UNFLYABLE); + T_EQ(M_UnitDynamicPathingFlags(unit, CM_PATHING_UNWALKABLE), 0); +} + +TEST(wc3_pathfinding, amphibious_command_destination_avoids_ground_and_sea_units) { + static UnitData_t const foot_data = { .moveTypeName = "foot" }; + static UnitData_t const float_data = { .moveTypeName = "float" }; + uint8_t cells[8 * 8] = { 0 }; + vec2_t const target = { 4.5f, 4.5f }; + vec2_t out; + edict_t *blocker; + + setup_test_pathmap(8, 8, cells); + reset_entities(); + blocker = make_unit_at(target.x, target.y); + blocker->svflags |= SVF_MONSTER; + blocker->collision = 0.1f; + + blocker->data.UnitData = &foot_data; + T_ASSERT(CM_ClosestPathablePointForRadiusFlags(&target, 0.0f, AMPH_PATHING_FLAGS, &out)); + T_ASSERT(Vector2_distance(&out, &target) > 0.01f); + T_ASSERT(CM_ClosestPathablePointForRadiusFlags(&target, 0.0f, CM_PATHING_UNSWIMMABLE, &out)); + T_FEQ(out.x, target.x, 0.001f); T_FEQ(out.y, target.y, 0.001f); + + blocker->data.UnitData = &float_data; + T_ASSERT(CM_ClosestPathablePointForRadiusFlags(&target, 0.0f, AMPH_PATHING_FLAGS, &out)); + T_ASSERT(Vector2_distance(&out, &target) > 0.01f); + T_ASSERT(CM_ClosestPathablePointForRadiusFlags(&target, 0.0f, CM_PATHING_UNWALKABLE, &out)); + T_FEQ(out.x, target.x, 0.001f); T_FEQ(out.y, target.y, 0.001f); +} + +TEST(wc3_pathfinding, float_route_finds_water_detour_around_land) { + enum { W = 7, H = 5 }; + uint8_t cells[W * H]; + vec2_t const from = { 1.5f, 2.5f }, target = { 5.5f, 2.5f }; + vec2_t waypoint = { 0 }; + pathAccelParams_t const params = { + .from = &from, .target = &target, .radius = 0.0f, .blocked_flags = CM_PATHING_UNSWIMMABLE + }; + + memset(cells, CM_PATHING_UNWALKABLE, sizeof(cells)); /* open water */ + for (int y = 1; y <= 3; y++) cells[y * W + 3] = CM_PATHING_UNSWIMMABLE; /* land island */ + setup_test_pathmap(W, H, cells); + + T_ASSERT(!CM_LineIsPathableForRadiusFlags(&from, &target, 0.0f, CM_PATHING_UNSWIMMABLE)); + T_ASSERT(CM_FindPathWaypoint(¶ms, &waypoint)); + T_ASSERT(fabsf(waypoint.y - from.y) > 0.01f); + T_ASSERT(CM_PointIsPathableForRadiusFlags(&waypoint, 0.0f, CM_PATHING_UNSWIMMABLE)); +} + +TEST(wc3_pathfinding, float_move_collision_uses_sea_domain) { + static UnitData_t const foot_data = { .moveTypeName = "foot" }; + static UnitData_t const float_data = { .moveTypeName = "float" }; + uint8_t cells[8 * 8]; + vec2_t const target = { 4.5f, 4.5f }; + edict_t *ship, *blocker; + + memset(cells, CM_PATHING_UNWALKABLE, sizeof(cells)); /* swimmable water */ + setup_test_pathmap(8, 8, cells); + reset_entities(); + ship = make_unit_at(3.5f, 4.5f); + blocker = make_unit_at(target.x, target.y); + ship->data.UnitData = &float_data; + blocker->data.UnitData = &foot_data; + ship->collision = blocker->collision = 0.25f; + gi.LinkEntity(ship); + gi.LinkEntity(blocker); + + T_ASSERT(M_MoveIsValid(ship, &target)); + + blocker->data.UnitData = &float_data; + T_ASSERT(!M_MoveIsValid(ship, &target)); +} + +TEST(wc3_pathfinding, amphibious_move_collision_overlaps_ground_and_sea_domains) { + static UnitData_t const foot_data = { .moveTypeName = "foot" }; + static UnitData_t const float_data = { .moveTypeName = "float" }; + static UnitData_t const amph_data = { .moveTypeName = "amph" }; + uint8_t land_cells[8 * 8], water_cells[8 * 8]; + vec2_t const target = { 4.5f, 4.5f }; + edict_t *amph, *blocker; + + memset(land_cells, CM_PATHING_UNSWIMMABLE, sizeof(land_cells)); + setup_test_pathmap(8, 8, land_cells); + reset_entities(); + amph = make_unit_at(3.5f, 4.5f); + blocker = make_unit_at(target.x, target.y); + amph->data.UnitData = &h_data; + blocker->data.UnitData = &foot_data; + amph->collision = blocker->collision = 0.25f; + gi.LinkEntity(amph); + gi.LinkEntity(blocker); + T_ASSERT(!M_MoveIsValid(amph, &target)); + + memset(water_cells, CM_PATHING_UNWALKABLE, sizeof(water_cells)); + setup_test_pathmap(8, 8, water_cells); + blocker->data.UnitData = &float_data; + T_ASSERT(!M_MoveIsValid(amph, &target)); +} + TEST(wc3_pathfinding, heatmap_cache_separates_ground_and_flying_pathing) { uint8_t cells[10 * 10] = { 0 }; edict_t *goal; diff --git a/server/sv_routing.c b/server/sv_routing.c index 940e57144..1450ac227 100644 --- a/server/sv_routing.c +++ b/server/sv_routing.c @@ -56,6 +56,8 @@ struct { uint32_t *pathheap; /* binary min-heap of pathmap indexes */ uint32_t *obstacle_prefix; /* summed-area table for static nowalk cells */ uint32_t *nofly_prefix; /* summed-area table for static nofly cells */ + uint32_t *nowater_prefix; /* summed-area table for static nowater cells */ + uint32_t *walk_water_prefix; /* summed-area table for cells with both walk and water blockers */ uint8_t *approach_mask; /* reusable footprint-approach proximity/candidate mask */ } pathmap = { 0 }; @@ -244,21 +246,30 @@ static void rebuild_static_obstacle_prefix(void) { uint32_t const stride = pathmap.width + 1; uint32_t const rows = pathmap.height + 1; - if (!pathmap.obstacle_prefix || !pathmap.nofly_prefix || !pathmap.original) + if (!pathmap.obstacle_prefix || !pathmap.nofly_prefix || !pathmap.nowater_prefix || + !pathmap.walk_water_prefix || !pathmap.original) return; memset(pathmap.obstacle_prefix, 0, stride * rows * sizeof(uint32_t)); memset(pathmap.nofly_prefix, 0, stride * rows * sizeof(uint32_t)); + memset(pathmap.nowater_prefix, 0, stride * rows * sizeof(uint32_t)); + memset(pathmap.walk_water_prefix, 0, stride * rows * sizeof(uint32_t)); FOR_LOOP(y, pathmap.height) { - uint32_t walk_row_sum = 0, fly_row_sum = 0; + uint32_t walk_row_sum = 0, fly_row_sum = 0, water_row_sum = 0, walk_water_row_sum = 0; FOR_LOOP(x, pathmap.width) { pathMapCell_t const *cell = &pathmap.original[x + y * pathmap.width]; walk_row_sum += cell->nowalk ? 1 : 0; fly_row_sum += cell->nofly ? 1 : 0; + water_row_sum += cell->nowater ? 1 : 0; + walk_water_row_sum += cell->nowalk && cell->nowater ? 1 : 0; pathmap.obstacle_prefix[(x + 1) + (y + 1) * stride] = pathmap.obstacle_prefix[(x + 1) + y * stride] + walk_row_sum; pathmap.nofly_prefix[(x + 1) + (y + 1) * stride] = pathmap.nofly_prefix[(x + 1) + y * stride] + fly_row_sum; + pathmap.nowater_prefix[(x + 1) + (y + 1) * stride] = + pathmap.nowater_prefix[(x + 1) + y * stride] + water_row_sum; + pathmap.walk_water_prefix[(x + 1) + (y + 1) * stride] = + pathmap.walk_water_prefix[(x + 1) + y * stride] + walk_water_row_sum; } } } @@ -275,6 +286,8 @@ void CM_SetupPathMap(uint32_t width, uint32_t height, uint8_t const *cells) { SAFE_DELETE(pathmap.pathheap, MemFree); SAFE_DELETE(pathmap.obstacle_prefix, MemFree); SAFE_DELETE(pathmap.nofly_prefix, MemFree); + SAFE_DELETE(pathmap.nowater_prefix, MemFree); + SAFE_DELETE(pathmap.walk_water_prefix, MemFree); SAFE_DELETE(pathmap.approach_mask, MemFree); SAFE_DELETE(heatmap_pending, MemFree); heatmap_pending_count = heatmap_pending_capacity = 0; @@ -298,6 +311,8 @@ void CM_SetupPathMap(uint32_t width, uint32_t height, uint8_t const *cells) { pathmap.pathheap = MemAlloc(n * sizeof(uint32_t)); pathmap.obstacle_prefix = MemAlloc((width + 1) * (height + 1) * sizeof(uint32_t)); pathmap.nofly_prefix = MemAlloc((width + 1) * (height + 1) * sizeof(uint32_t)); + pathmap.nowater_prefix = MemAlloc((width + 1) * (height + 1) * sizeof(uint32_t)); + pathmap.walk_water_prefix = MemAlloc((width + 1) * (height + 1) * sizeof(uint32_t)); pathmap.approach_mask = MemAlloc(n); if (cells) { @@ -339,13 +354,17 @@ inline static bool is_valid_point(uint32_t x, uint32_t y) { static bool path_cell_blocks(pathMapCell_t const *cell, uint8_t blocked_flags) { uint8_t const flags = normalize_blocked_flags(blocked_flags); - if (!cell) - return true; - if ((flags & CM_PATHING_UNWALKABLE) && cell->nowalk) - return true; - if ((flags & CM_PATHING_UNFLYABLE) && cell->nofly) - return true; - return false; + uint8_t const selected = flags & (CM_PATHING_UNWALKABLE | CM_PATHING_UNFLYABLE | CM_PATHING_UNSWIMMABLE); + + if (!cell) return true; + if (flags & CM_PATHING_REQUIRE_ALL) + return selected && + (!(selected & CM_PATHING_UNWALKABLE) || cell->nowalk) && + (!(selected & CM_PATHING_UNFLYABLE) || cell->nofly) && + (!(selected & CM_PATHING_UNSWIMMABLE) || cell->nowater); + return ((selected & CM_PATHING_UNWALKABLE) && cell->nowalk) || + ((selected & CM_PATHING_UNFLYABLE) && cell->nofly) || + ((selected & CM_PATHING_UNSWIMMABLE) && cell->nowater); } static void reset_pathmap_data(void) { @@ -537,11 +556,17 @@ static void stamp_entity_obstacle(edict_t const *ent, pathMapCell_t *target) { * authored deck. Clear pixels outside the blocked rails are * texture padding and must preserve the underlying river. */ if (walkable_surface) { - if (blocked) cell->nowalk = 1; - else if (pathtex_clear_pixel_is_bridge_deck(pt, (int)x, (int)y)) + if (blocked) { + cell->nowalk = 1; + cell->nowater = 1; + } else if (pathtex_clear_pixel_is_bridge_deck(pt, (int)x, (int)y)) { + /* A bridge opens the land route over water without + * changing the underlying water-pathing channel. */ cell->nowalk = 0; + } } else { cell->nowalk |= blocked; + cell->nowater |= blocked; } cell->nofly |= blocks_fly; } @@ -635,7 +660,7 @@ void CM_BakeStaticObstacles(void) { /* Apply only dynamic (unit/monster) obstacles into pathmap.data for * closest-pathable-point queries at command time. Static obstacles are * already baked into pathmap.original and copied in by reset_pathmap_data(). */ -static void apply_dynamic_obstacles(edict_t const *ignore) { +static void apply_dynamic_obstacles(edict_t const *ignore, uint8_t mover_flags) { FOR_LOOP(i, ge->num_edicts) { edict_t *ent = EDICT_NUM(i); if (!ent->inuse || ent == ignore) @@ -649,7 +674,7 @@ static void apply_dynamic_obstacles(edict_t const *ignore) { continue; point2_t p = LocationToPathMap(&ent->s.origin2); uint32_t radius = collision_radius_cells(ent->collision); - uint8_t const blocked_flags = entity_dynamic_pathing_flags(ent); + uint8_t const blocked_flags = entity_dynamic_pathing_flags(ent, mover_flags); FOR_LOOP(x, radius * 2) { FOR_LOOP(y, radius * 2) { int px = (int)x + p.x - (int)radius; @@ -658,6 +683,7 @@ static void apply_dynamic_obstacles(edict_t const *ignore) { pathMapCell_t *cell = path_node(px, py); if (blocked_flags & CM_PATHING_UNWALKABLE) cell->nowalk |= 1; if (blocked_flags & CM_PATHING_UNFLYABLE) cell->nofly |= 1; + if (blocked_flags & CM_PATHING_UNSWIMMABLE) cell->nowater |= 1; } } } @@ -768,7 +794,7 @@ bool CM_ClosestPathablePointForRadiusFlags(vec2_t const *location, float radius, } reset_pathmap_data(); - apply_dynamic_obstacles(NULL); + apply_dynamic_obstacles(NULL, blocked_flags); n = CM_GetNormalizedMapPosition(location->x, location->y); tx = (int)floorf(n.x * pathmap.width); ty = (int)floorf(n.y * pathmap.height); @@ -827,6 +853,8 @@ static bool is_pathable_node_original_for_radius_cells_flags(int x, int y, int r return false; prefix = flags == CM_PATHING_UNWALKABLE ? pathmap.obstacle_prefix : flags == CM_PATHING_UNFLYABLE ? pathmap.nofly_prefix + : flags == CM_PATHING_UNSWIMMABLE ? pathmap.nowater_prefix + : flags == (CM_PATHING_REQUIRE_ALL | CM_PATHING_UNWALKABLE | CM_PATHING_UNSWIMMABLE) ? pathmap.walk_water_prefix : NULL; if (!prefix) { for (int py = y0; py <= y1; py++) @@ -1811,7 +1839,8 @@ void CM_GetPathJobStatus(cmPathJobStatus_t *status) { #if defined(TOOL_COMMON_NO_MPQ) || defined(BZ_TESTS) /* Synthesize a pathmap from a raw byte array for unit tests. - * Each byte is treated as a pathMapCell_t (bit 1 = nowalk, bit 2 = nofly). + * Each byte is treated as a pathMapCell_t (bit 1 = nowalk, bit 2 = nofly, + * bit 6 = nowater). * The world coordinate system is set up so cell (x,y) maps to * world position (x * cell_size, y * cell_size). */ void CM_SetupTestPathmap(uint32_t width, uint32_t height, uint8_t const *cells) {