Index: binaries/data/mods/public/maps/scenarios/pickup_test_map.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/maps/scenarios/pickup_test_map.xml
@@ -0,0 +1,174 @@
+
+
+
+
+ sunny
+
+
+
+
+
+
+ 0
+ 0.5
+
+
+
+
+ ocean
+
+
+ 168.772
+ 1.36719
+ 0.836914
+ 0
+
+
+
+ 0
+ 1
+ 0.99
+ 0.1999
+ default
+
+
+
+
+
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_A
+ 1
+
+
+
+
+
+ trigger/trigger_point_B
+ 0
+
+
+
+
+
+ trigger/trigger_point_C
+ 0
+
+
+
+
+
+
+
Index: binaries/data/mods/public/maps/scenarios/pickup_test_map_triggers.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/maps/scenarios/pickup_test_map_triggers.js
@@ -0,0 +1,218 @@
+const UNIT_TEMPLATE = "units/athen_infantry_marine_archer_b";
+const SHIP_TEMPLATE = "units/athen_ship_trireme";
+const RAM_TEMPLATE = "units/brit_siege_ram";
+
+const point_plaza_nw = 14; // Center plaza #1 (NW)
+const point_plaza_ne = 15; // Center plaza #2 (NE)
+const point_plaza_se = 16; // Center plaza #3 (SE)
+const point_plaza_sw = 17; // Center plaza #4 (SW)
+const point_road_1 = 18; // 'Road' from plaza to land end in the NW, #1
+const point_road_2 = 19; // 'Road' from plaza to land end in the NW, #2
+const point_road_3 = 20; // 'Road' from plaza to land end in the NW, #3
+const point_road_4 = 21; // 'Road' from plaza to land end in the NW, #4
+const point_road_5 = 22; // 'Road' from plaza to land end in the NW, #5 and last
+const point_island_1 = 23; // Sand Island #1 (north)
+const point_island_2 = 24; // Sand Island #2 (south)
+const point_sea_1 = 25; // Sea #1 (west)
+const point_sea_2 = 26; // Sea #2 (east)
+const point_coast_1 = 27; // Coast point #1 (E)
+const point_coast_2 = 28; // Coast point #2 (W)
+
+
+var QuickSpawn = function(x, z, template, owner = 1)
+{
+ let ent = Engine.AddEntity(template);
+
+ let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ if (cmpEntOwnership)
+ cmpEntOwnership.SetOwner(owner);
+
+ let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
+ cmpEntPosition.JumpTo(x, z);
+ return ent;
+};
+
+var Rotate = function(angle, ent)
+{
+ let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
+ cmpEntPosition.SetYRotation(angle);
+ return ent;
+};
+
+var WalkTo = function(x, z, ent, owner = 1)
+{
+ ProcessCommand(owner, {
+ "type": "walk",
+ "entities": Array.isArray(ent) ? ent : [ent],
+ "x": x,
+ "z": z,
+ "queued": false
+ });
+ return ent;
+};
+
+
+var Do = function(name, data, ent, owner=1)
+{
+ let comm = {
+ "type": name,
+ "entities": Array.isArray(ent) ? ent : [ent],
+ "queued": false
+ };
+ for (let k in data)
+ comm[k] = data[k];
+ ProcessCommand(owner, comm);
+};
+
+var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
+
+Trigger.prototype.UnitsIntoRamClose = function()
+{
+ let pos = TriggerHelper.GetEntityPosition2D(point_plaza_nw);
+ let ram = QuickSpawn(pos.x, pos.y, RAM_TEMPLATE, 1);
+ for (let i = 0; i < 5; ++i)
+ {
+ pos = TriggerHelper.GetEntityPosition2D(point_plaza_ne);
+ Do("garrison", { "target": ram }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
+ }
+};
+
+
+Trigger.prototype.UnitsIntoRamFar = function()
+{
+ let pos = TriggerHelper.GetEntityPosition2D(point_road_5);
+ let ram = QuickSpawn(pos.x, pos.y, RAM_TEMPLATE, 1);
+ for (let i = 0; i < 5; ++i)
+ {
+ pos = TriggerHelper.GetEntityPosition2D(point_road_1);
+ Do("garrison", { "target": ram }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
+ }
+};
+
+Trigger.prototype.UnitsIntoRamOppositeDir = function()
+{
+ let pos = TriggerHelper.GetEntityPosition2D(point_plaza_sw);
+ let ram = QuickSpawn(pos.x, pos.y, RAM_TEMPLATE, 1);
+ let spawnpoints = [
+ TriggerHelper.GetEntityPosition2D(point_plaza_se),
+ TriggerHelper.GetEntityPosition2D(point_road_1)
+ ];
+ for (let i = 0; i < 6; ++i)
+ {
+ pos = spawnpoints[i%2];
+ Do("garrison", { "target": ram }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
+ }
+};
+
+
+Trigger.prototype.UnitsIntoRamImpossible = function()
+{
+ let pos = TriggerHelper.GetEntityPosition2D(point_island_1);
+ let ram = QuickSpawn(pos.x, pos.y, RAM_TEMPLATE, 1);
+ for (let i = 0; i < 3; ++i)
+ {
+ pos = TriggerHelper.GetEntityPosition2D(point_plaza_se);
+ Do("garrison", { "target": ram }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
+ }
+};
+
+
+Trigger.prototype.UnitsIntoShipClose = function()
+{
+ let pos = TriggerHelper.GetEntityPosition2D(point_sea_1);
+ let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
+ for (let i = 0; i < 6; ++i)
+ {
+ pos = TriggerHelper.GetEntityPosition2D(point_plaza_se);
+ Do("garrison", { "target": ship }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
+ }
+};
+Trigger.prototype.UnitsIntoShipFar = function()
+{
+ let pos = TriggerHelper.GetEntityPosition2D(point_sea_1);
+ let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
+ for (let i = 0; i < 50; ++i)
+ {
+ pos = TriggerHelper.GetEntityPosition2D(point_road_4);
+ Do("garrison", { "target": ship }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
+ }
+};
+
+Trigger.prototype.UnitsIntoShipTwoIslands = function()
+{
+ let pos = TriggerHelper.GetEntityPosition2D(point_sea_2);
+ let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
+ for (let i = 0; i < 3; ++i)
+ {
+ pos = TriggerHelper.GetEntityPosition2D(point_road_2);
+ Do("garrison", { "target": ship }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
+ }
+ for (let i = 0; i < 3; ++i)
+ {
+ pos = TriggerHelper.GetEntityPosition2D(point_island_2);
+ Do("garrison", { "target": ship }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
+ }
+};
+
+
+Trigger.prototype.UnitsIntoShipThenAnotherCloseBy = function()
+{
+ // This orders a unit to garrison in the ship, then orders another close by (by land)
+ // but not actually close by (by sea) so that the ship behaves erratically.
+ let pos = TriggerHelper.GetEntityPosition2D(point_sea_1);
+ let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
+
+ pos = TriggerHelper.GetEntityPosition2D(point_coast_1);
+ Do("garrison", { "target": ship }, QuickSpawn(pos.x, pos.y, UNIT_TEMPLATE));
+
+ // Don't do this at home
+ let holder = Engine.QueryInterface(ship, IID_GarrisonHolder);
+ let og = Object.getPrototypeOf(holder).PerformGarrison;
+ holder.PerformGarrison = (...args) => {
+ let res = og.apply(holder, args);
+ delete holder.PerformGarrison;
+ pos = TriggerHelper.GetEntityPosition2D(point_coast_2);
+ Do("garrison", { "target": ship }, QuickSpawn(pos.x, pos.y, UNIT_TEMPLATE));
+ return res;
+ };
+};
+
+
+Trigger.prototype.UnitsIntoShipAlreadyInRange = function()
+{
+ // Yay ship on land.
+ let pos = TriggerHelper.GetEntityPosition2D(point_road_3);
+ let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
+ for (let i = 0; i < 10; ++i)
+ {
+ pos = TriggerHelper.GetEntityPosition2D(point_road_3);
+ Do("garrison", { "target": ship }, QuickSpawn(pos.x, pos.y, UNIT_TEMPLATE));
+ }
+};
+
+
+Trigger.prototype.IslandUnitsPlayerGivesBadOrder = function()
+{
+ let pos = TriggerHelper.GetEntityPosition2D(point_sea_2);
+ let ship = QuickSpawn(pos.x, pos.y, SHIP_TEMPLATE, 1);
+ for (let i = 0; i < 3; ++i)
+ {
+ pos = TriggerHelper.GetEntityPosition2D(point_island_1);
+ Do("garrison", { "target": ship }, QuickSpawn(pos.x+i, pos.y, UNIT_TEMPLATE));
+ }
+ // Order the ship to go somewhere it can't pick up the units from.
+ WalkTo(567, 53, ship);
+};
+
+// Call everything here to easily disable/enable only some
+
+cmpTrigger.DoAfterDelay(200, "UnitsIntoRamClose", {});
+cmpTrigger.DoAfterDelay(200, "UnitsIntoRamFar", {});
+cmpTrigger.DoAfterDelay(200, "UnitsIntoRamOppositeDir", {});
+cmpTrigger.DoAfterDelay(200, "UnitsIntoRamImpossible", {});
+cmpTrigger.DoAfterDelay(200, "UnitsIntoShipClose", {});
+cmpTrigger.DoAfterDelay(200, "UnitsIntoShipFar", {});
+cmpTrigger.DoAfterDelay(200, "UnitsIntoShipTwoIslands", {});
+cmpTrigger.DoAfterDelay(200, "UnitsIntoShipThenAnotherCloseBy", {});
+cmpTrigger.DoAfterDelay(200, "UnitsIntoShipAlreadyInRange", {});
+cmpTrigger.DoAfterDelay(200, "IslandUnitsPlayerGivesBadOrder", {});
Index: binaries/data/mods/public/maps/scenarios/unit_motion_integration_test.xml
===================================================================
--- binaries/data/mods/public/maps/scenarios/unit_motion_integration_test.xml
+++ binaries/data/mods/public/maps/scenarios/unit_motion_integration_test.xml
@@ -1,6 +1,7 @@
+
default
Index: binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- binaries/data/mods/public/simulation/components/UnitAI.js
+++ binaries/data/mods/public/simulation/components/UnitAI.js
@@ -362,26 +362,24 @@
return;
}
+ let range = cmpGarrisonHolder.GetLoadingRange();
+ this.order.data.min = range.min;
+ this.order.data.max = range.max;
if (this.CheckRange(this.order.data))
{
this.FinishOrder();
return;
}
-
// Check if we need to move
- // TODO implement a better way to know if we are on the shoreline
- let needToMove = true;
- let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- if (this.lastShorelinePosition && cmpPosition && (this.lastShorelinePosition.x == cmpPosition.GetPosition().x) &&
- (this.lastShorelinePosition.z == cmpPosition.GetPosition().z))
- // we were already on the shoreline, and have not moved since
- if (DistanceBetweenEntities(this.entity, this.order.data.target) < 50)
- needToMove = false;
-
- if (needToMove)
- this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
- else
+ // If the target can reach us and we are reasonably close, don't move.
+ // TODO: it would be slightly more optimal to check for real, not bird-flight distance.
+ let cmpPassengerMotion = Engine.QueryInterface(this.order.data.target, IID_UnitMotion);
+ if (cmpPassengerMotion &&
+ cmpPassengerMotion.IsTargetRangeReachable(this.entity, range.min, range.max) &&
+ DistanceBetweenEntities(this.entity, this.order.data.target) < 200)
this.SetNextState("INDIVIDUAL.PICKUP.LOADING");
+ else
+ this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
},
"Order.Guard": function(msg) {
@@ -3122,7 +3120,8 @@
if (this.pickup)
{
var cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI);
- if (!cmpUnitAI || !cmpUnitAI.HasPickupOrder(this.entity))
+ if (!cmpUnitAI || (!cmpUnitAI.HasPickupOrder(this.entity) &&
+ !cmpUnitAI.IsIdle()))
{
this.FinishOrder();
return true;
@@ -4773,17 +4772,12 @@
UnitAI.prototype.CheckGarrisonRange = function(target)
{
- var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
+ let cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
- var range = cmpGarrisonHolder.GetLoadingRange();
- var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
- if (cmpObstruction)
- range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2)
-
- let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
- return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
+ let range = cmpGarrisonHolder.GetLoadingRange();
+ return this.CheckTargetRangeExplicit(target, range.min, range.max);
};
/**
Index: source/simulation2/components/CCmpPathfinder.cpp
===================================================================
--- source/simulation2/components/CCmpPathfinder.cpp
+++ source/simulation2/components/CCmpPathfinder.cpp
@@ -875,6 +875,18 @@
//////////////////////////////////////////////////////////
+bool CCmpPathfinder::IsGoalReachable(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass)
+{
+ PROFILE2("IsGoalReachable");
+
+ u16 i, j;
+ Pathfinding::NearestNavcell(x0, z0, i, j, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE);
+ if (!IS_PASSABLE(m_Grid->get(i, j), passClass))
+ m_PathfinderHier->FindNearestPassableNavcell(i, j, passClass);
+
+ return m_PathfinderHier->IsGoalReachable(i, j, goal, passClass);
+}
+
bool CCmpPathfinder::CheckMovement(const IObstructionTestFilter& filter,
entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r,
pass_class_t passClass) const
Index: source/simulation2/components/CCmpPathfinder_Common.h
===================================================================
--- source/simulation2/components/CCmpPathfinder_Common.h
+++ source/simulation2/components/CCmpPathfinder_Common.h
@@ -203,6 +203,8 @@
virtual u32 ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t clearance, entity_pos_t range, const PathGoal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t controller, entity_id_t notify);
+ virtual bool IsGoalReachable(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass);
+
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass);
virtual void SetDebugOverlay(bool enabled);
Index: source/simulation2/components/CCmpUnitMotion.cpp
===================================================================
--- source/simulation2/components/CCmpUnitMotion.cpp
+++ source/simulation2/components/CCmpUnitMotion.cpp
@@ -76,6 +76,16 @@
*/
static const entity_pos_t TARGET_UNCERTAINTY_MULTIPLIER = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2);
+/**
+ * When following a known imperfect path (i.e. a path that won't take us in range of our goal
+ * we still recompute a new path every N turn to adapt to moving targets (for example, ships that must pickup
+ * units may easily end up in this state, they still need to adjust to moving units).
+ * This is rather arbitrary and mostly for simplicity & optimisation (a better recomputing algorithm
+ * would not need this).
+ * Keep in mind that MP turns are currently 500ms.
+ */
+static const u8 KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN = 12;
+
/**
* When we fail more than this many path computations in a row, inform other components that the move will fail.
* Experimentally, this number needs to be somewhat high or moving groups of units will lead to stuck units.
@@ -136,12 +146,14 @@
// that the move will likely fail.
u8 m_FailedPathComputations = 0;
- // If true, PathingUpdateNeeded returns false always.
+ // If > 0, PathingUpdateNeeded returns false always.
// This exists because the goal may be unreachable to the short/long pathfinder.
- // In such cases, we would compute inacceptable paths and PathingUpdateNeeded would trigger every turn.
- // To avoid that, when we know the new path is imperfect, treat it as OK and follow it until the end.
- // When reaching the end, we'll run through HandleObstructedMove and this will be reset.
- bool m_FollowKnownImperfectPath = false;
+ // In such cases, we would compute inacceptable paths and PathingUpdateNeeded would trigger every turn,
+ // which would be quite bad for performance.
+ // To avoid that, when we know the new path is imperfect, treat it as OK and follow it anyways.
+ // When reaching the end, we'll go through HandleObstructedMove and reset regardless.
+ // To still recompute now and then (the target may be moving), this is a countdown decremented on each frame.
+ u8 m_FollowKnownImperfectPathCountdown = 0;
struct Ticket {
u32 m_Ticket = 0; // asynchronous request ID we're waiting for, or 0 if none
@@ -251,8 +263,8 @@
serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket.m_Ticket);
SerializeU8_Enum()(serialize, "ticket type", m_ExpectedPathTicket.m_Type);
- serialize.NumberU8("failed path computations", m_FailedPathComputations, 0, 255);
- serialize.Bool("followknownimperfectpath", m_FollowKnownImperfectPath);
+ serialize.NumberU8_Unbounded("failed path computations", m_FailedPathComputations);
+ serialize.NumberU8_Unbounded("followknownimperfectpath", m_FollowKnownImperfectPathCountdown);
SerializeU8_Enum()(serialize, "target type", m_MoveRequest.m_Type);
serialize.NumberU32_Unbounded("target entity", m_MoveRequest.m_Entity);
@@ -435,6 +447,8 @@
MoveTo(MoveRequest(target, CFixedVector2D(x, z)));
}
+ virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
+
virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z);
/**
@@ -513,15 +527,18 @@
/**
* Increment the number of failed path computations and notify other components if required.
+ * @returns true if the failure was notified, false otherwise.
*/
- void IncrementFailedPathComputationAndMaybeNotify()
+ bool IncrementFailedPathComputationAndMaybeNotify()
{
m_FailedPathComputations++;
if (m_FailedPathComputations >= MAX_FAILED_PATH_COMPUTATIONS)
{
MoveFailed();
m_FailedPathComputations = 0;
+ return true;
}
+ return false;
}
/**
@@ -711,7 +728,7 @@
m_LongPath = path;
- m_FollowKnownImperfectPath = false;
+ m_FollowKnownImperfectPathCountdown = 0;
// If there's no waypoints then we couldn't get near the target.
// Sort of hack: Just try going directly to the goal point instead
@@ -731,10 +748,18 @@
// (we will still fail the move when we arrive to the best possible position, and if we were blocked by
// an obstruction and it goes away we will notice when getting there as having no waypoint goes through
// HandleObstructedMove, so this is safe).
- // TODO: For now, we won't warn components straight away as that could lead to units idling earlier than expected,
- // but it should be done someday when the message can differentiate between different failure causes.
else if (PathingUpdateNeeded(pos))
- m_FollowKnownImperfectPath = true;
+ {
+ // Inform other components early, as they might have better behaviour than waiting for the path to carry out.
+ // Send OBSTRUCTED at first - moveFailed is likely to trigger path recomputation and we might end up
+ // recomputing too often for nothing.
+ if (!IncrementFailedPathComputationAndMaybeNotify())
+ {
+ CMessageMotionUpdate msg(CMessageMotionUpdate::OBSTRUCTED);
+ GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+ }
+ m_FollowKnownImperfectPathCountdown = KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN;
+ }
return;
}
@@ -747,11 +772,21 @@
m_ShortPath = path;
- m_FollowKnownImperfectPath = false;
+ m_FollowKnownImperfectPathCountdown = 0;
if (!m_ShortPath.m_Waypoints.empty())
{
if (PathingUpdateNeeded(pos))
- m_FollowKnownImperfectPath = true;
+ {
+ // Inform other components early, as they might have better behaviour than waiting for the path to carry out.
+ // Send OBSTRUCTED at first - moveFailed is likely to trigger path recomputation and we might end up
+ // recomputing too often for nothing.
+ if (!IncrementFailedPathComputationAndMaybeNotify())
+ {
+ CMessageMotionUpdate msg(CMessageMotionUpdate::OBSTRUCTED);
+ GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+ }
+ m_FollowKnownImperfectPathCountdown = KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN;
+ }
return;
}
@@ -856,6 +891,8 @@
if (ComputeGoal(goal, m_MoveRequest))
ComputePathToGoal(pos, goal);
}
+ else if (m_FollowKnownImperfectPathCountdown > 0)
+ --m_FollowKnownImperfectPathCountdown;
}
bool CCmpUnitMotion::PossiblyAtDestination() const
@@ -1163,7 +1200,7 @@
if (!ComputeTargetPosition(targetPos))
return false;
- if (m_FollowKnownImperfectPath)
+ if (m_FollowKnownImperfectPathCountdown > 0)
return false;
if (PossiblyAtDestination())
@@ -1456,12 +1493,29 @@
m_MoveRequest = request;
m_FailedPathComputations = 0;
- m_FollowKnownImperfectPath = false;
+ m_FollowKnownImperfectPathCountdown = 0;
ComputePathToGoal(cmpPosition->GetPosition2D(), goal);
return true;
}
+bool CCmpUnitMotion::IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
+{
+ CmpPtr cmpPosition(GetEntityHandle());
+ if (!cmpPosition || !cmpPosition->IsInWorld())
+ return false;
+
+ MoveRequest request(target, minRange, maxRange);
+ PathGoal goal;
+ if (!ComputeGoal(goal, request))
+ return false;
+
+ CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
+ CFixedVector2D pos = cmpPosition->GetPosition2D();
+ return cmpPathfinder->IsGoalReachable(pos.X, pos.Y, goal, m_PassClass);
+}
+
+
void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color)
{
bool floating = false;
Index: source/simulation2/components/ICmpPathfinder.h
===================================================================
--- source/simulation2/components/ICmpPathfinder.h
+++ source/simulation2/components/ICmpPathfinder.h
@@ -133,6 +133,12 @@
*/
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass) = 0;
+ /**
+ * @return true if the goal is reachable from (x0, z0) for the given passClass, false otherwise.
+ * Warning: this is synchronous, somewhat expensive and not should not be called too liberally.
+ */
+ virtual bool IsGoalReachable(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass) = 0;
+
/**
* Check whether the given movement line is valid and doesn't hit any obstructions
* or impassable terrain.
Index: source/simulation2/components/ICmpUnitMotion.h
===================================================================
--- source/simulation2/components/ICmpUnitMotion.h
+++ source/simulation2/components/ICmpUnitMotion.h
@@ -60,6 +60,15 @@
*/
virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0;
+ /**
+ * Check if the target is reachable.
+ * Don't take this as absolute gospel since there are things that the pathfinder may not detect, such as
+ * entity obstructions in the way, but in general it should return satisfactory results.
+ * The interface is similar to MoveToTargetRange but the move is not attempted.
+ * @return true if the target is assumed reachable, false otherwise.
+ */
+ virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
+
/**
* Turn to look towards the given point.
*/
Index: source/simulation2/components/ICmpUnitMotion.cpp
===================================================================
--- source/simulation2/components/ICmpUnitMotion.cpp
+++ source/simulation2/components/ICmpUnitMotion.cpp
@@ -26,6 +26,7 @@
DEFINE_INTERFACE_METHOD_4("MoveToPointRange", bool, ICmpUnitMotion, MoveToPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("MoveToTargetRange", bool, ICmpUnitMotion, MoveToTargetRange, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("MoveToFormationOffset", void, ICmpUnitMotion, MoveToFormationOffset, entity_id_t, entity_pos_t, entity_pos_t)
+DEFINE_INTERFACE_METHOD_3("IsTargetRangeReachable", bool, ICmpUnitMotion, IsTargetRangeReachable, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_2("FaceTowardsPoint", void, ICmpUnitMotion, FaceTowardsPoint, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_0("StopMoving", void, ICmpUnitMotion, StopMoving)
DEFINE_INTERFACE_METHOD_CONST_0("GetCurrentSpeed", fixed, ICmpUnitMotion, GetCurrentSpeed)
@@ -60,6 +61,11 @@
m_Script.CallVoid("MoveToFormationOffset", target, x, z);
}
+ virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
+ {
+ return m_Script.Call("IsTargetRangeReachable", target, minRange, maxRange);
+ }
+
virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z)
{
m_Script.CallVoid("FaceTowardsPoint", x, z);
Index: source/simulation2/helpers/HierarchicalPathfinder.h
===================================================================
--- source/simulation2/helpers/HierarchicalPathfinder.h
+++ source/simulation2/helpers/HierarchicalPathfinder.h
@@ -128,6 +128,12 @@
*/
bool MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass) const;
+ /**
+ * @return true if the goal is reachable from navcell i0, j0.
+ * (similar to MakeGoalReachable but only checking for reachability).
+ */
+ bool IsGoalReachable(u16 i0, u16 j0, const PathGoal& goal, pass_class_t passClass) const;
+
/**
* Updates @p i, @p j (which is assumed to be an impassable navcell)
* to the nearest passable navcell.
Index: source/simulation2/helpers/HierarchicalPathfinder.cpp
===================================================================
--- source/simulation2/helpers/HierarchicalPathfinder.cpp
+++ source/simulation2/helpers/HierarchicalPathfinder.cpp
@@ -702,6 +702,25 @@
return false;
}
+
+bool HierarchicalPathfinder::IsGoalReachable(u16 i0, u16 j0, const PathGoal& goal, pass_class_t passClass) const
+{
+ PROFILE2("IsGoalReachable");
+
+ u16 iGoal, jGoal;
+ Pathfinding::NearestNavcell(goal.x, goal.z, iGoal, jGoal, m_W, m_H);
+
+ std::set goalRegions(SortByBestToPoint(i0, j0));
+ // This returns goal regions ordered by distance from the best navcell in each region.
+ FindGoalRegionsAndBestNavcells(i0, j0, iGoal, jGoal, goal, goalRegions, passClass);
+
+ // Because of the sorting above, we can stop as soon as the first reachable goal region is found.
+ for (const InterestingRegion& region : goalRegions)
+ if (GetGlobalRegion(region.region, passClass) == GetGlobalRegion(i0, j0, passClass))
+ return true;
+ return false;
+}
+
void HierarchicalPathfinder::FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass) const
{
std::set regions(SortByCenterToPoint(i, j));