Index: binaries/data/mods/public/simulation/components/Builder.js =================================================================== --- binaries/data/mods/public/simulation/components/Builder.js +++ binaries/data/mods/public/simulation/components/Builder.js @@ -49,12 +49,7 @@ Builder.prototype.GetRange = function() { - let max = 2; - let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); - if (cmpObstruction) - max += cmpObstruction.GetUnitRadius(); - - return { "max": max, "min": 0 }; + return { "max": 1, "min": 0 }; }; Builder.prototype.GetRate = function() Index: binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/GuiInterface.js +++ binaries/data/mods/public/simulation/components/GuiInterface.js @@ -502,7 +502,9 @@ if (cmpUnitMotion) ret.speed = { "walk": cmpUnitMotion.GetWalkSpeed(), - "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunSpeedMultiplier() + "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunSpeedMultiplier(), + "current": cmpUnitMotion.GetSpeed(), + "tryingToMove" : cmpUnitMotion.IsTryingToMove() }; return ret; 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 @@ -143,7 +143,7 @@ // as switching states) }, - "MoveStarted": function() { + "enter": function() { // ignore spurious movement messages }, @@ -225,16 +225,16 @@ this.FinishOrder(); return; } - // Move a tile outside the building - let range = 4; - if (this.MoveToTargetRangeExplicit(msg.data.target, range, range)) + // Move outside the building. Since INDIVIDUAL.WALKING checks for a distance in [0,1], move to 0.5 + let range = 0.5; + if (this.MoveToTargetRangeExplicit(msg.data.target, range)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.WALKING"); } else { - // We are already at the target, or can't move at all + // We can't reach the target this.FinishOrder(); } }, @@ -283,7 +283,7 @@ if (!this.order.data.max) this.MoveToPoint(this.order.data.x, this.order.data.z); else - this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max); + this.MoveToPointRange(this.order.data.x, this.order.data.z, (this.order.data.min + this.order.data.max) / 2.0); if (this.IsAnimal()) this.SetNextState("ANIMAL.WALKING"); else @@ -344,7 +344,7 @@ } else { - // We are already at the target, or can't move at all + // We can't reach the target this.StopMoving(); this.FinishOrder(); } @@ -371,16 +371,8 @@ // TODO: what if the units are on a cliff ? the ship will go below the cliff // and the units won't be able to garrison. Should go to the nearest (accessible) shore - if (needToMove && this.MoveToTarget(this.order.data.target)) - { - this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING"); - } - else - { - // We are already at the target, or can't move at all - this.StopMoving(); - this.SetNextState("INDIVIDUAL.PICKUP.LOADING"); - } + this.MoveToTarget(this.order.data.target, true); + this.SetNextState("INDIVIDUAL.PICKUP"); }, "Order.Guard": function(msg) { @@ -390,7 +382,7 @@ return; } - if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) + if (this.MoveToTargetRangeExplicit(this.isGuardOf, this.guardRange, true)) this.SetNextState("INDIVIDUAL.GUARD.ESCORTING"); else this.SetNextState("INDIVIDUAL.GUARD.GUARDING"); @@ -400,7 +392,7 @@ // We use the distance between the entities to account for ranged attacks var distance = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (cmpUnitMotion.MoveToTargetRange(this.order.data.target, distance, -1)) + if (cmpUnitMotion.SetNewDestinationAsEntity(this.order.data.target, distance, true)) { // We've started fleeing from the given target if (this.IsAnimal()) @@ -410,7 +402,7 @@ } else { - // We are already at the target, or can't move at all + // We can't reach the target this.StopMoving(); this.FinishOrder(); } @@ -599,7 +591,7 @@ } else { - // We are already at the target, or can't move at all, + // We can't reach the target. // so try gathering it from here. // TODO: need better handling of the can't-reach-target case this.StopMoving(); @@ -676,7 +668,7 @@ } else { - // We are already at the target, or can't move at all, + // We can't reach the target. // so try repairing it from here. // TODO: need better handling of the can't-reach-target case this.StopMoving(); @@ -782,7 +774,7 @@ // Only used by other orders to walk there in formation "Order.WalkToTargetRange": function(msg) { - if (this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max)) + if (this.MoveToTargetRangeExplicit(this.order.data.target, (this.order.data.min, this.order.data.max) / 2.0)) this.SetNextState("WALKING"); else this.FinishOrder(); @@ -796,7 +788,7 @@ }, "Order.WalkToPointRange": function(msg) { - if (this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max)) + if (this.MoveToPointRange(this.order.data.x, this.order.data.z, (this.order.data.min, this.order.data.max) / 2.0)) this.SetNextState("WALKING"); else this.FinishOrder(); @@ -1012,24 +1004,25 @@ var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(false); }, - - "MoveStarted": function() { - let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.SetRearrange(true); - cmpFormation.MoveMembersIntoFormation(true, true); - } }, "WALKING": { - "MoveStarted": function(msg) { - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + "enter": function(msg) { + let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { - if (this.FinishOrder()) - this.CallMemberFunction("ResetFinishOrder", []); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (msg.data.error + || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true) + || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true)) + { + if (this.FinishOrder()) { + this.CallMemberFunction("ResetFinishOrder", []); + } + } }, }, @@ -1047,15 +1040,21 @@ this.StopTimer(); }, - "MoveStarted": function(msg) { + "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { - if (this.FinishOrder()) - this.CallMemberFunction("ResetFinishOrder", []); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (msg.data.error + || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true) + || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true)) + { + if (this.FinishOrder()) + this.CallMemberFunction("ResetFinishOrder", []); + } }, }, @@ -1088,28 +1087,34 @@ delete this.patrolStartPosOrder; }, - "MoveStarted": function(msg) { + "enter": function(msg) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function() { - /** - * A-B-A-B-..: - * if the user only commands one patrol order, the patrol will be between - * the last position and the defined waypoint - * A-B-C-..-A-B-..: - * otherwise, the patrol is only between the given patrol commands and the - * last position is not included (last position = the position where the unit - * is located at the time of the first patrol order) - */ - - if (this.orderQueue.length == 1) - this.PushOrder("Patrol", this.patrolStartPosOrder); - - this.PushOrder(this.order.type, this.order.data); - this.FinishOrder(); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (msg.data.error + || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true) + || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true)) + { + /** + * A-B-A-B-..: + * if the user only commands one patrol order, the patrol will be between + * the last position and the defined waypoint + * A-B-C-..-A-B-..: + * otherwise, the patrol is only between the given patrol commands and the + * last position is not included (last position = the position where the unit + * is located at the time of the first patrol order) + */ + + if (this.orderQueue.length == 1) + this.PushOrder("Patrol", this.patrolStartPosOrder); + + this.PushOrder(this.order.type, this.order.data); + this.FinishOrder(); + } }, }, @@ -1135,13 +1140,17 @@ "APPROACHING": { - "MoveStarted": function(msg) { + "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (msg.data.error + || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true) + || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true)) this.SetNextState("GARRISONING"); }, }, @@ -1161,40 +1170,49 @@ }, "FORMING": { - "MoveStarted": function(msg) { + "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, false); }, "MoveCompleted": function(msg) { - - if (this.FinishOrder()) + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (msg.data.error + || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true) + || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true)) { - this.CallMemberFunction("ResetFinishOrder", []); - return; + if (this.FinishOrder()) + { + this.CallMemberFunction("ResetFinishOrder", []); + return; + } + cmpFormation.FindInPosition(); } - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - - cmpFormation.FindInPosition(); } }, "COMBAT": { "APPROACHING": { - "MoveStarted": function(msg) { + "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, - "MoveCompleted": function(msg) { - var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - this.CallMemberFunction("Attack", [this.order.data.target, this.order.data.allowCapture, false]); - if (cmpAttack.CanAttackAsFormation()) - this.SetNextState("COMBAT.ATTACKING"); - else - this.SetNextState("MEMBER"); + "MoveCompleted": function(msg) { + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (msg.data.error + || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max, true) + || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min, this.order.data.max, true)) + { + var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + this.CallMemberFunction("Attack", [this.order.data.target, this.order.data.allowCapture, false]); + if (cmpAttack.CanAttackAsFormation()) + this.SetNextState("COMBAT.ATTACKING"); + else + this.SetNextState("MEMBER"); + } }, }, @@ -1352,25 +1370,22 @@ var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpFormation && cmpVisual) - { - cmpVisual.ReplaceMoveAnimation("walk", cmpFormation.GetFormationAnimation(this.entity, "walk")); - cmpVisual.ReplaceMoveAnimation("run", cmpFormation.GetFormationAnimation(this.entity, "run")); - } - this.SelectAnimation("move"); + this.SetAnimationVariant(cmpFormation.GetFormationAnimation(this.entity, "walk")); }, // Occurs when the unit has reached its destination and the controller // is done moving. The controller is notified. "MoveCompleted": function(msg) { - // We can only finish this order if the move was really completed. - if (!msg.data.error && this.FinishOrder()) + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (!msg.data.error && this.order.data.target && !cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, 0, 0, true)) return; + + this.StopMoving(); + this.FinishOrder(); + var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) - { - cmpVisual.ResetMoveAnimation("walk"); - cmpVisual.ResetMoveAnimation("run"); - } + this.SetDefaultAnimationVariant(); var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) @@ -1541,15 +1556,12 @@ this.RespondToHealableEntities(msg.data.added); }, - "MoveStarted": function() { - this.SelectAnimation("move"); - }, - - "MoveCompleted": function() { - this.SelectAnimation("idle"); - }, - "Timer": function(msg) { + // bit of a sanity check, but this happening would most likely mean a bug somewhere. + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + if (cmpUnitMotion && cmpUnitMotion.IsTryingToMove()) + warn("Entity " + this.entity + " is in the idle state but trying to move"); + if (!this.isIdle) { this.isIdle = true; @@ -1559,12 +1571,18 @@ }, "WALKING": { - "enter": function() { - this.SelectAnimation("move"); + "enter": function () { }, - "MoveCompleted": function() { - this.FinishOrder(); + "MoveCompleted": function(msg) { + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (msg.data.error + || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, 0, 1, true) + || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, 0, 1, true)) + { + this.StopMoving(); + this.FinishOrder(); + } }, }, @@ -1574,7 +1592,6 @@ this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); - this.SelectAnimation("move"); }, "Timer": function(msg) { @@ -1587,7 +1604,12 @@ }, "MoveCompleted": function() { - this.FinishOrder(); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, 0, 1), true) + { + this.StopMoving(); + this.FinishOrder(); + } }, }, @@ -1609,7 +1631,6 @@ this.StartTimer(0, 1000); this.SetAnimationVariant("combat"); - this.SelectAnimation("move"); }, "leave": function() { @@ -1623,6 +1644,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); if (this.orderQueue.length == 1) this.PushOrder("Patrol", this.patrolStartPosOrder); @@ -1643,7 +1665,6 @@ this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); - this.SelectAnimation("move"); this.SetHeldPositionOnEntity(this.isGuardOf); return false; }, @@ -1657,6 +1678,24 @@ return; } this.SetHeldPositionOnEntity(this.isGuardOf); + + this.SetMoveSpeedRatio(this.GetWalkSpeed()); + + // Adapt the speed to the one of the target if needed + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (cmpObstructionManager.IsInTargetRange(this.entity, this.isGuardOf, 0, 3*this.guardRange, true)) + { + var cmpOtherMotion = Engine.QueryInterface(this.isGuardOf, IID_UnitMotion); + if (cmpOtherMotion) + { + let otherSpeed = cmpOtherMotion.GetSpeed(); + let mySpeed = cmpUnitMotion.GetSpeed(); + let speed = otherSpeed / mySpeed; + if (speed < this.GetWalkSpeed()) + this.SetMoveSpeedRatio(speed); + } + } }, "leave": function(msg) { @@ -1665,7 +1704,7 @@ this.SetDefaultAnimationVariant(); }, - "MoveStarted": function(msg) { + "enter": function(msg) { // Adapt the speed to the one of the target if needed var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); if (cmpObstructionManager.IsInTargetRange(this.entity, this.isGuardOf, 0, 3 * this.guardRange, true)) @@ -1681,8 +1720,9 @@ }, "MoveCompleted": function() { + this.StopMoving(); this.ResetMoveSpeed(); - if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) + if (!this.MoveToTargetRangeExplicit(this.isGuardOf, this.guardRange)) this.SetNextState("GUARDING"); }, }, @@ -1710,7 +1750,8 @@ return; } // then check is the target has moved - if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) + // TODO: this should call isInRange, not this. + if (this.MoveToTargetRangeExplicit(this.isGuardOf, this.guardRange)) this.SetNextState("ESCORTING"); else { @@ -1736,10 +1777,7 @@ "FLEEING": { "enter": function() { this.PlaySound("panic"); - - // Run quickly - this.SelectAnimation("move"); - this.SetMoveSpeedRatio(this.GetRunMultiplier()); + this.ResetMoveSpeed(); }, "HealthChanged": function() { @@ -1752,6 +1790,7 @@ "MoveCompleted": function() { // When we've run far enough, stop fleeing + this.StopMoving(); this.FinishOrder(); }, @@ -1776,7 +1815,6 @@ // Show weapons rather than carried resources. this.SetAnimationVariant("combat"); - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, @@ -1797,10 +1835,19 @@ if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } + else if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) + { + this.StopMoving(); + // If the unit needs to unpack, do so + if (this.CanUnpack()) + this.SetNextState("UNPACKING"); + else + this.SetNextState("ATTACKING"); + } }, "MoveCompleted": function() { - + this.StopMoving(); if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { // If the unit needs to unpack, do so @@ -2026,13 +2073,11 @@ // Show weapons rather than carried resources. this.SetAnimationVariant("combat"); - this.SelectAnimation("move"); var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.IsFleeing()) - { // Run after a fleeing target this.SetMoveSpeedRatio(this.GetRunMultiplier()); - } + this.StartTimer(1000, 1000); }, @@ -2062,9 +2107,19 @@ if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } + else if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) + { + this.StopMoving(); + // If the unit needs to unpack, do so + if (this.CanUnpack()) + this.SetNextState("UNPACKING"); + else + this.SetNextState("ATTACKING"); + } }, "MoveCompleted": function() { + this.StopMoving(); this.SetNextState("ATTACKING"); }, }, @@ -2073,7 +2128,6 @@ "GATHER": { "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave". @@ -2133,12 +2187,14 @@ } return true; } + this.StartTimer(0, 500); return false; }, "MoveCompleted": function(msg) { if (msg.data.error) { + this.StopMoving(); // We failed to reach the target // remove us from the list of entities gathering from Resource. @@ -2179,12 +2235,24 @@ this.PerformGather(oldTarget, false, false); return; } + else if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) + { + this.StopMoving(); + // We reached the target - start gathering from it now + this.SetNextState("GATHERING"); + } + }, - // We reached the target - start gathering from it now - this.SetNextState("GATHERING"); + "Timer": function() { + if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) + { + this.StopMoving(); + this.SetNextState("GATHERING"); + } }, "leave": function() { + this.StopTimer(); // don't use ownership because this is called after a conversion/resignation // and the ownership would be invalid then. var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); @@ -2198,10 +2266,12 @@ // Walking to a good place to gather resources near, used by GatherNearPosition "WALKING": { "enter": function() { - this.SelectAnimation("move"); }, "MoveCompleted": function(msg) { + if (msg.data.error) + this.StopMoving(); + var resourceType = this.order.data.type; var resourceTemplate = this.order.data.template; @@ -2233,7 +2303,6 @@ this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } - // No dropsites, just give up }, }, @@ -2282,23 +2351,23 @@ return true; } - // Scale timing interval based on rate, and start timer - // The offset should be at least as long as the repeat time so we use the same value for both. - var offset = 1000/rate; - var repeat = offset; - this.StartTimer(offset, repeat); - - // We want to start the gather animation as soon as possible, - // but only if we're actually at the target and it's still alive - // (else it'll look like we're chopping empty air). - // (If it's not alive, the Timer handler will deal with sending us - // off to a different target.) + // Range check: if we are in-range, start the gathering animation and set the timer + // If we are not in-range, we'll set a different timer to avoid waiting an inordinate amount of time. if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) { this.SetDefaultAnimationVariant(); + this.FaceTowardsTarget(this.gatheringTarget); var typename = "gather_" + this.order.data.type.specific; this.SelectAnimation(typename); + + // Scale timing interval based on rate, and start timer + // The offset should be at least as long as the repeat time so we use the same value for both. + var offset = 1000/rate; + var repeat = offset; + this.StartTimer(offset, repeat); } + else + this.StartTimer(0, 1000); return false; }, @@ -2383,10 +2452,9 @@ // the old one. So try to get close to the old resource's // last known position - var maxRange = 8; // get close but not too close + var range = 4; // get close but not too close if (this.order.data.lastPos && - this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z, - 0, maxRange)) + this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z, range)) { this.SetNextState("APPROACHING"); return; @@ -2461,7 +2529,6 @@ "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, @@ -2482,6 +2549,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); this.SetNextState("HEALING"); }, }, @@ -2512,6 +2580,7 @@ }, "leave": function() { + this.SelectAnimation("idle"); this.StopTimer(); }, @@ -2567,7 +2636,6 @@ }, "CHASING": { "enter": function() { - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, @@ -2586,6 +2654,7 @@ } }, "MoveCompleted": function() { + this.StopMoving(); this.SetNextState("HEALING"); }, }, @@ -2595,19 +2664,41 @@ "RETURNRESOURCE": { "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); + this.StartTimer(0, 1000); }, - "MoveCompleted": function() { - // Switch back to idle animation to guarantee we won't - // get stuck with the carry animation after stopping moving - this.SelectAnimation("idle"); + "leave": function() { + this.StopTimer(); + }, + "Timer": function() { // Check the dropsite is in range and we can return our resource there // (we didn't get stopped before reaching it) - if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true)) + if (!this.CanReturnResource(this.order.data.target, true)) { + this.StopMoving(); + // The dropsite was destroyed, or we couldn't reach it, or ownership changed + // Look for a new one. + + var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + var genericType = cmpResourceGatherer.GetMainCarryingType(); + var nearby = this.FindNearestDropsite(genericType); + if (nearby) + { + this.FinishOrder(); + this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); + return; + } + + // Oh no, couldn't find any drop sites. Give up on returning. + this.FinishOrder(); + return; + } + if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer)) + { + this.StopMoving(); var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite); + // this ought to be redundant with the above check. if (cmpResourceDropsite) { // Dump any resources we can @@ -2625,23 +2716,9 @@ return; } } - - // The dropsite was destroyed, or we couldn't reach it, or ownership changed - // Look for a new one. - - var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); - var genericType = cmpResourceGatherer.GetMainCarryingType(); - var nearby = this.FindNearestDropsite(genericType); - if (nearby) - { - this.FinishOrder(); - this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); - return; - } - - // Oh no, couldn't find any drop sites. Give up on returning. - this.FinishOrder(); }, + + "MoveCompleted": "Timer", }, }, @@ -2653,18 +2730,28 @@ "APPROACHINGMARKET": { "enter": function() { - this.SelectAnimation("move"); + this.StartTimer(1000, 1000); }, - "MoveCompleted": function() { - if (this.waypoints && this.waypoints.length) + "leave": function() { + this.StopTimer(); + }, + + "Timer": function() { + if (this.CheckTargetRange(this.order.data.target, IID_Trader)) { - if (!this.MoveToMarket(this.order.data.target)) - this.StopTrading(); + this.StopMoving(); + if (this.waypoints && this.waypoints.length) + { + if (!this.MoveToMarket(this.order.data.target)) + this.StopTrading(); + } + else + this.PerformTradeAndMoveToNextMarket(this.order.data.target); } - else - this.PerformTradeAndMoveToNextMarket(this.order.data.target); }, + + "MoveCompleted": "Timer", }, "TradingCanceled": function(msg) { @@ -2674,17 +2761,30 @@ let otherMarket = cmpTrader && cmpTrader.GetFirstMarket(); this.StopTrading(); if (otherMarket) - this.WalkToTarget(otherMarket); + this.g(otherMarket); }, }, "REPAIR": { "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); + this.StartTimer(0, 1000); }, - "MoveCompleted": function() { + "leave": function() { + this.StopTimer(); + }, + + "Timer": function() { + if (this.CheckTargetRange(this.order.data.target, IID_Builder)) + { + this.StopMoving(); + this.SetNextState("REPAIRING"); + } + }, + // TODO: clean this up when MoveCompleted becomes MoveSuccesHint and MoveFailure or something + "MoveCompleted": function(msg) { + this.StopMoving(); this.SetNextState("REPAIRING"); }, }, @@ -2729,6 +2829,8 @@ if (cmpBuilderList) cmpBuilderList.AddBuilder(this.entity); + this.FaceTowardsTarget(this.repairTarget); + this.SelectAnimation("build"); this.StartTimer(1000, 1000); return false; @@ -2739,6 +2841,7 @@ if (cmpBuilderList) cmpBuilderList.RemoveBuilder(this.entity); delete this.repairTarget; + this.SelectAnimation("idle"); this.StopTimer(); }, @@ -2758,10 +2861,13 @@ // in that case, the repairTarget is deleted, and we can just return if (!this.repairTarget) return; - if (this.MoveToTargetRange(this.repairTarget, IID_Builder)) - this.SetNextState("APPROACHING"); - else if (!this.CheckTargetRange(this.repairTarget, IID_Builder)) - this.FinishOrder(); //can't approach and isn't in reach + if (!this.CheckTargetRange(this.repairTarget, IID_Builder)) + { + if (this.MoveToTargetRange(this.repairTarget, IID_Builder)) + this.SetNextState("APPROACHING"); + else + this.FinishOrder(); //can't approach and isn't in reach + } }, }, @@ -2878,12 +2984,39 @@ "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); + this.StartTimer(1000,1000); }, - "MoveCompleted": function() { - this.SetNextState("GARRISONED"); + "leave": function() { + this.StopTimer(); }, + + "Timer": function() { + if (this.IsUnderAlert() && this.alertGarrisoningTarget) + { + // check that we can garrison in the building we're supposed to garrison in + var cmpGarrisonHolder = Engine.QueryInterface(this.alertGarrisoningTarget, IID_GarrisonHolder); + if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) + { + // Try to find another nearby building + var nearby = this.FindNearbyGarrisonHolder(); + if (nearby) + { + this.alertGarrisoningTarget = nearby; + this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget}); + } + else + this.FinishOrder(); + return; + } + } + if (this.order.data.target && this.CheckGarrisonRange(this.order.data.target)) + this.SetNextState("GARRISONED"); + else if (this.alertGarrisoningTarget && this.CheckGarrisonRange(this.alertGarrisoningTarget)) + this.SetNextState("GARRISONED"); + }, + + "MoveCompleted": "Timer", }, "GARRISONED": { @@ -2971,7 +3104,7 @@ } } - if (this.MoveToTarget(target)) + if (this.MoveToTarget(target, true)) { this.SetNextState("APPROACHING"); return false; @@ -3002,6 +3135,7 @@ this.StopTimer(); var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(false); + this.SelectAnimation("idle"); }, "Timer": function(msg) { @@ -3046,36 +3180,19 @@ }, "PICKUP": { - "APPROACHING": { - "enter": function() { - this.SelectAnimation("move"); - }, - - "MoveCompleted": function() { - this.SetNextState("LOADING"); - }, - - "PickupCanceled": function() { - this.StopMoving(); + "enter": function() { + var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); + if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) + { this.FinishOrder(); - }, + return true; + } + return false; }, - "LOADING": { - "enter": function() { - this.SelectAnimation("idle"); - var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); - if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) - { - this.FinishOrder(); - return true; - } - return false; - }, - - "PickupCanceled": function() { - this.FinishOrder(); - }, + "PickupCanceled": function() { + this.StopMoving(); + this.FinishOrder(); }, }, }, @@ -3100,16 +3217,16 @@ }, "Order.LeaveFoundation": function(msg) { - // Move a tile outside the building - var range = 4; - if (this.MoveToTargetRangeExplicit(msg.data.target, range, range)) + // Move outside the building. Since INDIVIDUAL.WALKING checks for a distance in [0,1], move to 0.5 + var range = 0.5; + if (this.MoveToTargetRangeExplicit(msg.data.target, range)) { // We've started walking to the given point this.SetNextState("WALKING"); } else { - // We are already at the target, or can't move at all + // We cannot reach the target. this.FinishOrder(); } }, @@ -3127,8 +3244,6 @@ "ROAMING": { "enter": function() { // Walk in a random direction - this.SelectAnimation("walk", false, 1); - this.SetFacePointAfterMove(false); this.MoveRandomly(+this.template.RoamDistance); // Set a random timer to switch to feeding state this.StartTimer(randIntInclusive(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); @@ -3136,7 +3251,6 @@ "leave": function() { this.StopTimer(); - this.SetFacePointAfterMove(true); }, "LosRangeUpdate": function(msg) { @@ -3166,6 +3280,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); this.MoveRandomly(+this.template.RoamDistance); }, }, @@ -3198,7 +3313,7 @@ } }, - "MoveCompleted": function() { }, + "MoveCompleted": function() { this.StopMoving(); }, "Timer": function(msg) { this.SetNextState("ROAMING"); @@ -3569,6 +3684,9 @@ error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack); } + // Safety net, in general it's better if unitAI states handle this properly. + this.StopMoving(); + this.orderQueue.shift(); this.order = this.orderQueue[0]; @@ -3718,6 +3836,7 @@ this.UpdateWorkOrders(type); } + this.StopMoving(); let garrisonHolder = this.IsGarrisoned() && type != "Ungarrison" ? this.GetGarrisonHolder() : null; // Special cases of orders that shouldn't be replaced: @@ -3905,12 +4024,15 @@ //// Message handlers ///// -UnitAI.prototype.OnMotionChanged = function(msg) +UnitAI.prototype.OnMovePaused = function(msg) { - if (msg.starting && !msg.error) - this.UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg}); - else if (!msg.starting || msg.error) - this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg}); + // TODO: change this. Doesn't matter if UnitAI thinks it's completed for now since anyways the states do range checks. + this.UnitFsm.ProcessMessage(this, { "type": "MoveCompleted", "data": { "error" : false }}); +}; + +UnitAI.prototype.OnMoveFailure = function(msg) +{ + this.UnitFsm.ProcessMessage(this, { "type": "MoveCompleted", "data": { "error" : true }}); }; UnitAI.prototype.OnGlobalConstructionFinished = function(msg) @@ -4216,6 +4338,12 @@ return; } + // Set default values if unspecified + if (once === undefined) + once = false; + if (speed === undefined) + speed = 1.0; + cmpVisual.SelectAnimation(name, once, speed); }; @@ -4232,31 +4360,34 @@ UnitAI.prototype.StopMoving = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpUnitMotion.StopMoving(); + cmpUnitMotion.DiscardMove(); }; UnitAI.prototype.MoveToPoint = function(x, z) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToPointRange(x, z, 0, 0); + cmpUnitMotion.SetAbortIfStuck(30); + return cmpUnitMotion.SetNewDestinationAsPosition(x, z, 0, true); }; -UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) +UnitAI.prototype.MoveToPointRange = function(x, z, range, evenUnreachable = false) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); + cmpUnitMotion.SetAbortIfStuck(30); + return cmpUnitMotion.SetNewDestinationAsPosition(x, z, range, evenUnreachable); }; -UnitAI.prototype.MoveToTarget = function(target) +UnitAI.prototype.MoveToTarget = function(target, evenUnreachable = false) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, 0, 0); + cmpUnitMotion.SetAbortIfStuck(5); + return cmpUnitMotion.SetNewDestinationAsEntity(target, 0, evenUnreachable); }; -UnitAI.prototype.MoveToTargetRange = function(target, iid, type) +UnitAI.prototype.MoveToTargetRange = function(target, iid, type, evenUnreachable = false) { if (!this.CheckTargetVisible(target) || this.IsTurret()) return false; @@ -4267,7 +4398,10 @@ var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); + cmpUnitMotion.SetAbortIfStuck(5); + // generally speaking, try to aim for the middle of a range. + // + return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + range.max)/2.0, evenUnreachable); }; /** @@ -4275,7 +4409,7 @@ * for melee attacks, this goes straight to the default range checks * for ranged attacks, the parabolic range is used */ -UnitAI.prototype.MoveToTargetAttackRange = function(target, type) +UnitAI.prototype.MoveToTargetAttackRange = function(target, type, evenUnreachable = false) { // for formation members, the formation will take care of the range check if (this.IsFormationMember()) @@ -4290,7 +4424,7 @@ target = cmpFormation.GetClosestMember(this.entity); if (type != "Ranged") - return this.MoveToTargetRange(target, IID_Attack, type); + return this.MoveToTargetRange(target, IID_Attack, type, evenUnreachable); if (!this.CheckTargetVisible(target)) return false; @@ -4322,24 +4456,27 @@ // the parabole changes while walking, take something in the middle var guessedMaxRange = (range.max + parabolicMaxRange)/2; +// TODO: here we should give the desired range based on unit speed, our own desire to walk, and so on. var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange)) + cmpUnitMotion.SetAbortIfStuck(9); + if (cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + guessedMaxRange)/2.0, false)) return true; // if that failed, try closer - return cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange)); + return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + Math.min(range.max, parabolicMaxRange))/2.0, evenUnreachable); }; -UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) +UnitAI.prototype.MoveToTargetRangeExplicit = function(target, range, evenUnreachable = false) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, min, max); + cmpUnitMotion.SetAbortIfStuck(5); + return cmpUnitMotion.SetNewDestinationAsEntity(target, range, evenUnreachable); }; -UnitAI.prototype.MoveToGarrisonRange = function(target) +UnitAI.prototype.MoveToGarrisonRange = function(target, evenUnreachable = false) { if (!this.CheckTargetVisible(target)) return false; @@ -4350,12 +4487,13 @@ var range = cmpGarrisonHolder.GetLoadingRange(); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); + cmpUnitMotion.SetAbortIfStuck(5); + return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + range.max)/2.0, evenUnreachable); }; UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max) { - var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max, true); }; @@ -4366,7 +4504,7 @@ return false; var range = cmpRanged.GetRange(type); - var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, true); }; @@ -4415,13 +4553,13 @@ if (maxRangeSq < 0) return false; - var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, Math.sqrt(maxRangeSq), true); }; UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max) { - var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max, true); }; @@ -4436,7 +4574,7 @@ if (cmpObstruction) range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2) - var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, true); }; @@ -4992,7 +5130,7 @@ // We don't want to let healers walk to the target unit so they can be easily killed. // Instead we just let them get into healing range. if (this.IsHealer()) - this.MoveToTargetRange(target, IID_Heal); + this.MoveToTargetRange(target, IID_Heal, true); else this.WalkToTarget(target, queued); return; @@ -5867,21 +6005,9 @@ else if (Math.abs((ang - this.startAngle + Math.PI) % (2 * Math.PI) - Math.PI) < Math.abs(this.roamAngle / 2)) this.roamAngle *= randBool() ? 1 : -1; - let halfDelta = randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4); - // First half rotation to decrease the impression of immediate rotation - ang += halfDelta; - cmpUnitMotion.FaceTowardsPoint(pos.x + 0.5 * Math.sin(ang), pos.z + 0.5 * Math.cos(ang)); - // Then second half of the rotation - ang += halfDelta; + ang += randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4) * 2.0; let dist = randFloat(0.5, 1.5) * distance; - cmpUnitMotion.MoveToPointRange(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, dist); -}; - -UnitAI.prototype.SetFacePointAfterMove = function(val) -{ - var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (cmpMotion) - cmpMotion.SetFacePointAfterMove(val); + cmpUnitMotion.SetNewDestinationAsPosition(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, true); }; UnitAI.prototype.AttackEntitiesByPreference = function(ents) Index: binaries/data/mods/public/simulation/components/UnitMotionFlying.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitMotionFlying.js +++ binaries/data/mods/public/simulation/components/UnitMotionFlying.js @@ -203,7 +203,7 @@ if (!this.reachedTarget && this.targetMinRange <= distFromTarget && distFromTarget <= this.targetMaxRange) { this.reachedTarget = true; - Engine.PostMessage(this.entity, MT_MotionChanged, { "starting": false, "error": false }); + Engine.PostMessage(this.entity, MT_MovePaused, {}); } // If we're facing away from the target, and are still fairly close to it, @@ -245,33 +245,43 @@ cmpPosition.MoveTo(pos.x, pos.z); }; -UnitMotionFlying.prototype.MoveToPointRange = function(x, z, minRange, maxRange) +UnitMotionFlying.prototype.SetNewDestinationAsPosition = function(x, z, range) { this.hasTarget = true; this.landing = false; this.reachedTarget = false; this.targetX = x; this.targetZ = z; - this.targetMinRange = minRange; - this.targetMaxRange = maxRange; + this.targetMinRange = 0; + this.targetMaxRange = range; + + // we'll tell the visual actor to set our animation here. + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + cmpVisual.SetMovingSpeed(this.speed); return true; }; -UnitMotionFlying.prototype.MoveToTargetRange = function(target, minRange, maxRange) +UnitMotionFlying.prototype.SetNewDestinationAsEntity = function(target, range) { var cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return false; + // we'll tell the visual actor to set our animation here. + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + cmpVisual.SetMovingSpeed(this.speed); + var targetPos = cmpTargetPosition.GetPosition2D(); this.hasTarget = true; this.reachedTarget = false; this.targetX = targetPos.x; this.targetZ = targetPos.y; - this.targetMinRange = minRange; - this.targetMaxRange = maxRange; + this.targetMinRange = 0; + this.targetMaxRange = range; return true; }; @@ -306,22 +316,17 @@ return this.template.PassabilityClass; }; -UnitMotionFlying.prototype.GetPassabilityClass = function() +UnitMotionFlying.prototype.IsTryingToMove = function() { - return this.passabilityClass; -}; + return false; +} UnitMotionFlying.prototype.FaceTowardsPoint = function(x, z) { // Ignore this - angle is controlled by the target-seeking code instead }; -UnitMotionFlying.prototype.SetFacePointAfterMove = function() -{ - // Ignore this - angle is controlled by the target-seeking code instead -}; - -UnitMotionFlying.prototype.StopMoving = function() +UnitMotionFlying.prototype.DiscardMove = function() { //Invert if (!this.waterDeath) Index: binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -63,9 +63,6 @@ GetEnemies: function() { return []; }, }); - AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { - IsInTargetRange: function(ent, target, min, max, opposite) { return true; } - }); var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); @@ -88,6 +85,7 @@ AddMock(unit, IID_UnitMotion, { GetWalkSpeed: function() { return 1; }, MoveToFormationOffset: function(target, x, z) { }, + IsInTargetRange: function(target, min, max) { return true; }, MoveToTargetRange: function(target, min, max) { }, StopMoving: function() { }, GetPassabilityClassName: function() { return "default"; }, @@ -139,8 +137,7 @@ }); AddMock(controller, IID_UnitMotion, { - GetWalkSpeed: function() { return 1; }, - SetSpeedRatio: function(speed) { }, + SetSpeed: function(speed) { }, MoveToPointRange: function(x, z, minRange, maxRange) { }, GetPassabilityClassName: function() { return "default"; }, }); @@ -207,10 +204,6 @@ GetNumPlayers: function() { return 2; }, }); - AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { - IsInTargetRange: function(ent, target, min, max) { return true; } - }); - AddMock(playerEntity, IID_Player, { IsAlly: function() { return false; }, IsEnemy: function() { return true; }, @@ -243,6 +236,7 @@ AddMock(unit + i, IID_UnitMotion, { GetWalkSpeed: function() { return 1; }, MoveToFormationOffset: function(target, x, z) { }, + IsInTargetRange: function(target, min, max) { return true; }, MoveToTargetRange: function(target, min, max) { }, StopMoving: function() { }, GetPassabilityClassName: function() { return "default"; }, @@ -286,8 +280,7 @@ }); AddMock(controller, IID_UnitMotion, { - GetWalkSpeed: function() { return 1; }, - SetSpeedRatio: function(speed) { }, + SetSpeed: function(speed) { }, MoveToPointRange: function(x, z, minRange, maxRange) { }, IsInTargetRange: function(target, min, max) { return true; }, StopMoving: function() { }, Index: binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js +++ binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js @@ -35,7 +35,6 @@ TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetPassabilityClassName(), "unrestricted"); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetPassabilityClass(), 1 << 8); AddMock(entity, IID_Position, { "IsInWorld": () => true, @@ -78,8 +77,8 @@ TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetSpeedRatio(), 0); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.MoveToTargetRange(target, 0, 10), true); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.MoveToPointRange(100, 200, 0, 20), true); +TS_ASSERT_EQUALS(cmpUnitMotionFlying.SetNewDestinationAsEntity(target, 10), true); +TS_ASSERT_EQUALS(cmpUnitMotionFlying.SetNewDestinationAsPosition(100, 200, 20), true); // Take Off cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); @@ -111,7 +110,7 @@ TS_ASSERT_EQUALS(height, 105); // Land -cmpUnitMotionFlying.StopMoving(); +cmpUnitMotionFlying.DiscardMove(); cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1); TS_ASSERT_EQUALS(height, 105); Index: binaries/data/mods/public/simulation/templates/template_unit.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit.xml +++ binaries/data/mods/public/simulation/templates/template_unit.xml @@ -69,7 +69,7 @@ - 2.0 + 1.0 1.0 1 Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml @@ -56,7 +56,7 @@ 150 - 2.0 + 1.6 1.0 5 Index: binaries/data/mods/public/simulation/templates/template_unit_infantry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry.xml @@ -75,7 +75,6 @@ 100 - 2.0 1.0 0.5 Index: binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml +++ binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml @@ -50,7 +50,6 @@ - 2.0 1.0 1 Index: source/simulation2/MessageTypes.h =================================================================== --- source/simulation2/MessageTypes.h +++ source/simulation2/MessageTypes.h @@ -317,21 +317,31 @@ }; /** - * Sent by CCmpUnitMotion during Update, whenever the motion status has changed - * since the previous update. + * Sent by CCmpUnitMotion as a hint whenever the unit stops moving + * Thus it will be sent if the unit's path is blocked, or if it's reached its destination. */ -class CMessageMotionChanged : public CMessage +class CMessageMovePaused : public CMessage { public: - DEFAULT_MESSAGE_IMPL(MotionChanged) + DEFAULT_MESSAGE_IMPL(MovePaused) - CMessageMotionChanged(bool starting, bool error) : - starting(starting), error(error) + CMessageMovePaused() { } +}; + +/** + * Sent by CCmpUnitMotion when a unit has determined it has no chance + * of ever reaching its assigned destination. This is a catastrophic error. + */ +class CMessageMoveFailure : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(MoveFailure) - bool starting; // whether this is a start or end of movement - bool error; // whether we failed to start moving (couldn't find any path) + CMessageMoveFailure() + { + } }; /** Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -45,7 +45,8 @@ MESSAGE(PositionChanged) MESSAGE(InterpolatedPositionChanged) MESSAGE(TerritoryPositionChanged) -MESSAGE(MotionChanged) +MESSAGE(MovePaused) +MESSAGE(MoveFailure) MESSAGE(RangeUpdate) MESSAGE(TerrainChanged) MESSAGE(VisibilityChanged) @@ -187,6 +188,9 @@ COMPONENT(UnitMotion) // must be after Obstruction COMPONENT(UnitMotionScripted) +INTERFACE(UnitMotionManager) +COMPONENT(UnitMotionManager) + INTERFACE(UnitRenderer) COMPONENT(UnitRenderer) Index: source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion.cpp +++ source/simulation2/components/CCmpUnitMotion.cpp @@ -27,6 +27,8 @@ #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpValueModificationManager.h" +#include "simulation2/components/ICmpVisual.h" +#include "simulation2/components/ICmpUnitMotionManager.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Render.h" #include "simulation2/MessageTypes.h" @@ -39,217 +41,214 @@ #include "ps/Profile.h" #include "renderer/Scene.h" -// For debugging; units will start going straight to the target -// instead of calling the pathfinder -#define DISABLE_PATHFINDER 0 - -/** - * When advancing along the long path, and picking a new waypoint to move - * towards, we'll pick one that's up to this far from the unit's current - * position (to minimise the effects of grid-constrained movement) - */ -static const entity_pos_t WAYPOINT_ADVANCE_MAX = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*8); - /** - * Min/Max range to restrict short path queries to. (Larger ranges are slower, - * smaller ranges might miss some legitimate routes around large obstacles.) + * Minimal / usual / maximal range for a short path query. + * If the desired range for a short path goes beyond Max range, it gets hotswapped into a long range first + * as otherwise we can get large lag spikes. */ static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2); -static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*9); - -/** - * Minimum distance to goal for a long path request - */ -static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); +static const entity_pos_t SHORT_PATH_NORMAL_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*7); +static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*16); /** - * When short-pathing, and the short-range pathfinder failed to return a path, - * Assume we are at destination if we are closer than this distance to the target - * And we have no target entity. - * This is somewhat arbitrary, but setting a too big distance means units might lose sight of their end goal too much; + * Below this distance to the goal, if we're getting obstructed, we will recreate a brand new Goal for our short-pathfinder + * Instead of using the one given to us by RecomputeGoalPosition. + * This is unsafe from a shot/long pathfinder compatibility POV, so it should not be too big. */ -static const entity_pos_t SHORT_PATH_GOAL_RADIUS = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2); +static const entity_pos_t SHORT_PATH_GOAL_REDUX_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3); /** - * If we are this close to our target entity/point, then think about heading - * for it in a straight line instead of pathfinding. + * When we run in an obstruction and request a new short path, we'll drop waypoints that are + * close to our position, as defined by this constant. + * This is to avoid paths getting increasingly large and tangled and weird when units get repeatedly stuck. + * This would not be as necessary if we had pushing and could differentiate between unit/unit and unit/static collisions + * and oculd also be mitigated by checking (somehow) our path has grown larger than initially anticipated. + * This should probably be lower or at least cose to SHORT_PATH_GOAL_REDUX_DIST */ -static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); +static const entity_pos_t DISTANCE_FOR_WAYPOINT_DROP_WHEN_OBSTRUCTED = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3); -/** - * If we're following a target entity, - * we will recompute our path if the target has moved - * more than this distance from where we last pathed to. +/* + * How far ahead we'll check for collisions, to try and anticipate problems. + * This should be lowered whenever MP turns are lowered. */ -static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); +static const entity_pos_t LOOKAHEAD_DISTANCE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3); /** - * If we're following as part of a formation, - * but can't move to our assigned target point in a straight line, - * we will recompute our path if the target has moved - * more than this distance from where we last pathed to. + * Minimum distance to goal for a long path request + * Disabled, see note in RequestNewPath. */ -static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA_FORMATION = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1); +static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*0); /** - * If we're following something but it's more than this distance away along - * our path, then don't bother trying to repath regardless of how much it has - * moved, until we get this close to the end of our old path. + * If we are this close to our target entity/point, then think about heading + * for it in a straight line instead of pathfinding. + * TODO: this should probably be reintroduced */ -static const entity_pos_t CHECK_TARGET_MOVEMENT_AT_MAX_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*16); +static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); /** - * If we're following something and the angle between the (straight-line) directions to its previous target - * position and its present target position is greater than a given angle, recompute the path even far away - * (i.e. even if CHECK_TARGET_MOVEMENT_AT_MAX_DIST condition is not fulfilled). The actual check is done - * on the cosine of this angle, with a PI/6 angle. + * See unitmotion logic for details. Higher means units will retry more often before potentially failing. */ -static const fixed CHECK_TARGET_MOVEMENT_MIN_COS = fixed::FromInt(866)/1000; +static const size_t MAX_PATH_REATTEMPTS = 8; -static const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1); -static const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1); +static const CColor OVERLAY_COLOR_PATH(1, 0, 0, 1); +static const CColor OVERLAY_COLOR_ALTERNATIVE_PATH(0,1,0,1); class CCmpUnitMotion : public ICmpUnitMotion { -public: - static void ClassInit(CComponentManager& componentManager) +private: + struct SMotionGoal { - componentManager.SubscribeToMessageType(MT_Update_MotionFormation); - componentManager.SubscribeToMessageType(MT_Update_MotionUnit); - componentManager.SubscribeToMessageType(MT_PathResult); - componentManager.SubscribeToMessageType(MT_OwnershipChanged); - componentManager.SubscribeToMessageType(MT_ValueModification); - componentManager.SubscribeToMessageType(MT_Deserialized); - } + private: + bool m_Valid = false; - DEFAULT_COMPONENT_ALLOCATOR(UnitMotion) + entity_pos_t m_Range; + entity_id_t m_Entity; + CFixedVector2D m_Position; - bool m_DebugOverlayEnabled; - std::vector m_DebugOverlayLongPathLines; - std::vector m_DebugOverlayShortPathLines; + public: + SMotionGoal() : m_Valid(false) {}; - // Template state: + SMotionGoal(CFixedVector2D position, entity_pos_t range) + { + m_Entity = INVALID_ENTITY; + m_Range = range; + m_Position = position; - bool m_FormationController; + m_Valid = true; + } - fixed m_TemplateWalkSpeed, m_TemplateRunSpeedMultiplier; - pass_class_t m_PassClass; - std::string m_PassClassName; + SMotionGoal(entity_id_t target, entity_pos_t range) + { + m_Entity = target; + m_Range = range; + m_Position = CFixedVector2D(fixed::Zero(), fixed::Zero()); - // Dynamic state: + m_Valid = true; + } - entity_pos_t m_Clearance; + // For formations only + SMotionGoal(entity_id_t target, entity_pos_t x, entity_pos_t z) + { + m_Entity = target; + m_Range = fixed::Zero(); - // cached for efficiency - fixed m_WalkSpeed, m_RunSpeedMultiplier; + m_Position = CFixedVector2D(x,z); - bool m_Moving; - bool m_FacePointAfterMove; + m_Valid = true; + } - enum State - { - /* - * Not moving at all. - */ - STATE_IDLE, + template + void SerializeCommon(S& serialize) + { + serialize.Bool("valid", m_Valid); - /* - * Not moving at all. Will go to IDLE next turn. - * (This one-turn delay is a hack to fix animation timings.) - */ - STATE_STOPPING, + serialize.NumberFixed_Unbounded("range", m_Range); - /* - * Member of a formation. - * Pathing to the target (depending on m_PathState). - * Target is m_TargetEntity plus m_TargetOffset. - */ - STATE_FORMATIONMEMBER_PATH, + serialize.NumberU32_Unbounded("entity", m_Entity); - /* - * Individual unit or formation controller. - * Pathing to the target (depending on m_PathState). - * Target is m_TargetPos, m_TargetMinRange, m_TargetMaxRange; - * if m_TargetEntity is not INVALID_ENTITY then m_TargetPos is tracking it. - */ - STATE_INDIVIDUAL_PATH, + serialize.NumberFixed_Unbounded("x", m_Position.X); + serialize.NumberFixed_Unbounded("y", m_Position.Y); + } - STATE_MAX - }; - u8 m_State; + bool IsEntity() const { return m_Entity != INVALID_ENTITY; } + entity_id_t GetEntity() const { return m_Entity; } - enum PathState - { - /* - * There is no path. - * (This should only happen in IDLE and STOPPING.) - */ - PATHSTATE_NONE, + bool Valid() const { return m_Valid; } + void Clear() { m_Valid = false; } - /* - * We have an outstanding long path request. - * No paths are usable yet, so we can't move anywhere. - */ - PATHSTATE_WAITING_REQUESTING_LONG, + bool IsFormationGoal() const { return m_Valid && IsEntity() && !m_Position.IsZero(); } + entity_pos_t Range() const { return m_Range; }; - /* - * We have an outstanding short path request. - * m_LongPath is valid. - * m_ShortPath is not yet valid, so we can't move anywhere. - */ - PATHSTATE_WAITING_REQUESTING_SHORT, + CFixedVector2D GetPosition() const { return m_Position; } + }; - /* - * We are following our path, and have no path requests. - * m_LongPath and m_ShortPath are valid. - */ - PATHSTATE_FOLLOWING, +public: + static void ClassInit(CComponentManager& componentManager) + { + componentManager.SubscribeToMessageType(MT_PathResult); + componentManager.SubscribeToMessageType(MT_OwnershipChanged); + componentManager.SubscribeToMessageType(MT_ValueModification); + componentManager.SubscribeToMessageType(MT_Deserialized); + } - /* - * We are following our path, and have an outstanding long path request. - * (This is because our target moved a long way and we need to recompute - * the whole path). - * m_LongPath and m_ShortPath are valid. - */ - PATHSTATE_FOLLOWING_REQUESTING_LONG, + DEFAULT_COMPONENT_ALLOCATOR(UnitMotion) - /* - * We are following our path, and have an outstanding short path request. - * (This is because our target moved and we've got a new long path - * which we need to follow). - * m_LongPath is valid; m_ShortPath is valid but obsolete. - */ - PATHSTATE_FOLLOWING_REQUESTING_SHORT, + bool m_DebugOverlayEnabled; + std::vector m_DebugOverlayPathLines,m_DebugOverlayPathLines1; - PATHSTATE_MAX - }; - u8 m_PathState; + // Template state, never changed after init. + fixed m_TemplateWalkSpeed, m_TemplateRunSpeedMultiplier; + pass_class_t m_PassClass; + std::string m_PassClassName; + entity_pos_t m_Clearance; + bool m_FormationController; - u32 m_ExpectedPathTicket; // asynchronous request ID we're waiting for, or 0 if none + // cached for efficiency + fixed m_WalkSpeed, m_RunSpeedMultiplier; - entity_id_t m_TargetEntity; - CFixedVector2D m_TargetPos; - CFixedVector2D m_TargetOffset; - entity_pos_t m_TargetMinRange; - entity_pos_t m_TargetMaxRange; + // TARGET + // As long as we have a valid destination, the unit is seen as trying to move + // It may not be actually moving for a variety of reasons (no path, blocked path)… + // this is our final destination + SMotionGoal m_Destination; + // this is a "temporary" destination. Most of the time it will be the same as m_Destination, + // but it doesn't have to be. + // Can be used to temporarily re-route or pause a unit from a given component, for whatever reason. + // Should also probably be used whenever we implement step-by-step long paths using the hierarchical pathfinder. + SMotionGoal m_CurrentGoal; + + // Pathfinder-compliant goal. Should be reachable (at least when it's recomputed). + PathGoal m_Goal; + + // MOTION PLANNING + // We will abort if we are stuck after X tries, where X is the value in m_AbortIfStuck + u8 m_AbortIfStuck; + // actual unit speed, after technology and ratio - // Actual unit speed, after technology and ratio fixed m_Speed; // Convenience variable to avoid recomputing the ratio every time. Synchronised. fixed m_SpeedRatio; - // Current mean speed (over the last turn). - fixed m_CurSpeed; - - // Currently active paths (storing waypoints in reverse order). - // The last item in each path is the point we're currently heading towards. - WaypointPath m_LongPath; - WaypointPath m_ShortPath; - - // Motion planning - u8 m_Tries; // how many tries we've done to get to our current Final Goal. - - PathGoal m_FinalGoal; + // Metadata about our path requests. + struct SPathRequest + { + // 0 if none, otherwise the ID of the path request to drop outdated requests should that happen. + u32 expectedPathTicket; + + // If true, then the existing path will be scrapped. + bool dumpExistingPath; + // if true, some sanity checks will be performed on the returned path + bool runShortPathValidation; + // if true, this new path will not be added to the current path but instead to the alternative path, which may or may not get used. + bool isAlternativePath; + }; + SPathRequest m_PathRequest; + + // Currently active path (storing waypoints in reverse order). + WaypointPath m_Path; + // Alternative path. If the alternative path exists (ie not empty) and we run into an obstruction + // along the current path, we will in the same turn try the alternative path. If that fails too, we go through + // the normal "path-fixing" methods. If it succeeds, however, we swap the current path and the alternative path. + // this allows one-turn anticipation of obstacles without breaking everything and with as little downtime as possible. + // The alternative path is currently kept alive for one turn only. + WaypointPath m_AlternativePath; + + // used for the short pathfinder, incremented on each unsuccessful try. + u8 m_Tries; + // Turns to wait before a certain action. Works in concert with m_AbortIfStuck. + u8 m_WaitingTurns; + // if we actually started moving at some point. + bool m_StartedMoving; + + // Speed over the last turn + // cached so we can tell the visual actor when it changes + fixed m_ActualSpeed; + + // Movement metadata. + // Valid after TryMoving until the end of the turn. + // Would be external but it's costly to move it around. + MovementMetadata m_MoveMetadata; static std::string GetSchema() { @@ -277,14 +276,11 @@ virtual void Init(const CParamNode& paramNode) { - m_FormationController = paramNode.GetChild("FormationController").ToBool(); - - m_Moving = false; - m_FacePointAfterMove = true; - m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); + m_ActualSpeed = fixed::Zero(); + m_SpeedRatio = fixed::FromInt(1); - m_CurSpeed = fixed::Zero(); + m_Speed = fixed::Zero(); m_RunSpeedMultiplier = m_TemplateRunSpeedMultiplier = fixed::FromInt(1); if (paramNode.GetChild("RunMultiplier").IsOk()) @@ -302,55 +298,59 @@ cmpObstruction->SetUnitClearance(m_Clearance); } - m_State = STATE_IDLE; - m_PathState = PATHSTATE_NONE; - - m_ExpectedPathTicket = 0; + m_PathRequest.expectedPathTicket = 0; + m_PathRequest.dumpExistingPath = false; + m_PathRequest.runShortPathValidation = false; + m_PathRequest.isAlternativePath = false; m_Tries = 0; + m_WaitingTurns = 0; + + m_DebugOverlayEnabled = false; + m_AbortIfStuck = 0; - m_TargetEntity = INVALID_ENTITY; + CmpPtr cmpUnitMotionManager(GetSystemEntity()); - m_FinalGoal.type = PathGoal::POINT; + m_FormationController = paramNode.GetChild("FormationController").ToBool(); - m_DebugOverlayEnabled = false; + if (m_FormationController) + GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_Update_MotionFormation, this, true); + if (cmpUnitMotionManager && !paramNode.GetChild("FormationController").ToBool()) + cmpUnitMotionManager->RegisterUnit(GetEntityId()); } virtual void Deinit() { + CmpPtr cmpUnitMotionManager(GetSystemEntity()); + + if (cmpUnitMotionManager) + cmpUnitMotionManager->UnregisterUnit(GetEntityId()); } template void SerializeCommon(S& serialize) { - serialize.NumberU8("state", m_State, 0, STATE_MAX-1); - serialize.NumberU8("path state", m_PathState, 0, PATHSTATE_MAX-1); - - serialize.StringASCII("pass class", m_PassClassName, 0, 64); - - serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket); - - serialize.NumberU32_Unbounded("target entity", m_TargetEntity); - serialize.NumberFixed_Unbounded("target pos x", m_TargetPos.X); - serialize.NumberFixed_Unbounded("target pos y", m_TargetPos.Y); - serialize.NumberFixed_Unbounded("target offset x", m_TargetOffset.X); - serialize.NumberFixed_Unbounded("target offset y", m_TargetOffset.Y); - serialize.NumberFixed_Unbounded("target min range", m_TargetMinRange); - serialize.NumberFixed_Unbounded("target max range", m_TargetMaxRange); - + serialize.NumberU8("abort if stuck", m_AbortIfStuck, 0, 255); serialize.NumberFixed_Unbounded("speed ratio", m_SpeedRatio); - serialize.NumberFixed_Unbounded("current speed", m_CurSpeed); + serialize.NumberU32_Unbounded("path ticket", m_PathRequest.expectedPathTicket); + serialize.Bool("dump path", m_PathRequest.dumpExistingPath); + serialize.Bool("short path validation", m_PathRequest.runShortPathValidation); + serialize.Bool("alternative path", m_PathRequest.isAlternativePath); - serialize.Bool("moving", m_Moving); - serialize.Bool("facePointAfterMove", m_FacePointAfterMove); + SerializeVector()(serialize, "path", m_Path.m_Waypoints); serialize.NumberU8("tries", m_Tries, 0, 255); + serialize.NumberU8("waiting turns", m_WaitingTurns, 0, 255); - SerializeVector()(serialize, "long path", m_LongPath.m_Waypoints); - SerializeVector()(serialize, "short path", m_ShortPath.m_Waypoints); + serialize.Bool("started moving", m_StartedMoving); - SerializeGoal()(serialize, "goal", m_FinalGoal); + // strictly speaking this doesn't need to be serialized since it's graphics-only, but it's nicer to. + serialize.NumberFixed_Unbounded("actual speed", m_ActualSpeed); + + m_Destination.SerializeCommon(serialize); + m_CurrentGoal.SerializeCommon(serialize); + SerializeGoal()(serialize, "goal", m_Goal); } virtual void Serialize(ISerializer& serialize) @@ -369,26 +369,19 @@ m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName); } + // TODO: would be nice to listen to entity renamed messages, but those have no C++ interface so far. virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Update_MotionFormation: { - if (m_FormationController) - { - fixed dt = static_cast (msg).turnLength; - Move(dt); - } - break; - } - case MT_Update_MotionUnit: - { - if (!m_FormationController) - { - fixed dt = static_cast (msg).turnLength; - Move(dt); - } + fixed dt = static_cast (msg).turnLength; + ValidateCurrentPath(); + TryMoving(dt); + PerformActualMovement(dt); + HandleMoveFailures(); + AnticipatePathingNeeds(); break; } case MT_RenderSubmit: @@ -411,8 +404,8 @@ break; FALLTHROUGH; } - case MT_OwnershipChanged: case MT_Deserialized: + case MT_OwnershipChanged: { CmpPtr cmpValueModificationManager(GetSystemEntity()); if (!cmpValueModificationManager) @@ -436,9 +429,14 @@ GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender); } - virtual bool IsMoving() const + virtual bool IsActuallyMoving() + { + return m_StartedMoving; + } + + virtual bool IsTryingToMove() { - return m_Moving; + return m_Destination.Valid(); } virtual fixed GetSpeedRatio() const @@ -467,6 +465,18 @@ return m_WalkSpeed; } + // convenience wrapper + void SetActualSpeed(fixed newRealSpeed) + { + if (m_ActualSpeed == newRealSpeed) + return; + + m_ActualSpeed = newRealSpeed; + CmpPtr cmpVisualActor(GetEntityHandle()); + if (cmpVisualActor) + cmpVisualActor->SetMovingSpeed(m_ActualSpeed); + } + virtual pass_class_t GetPassabilityClass() const { return m_PassClass; @@ -477,7 +487,7 @@ return m_PassClassName; } - virtual void SetPassabilityClassName(const std::string& passClassName) + virtual void SetPassabilityClassName(std::string passClassName) { m_PassClassName = passClassName; CmpPtr cmpPathfinder(GetSystemEntity()); @@ -485,170 +495,165 @@ m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName); } - virtual fixed GetCurrentSpeed() const + virtual void SetDebugOverlay(bool enabled) { - return m_CurSpeed; + m_DebugOverlayEnabled = enabled; + UpdateMessageSubscriptions(); } - virtual void SetFacePointAfterMove(bool facePointAfterMove) + virtual entity_pos_t GetUnitClearance() const { - m_FacePointAfterMove = facePointAfterMove; + return m_Clearance; } - virtual void SetDebugOverlay(bool enabled) - { - m_DebugOverlayEnabled = enabled; - UpdateMessageSubscriptions(); - } + virtual bool SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool evenUnreachable); + virtual bool SetNewDestinationAsEntity(entity_id_t target, entity_pos_t range, bool evenUnreachable); - virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange); - virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z); - virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z); + virtual bool TemporaryRerouteToPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range); + virtual bool GoBackToOriginalDestination(); - virtual void StopMoving() - { - m_Moving = false; - m_ExpectedPathTicket = 0; - m_State = STATE_STOPPING; - m_PathState = PATHSTATE_NONE; - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); - } + // transform a motion goal into a corresponding PathGoal + // called by RecomputeGoalPosition + PathGoal CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal); - virtual entity_pos_t GetUnitClearance() const - { - return m_Clearance; - } + // take an arbitrary path goal and convert it to a 2D point goal, assign it to m_Goal. + bool RecomputeGoalPosition(PathGoal& goal); -private: - bool ShouldAvoidMovingUnits() const - { - return !m_FormationController; - } + virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z); + virtual void FaceTowardsEntity(entity_id_t ent); - bool IsFormationMember() const + virtual void SetAbortIfStuck(u8 shouldAbort) { - return m_State == STATE_FORMATIONMEMBER_PATH; + m_AbortIfStuck = shouldAbort; } - entity_id_t GetGroup() const + void StartMoving() { - return IsFormationMember() ? m_TargetEntity : GetEntityId(); - } + m_StartedMoving = true; - bool HasValidPath() const - { - return m_PathState == PATHSTATE_FOLLOWING - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT; + CmpPtr cmpObstruction(GetEntityHandle()); + if (cmpObstruction) + cmpObstruction->SetMovingFlag(true); } - void StartFailed() + void StopMoving() { - StopMoving(); - m_State = STATE_IDLE; // don't go through the STOPPING state since we never even started + m_StartedMoving = false; + + SetActualSpeed(fixed::Zero()); CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->SetMovingFlag(false); + } - CMessageMotionChanged msg(true, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + void ResetPathfinding() + { + m_Tries = 0; + m_WaitingTurns = 0; + + m_PathRequest.expectedPathTicket = 0; + m_Path.m_Waypoints.clear(); } - void MoveFailed() + virtual void DiscardMove() { StopMoving(); - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + ResetPathfinding(); - CMessageMotionChanged msg(false, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + m_CurrentGoal.Clear(); + m_Destination.Clear(); } - void StartSucceeded() + void MoveWillFail() { - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); - - m_Moving = true; - - CMessageMotionChanged msg(true, false); + CMessageMoveFailure msg; GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } - void MoveSucceeded() + void MoveHasPaused() { - m_Moving = false; + CMessageMovePaused msg; + GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + } - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + virtual bool HasValidPath() + { + return !m_Path.m_Waypoints.empty(); + } - // No longer moving, so speed is 0. - m_CurSpeed = fixed::Zero(); + virtual CFixedVector2D GetReachableGoalPosition() + { + if (!IsTryingToMove()) + return CFixedVector2D(fixed::Zero(),fixed::Zero()); - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + return CFixedVector2D(m_Goal.x, m_Goal.z); } - bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange, entity_id_t target); + virtual void ValidateCurrentPath(); - /** - * Handle the result of an asynchronous path query. - */ - void PathResult(u32 ticket, const WaypointPath& path); + virtual void TryMoving(fixed dt); - /** - * Do the per-turn movement and other updates. - */ - void Move(fixed dt); + virtual void PerformActualMovement(fixed dt); - /** - * Decide whether to approximate the given range from a square target as a circle, - * rather than as a square. - */ - bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const; + virtual void HandleMoveFailures(); - /** - * Computes the current location of our target entity (plus offset). - * Returns false if no target entity or no valid position. - */ - bool ComputeTargetPosition(CFixedVector2D& out) const; + virtual void AnticipatePathingNeeds(); - /** - * Attempts to replace the current path with a straight line to the goal, - * if this goal is a point, is close enough and the route is not obstructed. - */ - bool TryGoingStraightToGoalPoint(const CFixedVector2D& from); +private: + CFixedVector2D GetGoalPosition(const SMotionGoal& goal) const + { + ENSURE (goal.Valid()); - /** - * Attempts to replace the current path with a straight line to the target - * entity, if it's close enough and the route is not obstructed. - */ - bool TryGoingStraightToTargetEntity(const CFixedVector2D& from); + if (goal.IsEntity()) + { + CmpPtr cmpPosition(GetSimContext(), goal.GetEntity()); + ENSURE(cmpPosition); + ENSURE(cmpPosition->IsInWorld()); // TODO: do something? Like return garrisoned building or such? + return cmpPosition->GetPosition2D() + goal.GetPosition(); // use position as an offset for formations. + } + else + return goal.GetPosition(); + } + + bool CurrentGoalHasValidPosition() + { + if (!m_CurrentGoal.Valid()) + return false; + + if (m_CurrentGoal.IsEntity()) + { + CmpPtr cmpPosition(GetSimContext(), m_CurrentGoal.GetEntity()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return false; + return true; + } + else + return true; + } + + bool IsFormationMember() const; /** - * Returns whether the target entity has moved more than minDelta since our - * last path computations, and we're close enough to it to care. + * Handle the result of an asynchronous path query. */ - bool CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta); + void PathResult(u32 ticket, const WaypointPath& path); + void ActuallyValidateCurrentPath(); + /** - * Update goal position if moving target + * take a 2D position and return an updated one based on target position, taking velocity into account. */ - void UpdateFinalGoal(); + inline void UpdatePositionForTarget(entity_id_t ent, entity_pos_t& x, entity_pos_t& z, fixed& certainty); /** * Returns whether we are close enough to the target to assume it's a good enough * position to stop. */ - bool ShouldConsiderOurselvesAtDestination(const CFixedVector2D& from); + bool ShouldConsiderOurselvesAtDestination(SMotionGoal& goal); /** * Returns whether the length of the given path, plus the distance from @@ -663,17 +668,14 @@ /** * Returns an appropriate obstruction filter for use with path requests. - * noTarget is true only when used inside tryGoingStraightToTargetEntity, - * in which case we do not want the target obstruction otherwise it would always fail */ - ControlGroupMovementObstructionFilter GetObstructionFilter(bool noTarget = false) const; + ControlGroupMovementObstructionFilter GetObstructionFilter(bool evenMovingUnits = true) const; /** - * Start moving to the given goal, from our current position 'from'. - * Might go in a straight line immediately, or might start an asynchronous - * path request. + * Dumps current path and request a new one asynchronously. + * Inside of UnitMotion, do not set evenUnreachable to false unless you REALLY know what you're doing. */ - void BeginPathing(const CFixedVector2D& from, const PathGoal& goal); + bool RequestNewPath(bool evenUnreachable = true); /** * Start an asynchronous long path query. @@ -683,7 +685,12 @@ /** * Start an asynchronous short path query. */ - void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool avoidMovingUnits); + void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool avoidMovingUnits, bool alternativePath = false); + + /** + * Drop waypoints that are close to us. Used by Move() before calling RequestShortPath and for anticipating obstructions. + */ + void DropCloseWaypoints(const CFixedVector2D& position, std::vector& waypoints); /** * Convert a path into a renderable list of lines @@ -697,534 +704,501 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path) { - // reset our state for sanity. - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - m_Moving = false; - // Ignore obsolete path requests - if (ticket != m_ExpectedPathTicket) + if (ticket != m_PathRequest.expectedPathTicket) return; - m_ExpectedPathTicket = 0; // we don't expect to get this result again + m_PathRequest.expectedPathTicket = 0; // we don't expect to get this result again - // Check that we are still able to do something with that path - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) + if (m_PathRequest.dumpExistingPath) { - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG || m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT) - StartFailed(); - else if (m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT) - StopMoving(); - return; + if (m_PathRequest.isAlternativePath) + m_AlternativePath.m_Waypoints.clear(); + else + m_Path.m_Waypoints.clear(); + m_PathRequest.dumpExistingPath = false; } - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG) - { - m_LongPath = path; - - // If we are following a path, leave the old m_ShortPath so we can carry on following it - // until a new short path has been computed - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG) - m_ShortPath.m_Waypoints.clear(); + if (!m_Destination.Valid()) + return; - // 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 - // (via the short pathfinder), so if we're stuck and the user clicks - // close enough to the unit then we can probably get unstuck - if (m_LongPath.m_Waypoints.empty()) - m_LongPath.m_Waypoints.emplace_back(Waypoint{ m_FinalGoal.x, m_FinalGoal.z }); + if (path.m_Waypoints.empty()) + { + // no waypoints, path failed. + // if we have some room, pop waypoint + // TODO: this isn't particularly bright. + if (!m_Path.m_Waypoints.empty()) + m_Path.m_Waypoints.pop_back(); - if (!HasValidPath()) - StartSucceeded(); + // we will then deal with this on the next Move() call. + return; + } - m_PathState = PATHSTATE_FOLLOWING; + // if this is a short path, verify some things + // Namely reject any path that takes us in another global region + // and any waypoint that's not passable/next to a passable cell. + // this will ensure minimal changes of long/short rance pathfinder discrepancies + if (m_PathRequest.runShortPathValidation) + { + CmpPtr cmpPathfinder(GetSystemEntity()); + ENSURE (cmpPathfinder); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); + CmpPtr cmpPosition(GetEntityHandle()); + ENSURE(cmpPosition); - m_Moving = true; - } - else if (m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT) - { - m_ShortPath = path; + u16 i0, j0; + cmpPathfinder->FindNearestPassableNavcell(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y, i0, j0, m_PassClass); - // If there's no waypoints then we couldn't get near the target - if (m_ShortPath.m_Waypoints.empty()) + m_PathRequest.runShortPathValidation = false; + for (const Waypoint& wpt : path.m_Waypoints) { - // If we're globally following a long path, try to remove the next waypoint, it might be obstructed - // If not, and we are not in a formation, retry - // unless we are close to our target and we don't have a target entity. - // This makes sure that units don't clump too much when they are not in a formation and tasked to move. - if (m_LongPath.m_Waypoints.size() > 1) - m_LongPath.m_Waypoints.pop_back(); - else if (IsFormationMember()) + u16 i1, j1; + u32 dist = cmpPathfinder->FindNearestPassableNavcell(wpt.x, wpt.z, i1, j1, m_PassClass); + if (dist > 2 || !cmpPathfinder->NavcellIsReachable(i0, j0, i1, j1, m_PassClass)) { - m_Moving = false; - CMessageMotionChanged msg(true, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + MoveWillFail(); + // we will then deal with this on the next Move() call. return; } + } + } - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - - CFixedVector2D pos = cmpPosition->GetPosition2D(); + std::vector& targetPath = m_PathRequest.isAlternativePath ? m_AlternativePath.m_Waypoints : m_Path.m_Waypoints; - if (ShouldConsiderOurselvesAtDestination(pos)) - return; + if (m_PathRequest.isAlternativePath) + m_PathRequest.isAlternativePath = false; - UpdateFinalGoal(); - RequestLongPath(pos, m_FinalGoal); - m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; + // if we're currently moving, we have a path, so check if the first waypoint can be removed + // it's not impossible that we've actually reached it already. + if (IsActuallyMoving() && path.m_Waypoints.size() >= 2) + { + CmpPtr cmpPosition(GetEntityHandle()); + CFixedVector2D nextWp = CFixedVector2D(path.m_Waypoints.back().x,path.m_Waypoints.back().z) - cmpPosition->GetPosition2D(); + if (nextWp.CompareLength(GetSpeed()/2) <= 0) + { + targetPath.insert(targetPath.end(), path.m_Waypoints.begin(), path.m_Waypoints.end()-1); return; } + } + targetPath.insert(targetPath.end(), path.m_Waypoints.begin(), path.m_Waypoints.end()); +} - // else we could, so reset our number of tries. - m_Tries = 0; - - // Now we've got a short path that we can follow - if (!HasValidPath()) - StartSucceeded(); +/** + * So long as they are handled properly, it is OK to send messages here. + */ +void CCmpUnitMotion::ValidateCurrentPath() +{ + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) + return; - m_PathState = PATHSTATE_FOLLOWING; + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); + ActuallyValidateCurrentPath(); - m_Moving = true; - } - else - LOGWARNING("unexpected PathResult (%u %d %d)", GetEntityId(), m_State, m_PathState); + // validate our state following the messages potentially sent above. + if (!IsTryingToMove() || !cmpPosition || !cmpPosition->IsInWorld() || !CurrentGoalHasValidPosition()) + // One of the messages we sent UnitAI caused us to stop moving entirely. + // Tell the visual actor we're not moving this turn to avoid gliding. + SetActualSpeed(fixed::Zero()); } -void CCmpUnitMotion::Move(fixed dt) +void CCmpUnitMotion::ActuallyValidateCurrentPath() { - PROFILE("Move"); + // this should be kept in sync with RequestNewPath otherwise we'll spend our whole life repathing. - if (m_State == STATE_STOPPING) - { - m_State = STATE_IDLE; - MoveSucceeded(); - return; - } + CmpPtr cmpPosition(GetEntityHandle()); - if (m_State == STATE_IDLE) + m_MoveMetadata = MovementMetadata{cmpPosition->GetPosition2D(), cmpPosition->GetPosition2D(), true, false}; + + // don't validate points, they never change + if (!m_CurrentGoal.IsEntity()) return; - switch (m_PathState) - { - case PATHSTATE_NONE: + // TODO: figure out what to do when the goal dies. + // for now we'll keep on keeping on, but reset as if our goal was a position + // and send a failure message to UnitAI in case it wants to do something + // use position as a proxy for existence + CmpPtr cmpTargetPosition(GetSimContext(), m_CurrentGoal.GetEntity()); + if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) { - // If we're not pathing, do nothing + // TODO: this should call a custom function for this + SMotionGoal newGoal(CFixedVector2D(m_Goal.x, m_Goal.z), m_CurrentGoal.Range()); + m_Destination = newGoal; + m_CurrentGoal = newGoal; + RequestNewPath(); + MoveWillFail(); return; } - case PATHSTATE_WAITING_REQUESTING_LONG: - case PATHSTATE_WAITING_REQUESTING_SHORT: - { - // If we're waiting for a path and don't have one yet, do nothing + // don't validate if no path. + if (!HasValidPath()) return; - } - case PATHSTATE_FOLLOWING: - case PATHSTATE_FOLLOWING_REQUESTING_SHORT: - case PATHSTATE_FOLLOWING_REQUESTING_LONG: - { - // TODO: there's some asymmetry here when units look at other - // units' positions - the result will depend on the order of execution. - // Maybe we should split the updates into multiple phases to minimise - // that problem. + // if we have an alternative path, follow it, it's likely to be better. + if (!m_AlternativePath.m_Waypoints.empty()) + // switch both paths. Presumably the alternative one is newer and better, so don't switch back if that one fails too. + m_Path.m_Waypoints.swap(m_AlternativePath.m_Waypoints); - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return; + // clear alternative paths whatever happened. + m_AlternativePath.m_Waypoints.clear(); - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; + // TODO: check LOS here (instead of in UnitAI like we do now). - CFixedVector2D initialPos = cmpPosition->GetPosition2D(); + // if our goal can move, then perhaps it has. + CmpPtr cmpTargetUnitMotion(GetSimContext(), m_CurrentGoal.GetEntity()); + if (!cmpTargetUnitMotion) + return; - // If we're chasing a potentially-moving unit and are currently close - // enough to its current position, and we can head in a straight line - // to it, then throw away our current path and go straight to it - if (m_PathState == PATHSTATE_FOLLOWING) - TryGoingStraightToTargetEntity(initialPos); + // Check if our current Goal's position (ie m_Goal, not m_CurrentGoal) is sensible. - // Keep track of the current unit's position during the update - CFixedVector2D pos = initialPos; + // TODO: this will probably be called every turn if the entity tries to go to an unreachable unit + // In those cases, UnitAI should be warned that the unit is unreachable and tell us to do something else. - fixed basicSpeed = m_Speed; + CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); + fixed certainty = (m_Clearance + cmpTargetUnitMotion->GetUnitClearance()) * 3 / 2; + UpdatePositionForTarget(m_CurrentGoal.GetEntity(), targetPos.X, targetPos.Y, certainty); - // Find the speed factor of the underlying terrain - // (We only care about the tile we start on - it doesn't matter if we're moving - // partially onto a much slower/faster tile) - // TODO: Terrain-dependent speeds are not currently supported - fixed terrainSpeed = fixed::FromInt(1); + CmpPtr cmpObstructionManager(GetSystemEntity()); + if (!cmpObstructionManager->IsPointInPointRange(m_Goal.x, m_Goal.z, targetPos.X, targetPos.Y, m_CurrentGoal.Range() - certainty, m_CurrentGoal.Range() + certainty)) + RequestNewPath(); +} - fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); +void CCmpUnitMotion::UpdatePositionForTarget(entity_id_t ent, entity_pos_t& x, entity_pos_t& z, fixed& certainty) +{ + CmpPtr cmpTargetPosition(GetSimContext(), ent); + CmpPtr cmpTargetUnitMotion(GetSimContext(), ent); + if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld() || !cmpTargetUnitMotion) + return; - bool wasObstructed = false; - // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint + if (!cmpTargetUnitMotion->IsActuallyMoving() || IsFormationMember()) + { + // go directly towards the unit. + x = cmpTargetPosition->GetPosition2D().X; + z = cmpTargetPosition->GetPosition2D().Y; + certainty = fixed::Zero(); + return; + } - fixed timeLeft = dt; - fixed zero = fixed::Zero(); + // So here we'll try to estimate where the unit will be by the time we reach it. + // This can be done perfectly but I cannot think of a non-iterative process and this seems complicated for our purposes here + // so just get our direct distance and do some clever things, we'll correct later on anyhow so it doesn't matter. + CmpPtr cmpPosition(GetEntityHandle()); - while (timeLeft > zero) - { - // If we ran out of path, we have to stop - if (m_ShortPath.m_Waypoints.empty() && m_LongPath.m_Waypoints.empty()) - break; + // try to estimate in how much time we'll reach it. + fixed distance = (cmpTargetPosition->GetPosition2D() - cmpPosition->GetPosition2D()).Length(); - CFixedVector2D target; - if (m_ShortPath.m_Waypoints.empty()) - target = CFixedVector2D(m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z); - else - target = CFixedVector2D(m_ShortPath.m_Waypoints.back().x, m_ShortPath.m_Waypoints.back().z); + if (GetSpeed() < fixed::Epsilon()) + return; - CFixedVector2D offset = target - pos; + fixed time = std::min(distance / GetSpeed(), fixed::FromInt(5)); // don't try from too far away or this is just dumb. - // Work out how far we can travel in timeLeft - fixed maxdist = maxSpeed.Multiply(timeLeft); + CFixedVector2D travelVector = (cmpTargetPosition->GetPosition2D() - cmpTargetPosition->GetPreviousPosition2D()).Multiply(time) * 2; + x += travelVector.X; + z += travelVector.Y; - // If the target is close, we can move there directly - fixed offsetLength = offset.Length(); - if (offsetLength <= maxdist) - { - if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) - { - pos = target; + certainty += time * 2; +} - // Spend the rest of the time heading towards the next waypoint - timeLeft = timeLeft - (offsetLength / maxSpeed); +/** + * Sending messages in this function is _very_ forbidden + */ +void CCmpUnitMotion::TryMoving(fixed dt) +{ + PROFILE("TryMoving"); - if (m_ShortPath.m_Waypoints.empty()) - m_LongPath.m_Waypoints.pop_back(); - else - m_ShortPath.m_Waypoints.pop_back(); + if (!IsTryingToMove()) + return; - continue; - } - else - { - // Error - path was obstructed - wasObstructed = true; - break; - } - } - else - { - // Not close enough, so just move in the right direction - offset.Normalize(maxdist); - target = pos + offset; + CmpPtr cmpPathfinder(GetSystemEntity()); - if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) - pos = target; - else - wasObstructed = true; // Error - path was obstructed + // We want to move (at most) m_Speed*dt units from pos towards the next waypoint + fixed timeLeft = dt; - break; - } - } + // TODO: I think this may be a little buggy if we want to compute it several times per turn. + while (timeLeft > fixed::Zero()) + { + // If we ran out of path, we have to stop there (move is still OK though). + if (!HasValidPath()) + break; - // Update the Position component after our movement (if we actually moved anywhere) - if (pos != initialPos) - { - CFixedVector2D offset = pos - initialPos; + CFixedVector2D target; + target = CFixedVector2D(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z); - // Face towards the target - entity_angle_t angle = atan2_approx(offset.X, offset.Y); - cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle); + CFixedVector2D offset = target - m_MoveMetadata.currentPos; + fixed offsetLength = offset.Length(); + // Work out how far we can travel in timeLeft + fixed maxdist = m_Speed.Multiply(timeLeft); - // Calculate the mean speed over this past turn. - m_CurSpeed = cmpPosition->GetDistanceTravelled() / dt; + CFixedVector2D destination; + if (offsetLength <= maxdist) + destination = target; + else + { + offset.Normalize(maxdist); + destination = m_MoveMetadata.currentPos + offset; } - if (wasObstructed) + // TODO: try moving as much as we can still? + // TODO: get more information about what blocked us. + if (cmpPathfinder->CheckMovement(GetObstructionFilter(), m_MoveMetadata.currentPos.X, m_MoveMetadata.currentPos.Y, destination.X, destination.Y, m_Clearance, m_PassClass)) { - // Oops, we hit something (very likely another unit). - // This is when we might easily get stuck wrongly. + m_MoveMetadata.currentPos = destination; - // check if we've arrived. - if (ShouldConsiderOurselvesAtDestination(pos)) - return; + timeLeft = (timeLeft.Multiply(m_Speed) - offsetLength) / m_Speed; - // If we still have long waypoints, try and compute a short path - // This will get us around units, amongst others. - // However in some cases a long waypoint will be in located in the obstruction of - // an idle unit. In that case, we need to scrap that waypoint or we might never be able to reach it. - // I am not sure why this happens but the following code seems to work. - if (!m_LongPath.m_Waypoints.empty()) - { - CmpPtr cmpObstructionManager(GetSystemEntity()); - if (cmpObstructionManager) - { - // create a fake obstruction to represent our waypoint. - ICmpObstructionManager::ObstructionSquare square; - square.hh = m_Clearance; - square.hw = m_Clearance; - square.u = CFixedVector2D(entity_pos_t::FromInt(1),entity_pos_t::FromInt(0)); - square.v = CFixedVector2D(entity_pos_t::FromInt(0),entity_pos_t::FromInt(1)); - square.x = m_LongPath.m_Waypoints.back().x; - square.z = m_LongPath.m_Waypoints.back().z; - std::vector unitOnGoal; - // don't ignore moving units as those might be units like us, ie not really moving. - cmpObstructionManager->GetUnitsOnObstruction(square, unitOnGoal, GetObstructionFilter(), true); - if (!unitOnGoal.empty()) - m_LongPath.m_Waypoints.pop_back(); - } - if (!m_LongPath.m_Waypoints.empty()) - { - PathGoal goal; - if (m_LongPath.m_Waypoints.size() > 1 || m_FinalGoal.DistanceToPoint(pos) > LONG_PATH_MIN_DIST) - goal = { PathGoal::POINT, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z }; - else - { - UpdateFinalGoal(); - goal = m_FinalGoal; - m_LongPath.m_Waypoints.clear(); - CFixedVector2D target = goal.NearestPointOnGoal(pos); - m_LongPath.m_Waypoints.emplace_back(Waypoint{ target.X, target.Y }); - } - RequestShortPath(pos, goal, true); - m_PathState = PATHSTATE_WAITING_REQUESTING_SHORT; - return; - } - } - // Else, just entirely recompute - UpdateFinalGoal(); - BeginPathing(pos, m_FinalGoal); - - // potential TODO: We could switch the short-range pathfinder for something else entirely. + if (destination == target) + m_Path.m_Waypoints.pop_back(); + continue; + } + else + { + // Error - path was obstructed + m_MoveMetadata.wasObstructed = true; + m_MoveMetadata.moveWentOK = false; return; } + } - // We successfully moved along our path, until running out of - // waypoints or time. + m_Tries = 0; + m_WaitingTurns = 0; +} - if (m_PathState == PATHSTATE_FOLLOWING) - { - // If we're not currently computing any new paths: - if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty()) - { - if (IsFormationMember()) - { - // We've reached our assigned position. If the controller - // is idle, send a notification in case it should disband, - // otherwise continue following the formation next turn. - CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity); - if (cmpUnitMotion && !cmpUnitMotion->IsMoving()) - { - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - m_Moving = false; - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); - } - } - else - { - // check if target was reached in case of a moving target - CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity); - if (cmpUnitMotion && cmpUnitMotion->IsMoving() && - MoveToTargetRange(m_TargetEntity, m_TargetMinRange, m_TargetMaxRange)) - return; - - // Not in formation, so just finish moving - StopMoving(); - m_State = STATE_IDLE; - MoveSucceeded(); - - if (m_FacePointAfterMove) - FaceTowardsPointFromPos(pos, m_FinalGoal.x, m_FinalGoal.z); - // TODO: if the goal was a square building, we ought to point towards the - // nearest point on the square, not towards its center - } - } +void CCmpUnitMotion::PerformActualMovement(fixed dt) +{ + if (m_MoveMetadata.currentPos == m_MoveMetadata.initialPos) + { + // We didn't actually move, for whatever reason, despite wanting to. + // Dispatch to components that we no longer actually move. + if (m_StartedMoving) + StopMoving(); - // If we have a target entity, and we're not miles away from the end of - // our current path, and the target moved enough, then recompute our - // whole path - if (IsFormationMember()) - CheckTargetMovement(pos, CHECK_TARGET_MOVEMENT_MIN_DELTA_FORMATION); - else - CheckTargetMovement(pos, CHECK_TARGET_MOVEMENT_MIN_DELTA); - } - } + return; } + + CFixedVector2D offset = m_MoveMetadata.currentPos - m_MoveMetadata.initialPos; + + if (!m_StartedMoving) + // tell other components (visual actor,...) we are now moving. + StartMoving(); + + CmpPtr cmpPosition(GetEntityHandle()); + + // Move facing towards the target. + entity_angle_t angle = atan2_approx(offset.X, offset.Y); + cmpPosition->MoveAndTurnTo(m_MoveMetadata.currentPos.X, m_MoveMetadata.currentPos.Y, angle); + + // Calculate the mean speed over this past turn. + // TODO: this is often just a little different from our actual top speed + // so we end up changing the actual speed quite often, which is a little silly. + SetActualSpeed(cmpPosition->GetDistanceTravelled() / dt); } -bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out) const +/** + * So long as they are handled properly, it is OK to send messages here. + */ +void CCmpUnitMotion::HandleMoveFailures() { - if (m_TargetEntity == INVALID_ENTITY) - return false; + if (m_MoveMetadata.moveWentOK) + return; - CmpPtr cmpPosition(GetSimContext(), m_TargetEntity); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + CmpPtr cmpPathfinder(GetSystemEntity()); + CmpPtr cmpPosition(GetEntityHandle()); + + CFixedVector2D pos = cmpPosition->GetPosition2D(); - if (m_TargetOffset.IsZero()) + // We've stopped moving if we're here. + if (m_StartedMoving) { - // No offset, just return the position directly - out = cmpPosition->GetPosition2D(); + StopMoving(); + + MoveHasPaused(); + // Validate that the MoveHasPaused message hint has not caused us to be in an invalid state. + if (!IsTryingToMove() || !cmpPosition || !cmpPosition->IsInWorld() || !CurrentGoalHasValidPosition()) + return; } - else + + if (ShouldConsiderOurselvesAtDestination(m_CurrentGoal)) + // If we're out of path (ie not moving) but have a valid destination (IsTryingToMove()), we'll end up here every turn. + // We should not repath if we actually are where we want to be (ie at destination). + // That we do this means that the range check performed by whoever is calling us must be more permissive than this one though + // otherwise we'll never reach our target. + return; + + // Oops, we have a problem. Either we were obstructed, we plan to be obstructed soon, or we ran out of path (but still have a goal). + // Handle it. + // Failure to handle it will result in stuckness and players complaining. + + if (m_PathRequest.expectedPathTicket != 0) + // wait until we get our path to see where that leads us. + return; + + // TODO: it would be nice to react differently to different problems + // eg running into a static obstruction can be handled by the long-range pathfinder + // but running in a unit cannot. + if (m_WaitingTurns == 0) { - // There is an offset, so compute it relative to orientation - entity_angle_t angle = cmpPosition->GetRotation().Y; - CFixedVector2D offset = m_TargetOffset.Rotate(angle); - out = cmpPosition->GetPosition2D() + offset; + if (HasValidPath()) + m_WaitingTurns = MAX_PATH_REATTEMPTS; // You can add some "waiting" turns by bumping this up, but for now let's not do it. + else + m_WaitingTurns = 3; } - return true; -} -bool CCmpUnitMotion::TryGoingStraightToGoalPoint(const CFixedVector2D& from) -{ - // Make sure the goal is a point (and not a point-like target like a formation controller) - if (m_FinalGoal.type != PathGoal::POINT || m_TargetEntity != INVALID_ENTITY) - return false; + --m_WaitingTurns; - // Fail if the goal is too far away - CFixedVector2D goalPos(m_FinalGoal.x, m_FinalGoal.z); - if ((goalPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) - return false; + // MAX_PATH_REATTEMPTS and above: we wait. + if (m_WaitingTurns >= MAX_PATH_REATTEMPTS) + return; - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; + // If we're here we want to compute a short path. + if (m_WaitingTurns >= 3) + { + if (m_Path.m_Waypoints.empty()) + { + RequestNewPath(); + return; + } + /** + * Here there are two cases: + * 1) We are somewhat far away from the goal + * 2) We're really close to the goal. + * If it's (2) it's likely that we are running into units that are currently doing the same thing we want to do (gathering from the same tree…) + * Since the initial call to MakeGoalReachable gave us a specific 2D coordinate, and we can't reach it, + * We have a relatively high chance of never being able to reach that particular point. + * So we need to recreate the actual goal for this entity. This is a little dangerous in terms of short/long pathfinder compatibility + * So we'll run sanity checks on the output to try and not get stuck/go where we shouldn't. + */ + PathGoal goal; - // Check if there's any collisions on that route - if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) - return false; + CFixedVector2D endWptPos(m_Path.m_Waypoints.front().x, m_Path.m_Waypoints.front().z); + bool redraw = true; + if ((endWptPos - pos).CompareLength(SHORT_PATH_GOAL_REDUX_DIST) > 0) + { + DropCloseWaypoints(pos, m_Path.m_Waypoints); - // That route is okay, so update our path - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); + if (!m_Path.m_Waypoints.empty()) + { + redraw = false; + goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z }; + } + } + // sanity check on the right, but the code above should never allow that to happen. + if (redraw || m_Path.m_Waypoints.empty()) + { + goal = CreatePathGoalFromMotionGoal(m_CurrentGoal); + m_PathRequest.dumpExistingPath = true; + } - return true; -} + RequestShortPath(pos, goal, false); + return; + } -bool CCmpUnitMotion::TryGoingStraightToTargetEntity(const CFixedVector2D& from) -{ - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return false; + // Last resort, compute a long path + if (m_WaitingTurns == 2) + { + if (m_Path.m_Waypoints.empty()) + { + RequestNewPath(); + return; + } + PathGoal goal; + goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z }; + m_Path.m_Waypoints.pop_back(); - // Fail if the target is too far away - if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) - return false; + RequestLongPath(pos, goal); + return; + } - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; - // Move the goal to match the target entity's new position - PathGoal goal = m_FinalGoal; - goal.x = targetPos.X; - goal.z = targetPos.Y; - // (we ignore changes to the target's rotation, since only buildings are - // square and buildings don't move) + // m_waitingTurns == 1 here - // Find the point on the goal shape that we should head towards - CFixedVector2D goalPos = goal.NearestPointOnGoal(from); + // we tried getting a renewed path and still got stuck + if (m_AbortIfStuck == 0) + { + // TODO: inform the player maybe? + // TODO: this should be sent to UnitAI with a stronger "definitely failed" parameter. + MoveWillFail(); + // fall through, we don't want to discard the move on our own. + } - // Check if there's any collisions on that route - if (!cmpPathfinder->CheckMovement(GetObstructionFilter(true), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) - return false; + --m_AbortIfStuck; - // That route is okay, so update our path - m_FinalGoal = goal; - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); + // Recompute a new path, but wait a few turns first + m_WaitingTurns = 4 + MAX_PATH_REATTEMPTS; - return true; + return; } -bool CCmpUnitMotion::CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta) +void CCmpUnitMotion::AnticipatePathingNeeds() { - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return false; - - // Fail unless the target has moved enough - CFixedVector2D oldTargetPos(m_FinalGoal.x, m_FinalGoal.z); - if ((targetPos - oldTargetPos).CompareLength(minDelta) < 0) - return false; - + CmpPtr cmpPathfinder(GetSystemEntity()); CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + + // don't rely on the move metadata, our position may well have changed by now + // (we could have garrisoned…) CFixedVector2D pos = cmpPosition->GetPosition2D(); - CFixedVector2D oldDir = (oldTargetPos - pos); - CFixedVector2D newDir = (targetPos - pos); - oldDir.Normalize(); - newDir.Normalize(); - - // Fail unless we're close enough to the target to care about its movement - // and the angle between the (straight-line) directions of the previous and new target positions is small - if (oldDir.Dot(newDir) > CHECK_TARGET_MOVEMENT_MIN_COS && !PathIsShort(m_LongPath, from, CHECK_TARGET_MOVEMENT_AT_MAX_DIST)) - return false; - // Fail if the target is no longer visible to this entity's owner - // (in which case we'll continue moving to its last known location, - // unless it comes back into view before we reach that location) - CmpPtr cmpOwnership(GetEntityHandle()); - if (cmpOwnership) + bool pathWillBeObstructed = !m_MoveMetadata.wasObstructed && HasValidPath() && !m_Path.m_Waypoints.empty(); + if (!pathWillBeObstructed) + return; + + pathWillBeObstructed = false; + // check any waypoint closer to us than LOOKAHEAD_DISTANCE. + ssize_t index = m_Path.m_Waypoints.size() - 1; + CFixedVector2D checkPos = pos; + fixed distance = (CFixedVector2D(m_Path.m_Waypoints[index].x,m_Path.m_Waypoints[index].z) - checkPos).Length(); + while (index >= 0 && distance < LOOKAHEAD_DISTANCE) { - CmpPtr cmpRangeManager(GetSystemEntity()); - if (cmpRangeManager && cmpRangeManager->GetLosVisibility(m_TargetEntity, cmpOwnership->GetOwner()) == ICmpRangeManager::VIS_HIDDEN) - return false; + if (!cmpPathfinder->CheckMovement(GetObstructionFilter(true), + checkPos.X, checkPos.Y, m_Path.m_Waypoints[index].x, m_Path.m_Waypoints[index].z, + m_Clearance, m_PassClass)) + { + pathWillBeObstructed = true; + break; + } + CFixedVector2D wptPos = CFixedVector2D(m_Path.m_Waypoints[index].x,m_Path.m_Waypoints[index].z); + index--; + distance += (wptPos - checkPos).Length(); + checkPos = wptPos; } + if (!pathWillBeObstructed) + return; - // The target moved and we need to update our current path; - // change the goal here and expect our caller to start the path request - m_FinalGoal.x = targetPos.X; - m_FinalGoal.z = targetPos.Y; - RequestLongPath(from, m_FinalGoal); - m_PathState = PATHSTATE_FOLLOWING_REQUESTING_LONG; + std::vector alternativePath = m_Path.m_Waypoints; - return true; -} + DropCloseWaypoints(pos, alternativePath); + PathGoal goal; + if (!alternativePath.empty()) + goal = { PathGoal::POINT, alternativePath.back().x, alternativePath.back().z }; + else + { + goal = CreatePathGoalFromMotionGoal(m_CurrentGoal); + m_PathRequest.dumpExistingPath = true; + } + // request short path as alternative path. + RequestShortPath(pos, goal, true, true); -void CCmpUnitMotion::UpdateFinalGoal() -{ - if (m_TargetEntity == INVALID_ENTITY) - return; - CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity); - if (!cmpUnitMotion) - return; - if (IsFormationMember()) - return; - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return; - m_FinalGoal.x = targetPos.X; - m_FinalGoal.z = targetPos.Y; + // save waypoints kept so far. + m_AlternativePath.m_Waypoints = alternativePath; } -bool CCmpUnitMotion::ShouldConsiderOurselvesAtDestination(const CFixedVector2D& from) +// Only used to send a "hint" to unitAI. +bool CCmpUnitMotion::ShouldConsiderOurselvesAtDestination(SMotionGoal& goal) { - if (m_TargetEntity != INVALID_ENTITY || m_FinalGoal.DistanceToPoint(from) > SHORT_PATH_GOAL_RADIUS) - return false; + if (HasValidPath()) + return false; // wait until we're done. - StopMoving(); - MoveSucceeded(); + CmpPtr cmpObstructionManager(GetSystemEntity()); + if (!cmpObstructionManager) + return true; // what's a sane default here? - if (m_FacePointAfterMove) - FaceTowardsPointFromPos(from, m_FinalGoal.x, m_FinalGoal.z); - return true; + if (goal.IsEntity()) + return cmpObstructionManager->IsInTargetRange(GetEntityId(), goal.GetEntity(), goal.Range(), goal.Range(), true); + else + return cmpObstructionManager->IsInPointRange(GetEntityId(), goal.GetPosition().X, goal.GetPosition().Y, goal.Range(), goal.Range(), true); } bool CCmpUnitMotion::PathIsShort(const WaypointPath& path, const CFixedVector2D& from, entity_pos_t minDistance) const @@ -1274,385 +1248,356 @@ } } -ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter(bool noTarget) const -{ - entity_id_t group = noTarget ? m_TargetEntity : GetGroup(); - return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), group); -} - - - -void CCmpUnitMotion::BeginPathing(const CFixedVector2D& from, const PathGoal& goal) +void CCmpUnitMotion::FaceTowardsEntity(entity_id_t ent) { - // reset our state for sanity. - m_ExpectedPathTicket = 0; - - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - m_Moving = false; - - m_PathState = PATHSTATE_NONE; - -#if DISABLE_PATHFINDER - { - CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY); - CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from); - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); - m_PathState = PATHSTATE_FOLLOWING; + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) return; - } -#endif - // If we're aiming at a target entity and it's close and we can reach - // it in a straight line, then we'll just go along the straight line - // instead of computing a path. - if (TryGoingStraightToTargetEntity(from)) - { - if (!HasValidPath()) - StartSucceeded(); - m_PathState = PATHSTATE_FOLLOWING; + CmpPtr cmpTargetPosition(GetSimContext(), ent); + if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) return; - } - // Same thing applies to non-entity points - if (TryGoingStraightToGoalPoint(from)) + CFixedVector2D pos = cmpPosition->GetPosition2D(); + CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); + + CFixedVector2D offset = targetPos - pos; + if (!offset.IsZero()) { - if (!HasValidPath()) - StartSucceeded(); - m_PathState = PATHSTATE_FOLLOWING; - return; + entity_angle_t angle = atan2_approx(offset.X, offset.Y); + cmpPosition->TurnTo(angle); } - // Otherwise we need to compute a path. +} - // If it's close then just do a short path, not a long path - // TODO: If it's close on the opposite side of a river then we really - // need a long path, so we shouldn't simply check linear distance - // the check is arbitrary but should be a reasonably small distance. - if (goal.DistanceToPoint(from) < LONG_PATH_MIN_DIST) - { - // add our final goal as a long range waypoint so we don't forget - // where we are going if the short-range pathfinder returns - // an aborted path. - m_LongPath.m_Waypoints.clear(); - CFixedVector2D target = m_FinalGoal.NearestPointOnGoal(from); - m_LongPath.m_Waypoints.emplace_back(Waypoint{ target.X, target.Y }); - m_PathState = PATHSTATE_WAITING_REQUESTING_SHORT; - RequestShortPath(from, goal, true); - } +ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter(bool evenMovingUnits) const +{ + if (IsFormationMember()) + return ControlGroupMovementObstructionFilter(evenMovingUnits, m_Destination.GetEntity()); else - { - m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; - RequestLongPath(from, goal); - } + return ControlGroupMovementObstructionFilter(evenMovingUnits, GetEntityId()); } -void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal) +// TODO: this can be improved, it's a little limited +// e.g. use of hierarchical pathfinder,… +// Also adding back the "straight-line if close enough" test could be good. +bool CCmpUnitMotion::RequestNewPath(bool evenUnreachable) { - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return; + ENSURE(m_PathRequest.isAlternativePath || m_PathRequest.expectedPathTicket == 0); - // this is by how much our waypoints will be apart at most. - // this value here seems sensible enough. - PathGoal improvedGoal = goal; - improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1); + ENSURE(CurrentGoalHasValidPosition()); - cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass); + CmpPtr cmpPosition(GetEntityHandle()); + ENSURE (cmpPosition); - m_ExpectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId()); -} + CFixedVector2D position = cmpPosition->GetPosition2D(); -void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits) -{ - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return; + m_PathRequest.dumpExistingPath = true; + m_PathRequest.isAlternativePath = false; - // wrapping around on m_Tries isn't really a problem so don't check for overflow. - fixed searchRange = std::max(SHORT_PATH_MIN_SEARCH_RANGE * ++m_Tries, goal.DistanceToPoint(from)); - if (goal.type != PathGoal::POINT && searchRange < goal.hw && searchRange < SHORT_PATH_MIN_SEARCH_RANGE * 2) - searchRange = std::min(goal.hw, SHORT_PATH_MIN_SEARCH_RANGE * 2); - if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE) - searchRange = SHORT_PATH_MAX_SEARCH_RANGE; + bool reachable = RecomputeGoalPosition(m_Goal); - m_ExpectedPathTicket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetGroup(), GetEntityId()); -} + if (!reachable && !evenUnreachable) + { + // Do not submit a path request if we've been told it's not going to be used anyhow. + DiscardMove(); + return false; + } -bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) -{ - return MoveToPointRange(x, z, minRange, maxRange, INVALID_ENTITY); + ENSURE(m_Goal.x >= fixed::Zero()); + + /** + * A (long) note on short vs long range pathfinder, their conflict, and stuck units. + * A long-standing issue with 0 A.D.'s pathfinding has been that the short-range pathfinder is "better" than the long-range + * Indeed it can find paths that the long-range one cannot, since the grid is coarser than the real vector representation. + * This leads to units going where they shouldn't go, notably impassable and "unreachable" areas. + * This has been a -real- plague. Made worse by the facts that groups of trees tended to trigger it, leading to stuck gatherers… + * Thus, in general, we'd want the short-range and the long-range pathfinder to coincide. But making the short-range pathfinder + * register all impassable navcells as edges would just be way too slow, so we can't do that, so we -cannot- fix the issue + * by just changing the pathfinders' behavior. + * + * All hope is not lost, however. + * + * A big part of the problem is that before the unitMotion rewrite, UnitMotion requested a path to the goal, and then the pathfinder + * made that goal "reachable" by calling MakeGoalReachable, which uses the same grid as the long-range pathfinder. Thus, over short ranges, + * the pathfinder entirely short-circuited this. Since UnitMotion now calls MakeGoalReachable on its own, it only ever requests + * paths to points that are indeed supposed to be reachable. This does fix a number of cases. + * + * But then, why set LONG_PATH_MIN_DIST to 0 and disable the use of short paths here? Well it turns out you still had a few edge cases. + * + * Imagine two houses next to each other, with a space between them just wide enough that there are no passable navcells, + * but enough space for the short-range pathfinder to return a path through them (make them in a test map if you have to). + * If you ask a unit to cross there, the goal won't change: it's reachable by the long-range pathfinder by going around the house. + * However, the distance is < LONG_PATH_MIN_DIST, so the short-range pathfinder is called, so it goes through the house. Edge case. + * There's a variety of similar cases that can be imagined around the idea that there exists a shorter path visible only by the short-range pathfinder. + * If we never use the short-pathfinder in RequestNewPath, we can safely avoid those edge cases. + * + * However, we still call the short-pathfinder when running into an obstruction to avoid units. Can't that get us stuck too? + * Well, it probably can. But there's a few things to consider: + * -It's harder to trigger it if you actually have to run into a unit + * -In those cases, UnitMotion requests a path to the next existing waypoint (if there are none, it calls requestnewPath to get those) + * and the next existing waypoint has -necessarily- been given to us by the long-range pathfinder since we're using it here + * -We are running sanity checks on the output (see PathResult). + * Thus it's far less likely that the short-range pathfinder will return us an impassable path. + * It -is- not entirely impossible. A freak construction with many units strategically positionned could probably reveal the bug. + * But it's in my opinion rare enough that this discrepancy can be considered fixed. + */ + + if (m_Goal.DistanceToPoint(position) < LONG_PATH_MIN_DIST) + RequestShortPath(position, m_Goal, true); + else + RequestLongPath(position, m_Goal); + + return reachable; } -bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange, entity_id_t target) +PathGoal CCmpUnitMotion::CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal) { - PROFILE("MoveToPointRange"); + PathGoal goal = PathGoal(); + goal.x = fixed::FromInt(-1); // to figure out whether it's false-unreachable or false-buggy CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + ENSURE(cmpPosition); CFixedVector2D pos = cmpPosition->GetPosition2D(); - PathGoal goal; - goal.x = x; - goal.z = z; + // The point of this function is to get a reachable navcell where we want to go. + // It calls the hierarchical pathfinder's MakeGoalReachable function directly + // and analyzes to result to return something acceptable. + // "acceptable" means that if there is a path, once the unit has reached its destination, + // the ObstructionManager's "IsInPointRange/IsInTargetRange" should consider it in range. + // So we need to make sure MakeGoalReachable will return something in sync. - if (minRange.IsZero() && maxRange.IsZero()) - { - // Non-ranged movement: + // defaut to point at position + goal.type = PathGoal::POINT; + goal.x = GetGoalPosition(motionGoal).X; + goal.z = GetGoalPosition(motionGoal).Y; - // Head directly for the goal - goal.type = PathGoal::POINT; - } - else + // few cases to consider. + if (motionGoal.IsEntity()) { - // Ranged movement: + CmpPtr cmpObstruction(GetSimContext(), motionGoal.GetEntity()); + if (cmpObstruction) + { + ICmpObstructionManager::ObstructionSquare obstruction; + bool hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); + if (hasObstruction) + { + fixed certainty; + UpdatePositionForTarget(motionGoal.GetEntity(), obstruction.x, obstruction.z, certainty); - entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length(); + goal.type = PathGoal::CIRCLE; + goal.x = obstruction.x; + goal.z = obstruction.z; + goal.hw = obstruction.hw + motionGoal.Range() + m_Clearance; - if (distance < minRange) - { - // Too close to target - move outwards to a circle - // that's slightly larger than the min range - goal.type = PathGoal::INVERTED_CIRCLE; - goal.hw = minRange + Pathfinding::GOAL_DELTA; - } - else if (maxRange >= entity_pos_t::Zero() && distance > maxRange) - { - // Too far from target - move inwards to a circle - // that's slightly smaller than the max range - goal.type = PathGoal::CIRCLE; - goal.hw = maxRange - Pathfinding::GOAL_DELTA; - - // If maxRange was abnormally small, - // collapse the circle into a point - if (goal.hw <= entity_pos_t::Zero()) - goal.type = PathGoal::POINT; - } - else - { - // We're already in range - no need to move anywhere - if (m_FacePointAfterMove) - FaceTowardsPointFromPos(pos, x, z); - return false; + // if not a unit, treat as a square + if (cmpObstruction->GetUnitRadius() == fixed::Zero()) + { + goal.type = PathGoal::SQUARE; + goal.hh = obstruction.hh + motionGoal.Range() + m_Clearance; + goal.u = obstruction.u; + goal.v = obstruction.v; + + fixed distance = Geometry::DistanceToSquare(pos - CFixedVector2D(goal.x,goal.z), goal.u, goal.v, CFixedVector2D(goal.hw, goal.hh), true); + if (distance == fixed::Zero()) + goal.type = PathGoal::INVERTED_SQUARE; + } + else if ((pos - CFixedVector2D(goal.x,goal.z)).CompareLength(goal.hw) <= 0) + goal.type = PathGoal::INVERTED_CIRCLE; + } } + // if no obstruction, keep treating as a point + } + if (goal.type == PathGoal::POINT && motionGoal.Range() > fixed::Zero()) + { + goal.type = PathGoal::CIRCLE; + goal.hw = motionGoal.Range(); + if ((pos - CFixedVector2D(goal.x,goal.z)).CompareLength(goal.hw) <= 0) + goal.type = PathGoal::INVERTED_CIRCLE; } - m_State = STATE_INDIVIDUAL_PATH; - m_TargetEntity = target; - m_TargetOffset = CFixedVector2D(); - m_TargetMinRange = minRange; - m_TargetMaxRange = maxRange; - m_FinalGoal = goal; - m_Tries = 0; - - BeginPathing(pos, goal); - - return true; + return goal; } -bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const +bool CCmpUnitMotion::RecomputeGoalPosition(PathGoal& goal) { - // Given a square, plus a target range we should reach, the shape at that distance - // is a round-cornered square which we can approximate as either a circle or as a square. - // Previously, we used the shape that minimized the worst-case error. - // However that is unsage in some situations. So let's be less clever and - // just check if our range is at least three times bigger than the circleradius - return (range > circleRadius*3); -} + if (!CurrentGoalHasValidPosition()) + return false; // we're not going anywhere -bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) -{ - PROFILE("MoveToTargetRange"); + goal = CreatePathGoalFromMotionGoal(m_CurrentGoal); + + // We now have a correct goal. + // Make it reachable + + CmpPtr cmpPathfinder(GetSystemEntity()); + ENSURE(cmpPathfinder); CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + ENSURE(cmpPosition); CFixedVector2D pos = cmpPosition->GetPosition2D(); - CmpPtr cmpObstructionManager(GetSystemEntity()); - if (!cmpObstructionManager) - return false; + bool reachable = cmpPathfinder->MakeGoalReachable(pos.X, pos.Y, goal, m_PassClass); - bool hasObstruction = false; - ICmpObstructionManager::ObstructionSquare obstruction; - CmpPtr cmpObstruction(GetSimContext(), target); - if (cmpObstruction) - hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); + // TODO: ought to verify that the returned navcell is in range if it's reachable as a sanity check - if (!hasObstruction) - { - // The target didn't have an obstruction or obstruction shape, so treat it as a point instead + m_Goal = goal; - CmpPtr cmpTargetPosition(GetSimContext(), target); - if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) - return false; + return reachable; +} - CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); +void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal) +{ + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) + return; - return MoveToPointRange(targetPos.X, targetPos.Y, minRange, maxRange); - } + m_PathRequest.runShortPathValidation = false; + m_PathRequest.isAlternativePath = false; - /* - * If we're starting outside the maxRange, we need to move closer in. - * If we're starting inside the minRange, we need to move further out. - * These ranges are measured from the center of this entity to the edge of the target; - * we add the goal range onto the size of the target shape to get the goal shape. - * (Then we extend it outwards/inwards by a little bit to be sure we'll end up - * within the right range, in case of minor numerical inaccuracies.) - * - * There's a bit of a problem with large square targets: - * the pathfinder only lets us move to goals that are squares, but the points an equal - * distance from the target make a rounded square shape instead. - * - * When moving closer, we could shrink the goal radius to 1/sqrt(2) so the goal shape fits entirely - * within the desired rounded square, but that gives an unfair advantage to attackers who approach - * the target diagonally. - * - * If the target is small relative to the range (e.g. archers attacking anything), - * then we cheat and pretend the target is actually a circle. - * (TODO: that probably looks rubbish for things like walls?) - * - * If the target is large relative to the range (e.g. melee units attacking buildings), - * then we multiply maxRange by approx 1/sqrt(2) to guarantee they'll always aim close enough. - * (Those units should set minRange to 0 so they'll never be considered *too* close.) - */ - - CFixedVector2D halfSize(obstruction.hw, obstruction.hh); - PathGoal goal; - goal.x = obstruction.x; - goal.z = obstruction.z; + // this is by how much our waypoints will be apart at most. + // this value here seems sensible enough. + PathGoal improvedGoal = goal; + improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1); - entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize, true); + cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass); - // Compare with previous obstruction - ICmpObstructionManager::ObstructionSquare previousObstruction; - cmpObstruction->GetPreviousObstructionSquare(previousObstruction); - entity_pos_t previousDistance = Geometry::DistanceToSquare(pos - CFixedVector2D(previousObstruction.x, previousObstruction.z), obstruction.u, obstruction.v, halfSize, true); + m_PathRequest.expectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId()); +} - bool inside = distance.IsZero() && !Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize).IsZero(); - if ((distance < minRange && previousDistance < minRange) || inside) - { - // Too close to the square - need to move away +void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits, bool alternativePath) +{ + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) + return; - // Circumscribe the square - entity_pos_t circleRadius = halfSize.Length(); + // restrict the search space as much as possible, bumping it up on increasing m_tries if it wasn't enough to find a path. + // TODO: this might not be very useful following my unitMotion rewrite, and could probably be removed somewhat safely. + fixed distanceToGoal = goal.DistanceToPoint(from) + std::max(fixed::FromInt(TERRAIN_TILE_SIZE), goal.hw); - entity_pos_t goalDistance = minRange + Pathfinding::GOAL_DELTA; + // wrapping around on m_Tries isn't really a problem so don't check for overflow. + fixed searchRange = std::max(SHORT_PATH_MIN_SEARCH_RANGE * (++m_Tries + 1), distanceToGoal); + fixed upperBound = std::max(SHORT_PATH_NORMAL_SEARCH_RANGE, distanceToGoal); - if (ShouldTreatTargetAsCircle(minRange, circleRadius)) - { - // The target is small relative to our range, so pretend it's a circle - goal.type = PathGoal::INVERTED_CIRCLE; - goal.hw = circleRadius + goalDistance; - } - else - { - goal.type = PathGoal::INVERTED_SQUARE; - goal.u = obstruction.u; - goal.v = obstruction.v; - goal.hw = obstruction.hw + goalDistance; - goal.hh = obstruction.hh + goalDistance; - } - } - else if (maxRange < entity_pos_t::Zero() || distance < maxRange || previousDistance < maxRange) + if (searchRange > upperBound) + searchRange = upperBound; + + if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE && alternativePath) { - // We're already in range - no need to move anywhere - FaceTowardsPointFromPos(pos, goal.x, goal.z); - return false; + // this is too far away, trash our path + if (m_PathRequest.expectedPathTicket != 0) + return; + + m_PathRequest.isAlternativePath = false; + RequestNewPath(); + return; } - else - { - // We might need to move closer: - // Circumscribe the square - entity_pos_t circleRadius = halfSize.Length(); + m_PathRequest.runShortPathValidation = true; + m_PathRequest.isAlternativePath = alternativePath; - if (ShouldTreatTargetAsCircle(maxRange, circleRadius)) - { - // The target is small relative to our range, so pretend it's a circle + m_PathRequest.expectedPathTicket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetEntityId(), GetEntityId()); +} - // Note that the distance to the circle will always be less than - // the distance to the square, so the previous "distance < maxRange" - // check is still valid (though not sufficient) - entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius; - entity_pos_t previousCircleDistance = (pos - CFixedVector2D(previousObstruction.x, previousObstruction.z)).Length() - circleRadius; +void CCmpUnitMotion::DropCloseWaypoints(const CFixedVector2D& position, std::vector& waypoints) +{ + CmpPtr cmpPathfinder(GetSystemEntity()); - if (circleDistance < maxRange || previousCircleDistance < maxRange) - { - // We're already in range - no need to move anywhere - if (m_FacePointAfterMove) - FaceTowardsPointFromPos(pos, goal.x, goal.z); - return false; - } + // we're far away, just drop waypoints up to + // Check wether our next waypoint is obstructed in which case skip it. + // TODO: would be good to have a faster function here. + if (!cmpPathfinder->CheckMovement(GetObstructionFilter(false), waypoints.back().x, waypoints.back().z, waypoints.back().x, waypoints.back().z, m_Clearance, m_PassClass)) + waypoints.pop_back(); - entity_pos_t goalDistance = maxRange - Pathfinding::GOAL_DELTA; + // let's drop waypoints until we have dropped enough or are out of waypoints. + fixed droppedDistance = fixed::Zero(); + CFixedVector2D currentPos = position; + while (!waypoints.empty() && droppedDistance < DISTANCE_FOR_WAYPOINT_DROP_WHEN_OBSTRUCTED) + { + CFixedVector2D nextWpt(waypoints.back().x, waypoints.back().z); + droppedDistance += (nextWpt - currentPos).Length(); + currentPos = nextWpt; + if (droppedDistance < DISTANCE_FOR_WAYPOINT_DROP_WHEN_OBSTRUCTED) + waypoints.pop_back(); + } +} - goal.type = PathGoal::CIRCLE; - goal.hw = circleRadius + goalDistance; - } - else - { - // The target is large relative to our range, so treat it as a square and - // get close enough that the diagonals come within range +bool CCmpUnitMotion::SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool evenUnreachable) +{ + // This sets up a new destination, scrap whatever came before. + DiscardMove(); - entity_pos_t goalDistance = (maxRange - Pathfinding::GOAL_DELTA)*2 / 3; // multiply by slightly less than 1/sqrt(2) + m_Destination = SMotionGoal(CFixedVector2D(x, z), range); + m_CurrentGoal = m_Destination; - goal.type = PathGoal::SQUARE; - goal.u = obstruction.u; - goal.v = obstruction.v; - entity_pos_t delta = std::max(goalDistance, m_Clearance + entity_pos_t::FromInt(TERRAIN_TILE_SIZE)/16); // ensure it's far enough to not intersect the building itself - goal.hw = obstruction.hw + delta; - goal.hh = obstruction.hh + delta; - } - } + bool reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition - m_State = STATE_INDIVIDUAL_PATH; - m_TargetEntity = target; - m_TargetOffset = CFixedVector2D(); - m_TargetMinRange = minRange; - m_TargetMaxRange = maxRange; - m_FinalGoal = goal; - m_Tries = 0; + return reachable; +} - BeginPathing(pos, goal); +bool CCmpUnitMotion::SetNewDestinationAsEntity(entity_id_t ent, entity_pos_t range, bool evenUnreachable) +{ + // This sets up a new destination, scrap whatever came before. + DiscardMove(); - return true; + // validate entity's existence. + CmpPtr cmpPosition(GetSimContext(), ent); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return false; + + m_Destination = SMotionGoal(ent, range); + m_CurrentGoal = m_Destination; + + bool reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition + + return reachable; +} + +bool CCmpUnitMotion::IsFormationMember() const { + return m_Destination.IsFormationGoal(); } void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) { + // This sets up a new destination, scrap whatever came before. + DiscardMove(); + + // validate entity's existence. CmpPtr cmpPosition(GetSimContext(), target); if (!cmpPosition || !cmpPosition->IsInWorld()) return; - CFixedVector2D pos = cmpPosition->GetPosition2D(); - - PathGoal goal; - goal.type = PathGoal::POINT; - goal.x = pos.X; - goal.z = pos.Y; - - m_State = STATE_FORMATIONMEMBER_PATH; - m_TargetEntity = target; - m_TargetOffset = CFixedVector2D(x, z); - m_TargetMinRange = entity_pos_t::Zero(); - m_TargetMaxRange = entity_pos_t::Zero(); - m_FinalGoal = goal; - m_Tries = 0; + m_Destination = SMotionGoal(target, x, z); + m_CurrentGoal = m_Destination; - BeginPathing(pos, goal); + RequestNewPath(true); // calls RecomputeGoalPosition } +bool CCmpUnitMotion::TemporaryRerouteToPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range) +{ + // Does not reset destination, this is a temporary rerouting. + StopMoving(); + ResetPathfinding(); + m_CurrentGoal = SMotionGoal(CFixedVector2D(x, z), range); + // we must set evenUnreachable to true otherwise if the path is unreachable we'll DiscardMove() and we don't want to do that. + bool reachable = RequestNewPath(true); // calls RecomputeGoalPosition + // if this is false, whoever called this function should probably un-reroute us and let us go on our way. + return reachable; +} +bool CCmpUnitMotion::GoBackToOriginalDestination() +{ + SMotionGoal original = m_Destination; + + // assume evenUnreachable, since if it was false originally we'd have stopped by now. + if (original.IsEntity()) + return SetNewDestinationAsEntity(original.GetEntity(), original.Range(), true); + else + return SetNewDestinationAsPosition(original.GetPosition().X,original.GetPosition().Y, original.Range(), true); +} void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color) { @@ -1673,8 +1618,24 @@ lines.back().m_Color = color; SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating); } - float x = cmpPosition->GetPosition2D().X.ToFloat(); - float z = cmpPosition->GetPosition2D().Y.ToFloat(); + + if (CurrentGoalHasValidPosition()) + { + float x = GetGoalPosition(m_CurrentGoal).X.ToFloat(); + float z = GetGoalPosition(m_CurrentGoal).Y.ToFloat(); + lines.push_back(SOverlayLine()); + lines.back().m_Color = CColor(0.0f, 1.0f, 0.0f, 1.0f); + SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.4f, lines.back(), floating); + } + + float x = m_Goal.x.ToFloat(); + float z = m_Goal.z.ToFloat(); + lines.push_back(SOverlayLine()); + lines.back().m_Color = CColor(0.0f, 1.0f, 1.0f, 1.0f); + SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating); + + x = cmpPosition->GetPosition2D().X.ToFloat(); + z = cmpPosition->GetPosition2D().Y.ToFloat(); waypointCoords.push_back(x); waypointCoords.push_back(z); lines.push_back(SOverlayLine()); @@ -1688,12 +1649,11 @@ if (!m_DebugOverlayEnabled) return; - RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH); - RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH); - - for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i) - collector.Submit(&m_DebugOverlayLongPathLines[i]); + RenderPath(m_Path, m_DebugOverlayPathLines, OVERLAY_COLOR_PATH); + RenderPath(m_AlternativePath, m_DebugOverlayPathLines1, OVERLAY_COLOR_ALTERNATIVE_PATH); - for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i) - collector.Submit(&m_DebugOverlayShortPathLines[i]); + for (size_t i = 0; i < m_DebugOverlayPathLines1.size(); ++i) + collector.Submit(&m_DebugOverlayPathLines1[i]); + for (size_t i = 0; i < m_DebugOverlayPathLines.size(); ++i) + collector.Submit(&m_DebugOverlayPathLines[i]); } Index: source/simulation2/components/CCmpUnitMotionManager.cpp =================================================================== --- /dev/null +++ source/simulation2/components/CCmpUnitMotionManager.cpp @@ -0,0 +1,135 @@ +/* Copyright (C) 2018 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "simulation2/system/Component.h" +#include "simulation2/system/EntityMap.h" +#include "ICmpUnitMotionManager.h" + +#include "ICmpUnitMotion.h" + +#include "simulation2/MessageTypes.h" + +/** + + */ +class CCmpUnitMotionManager : public ICmpUnitMotionManager +{ +public: + static void ClassInit(CComponentManager& componentManager) + { + componentManager.SubscribeToMessageType(MT_Update_MotionUnit); + } + + DEFAULT_COMPONENT_ALLOCATOR(UnitMotionManager) + + static std::string GetSchema() + { + return ""; + } + + EntityMap m_Units; + + virtual void Init(const CParamNode& UNUSED(paramNode)) + { + } + + virtual void Deinit() + { + } + + virtual void Serialize(ISerializer& UNUSED(serialize)) + { + } + + virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) + { + Init(paramNode); + } + + virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) + { + switch (msg.GetType()) + { + case MT_Update_MotionUnit: + { + fixed dt = static_cast (msg).turnLength; + ValidateAllUnits(); + MoveAllUnits(dt); + HandleAllFailuresToMove(); + break; + } + } + } + + virtual void RegisterUnit(entity_id_t ent) + { + m_Units.insert(ent, ent); + } + + virtual void UnregisterUnit(entity_id_t ent) + { + m_Units.erase(ent); + } + + void ValidateAllUnits(); + void MoveAllUnits(fixed dt); + void HandleAllFailuresToMove(); +}; + +void CCmpUnitMotionManager::ValidateAllUnits() +{ + for (auto entity : m_Units) + { + CmpPtr cmpUnitMotion(GetSimContext(), entity.first); + // no need to check for cmpUnitMotion's existence here. + cmpUnitMotion->ValidateCurrentPath(); + } +} + +void CCmpUnitMotionManager::MoveAllUnits(fixed dt) +{ + /** + * First pass: try moving units along their path + * Second pass: handle consequences of that + * This architecture will let us easily add pushing to the first pass, + * and would help threading it in the future (as TryMoving never sends messages). + */ + for (auto entity : m_Units) + { + CmpPtr cmpUnitMotion(GetSimContext(), entity.first); + // no need to check for cmpUnitMotion's existence here. + + cmpUnitMotion->TryMoving(dt); + } + for (auto entity : m_Units) + { + CmpPtr cmpUnitMotion(GetSimContext(), entity.first); + cmpUnitMotion->PerformActualMovement(dt); + cmpUnitMotion->HandleMoveFailures(); + cmpUnitMotion->AnticipatePathingNeeds(); + } + +} + +void CCmpUnitMotionManager::HandleAllFailuresToMove() +{ + +} + +REGISTER_COMPONENT_TYPE(UnitMotionManager) Index: source/simulation2/components/CCmpVisualActor.cpp =================================================================== --- source/simulation2/components/CCmpVisualActor.cpp +++ source/simulation2/components/CCmpVisualActor.cpp @@ -55,7 +55,6 @@ public: static void ClassInit(CComponentManager& componentManager) { - componentManager.SubscribeToMessageType(MT_Update_Final); componentManager.SubscribeToMessageType(MT_InterpolatedPositionChanged); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_ValueModification); @@ -72,18 +71,18 @@ fixed m_R, m_G, m_B; // shading color - std::map m_AnimOverride; - // Current animation state - fixed m_AnimRunThreshold; // if non-zero this is the special walk/run mode std::string m_AnimName; bool m_AnimOnce; fixed m_AnimSpeed; std::wstring m_SoundGroup; fixed m_AnimDesync; - fixed m_AnimSyncRepeatTime; // 0.0 if not synced + fixed m_AnimSyncRepeatTime; fixed m_AnimSyncOffsetTime; + std::string m_MovingPrefix; + fixed m_MovingSpeed; + std::map m_VariantSelections; u32 m_Seed; // seed used for random variations @@ -191,6 +190,7 @@ { m_Unit = NULL; m_R = m_G = m_B = fixed::FromInt(1); + m_MovingSpeed = fixed::FromInt(1); m_ConstructionPreview = paramNode.GetChild("ConstructionPreview").IsOk(); @@ -226,9 +226,6 @@ serialize.NumberFixed_Unbounded("g", m_G); serialize.NumberFixed_Unbounded("b", m_B); - SerializeMap()(serialize, "anim overrides", m_AnimOverride); - - serialize.NumberFixed_Unbounded("anim run threshold", m_AnimRunThreshold); serialize.StringASCII("anim name", m_AnimName, 0, 256); serialize.Bool("anim once", m_AnimOnce); serialize.NumberFixed_Unbounded("anim speed", m_AnimSpeed); @@ -237,6 +234,9 @@ serialize.NumberFixed_Unbounded("anim sync repeat time", m_AnimSyncRepeatTime); serialize.NumberFixed_Unbounded("anim sync offset time", m_AnimSyncOffsetTime); + serialize.NumberFixed_Unbounded("anim moving speed", m_MovingSpeed); + serialize.StringASCII("anim moving prefix", m_MovingPrefix, 0, 256); + SerializeMap()(serialize, "variation", m_VariantSelections); serialize.NumberU32_Unbounded("seed", m_Seed); @@ -283,12 +283,6 @@ { switch (msg.GetType()) { - case MT_Update_Final: - { - const CMessageUpdate_Final& msgData = static_cast (msg); - Update(msgData.turnLength); - break; - } case MT_OwnershipChanged: { if (!m_Unit) @@ -439,7 +433,6 @@ virtual void SelectAnimation(const std::string& name, bool once = false, fixed speed = fixed::FromInt(1)) { - m_AnimRunThreshold = fixed::Zero(); m_AnimName = name; m_AnimOnce = once; m_AnimSpeed = speed; @@ -448,7 +441,12 @@ m_AnimSyncRepeatTime = fixed::Zero(); m_AnimSyncOffsetTime = fixed::Zero(); - SetVariant("animation", m_AnimName); + // TODO: change this once we support walk/run-anims + std::string animName = name; + /*if (!m_MovingPrefix.empty() && m_AnimName != "idle") + animName = m_MovingPrefix + "-" + m_AnimName; + else */if (!m_MovingPrefix.empty()) + animName = m_MovingPrefix; if (!m_Unit || !m_Unit->GetAnimation() || !m_Unit->GetID()) return; @@ -457,24 +455,29 @@ if (cmpSound) m_SoundGroup = cmpSound->GetSoundGroup(wstring_from_utf8(m_AnimName)); - m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str()); - } + SetVariant("animation", animName); - virtual void ReplaceMoveAnimation(const std::string& name, const std::string& replace) - { - m_AnimOverride[name] = replace; + if (m_Unit && m_Unit->GetAnimation()) + m_Unit->GetAnimation()->SetAnimationState(animName, m_AnimOnce, m_MovingSpeed.Multiply(m_AnimSpeed).ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str()); } - virtual void ResetMoveAnimation(const std::string& name) + virtual void SetMovingSpeed(fixed movingSpeed) { - std::map::const_iterator it = m_AnimOverride.find(name); - if (it != m_AnimOverride.end()) - m_AnimOverride.erase(name); - } + // TODO: don't copy strings for fun. + std::string prefix; + if (movingSpeed.IsZero()) + prefix = ""; + else + { + CmpPtr cmpUnitMotion(GetEntityHandle()); + if (!cmpUnitMotion) + return; + prefix = cmpUnitMotion->GetSpeedRatio() <= fixed::FromInt(1) ? "walk" : "run"; + } + m_MovingPrefix = prefix; + m_MovingSpeed = movingSpeed.IsZero() ? fixed::FromInt(1) : movingSpeed; - virtual void SelectMovementAnimation(fixed runThreshold) - { - m_AnimRunThreshold = runThreshold; + SelectAnimation(m_AnimName, m_AnimOnce, m_AnimSpeed); } virtual void SetAnimationSyncRepeat(fixed repeattime) @@ -555,8 +558,6 @@ // ReloadUnitAnimation is used for a minimal reloading upon deserialization, when the actor and seed are identical. // It is also used by ReloadActor. void ReloadUnitAnimation(); - - void Update(fixed turnLength); }; REGISTER_COMPONENT_TYPE(VisualActor) @@ -762,45 +763,3 @@ m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat()); } -void CCmpVisualActor::Update(fixed UNUSED(turnLength)) -{ - // This function is currently only used to update the animation if the speed in - // CCmpUnitMotion changes. This also only happens in the "special movement mode" - // triggered by SelectMovementAnimation. - - // TODO: This should become event based, in order to save performance and to make the code - // far less hacky. We should also take into account the speed when the animation is different - // from the "special movement mode" walking animation. - - // If we're not in the special movement mode, nothing to do. - if (m_AnimRunThreshold.IsZero()) - return; - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - - CmpPtr cmpUnitMotion(GetEntityHandle()); - if (!cmpUnitMotion) - return; - - fixed speed = cmpUnitMotion->GetCurrentSpeed(); - std::string name; - - if (speed.IsZero()) - { - speed = fixed::FromFloat(1.f); - name = "idle"; - } - else - name = speed < m_AnimRunThreshold ? "walk" : "run"; - - std::map::const_iterator it = m_AnimOverride.find(name); - if (it != m_AnimOverride.end()) - name = it->second; - - // Selecting the animation is going to reset the anim run threshold, so save it - fixed runThreshold = m_AnimRunThreshold; - SelectAnimation(name, false, speed); - m_AnimRunThreshold = runThreshold; -} Index: source/simulation2/components/ICmpUnitMotion.h =================================================================== --- source/simulation2/components/ICmpUnitMotion.h +++ source/simulation2/components/ICmpUnitMotion.h @@ -27,62 +27,83 @@ * Motion interface for entities with complex movement capabilities. * (Simpler motion is handled by ICmpMotion instead.) * - * It should eventually support different movement speeds, moving to areas - * instead of points, moving as part of a group, moving as part of a formation, - * etc. + * This component is designed to handle clever individual movement, + * not movement as part of an integrated group (formation, bataillon…) + * and another component should probably be designed for that. */ class ICmpUnitMotion : public IComponent { public: /** - * Attempt to walk into range of a to a given point, or as close as possible. - * The range is measured from the center of the unit. - * If the unit is already in range, or cannot move anywhere at all, or if there is - * some other error, then returns false. - * Otherwise, returns true and sends a MotionChanged message after starting to move, - * and sends another MotionChanged after finishing moving. - * If maxRange is negative, then the maximum range is treated as infinity. + * Resets motion and assigns a new 2D position as destination. + * Returns false if the position is unreachable (or if the move could not be completed for any other reason). + * Otherwise, returns true. + * If evenUnreachable is false, and the point is unreachable, then the unit will not start moving. + * Otherwise, the unit will try to go to another position as close as possible to the destination. */ - virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0; + virtual bool SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool evenUnreachable) = 0; /** - * Attempt to walk into range of a given target entity, or as close as possible. - * The range is measured between approximately the edges of the unit and the target, so that - * maxRange=0 is not unreachably close to the target. - * If the unit is already in range, or cannot move anywhere at all, or if there is - * some other error, then returns false. - * Otherwise, returns true and sends a MotionChanged message after starting to move, - * and sends another MotionChanged after finishing moving. - * If maxRange is negative, then the maximum range is treated as infinity. + * Resets motion and assigns a new entity as destination. + * Returns false if the entity is unreachable (or if the move could not be completed for any other reason). + * Otherwise, returns true. + * If evenUnreachable is false, and the point is unreachable, then the unit will not start moving. + * Otherwise, the unit will try to go to another position as close as possible to the destination. */ - virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; + virtual bool SetNewDestinationAsEntity(entity_id_t target, entity_pos_t range, bool evenUnreachable) = 0; /** - * Join a formation, and move towards a given offset relative to the formation controller entity. - * Continues following the formation until given a different command. + * Acts like SetNewDestinationAsEntity with an offset. */ virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0; + /** + * Set m_CurrentGoal to the given position. This does not affect m_Destination. + * This does not reset the current move or send any specific message. + */ + virtual bool TemporaryRerouteToPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range) = 0; + + /** + * Resets our pathfinding towards the original m_Destination. + * If it wasn't modified by a TemporaryRerouteToPosition, this does nothing (except potentially stop the unit for one turn). + * returns the same as the original SetNewDestinationAsXYZ call, with evenUnreachable set to true. + */ + virtual bool GoBackToOriginalDestination() = 0; + /** * Turn to look towards the given point. */ virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; /** - * Stop moving immediately. + * Turn to look towards the given entity. + */ + virtual void FaceTowardsEntity(entity_id_t ent) = 0; + + /** + * Determine whether to abort or retry X times if pathing fails. + * Generally safer to let it abort and inform us. + */ + virtual void SetAbortIfStuck(u8 shouldAbort) = 0; + + /** + * Stop moving, clear any destination, path, and ticket pending. + * Basically resets the unit's motion. + * Won't send any message. */ - virtual void StopMoving() = 0; + virtual void DiscardMove() = 0; /** - * Get the current movement speed. + * Returns the reachable goal of the unit, ie the goal it's actually moving towards. + * If the caller knows the original goal, it can more intelligently decide to stop or continue walking order. */ - virtual fixed GetCurrentSpeed() const = 0; + virtual CFixedVector2D GetReachableGoalPosition() = 0; /** - * Get whether the unit is moving. + * Asks wether the unit has a path to follow */ - virtual bool IsMoving() const = 0; + virtual bool HasValidPath() = 0; /** * Get how much faster/slower we are than normal. @@ -100,6 +121,17 @@ */ virtual void SetSpeedRatio(fixed ratio) = 0; + /** + * Get whether the unit is actually moving on the map this turn. + */ + virtual bool IsActuallyMoving() = 0; + + /** + * Get whether a unit is trying to go somewhere + * NB: this does not mean its position is actually changing right now. + */ + virtual bool IsTryingToMove() = 0; + /** * Get the unit theoretical speed in metres per second. * This is affected by SetSpeedRatio. @@ -111,10 +143,6 @@ * Calls to SetSpeedRatio have no effect on this (as that affects actual speed, not template). */ virtual fixed GetWalkSpeed() const = 0; - /** - * Set whether the unit will turn to face the target point after finishing moving. - */ - virtual void SetFacePointAfterMove(bool facePointAfterMove) = 0; /** * Get the unit's passability class. @@ -136,6 +164,48 @@ */ virtual void SetDebugOverlay(bool enabled) = 0; + /** + * Quick note on how clever UnitMotion should be: UnitMotion should try to reach the current target (m_CurrentGoal) + * as well as it can. But it should not take any particular guess on wether something CAN or SHOULD be reached. + * Examples: chasing a unit that's faster than us is probably stupid. This is not UnitMotion's to say, UnitMotion should try. + * Likewise when requesting a new path, even if it's unreachable unitMotion must try its best (but can inform unitAI that it's being obtuse) + * However, if a chased unit is moving, we should try to anticipate its moves by any means possible. + */ + + /** + * Check that we still want to move, and whether our path is sensible if yes. + */ + virtual void ValidateCurrentPath() = 0; + + struct MovementMetadata + { + CFixedVector2D currentPos; + CFixedVector2D initialPos; + bool moveWentOK; + bool wasObstructed; + }; + + /** + * "Virtually" move the unit along its current path. May be obstructed... + * Sets m_MoveMetadata for the current turn. + */ + virtual void TryMoving(fixed dt) = 0; + + /** + * Actually move the position of the unit, by updating cmpPosition. + */ + virtual void PerformActualMovement(fixed dt) = 0; + + /** + * If we failed to move, we should act upon it. + */ + virtual void HandleMoveFailures() = 0; + + /** + * Request new paths if these seem useful. + */ + virtual void AnticipatePathingNeeds() = 0; + DECLARE_INTERFACE_TYPE(UnitMotion) }; Index: source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- source/simulation2/components/ICmpUnitMotion.cpp +++ source/simulation2/components/ICmpUnitMotion.cpp @@ -23,41 +23,55 @@ #include "simulation2/scripting/ScriptComponent.h" BEGIN_INTERFACE_WRAPPER(UnitMotion) -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_4("SetNewDestinationAsPosition", bool, ICmpUnitMotion, SetNewDestinationAsPosition, entity_pos_t, entity_pos_t, entity_pos_t, bool) +DEFINE_INTERFACE_METHOD_3("SetNewDestinationAsEntity", bool, ICmpUnitMotion, SetNewDestinationAsEntity, entity_id_t, entity_pos_t, bool) DEFINE_INTERFACE_METHOD_3("MoveToFormationOffset", void, ICmpUnitMotion, MoveToFormationOffset, 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) +DEFINE_INTERFACE_METHOD_1("FaceTowardsEntity", void, ICmpUnitMotion, FaceTowardsEntity, entity_id_t) +DEFINE_INTERFACE_METHOD_1("SetAbortIfStuck", void, ICmpUnitMotion, SetAbortIfStuck, u8) +DEFINE_INTERFACE_METHOD_0("DiscardMove", void, ICmpUnitMotion, DiscardMove) +DEFINE_INTERFACE_METHOD_0("GetReachableGoalPosition", CFixedVector2D, ICmpUnitMotion, GetReachableGoalPosition) +DEFINE_INTERFACE_METHOD_0("HasValidPath", bool, ICmpUnitMotion, HasValidPath) DEFINE_INTERFACE_METHOD_CONST_0("GetRunSpeedMultiplier", fixed, ICmpUnitMotion, GetRunSpeedMultiplier) DEFINE_INTERFACE_METHOD_1("SetSpeedRatio", void, ICmpUnitMotion, SetSpeedRatio, fixed) -DEFINE_INTERFACE_METHOD_CONST_0("IsMoving", bool, ICmpUnitMotion, IsMoving) +DEFINE_INTERFACE_METHOD_0("IsActuallyMoving", bool, ICmpUnitMotion, IsActuallyMoving) +DEFINE_INTERFACE_METHOD_0("IsTryingToMove", bool, ICmpUnitMotion, IsTryingToMove) DEFINE_INTERFACE_METHOD_CONST_0("GetSpeed", fixed, ICmpUnitMotion, GetSpeed) DEFINE_INTERFACE_METHOD_CONST_0("GetWalkSpeed", fixed, ICmpUnitMotion, GetWalkSpeed) DEFINE_INTERFACE_METHOD_CONST_0("GetPassabilityClassName", std::string, ICmpUnitMotion, GetPassabilityClassName) DEFINE_INTERFACE_METHOD_CONST_0("GetUnitClearance", entity_pos_t, ICmpUnitMotion, GetUnitClearance) -DEFINE_INTERFACE_METHOD_1("SetFacePointAfterMove", void, ICmpUnitMotion, SetFacePointAfterMove, bool) DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpUnitMotion, SetDebugOverlay, bool) END_INTERFACE_WRAPPER(UnitMotion) + class CCmpUnitMotionScripted : public ICmpUnitMotion { public: DEFAULT_SCRIPT_WRAPPER(UnitMotionScripted) - virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) + virtual bool SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool UNUSED(evenUnreachable)) { - return m_Script.Call("MoveToPointRange", x, z, minRange, maxRange); + return m_Script.Call("SetNewDestinationAsPosition", x, z, range, true); } - virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) + virtual bool SetNewDestinationAsEntity(entity_id_t target, entity_pos_t range, bool UNUSED(evenUnreachable)) { - return m_Script.Call("MoveToTargetRange", target, minRange, maxRange); + return m_Script.Call("SetNewDestinationAsEntity", target, range, true); } virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) { - m_Script.CallVoid("MoveToFormationOffset", target, x, z); + return m_Script.CallVoid("MoveToFormationOffset", target, x, z); + } + + virtual bool TemporaryRerouteToPosition(entity_pos_t, entity_pos_t, entity_pos_t) + { + return false; + } + + virtual bool GoBackToOriginalDestination() + { + return true; } virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) @@ -65,14 +79,24 @@ m_Script.CallVoid("FaceTowardsPoint", x, z); } - virtual void StopMoving() + virtual void FaceTowardsEntity(entity_id_t ent) + { + m_Script.CallVoid("FaceTowardsEntity", ent); + } + + virtual void DiscardMove() { - m_Script.CallVoid("StopMoving"); + m_Script.CallVoid("DiscardMove"); } - virtual fixed GetCurrentSpeed() const + virtual void SetAbortIfStuck(u8 shouldAbort) { - return m_Script.Call("GetCurrentSpeed"); + m_Script.CallVoid("SetAbortIfStuck", shouldAbort); + } + + virtual fixed GetActualSpeed() + { + return m_Script.Call("GetActualSpeed"); } virtual void SetSpeedRatio(fixed ratio) @@ -85,9 +109,24 @@ return m_Script.Call("GetRunSpeedMultiplier"); } - virtual bool IsMoving() const + virtual CFixedVector2D GetReachableGoalPosition() + { + return m_Script.Call("GetReachableGoalPosition"); + } + + virtual bool HasValidPath() { - return m_Script.Call("IsMoving"); + return m_Script.Call("HasValidPath"); + } + + virtual bool IsActuallyMoving() + { + return m_Script.Call("IsActuallyMoving"); + } + + virtual bool IsTryingToMove() + { + return m_Script.Call("IsTryingToMove"); } virtual fixed GetSpeed() const @@ -100,11 +139,6 @@ return m_Script.Call("GetWalkSpeed"); } - virtual void SetFacePointAfterMove(bool facePointAfterMove) - { - m_Script.CallVoid("SetFacePointAfterMove", facePointAfterMove); - } - virtual pass_class_t GetPassabilityClass() const { return m_Script.Call("GetPassabilityClass"); @@ -130,6 +164,30 @@ m_Script.CallVoid("SetDebugOverlay", enabled); } + virtual void ValidateCurrentPath() + { + m_Script.CallVoid("ValidateCurrentPath"); + } + + virtual void TryMoving(fixed dt) + { + m_Script.CallVoid("TryMoving", dt); + } + + virtual void PerformActualMovement(fixed dt) + { + m_Script.CallVoid("PerformActualMovement", dt); + } + + virtual void HandleMoveFailures() + { + m_Script.CallVoid("HandleMoveFailures"); + } + + virtual void AnticipatePathingNeeds() + { + m_Script.CallVoid("AnticipatePathingNeeds"); + } }; REGISTER_COMPONENT_SCRIPT_WRAPPER(UnitMotionScripted) Index: source/simulation2/components/ICmpUnitMotionManager.h =================================================================== --- /dev/null +++ source/simulation2/components/ICmpUnitMotionManager.h @@ -0,0 +1,34 @@ +/* Copyright (C) 2018 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_ICMPUNITMOTIONMANAGER +#define INCLUDED_ICMPUNITMOTIONMANAGER + +#include "simulation2/system/Interface.h" + +class ICmpUnitMotionManager : public IComponent +{ +public: + DECLARE_INTERFACE_TYPE(UnitMotionManager) + + virtual void RegisterUnit(entity_id_t ent) = 0; + + virtual void UnregisterUnit(entity_id_t ent) = 0; + +}; + +#endif // INCLUDED_ICMPUNITMOTIONMANAGER Index: source/simulation2/components/ICmpUnitMotionManager.cpp =================================================================== --- /dev/null +++ source/simulation2/components/ICmpUnitMotionManager.cpp @@ -0,0 +1,25 @@ +/* Copyright (C) 2018 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "ICmpUnitMotionManager.h" + +#include "simulation2/system/InterfaceScripted.h" + +BEGIN_INTERFACE_WRAPPER(UnitMotionManager) +END_INTERFACE_WRAPPER(UnitMotionManager) Index: source/simulation2/components/ICmpVisual.h =================================================================== --- source/simulation2/components/ICmpVisual.h +++ source/simulation2/components/ICmpVisual.h @@ -106,25 +106,10 @@ virtual void SelectAnimation(const std::string& name, bool once, fixed speed) = 0; /** - * Replaces a specified animation with another. Only affects the special speed-based - * animation determination behaviour. - * @param name Animation to match. - * @param replace Animation that should replace the matched animation. + * Tell the visual actor that the unit is currently moving at the given speed. + * If speed is 0, the unit will become idle. */ - virtual void ReplaceMoveAnimation(const std::string& name, const std::string& replace) = 0; - - /** - * Ensures that the given animation will be used when it normally would be, - * removing reference to any animation that might replace it. - * @param name Animation name to remove from the replacement map. - */ - virtual void ResetMoveAnimation(const std::string& name) = 0; - - /** - * Start playing the walk/run animations, scaled to the unit's movement speed. - * @param runThreshold movement speed at which to switch to the run animation - */ - virtual void SelectMovementAnimation(fixed runThreshold) = 0; + virtual void SetMovingSpeed(fixed movingSpeed) = 0; /** * Adjust the speed of the current animation, so it can match simulation events. Index: source/simulation2/components/ICmpVisual.cpp =================================================================== --- source/simulation2/components/ICmpVisual.cpp +++ source/simulation2/components/ICmpVisual.cpp @@ -27,13 +27,11 @@ DEFINE_INTERFACE_METHOD_CONST_0("GetProjectileActor", std::wstring, ICmpVisual, GetProjectileActor) DEFINE_INTERFACE_METHOD_CONST_0("GetProjectileLaunchPoint", CFixedVector3D, ICmpVisual, GetProjectileLaunchPoint) DEFINE_INTERFACE_METHOD_3("SelectAnimation", void, ICmpVisual, SelectAnimation, std::string, bool, fixed) -DEFINE_INTERFACE_METHOD_1("SelectMovementAnimation", void, ICmpVisual, SelectMovementAnimation, fixed) -DEFINE_INTERFACE_METHOD_1("ResetMoveAnimation", void, ICmpVisual, ResetMoveAnimation, std::string) -DEFINE_INTERFACE_METHOD_2("ReplaceMoveAnimation", void, ICmpVisual, ReplaceMoveAnimation, std::string, std::string) DEFINE_INTERFACE_METHOD_1("SetAnimationSyncRepeat", void, ICmpVisual, SetAnimationSyncRepeat, fixed) DEFINE_INTERFACE_METHOD_1("SetAnimationSyncOffset", void, ICmpVisual, SetAnimationSyncOffset, fixed) DEFINE_INTERFACE_METHOD_4("SetShadingColor", void, ICmpVisual, SetShadingColor, fixed, fixed, fixed, fixed) DEFINE_INTERFACE_METHOD_2("SetVariable", void, ICmpVisual, SetVariable, std::string, float) +DEFINE_INTERFACE_METHOD_1("SetMovingSpeed", void, ICmpVisual, SetMovingSpeed, fixed) DEFINE_INTERFACE_METHOD_CONST_0("GetActorSeed", u32, ICmpVisual, GetActorSeed) DEFINE_INTERFACE_METHOD_1("SetActorSeed", void, ICmpVisual, SetActorSeed, u32) DEFINE_INTERFACE_METHOD_CONST_0("HasConstructionPreview", bool, ICmpVisual, HasConstructionPreview) Index: source/simulation2/helpers/Pathfinding.h =================================================================== --- source/simulation2/helpers/Pathfinding.h +++ source/simulation2/helpers/Pathfinding.h @@ -129,6 +129,7 @@ * between translation units. * TODO: figure out whether this is actually needed. It was added back in r8751 (in 2010) for unclear reasons * and it does not seem to really improve behavior today + * Note by Wraitii to wraitii: you just removed this in UnitMotion, delete it if it ends up being unecessary as expected. */ const entity_pos_t GOAL_DELTA = NAVCELL_SIZE/8; Index: source/simulation2/helpers/Rasterize.cpp =================================================================== --- source/simulation2/helpers/Rasterize.cpp +++ source/simulation2/helpers/Rasterize.cpp @@ -40,6 +40,10 @@ // A side effect is that the basic clearance has been set to 0.8, so removing this constant should be done // in parallel with setting clearance back to 1 for the default passability class (though this isn't strictly necessary). // Also: the code detecting foundation obstruction in CcmpObstructionManager had to be changed similarly. + // + // NB: because of this, if a entity wants to move very close to the actual obstruction of another entity + // it may never be able to get there, as there could be 1 extra navcell in between the actual square shape and the raster. + // So while this stands, max ranges should not be too small, or units might get stuck. entity_pos_t rasterClearance = clearance + Pathfinding::CLEARANCE_EXTENSION_RADIUS; // Get the bounds of cells that might possibly be within the shape Index: source/simulation2/scripting/MessageTypeConversions.cpp =================================================================== --- source/simulation2/scripting/MessageTypeConversions.cpp +++ source/simulation2/scripting/MessageTypeConversions.cpp @@ -266,20 +266,30 @@ //////////////////////////////// -JS::Value CMessageMotionChanged::ToJSVal(const ScriptInterface& scriptInterface) const +JS::Value CMessageMovePaused::ToJSVal(const ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); - SET_MSG_PROPERTY(starting); - SET_MSG_PROPERTY(error); return JS::ObjectValue(*obj); } -CMessage* CMessageMotionChanged::FromJSVal(const ScriptInterface& scriptInterface, JS::HandleValue val) +CMessage* CMessageMovePaused::FromJSVal(const ScriptInterface& scriptInterface, JS::HandleValue val) { FROMJSVAL_SETUP(); - GET_MSG_PROPERTY(bool, starting); - GET_MSG_PROPERTY(bool, error); - return new CMessageMotionChanged(starting, error); + return new CMessageMovePaused(); +} + +//////////////////////////////// + +JS::Value CMessageMoveFailure::ToJSVal(const ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + return JS::ObjectValue(*obj); +} + +CMessage* CMessageMoveFailure::FromJSVal(const ScriptInterface& scriptInterface, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + return new CMessageMoveFailure(); } //////////////////////////////// Index: source/simulation2/system/ComponentManager.cpp =================================================================== --- source/simulation2/system/ComponentManager.cpp +++ source/simulation2/system/ComponentManager.cpp @@ -727,6 +727,7 @@ AddComponent(m_SystemEntity, CID_Terrain, noParam); AddComponent(m_SystemEntity, CID_TerritoryManager, noParam); AddComponent(m_SystemEntity, CID_UnitRenderer, noParam); + AddComponent(m_SystemEntity, CID_UnitMotionManager, noParam); AddComponent(m_SystemEntity, CID_WaterManager, noParam); // Add scripted system components: