Index: ps/trunk/binaries/data/mods/public/maps/scenarios/pickup_test_map.xml =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scenarios/pickup_test_map.xml +++ ps/trunk/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 + + + + + + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 0 + + + + + + + 0 + + + + + + + Index: ps/trunk/binaries/data/mods/public/maps/scenarios/pickup_test_map_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scenarios/pickup_test_map_triggers.js +++ ps/trunk/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: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js @@ -366,26 +366,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) { @@ -3089,10 +3087,6 @@ // If a pickup has been requested, remove it if (this.pickup) { - var cmpHolderPosition = Engine.QueryInterface(target, IID_Position); - var cmpHolderUnitAI = Engine.QueryInterface(target, IID_UnitAI); - if (cmpHolderUnitAI && cmpHolderPosition) - cmpHolderUnitAI.lastShorelinePosition = cmpHolderPosition.GetPosition(); Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } @@ -3113,7 +3107,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; @@ -4830,17 +4825,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: ps/trunk/source/simulation2/components/CCmpPathfinder.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpPathfinder.cpp +++ ps/trunk/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: ps/trunk/source/simulation2/components/CCmpPathfinder_Common.h =================================================================== --- ps/trunk/source/simulation2/components/CCmpPathfinder_Common.h +++ ps/trunk/source/simulation2/components/CCmpPathfinder_Common.h @@ -204,6 +204,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: ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp +++ ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp @@ -77,6 +77,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. * However, too high means units will look idle for a long time when they are failing to move. @@ -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); @@ -440,6 +452,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); /** @@ -547,15 +561,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; } /** @@ -745,7 +762,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 @@ -765,10 +782,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; } @@ -781,11 +806,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; } @@ -883,6 +918,8 @@ if (ComputeGoal(goal, m_MoveRequest)) ComputePathToGoal(pos, goal); } + else if (m_FollowKnownImperfectPathCountdown > 0) + --m_FollowKnownImperfectPathCountdown; } bool CCmpUnitMotion::PossiblyAtDestination() const @@ -1187,7 +1224,7 @@ if (!ComputeTargetPosition(targetPos)) return false; - if (m_FollowKnownImperfectPath) + if (m_FollowKnownImperfectPathCountdown > 0) return false; if (PossiblyAtDestination()) @@ -1480,12 +1517,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: ps/trunk/source/simulation2/components/ICmpPathfinder.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpPathfinder.h +++ ps/trunk/source/simulation2/components/ICmpPathfinder.h @@ -134,6 +134,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. * Returns true if the movement is okay. Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpUnitMotion.h +++ ps/trunk/source/simulation2/components/ICmpUnitMotion.h @@ -61,6 +61,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. */ virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp +++ ps/trunk/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) @@ -61,6 +62,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: ps/trunk/source/simulation2/helpers/HierarchicalPathfinder.h =================================================================== --- ps/trunk/source/simulation2/helpers/HierarchicalPathfinder.h +++ ps/trunk/source/simulation2/helpers/HierarchicalPathfinder.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -129,6 +129,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: ps/trunk/source/simulation2/helpers/HierarchicalPathfinder.cpp =================================================================== --- ps/trunk/source/simulation2/helpers/HierarchicalPathfinder.cpp +++ ps/trunk/source/simulation2/helpers/HierarchicalPathfinder.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -704,6 +704,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));