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/Formation.js =================================================================== --- binaries/data/mods/public/simulation/components/Formation.js +++ binaries/data/mods/public/simulation/components/Formation.js @@ -398,7 +398,7 @@ for (var i = 0; i < this.members.length; ++i) { var cmpUnitMotion = Engine.QueryInterface(this.members[i], IID_UnitMotion); - if (!cmpUnitMotion.IsMoving()) + if (!cmpUnitMotion.IsActuallyMoving()) { // Verify that members are stopped in FORMATIONMEMBER.WALKING var cmpUnitAI = Engine.QueryInterface(this.members[i], IID_UnitAI); @@ -858,7 +858,7 @@ minSpeed *= this.GetSpeedMultiplier(); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpUnitMotion.SetSpeedRatio(minSpeed); + cmpUnitMotion.SetSpeedRatio(minSpeed / cmpUnitMotion.GetWalkSpeed()); }; Formation.prototype.ShapeUpdate = function() Index: binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/GarrisonHolder.js +++ binaries/data/mods/public/simulation/components/GarrisonHolder.js @@ -217,9 +217,6 @@ cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + visibleGarrisonPoint.angle); else if (!vgpEntity && !cmpPosition.IsInWorld()) cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + Math.PI); - let cmpUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion); - if (cmpUnitMotion) - cmpUnitMotion.SetFacePointAfterMove(false); cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset); let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); if (cmpUnitAI) @@ -318,9 +315,6 @@ if (vgp.entity != entity) continue; cmpEntPosition.SetTurretParent(INVALID_ENTITY, new Vector3D()); - let cmpEntUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion); - if (cmpEntUnitMotion) - cmpEntUnitMotion.SetFacePointAfterMove(true); if (cmpEntUnitAI) cmpEntUnitAI.ResetTurretStance(); vgp.entity = null; 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 + // Move outside the building let range = 4; - if (this.MoveToTargetRangeExplicit(msg.data.target, range, range)) + if (this.MoveToTargetRangeExplicit(msg.data.target, range)) { // We've started walking to the given point this.SetNextStateAlwaysEntering("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.SetNextStateAlwaysEntering("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.SetNextStateAlwaysEntering("INDIVIDUAL.PICKUP.APPROACHING"); - } - else - { - // We are already at the target, or can't move at all - this.StopMoving(); - this.SetNextStateAlwaysEntering("INDIVIDUAL.PICKUP.LOADING"); - } + this.MoveToTarget(this.order.data.target, true); + this.SetNextStateAlwaysEntering("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.SetNextStateAlwaysEntering("INDIVIDUAL.GUARD.ESCORTING"); else this.SetNextStateAlwaysEntering("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.SetNextStateAlwaysEntering("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.SetNextStateAlwaysEntering("WALKING"); else this.FinishOrder(); @@ -1012,51 +1004,56 @@ 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); + this.StartTimer(0, 1000); }, - - "MoveCompleted": function(msg) { - if (this.FinishOrder()) - this.CallMemberFunction("ResetFinishOrder", []); + "leave": function(msg) { + this.StopTimer(); + }, + "Timer": 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.SetNextStateAlwaysEntering("MEMBER"); + } }, + "MoveCompleted": "Timer", }, "WALKINGANDFIGHTING": { "enter": function(msg) { + let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + cmpFormation.SetRearrange(true); + cmpFormation.MoveMembersIntoFormation(true, true); this.StartTimer(0, 1000); }, "Timer": function(msg) { // check if there are no enemies to attack this.FindWalkAndFightTargets(); + + 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.SetNextStateAlwaysEntering("MEMBER"); + } }, "leave": function(msg) { this.StopTimer(); }, - "MoveStarted": 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", []); - }, + "MoveCompleted": "Timer", }, "PATROL": { @@ -1075,12 +1072,38 @@ this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture; } + let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + cmpFormation.SetRearrange(true); + cmpFormation.MoveMembersIntoFormation(true, true); + this.StartTimer(0, 1000); }, "Timer": function(msg) { // Check if there are no enemies to attack this.FindWalkAndFightTargets(); + + 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)) + { + /** + * 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(); + } }, "leave": function(msg) { @@ -1088,29 +1111,7 @@ delete this.patrolStartPosOrder; }, - "MoveStarted": 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(); - }, + "MoveCompleted": "Timer", }, "GARRISON":{ @@ -1135,15 +1136,21 @@ "APPROACHING": { - "MoveStarted": function(msg) { + "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); + this.StartTimer(0, 1000); }, - - "MoveCompleted": function(msg) { - this.SetNextState("GARRISONING"); + "leave": function(msg) { + this.StopTimer(); + }, + "Timer": function(msg) { + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (msg.data.error || this.CheckGarrisonRange(this.order.data.target)) + this.SetNextState("GARRISONING"); }, + "MoveCompleted" : "Timer", }, "GARRISONING": { @@ -1161,41 +1168,52 @@ }, "FORMING": { - "MoveStarted": function(msg) { + "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, false); + this.StartTimer(0, 1000); }, - - "MoveCompleted": function(msg) { - - if (this.FinishOrder()) + "leave": function(msg) { + this.StopTimer(); + }, + "Timer": 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.CallMemberFunction("ResetFinishOrder", []); - return; + this.SetNextStateAlwaysEntering("MEMBER"); } - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - - cmpFormation.FindInPosition(); - } + }, + "MoveCompleted": "Timer", }, "COMBAT": { "APPROACHING": { - "MoveStarted": function(msg) { + "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); + this.StartTimer(0, 1000); }, - - "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"); + "leave": function(msg) { + this.StopTimer(); + }, + "Timer": 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 || 0, this.order.data.max || 1, true) + || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min || 0, this.order.data.max || 1, 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"); + } }, + "MoveCompleted": "Timer", }, "ATTACKING": { @@ -1294,6 +1312,10 @@ // Stop moving as soon as the formation disbands this.StopMoving(); + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + this.SetDefaultAnimationVariant(); + // If the controller handled an order but some members rejected it, // they will have no orders and be in the FORMATIONMEMBER.IDLE state. if (this.orderQueue.length) @@ -1352,30 +1374,30 @@ 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")); + this.StartTimer(0, 500); + }, + "leave": function() { + this.StopTimer(); }, + "Timer": function(msg) { + 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; - // 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 cmpControllerAI = Engine.QueryInterface(this.formationController, IID_UnitAI); + if (!cmpControllerAI.IsControllerWaiting()) return; - var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); - if (cmpVisual) - { - cmpVisual.ResetMoveAnimation("walk"); - cmpVisual.ResetMoveAnimation("run"); - } - var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); + this.StopMoving(); + this.FinishOrder(); + + let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.SetInPosition(this.entity); }, + + "MoveCompleted": "Timer", }, // Special case used by Order.LeaveFoundation @@ -1384,12 +1406,19 @@ var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.UnsetInPosition(this.entity); - this.SelectAnimation("move"); + this.StartTimer(0, 500); + }, + "leave": function() { + this.StopTimer(); }, + "Timer": function(msg) { + let cmpControllerAI = Engine.QueryInterface(this.formationController, IID_UnitAI); + if (!cmpControllerAI.IsControllerWaiting()) + return; - "MoveCompleted": function() { this.FinishOrder(); }, + "MoveCompleted": "Timer", }, }, @@ -1466,14 +1495,7 @@ "enter": function() { // Switch back to idle animation to guarantee we won't // get stuck with an incorrect animation - var animationName = "idle"; - if (this.IsFormationMember()) - { - var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); - if (cmpFormation) - animationName = cmpFormation.GetFormationAnimation(this.entity, animationName); - } - this.SelectAnimation(animationName); + this.SelectAnimation("idle"); // If we have some orders, it is because we are in an intermediary state // from FinishOrder (SetNextState("IDLE") is only executed when we get @@ -1541,15 +1563,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 +1578,22 @@ }, "WALKING": { - "enter": function() { - this.SelectAnimation("move"); + "enter": function () { + this.StartTimer(0, 1000); }, - "MoveCompleted": function() { - this.FinishOrder(); + "leave": function () { + this.StopTimer(); + }, + + "Timer": function(msg) { + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (!this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, this.order.data.min || 0, this.order.data.max || 1, true) + || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, this.order.data.min || 0, this.order.data.max || 1, true)) + { + this.StopMoving(); + this.FinishOrder(); + } }, }, @@ -1574,11 +1603,16 @@ this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); - this.SelectAnimation("move"); }, "Timer": function(msg) { this.FindWalkAndFightTargets(); + 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(); + } }, "leave": function(msg) { @@ -1587,7 +1621,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 +1648,6 @@ this.StartTimer(0, 1000); this.SetAnimationVariant("combat"); - this.SelectAnimation("move"); }, "leave": function() { @@ -1623,6 +1661,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); if (this.orderQueue.length == 1) this.PushOrder("Patrol", this.patrolStartPosOrder); @@ -1643,7 +1682,6 @@ this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); - this.SelectAnimation("move"); this.SetHeldPositionOnEntity(this.isGuardOf); return false; }, @@ -1657,6 +1695,24 @@ return; } this.SetHeldPositionOnEntity(this.isGuardOf); + + this.ResetMoveSpeed(); + + // 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 +1721,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 +1737,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"); }, }, @@ -1692,7 +1749,6 @@ this.StartTimer(1000, 1000); this.SetHeldPositionOnEntity(this.entity); this.SetAnimationVariant("combat"); - this.SelectAnimation("idle"); return false; }, @@ -1710,7 +1766,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 +1793,7 @@ "FLEEING": { "enter": function() { this.PlaySound("panic"); - - // Run quickly - this.SelectAnimation("move"); - this.SetMoveSpeedRatio(this.GetRunMultiplier()); + this.ResetMoveSpeed(); }, "HealthChanged": function() { @@ -1752,6 +1806,7 @@ "MoveCompleted": function() { // When we've run far enough, stop fleeing + this.StopMoving(); this.FinishOrder(); }, @@ -1759,6 +1814,15 @@ }, "COMBAT": { + "enter": function() { + // Show weapons rather than carried resources. + this.SetAnimationVariant("combat"); + }, + + "leave": function() { + this.SetDefaultAnimationVariant(); + }, + "Order.LeaveFoundation": function(msg) { // Ignore the order as we're busy. return { "discardOrder": true }; @@ -1773,17 +1837,10 @@ "APPROACHING": { "enter": function() { - // Show weapons rather than carried resources. - this.SetAnimationVariant("combat"); - - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function() { - // Show carried resources when walking. - this.SetDefaultAnimationVariant(); - this.StopTimer(); }, @@ -1797,10 +1854,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 @@ -1879,7 +1945,6 @@ if (cmpFormation) animationName = cmpFormation.GetFormationAnimation(this.entity, animationName); } - this.SetAnimationVariant("combat"); this.SelectAnimation(animationName); this.SetAnimationSync(prepare, this.attackTimers.repeat); this.StartTimer(prepare, this.attackTimers.repeat); @@ -1900,7 +1965,8 @@ if (cmpBuildingAI) cmpBuildingAI.SetUnitAITarget(0); this.StopTimer(); - this.SetDefaultAnimationVariant(); + // Reset combat animation that may have been set. + this.SelectAnimation("idle"); }, "Timer": function(msg) { @@ -2026,13 +2092,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 +2126,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 +2147,6 @@ "GATHER": { "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave". @@ -2133,12 +2206,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,29 +2254,42 @@ 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); if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); delete this.gatheringTarget; - this.SetDefaultAnimationVariant(); }, }, // 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 +2321,6 @@ this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } - // No dropsites, just give up }, }, @@ -2282,23 +2369,24 @@ 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 will show the carried resource if relevant 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; }, @@ -2312,7 +2400,9 @@ cmpSupply.RemoveGatherer(this.entity); delete this.gatheringTarget; - // Show the carried resource, if we've gathered anything. + this.SelectAnimation("idle"); + + // This will show the carried resource if relevant this.SetDefaultAnimationVariant(); }, @@ -2346,6 +2436,9 @@ // Collect from the target var status = cmpResourceGatherer.PerformGather(this.gatheringTarget); + // This will show the carried resource if relevant + this.SetDefaultAnimationVariant(); + // If we've collected as many resources as possible, // return to the nearest dropsite if (status.filled) @@ -2383,10 +2476,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 +2553,6 @@ "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, @@ -2482,6 +2573,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); this.SetNextState("HEALING"); }, }, @@ -2512,6 +2604,7 @@ }, "leave": function() { + this.SelectAnimation("idle"); this.StopTimer(); }, @@ -2567,7 +2660,6 @@ }, "CHASING": { "enter": function() { - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, @@ -2586,6 +2678,7 @@ } }, "MoveCompleted": function() { + this.StopMoving(); this.SetNextState("HEALING"); }, }, @@ -2595,19 +2688,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 +2740,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 +2754,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 +2785,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 +2853,8 @@ if (cmpBuilderList) cmpBuilderList.AddBuilder(this.entity); + this.FaceTowardsTarget(this.repairTarget); + this.SelectAnimation("build"); this.StartTimer(1000, 1000); return false; @@ -2739,6 +2865,7 @@ if (cmpBuilderList) cmpBuilderList.RemoveBuilder(this.entity); delete this.repairTarget; + this.SelectAnimation("idle"); this.StopTimer(); }, @@ -2758,10 +2885,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 +3008,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 +3128,7 @@ } } - if (this.MoveToTarget(target)) + if (this.MoveToTarget(target, true)) { this.SetNextState("APPROACHING"); return false; @@ -3002,6 +3159,7 @@ this.StopTimer(); var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(false); + this.SelectAnimation("idle"); }, "Timer": function(msg) { @@ -3046,36 +3204,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 +3241,16 @@ }, "Order.LeaveFoundation": function(msg) { - // Move a tile outside the building + // Move outside the building. var range = 4; - if (this.MoveToTargetRangeExplicit(msg.data.target, range, range)) + 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 +3268,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 +3275,6 @@ "leave": function() { this.StopTimer(); - this.SetFacePointAfterMove(true); }, "LosRangeUpdate": function(msg) { @@ -3166,6 +3304,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); this.MoveRandomly(+this.template.RoamDistance); }, }, @@ -3179,6 +3318,7 @@ }, "leave": function() { + this.SelectAnimation("idle"); this.StopTimer(); }, @@ -3198,7 +3338,7 @@ } }, - "MoveCompleted": function() { }, + "MoveCompleted": function() { this.StopMoving(); }, "Timer": function(msg) { this.SetNextState("ROAMING"); @@ -3569,6 +3709,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 +3861,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 +4049,15 @@ //// Message handlers ///// -UnitAI.prototype.OnMotionChanged = function(msg) +UnitAI.prototype.OnMovePaused = function(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) { - 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}); + this.UnitFsm.ProcessMessage(this, { "type": "MoveCompleted", "data": { "error" : true }}); }; UnitAI.prototype.OnGlobalConstructionFinished = function(msg) @@ -4216,6 +4363,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 +4385,31 @@ 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); + 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); + 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); + 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; @@ -4266,8 +4419,12 @@ return false; var range = cmpRanged.GetRange(type); + this.order.data.min = range.min; + this.order.data.max = range.max; + var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); + // Generally speaking, try to aim for the middle of a range. + return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + range.max)/2.0, evenUnreachable); }; /** @@ -4275,7 +4432,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 +4447,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 +4479,31 @@ // the parabole changes while walking, take something in the middle var guessedMaxRange = (range.max + parabolicMaxRange)/2; + this.order.data.min = range.min; + this.order.data.max = range.max; + + // 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)) + 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; + this.order.data.min = range; + this.order.data.max = range; + var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, min, max); + return cmpUnitMotion.SetNewDestinationAsEntity(target, range, evenUnreachable); }; -UnitAI.prototype.MoveToGarrisonRange = function(target) +UnitAI.prototype.MoveToGarrisonRange = function(target, evenUnreachable = false) { if (!this.CheckTargetVisible(target)) return false; @@ -4349,13 +4513,16 @@ return false; var range = cmpGarrisonHolder.GetLoadingRange(); + this.order.data.min = range.min; + this.order.data.max = range.max; + var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); + 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 +4533,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 +4582,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 +4603,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 +5159,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 +6034,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) @@ -5931,6 +6086,14 @@ return this.RespondToTargetedEntities(entsWithoutPref); }; +UnitAI.prototype.IsControllerWaiting = function() +{ + if (!this.IsFormationController()) + return false; + var state = this.GetCurrentState().split(".").pop(); + return (state == "MEMBER"); +}; + /** * Call obj.funcname(args) on UnitAI components of all formation members. */ 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 @@ -38,6 +38,10 @@ SetTimeout: function() { }, }); + AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { + IsInTargetRange: function(target, min, max) { return true; }, + }); + AddMock(SYSTEM_ENTITY, IID_RangeManager, { CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) { return 1; @@ -63,9 +67,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,8 +89,8 @@ AddMock(unit, IID_UnitMotion, { GetWalkSpeed: function() { return 1; }, MoveToFormationOffset: function(target, x, z) { }, - MoveToTargetRange: function(target, min, max) { }, - StopMoving: function() { }, + SetNewDestinationAsEntity: function() { }, + DiscardMove: function() { }, GetPassabilityClassName: function() { return "default"; }, }); @@ -139,9 +140,10 @@ }); AddMock(controller, IID_UnitMotion, { + DiscardMove: function() { }, GetWalkSpeed: function() { return 1; }, SetSpeedRatio: function(speed) { }, - MoveToPointRange: function(x, z, minRange, maxRange) { }, + SetNewDestinationAsPosition: function() { }, GetPassabilityClassName: function() { return "default"; }, }); @@ -153,7 +155,6 @@ controllerFormation.SetMembers([unit]); controllerAI.Walk(100, 100, false); - controllerAI.OnMotionChanged({ "starting": true }); TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.WALKING"); TS_ASSERT_EQUALS(unitAI.fsmStateName, "FORMATIONMEMBER.WALKING"); @@ -188,6 +189,10 @@ }); + AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { + IsInTargetRange: function(target, min, max) { return true; }, + }); + AddMock(SYSTEM_ENTITY, IID_RangeManager, { CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) { return 1; @@ -207,10 +212,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,8 +244,9 @@ AddMock(unit + i, IID_UnitMotion, { GetWalkSpeed: function() { return 1; }, MoveToFormationOffset: function(target, x, z) { }, - MoveToTargetRange: function(target, min, max) { }, - StopMoving: function() { }, + SetNewDestinationAsPosition: function() { }, + SetNewDestinationAsEntity: function() { }, + DiscardMove: function() { }, GetPassabilityClassName: function() { return "default"; }, }); @@ -288,9 +290,8 @@ AddMock(controller, IID_UnitMotion, { GetWalkSpeed: function() { return 1; }, SetSpeedRatio: function(speed) { }, - MoveToPointRange: function(x, z, minRange, maxRange) { }, - IsInTargetRange: function(target, min, max) { return true; }, - StopMoving: function() { }, + SetNewDestinationAsPosition: function() { }, + DiscardMove: function() { }, GetPassabilityClassName: function() { return "default"; }, }); 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_formation.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_formation.xml +++ binaries/data/mods/public/simulation/templates/template_formation.xml @@ -24,7 +24,7 @@ 1 - 1 + 0.9 square Hero Champion Cavalry Melee Ranged false @@ -58,6 +58,8 @@ true 1.0 + + 10.0 large 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/CCmpPathfinder.cpp =================================================================== --- source/simulation2/components/CCmpPathfinder.cpp +++ source/simulation2/components/CCmpPathfinder.cpp @@ -699,6 +699,14 @@ return m_LongPathfinder.NavcellIsReachable(i0, j0, i1, j1, passClass); } +bool CCmpPathfinder::OnSameNavcell(entity_pos_t xa, entity_pos_t za, entity_pos_t xb, entity_pos_t zb) const +{ + u16 ia, ja, ib, jb; + Pathfinding::NearestNavcell(xa, za, ia, ja, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE); + Pathfinding::NearestNavcell(xb, zb, ib, jb, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE); + return ia == ib && ja == jb; +} + ////////////////////////////////////////////////////////// // Async path requests: Index: source/simulation2/components/CCmpPathfinder_Common.h =================================================================== --- source/simulation2/components/CCmpPathfinder_Common.h +++ source/simulation2/components/CCmpPathfinder_Common.h @@ -252,6 +252,8 @@ virtual bool NavcellIsReachable(u16 i0, u16 j0, u16 i1, u16 j1, pass_class_t passClass); + virtual bool OnSameNavcell(entity_pos_t xa, entity_pos_t za, entity_pos_t xb, entity_pos_t zb) const; + virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, WaypointPath& ret) { m_LongPathfinder.ComputePath(x0, z0, goal, passClass, ret); 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,207 @@ #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); +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); /** - * Minimum distance to goal for a long path request + * 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 LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); +static const entity_pos_t SHORT_PATH_GOAL_REDUX_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3); /** - * 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; + * 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 SHORT_PATH_GOAL_RADIUS = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2); +static const entity_pos_t DISTANCE_FOR_WAYPOINT_DROP_WHEN_OBSTRUCTED = 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. +/* + * 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 DIRECT_PATH_RANGE = 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 a target entity, - * 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 = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); +static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*0); /** - * 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. + * 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_MIN_DELTA_FORMATION = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1); +static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); /** - * 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. + * See unitmotion logic for details. Higher means units will retry more often before potentially failing. */ -static const entity_pos_t CHECK_TARGET_MOVEMENT_AT_MAX_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*16); +static const size_t MAX_PATH_REATTEMPTS = 5; /** - * 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. + * Once we've retried MAX_PATH_REATTEMPS - PATH_REATTEMPS_RETRY_TRESHOLD times, we'll change our tactic and recompute more things. */ -static const fixed CHECK_TARGET_MOVEMENT_MIN_COS = fixed::FromInt(866)/1000; +static const size_t PATH_REATTEMPS_RETRY_TRESHOLD = 2; -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); 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); - } - - DEFAULT_COMPONENT_ALLOCATOR(UnitMotion) + private: + bool m_Valid = false; - bool m_DebugOverlayEnabled; - std::vector m_DebugOverlayLongPathLines; - std::vector m_DebugOverlayShortPathLines; + entity_pos_t m_Range; + entity_id_t m_Entity; + CFixedVector2D m_Position; - // Template state: + public: + SMotionGoal() : m_Valid(false) {}; - bool m_FormationController; + SMotionGoal(CFixedVector2D position, entity_pos_t range) + { + m_Entity = INVALID_ENTITY; + m_Range = range; + m_Position = position; - fixed m_TemplateWalkSpeed, m_TemplateRunSpeedMultiplier; - pass_class_t m_PassClass; - std::string m_PassClassName; + m_Valid = true; + } - // Dynamic state: + SMotionGoal(entity_id_t target, entity_pos_t range) + { + m_Entity = target; + m_Range = range; + m_Position = CFixedVector2D(fixed::Zero(), fixed::Zero()); - entity_pos_t m_Clearance; + m_Valid = true; + } - // cached for efficiency - fixed m_WalkSpeed, m_RunSpeedMultiplier; + // For formations only + SMotionGoal(entity_id_t target, entity_pos_t x, entity_pos_t z) + { + m_Entity = target; + m_Range = fixed::Zero(); - bool m_Moving; - bool m_FacePointAfterMove; + m_Position = CFixedVector2D(x,z); - enum State - { - /* - * Not moving at all. - */ - STATE_IDLE, + m_Valid = true; + } - /* - * Not moving at all. Will go to IDLE next turn. - * (This one-turn delay is a hack to fix animation timings.) - */ - STATE_STOPPING, + template + void SerializeCommon(S& serialize) + { + serialize.Bool("valid", m_Valid); - /* - * Member of a formation. - * Pathing to the target (depending on m_PathState). - * Target is m_TargetEntity plus m_TargetOffset. - */ - STATE_FORMATIONMEMBER_PATH, + serialize.NumberFixed_Unbounded("range", m_Range); - /* - * 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.NumberU32_Unbounded("entity", m_Entity); - STATE_MAX - }; - u8 m_State; + serialize.NumberFixed_Unbounded("x", m_Position.X); + serialize.NumberFixed_Unbounded("y", m_Position.Y); + } - enum PathState - { - /* - * There is no path. - * (This should only happen in IDLE and STOPPING.) - */ - PATHSTATE_NONE, + bool IsEntity() const { return m_Entity != INVALID_ENTITY; } + entity_id_t GetEntity() const { return m_Entity; } - /* - * We have an outstanding long path request. - * No paths are usable yet, so we can't move anywhere. - */ - PATHSTATE_WAITING_REQUESTING_LONG, + bool Valid() const { return m_Valid; } + void Clear() { m_Valid = false; } - /* - * 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, + bool IsFormationGoal() const { return m_Valid && IsEntity() && !m_Position.IsZero(); } + entity_pos_t Range() const { return m_Range; }; - /* - * We are following our path, and have no path requests. - * m_LongPath and m_ShortPath are valid. - */ - PATHSTATE_FOLLOWING, + CFixedVector2D GetPosition() const { return m_Position; } + }; - /* - * 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, +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 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, + DEFAULT_COMPONENT_ALLOCATOR(UnitMotion) - PATHSTATE_MAX - }; - u8 m_PathState; + bool m_DebugOverlayEnabled; + std::vector m_DebugOverlayPathLines,m_DebugOverlayPathLines1; - u32 m_ExpectedPathTicket; // asynchronous request ID we're waiting for, or 0 if none + // 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; - entity_id_t m_TargetEntity; - CFixedVector2D m_TargetPos; - CFixedVector2D m_TargetOffset; - entity_pos_t m_TargetMinRange; - entity_pos_t m_TargetMaxRange; + // cached for efficiency + fixed m_WalkSpeed, m_RunSpeedMultiplier; - // Actual unit speed, after technology and ratio + // 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 + // 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; + // 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; + }; + SPathRequest m_PathRequest; + + // Currently active path (storing waypoints in reverse order). + WaypointPath m_Path; - // 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; + // used for the short pathfinder, incremented on each unsuccessful try. + u8 m_Tries; + // Turns to wait before a certain action. + u8 m_WaitingTurns; + // if we actually started moving at some point. + bool m_StartedMoving; - // Motion planning - u8 m_Tries; // how many tries we've done to get to our current Final Goal. + // Speed over the last turn + // cached so we can tell the visual actor when it changes + fixed m_ActualSpeed; - PathGoal m_FinalGoal; + // 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 +269,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,18 +291,23 @@ 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_Tries = 0; + m_WaitingTurns = 0; - m_TargetEntity = INVALID_ENTITY; + m_DebugOverlayEnabled = false; - m_FinalGoal.type = PathGoal::POINT; + CmpPtr cmpUnitMotionManager(GetSystemEntity()); - m_DebugOverlayEnabled = false; + m_FormationController = paramNode.GetChild("FormationController").ToBool(); + + if (m_FormationController) + GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_Update_MotionFormation, this, true); + if (!ENTITY_IS_LOCAL(GetEntityId()) && cmpUnitMotionManager && !paramNode.GetChild("FormationController").ToBool()) + cmpUnitMotionManager->RegisterUnit(GetEntityId()); } virtual void Deinit() @@ -323,34 +317,25 @@ 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.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("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 +354,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: @@ -404,27 +382,31 @@ PathResult(msgData.ticket, msgData.path); break; } + case MT_OwnershipChanged: + { + const CMessageOwnershipChanged& msgData = static_cast (msg); + + if (msgData.to == INVALID_PLAYER) + { + CmpPtr cmpUnitMotionManager(GetSystemEntity()); + + if (cmpUnitMotionManager) + cmpUnitMotionManager->UnregisterUnit(GetEntityId()); + } + AdjustSpeed(); + break; + } case MT_ValueModification: { const CMessageValueModification& msgData = static_cast (msg); if (msgData.component != L"UnitMotion") break; - FALLTHROUGH; + AdjustSpeed(); + break; } - case MT_OwnershipChanged: case MT_Deserialized: { - CmpPtr cmpValueModificationManager(GetSystemEntity()); - if (!cmpValueModificationManager) - break; - - m_WalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId()); - m_RunSpeedMultiplier = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateRunSpeedMultiplier, GetEntityId()); - - // Adjust our speed. UnitMotion cannot know if this speed is on purpose or not so always adjust and let unitAI and such adapt. - m_SpeedRatio = std::min(m_SpeedRatio, m_RunSpeedMultiplier); - m_Speed = m_SpeedRatio.Multiply(GetWalkSpeed()); - + AdjustSpeed(); break; } } @@ -436,9 +418,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 +454,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 +476,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 +484,167 @@ 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 bool TemporaryRerouteToPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range); + virtual bool GoBackToOriginalDestination(); + + // transform a motion goal into a corresponding PathGoal + // called by RecomputeGoalPosition + PathGoal CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal); + + // take an arbitrary path goal and convert it to a 2D point goal, assign it to m_Goal. + bool RecomputeGoalPosition(PathGoal& goal); + virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z); + virtual void FaceTowardsEntity(entity_id_t ent); - virtual void StopMoving() + void StartMoving() { - m_Moving = false; - m_ExpectedPathTicket = 0; - m_State = STATE_STOPPING; - m_PathState = PATHSTATE_NONE; - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); - } + m_StartedMoving = true; - virtual entity_pos_t GetUnitClearance() const - { - return m_Clearance; + CmpPtr cmpObstruction(GetEntityHandle()); + if (cmpObstruction) + cmpObstruction->SetMovingFlag(true); } -private: - bool ShouldAvoidMovingUnits() const + void StopMoving() { - return !m_FormationController; - } + m_StartedMoving = false; - bool IsFormationMember() const - { - return m_State == STATE_FORMATIONMEMBER_PATH; - } + SetActualSpeed(fixed::Zero()); - entity_id_t GetGroup() const - { - return IsFormationMember() ? m_TargetEntity : GetEntityId(); + CmpPtr cmpObstruction(GetEntityHandle()); + if (cmpObstruction) + cmpObstruction->SetMovingFlag(false); } - bool HasValidPath() const + void ResetPathfinding() { - return m_PathState == PATHSTATE_FOLLOWING - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT; + m_Tries = 0; + m_WaitingTurns = 0; + + m_PathRequest.expectedPathTicket = 0; + m_Path.m_Waypoints.clear(); } - void StartFailed() + virtual void DiscardMove() { StopMoving(); - m_State = STATE_IDLE; // don't go through the STOPPING state since we never even started - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + ResetPathfinding(); - CMessageMotionChanged msg(true, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + m_CurrentGoal.Clear(); + m_Destination.Clear(); } - void MoveFailed() + void MoveWillFail() { - StopMoving(); - - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + CMessageMoveFailure msg; + GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + } - CMessageMotionChanged msg(false, true); + void MoveHasPaused() + { + CMessageMovePaused msg; GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } - void StartSucceeded() + virtual bool HasValidPath() { - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); + return !m_Path.m_Waypoints.empty(); + } - m_Moving = true; + virtual CFixedVector2D GetReachableGoalPosition() + { + if (!IsTryingToMove()) + return CFixedVector2D(fixed::Zero(),fixed::Zero()); - CMessageMotionChanged msg(true, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + return CFixedVector2D(m_Goal.x, m_Goal.z); } - void MoveSucceeded() - { - m_Moving = false; + virtual void ValidateCurrentPath(); - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + virtual void TryMoving(fixed dt); - // No longer moving, so speed is 0. - m_CurSpeed = fixed::Zero(); + virtual void PerformActualMovement(fixed dt); - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); - } + virtual void HandleMoveFailures(); - bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange, entity_id_t target); + virtual void AnticipatePathingNeeds(); +private: /** - * Handle the result of an asynchronous path query. + * @returns true if the unit has a valid position. */ - void PathResult(u32 ticket, const WaypointPath& path); + inline bool MayMove() + { + CmpPtr cmpPosition(GetEntityHandle()); + return cmpPosition && cmpPosition->IsInWorld(); + } - /** - * Do the per-turn movement and other updates. - */ - void Move(fixed dt); + CFixedVector2D GetGoalPosition(const SMotionGoal& goal) const + { + ENSURE (goal.Valid()); - /** - * 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; + 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(); + } - /** - * 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; + bool CurrentGoalHasValidPosition() + { + if (!m_CurrentGoal.Valid()) + return false; - /** - * 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); + if (m_CurrentGoal.IsEntity()) + { + CmpPtr cmpPosition(GetSimContext(), m_CurrentGoal.GetEntity()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return false; + return true; + } + else + return true; + } - /** - * 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); + 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); + /** - * 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 @@ -656,6 +652,11 @@ */ bool PathIsShort(const WaypointPath& path, const CFixedVector2D& from, entity_pos_t minDistance) const; + /** + * Recompute our actual speed, applying technologies and other modifiers. + */ + void AdjustSpeed(); + /** * Rotate to face towards the target point, given the current pos */ @@ -663,17 +664,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. @@ -685,6 +683,11 @@ */ void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool avoidMovingUnits); + /** + * 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 +700,470 @@ 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; + 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(); - - if (ShouldConsiderOurselvesAtDestination(pos)) - return; + std::vector& targetPath = m_Path.m_Waypoints; - 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; } - - // 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(); - - m_PathState = PATHSTATE_FOLLOWING; - - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); - - m_Moving = true; } - else - LOGWARNING("unexpected PathResult (%u %d %d)", GetEntityId(), m_State, m_PathState); + targetPath.insert(targetPath.end(), path.m_Waypoints.begin(), path.m_Waypoints.end()); } -void CCmpUnitMotion::Move(fixed dt) +void CCmpUnitMotion::ValidateCurrentPath() { - 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(); + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) return; - } - if (m_State == STATE_IDLE) + CmpPtr cmpPosition(GetEntityHandle()); + if (!MayMove()) return; - switch (m_PathState) - { - case PATHSTATE_NONE: + // Initialise MoveMetadata here as it is used by further "Move" functions and this always gets called. + // The above two checks are performed by every "Move" function so there is no need to init this before. + // The init says our move went OK and we were not obstructed. + m_MoveMetadata = MovementMetadata{cmpPosition->GetPosition2D(), cmpPosition->GetPosition2D(), true, false}; + + // Early exit: we cannot validate a path if we don't have a destination. + if (!IsTryingToMove()) + return; + + if (ShouldConsiderOurselvesAtDestination(m_CurrentGoal)) + return; + + // We're generally trying to reach a passable navcell, not specifically the exact coordinates we want + // (see requestNewPath for notes on why). + // However if our next waypoint is the last and on the same navcell as our goal, we can fairly safely + // switch to the exact coordinates. + // This only applies to point goals as "exactness" isn't that relevant with entities. + if (!m_CurrentGoal.IsEntity()) { - // If we're not pathing, do nothing + CFixedVector2D goalPos = GetGoalPosition(m_CurrentGoal); + if (HasValidPath() && m_Path.m_Waypoints.size() == 1 && + goalPos.X != m_Path.m_Waypoints.front().x && goalPos.Y != m_Path.m_Waypoints.front().z && + cmpPathfinder->OnSameNavcell(goalPos.X, goalPos.Y, m_Path.m_Waypoints.front().x, m_Path.m_Waypoints.front().z)) + { + m_Path.m_Waypoints.clear(); + m_Path.m_Waypoints.push_back(Waypoint{goalPos.X, goalPos.Y}); + } + // Return anyways, further checks do not apply to point goals. return; } - case PATHSTATE_WAITING_REQUESTING_LONG: - case PATHSTATE_WAITING_REQUESTING_SHORT: + // 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. + CmpPtr cmpTargetPosition(GetSimContext(), m_CurrentGoal.GetEntity()); + if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) { - // If we're waiting for a path and don't have one yet, do nothing + SMotionGoal newGoal(CFixedVector2D(m_Goal.x, m_Goal.z), m_CurrentGoal.Range()); + m_Destination = newGoal; + m_CurrentGoal = newGoal; + RequestNewPath(); + MoveWillFail(); return; } - case PATHSTATE_FOLLOWING: - case PATHSTATE_FOLLOWING_REQUESTING_SHORT: - case PATHSTATE_FOLLOWING_REQUESTING_LONG: + // If we have no path and are not requesting one, request one. + if (!HasValidPath() && m_PathRequest.expectedPathTicket == 0) { - // 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. + RequestNewPath(); + return; + } - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return; + // TODO: check LOS here, should the unit be gone beyond our FOV or in the SOW. - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; + // Check if our current Goal's position (ie m_Goal, not m_CurrentGoal) is sensible if our goal is an entity. + // 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. - CFixedVector2D initialPos = cmpPosition->GetPosition2D(); + CmpPtr cmpObstructionManager(GetSystemEntity()); + CmpPtr cmpTargetUnitMotion(GetSimContext(), m_CurrentGoal.GetEntity()); + if (!IsFormationMember() && (!cmpTargetUnitMotion || !cmpTargetUnitMotion->IsActuallyMoving())) + { + if (!cmpObstructionManager->IsInPointRange(m_CurrentGoal.GetEntity(), m_Goal.x, m_Goal.z, m_CurrentGoal.Range() - m_Clearance, m_CurrentGoal.Range() + m_Clearance, true)) + RequestNewPath(); + 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); + CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); + fixed certainty = fixed::Zero(); + UpdatePositionForTarget(m_CurrentGoal.GetEntity(), targetPos.X, targetPos.Y, certainty); - // Keep track of the current unit's position during the update - CFixedVector2D pos = initialPos; + if (!cmpObstructionManager->IsPointInPointRange(m_Goal.x, m_Goal.z, targetPos.X, targetPos.Y, m_CurrentGoal.Range() - certainty, m_CurrentGoal.Range() + certainty)) + RequestNewPath(); +} - fixed basicSpeed = m_Speed; +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); - // 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); + // If the target isn't moving, we want to go to our goal (other functions ensure this goal is in range). + if (!cmpTargetUnitMotion || !cmpTargetUnitMotion->IsActuallyMoving()) + return; - fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); + // Formation hack: we want to always update our path pretty much for it to look good. + if (IsFormationMember()) + { + x = GetGoalPosition(m_CurrentGoal).X; + z = GetGoalPosition(m_CurrentGoal).Y; + certainty = fixed::Zero(); + return; + } - bool wasObstructed = false; + certainty = (m_Clearance + cmpTargetUnitMotion->GetUnitClearance()) * 3 / 2; - // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint + // 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()); - fixed timeLeft = dt; - fixed zero = fixed::Zero(); + // try to estimate in how much time we'll reach it. + fixed distance = (cmpTargetPosition->GetPosition2D() - cmpPosition->GetPosition2D()).Length(); - 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; + if (GetSpeed() < fixed::Epsilon()) + return; - 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); + fixed time = std::min(distance / GetSpeed(), fixed::FromInt(5)); // don't try from too far away or this is just dumb. - CFixedVector2D offset = target - pos; + CFixedVector2D travelVector = (cmpTargetPosition->GetPosition2D() - cmpTargetPosition->GetPreviousPosition2D()).Multiply(time) * 2; + x += travelVector.X; + z += travelVector.Y; - // Work out how far we can travel in timeLeft - fixed maxdist = maxSpeed.Multiply(timeLeft); + certainty += time * 2; +} - // 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; +/** + * Warning: sending messages in this function is _very_ forbidden + */ +void CCmpUnitMotion::TryMoving(fixed dt) +{ + PROFILE("TryMoving"); - // Spend the rest of the time heading towards the next waypoint - timeLeft = timeLeft - (offsetLength / maxSpeed); + // We don't actually need to be TryingToMove to move here, we just need waypoints. - if (m_ShortPath.m_Waypoints.empty()) - m_LongPath.m_Waypoints.pop_back(); - else - m_ShortPath.m_Waypoints.pop_back(); + CmpPtr cmpPathfinder(GetSystemEntity()); - 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; + // We want to move (at most) m_Speed*dt units from pos towards the next waypoint + fixed timeLeft = dt; - 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 + while (timeLeft > fixed::Zero()) + { + // If we ran out of path, we have to stop there (move is still OK though). + if (!HasValidPath()) + break; - break; - } - } + if (m_Speed == fixed::Zero()) + 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. - return; + if (destination == target) + m_Path.m_Waypoints.pop_back(); + continue; } - - // We successfully moved along our path, until running out of - // waypoints or time. - - if (m_PathState == PATHSTATE_FOLLOWING) + else { - // 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 - } - } - - // 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); + // Error - path was obstructed + m_MoveMetadata.wasObstructed = true; + m_MoveMetadata.moveWentOK = false; + return; } } - } + // This is only reached if our move went OK. + + // Reset "retry" counters since we successfully moved. + m_Tries = 0; + m_WaitingTurns = 0; } -bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out) const +void CCmpUnitMotion::PerformActualMovement(fixed dt) { - if (m_TargetEntity == INVALID_ENTITY) - return false; - - CmpPtr cmpPosition(GetSimContext(), m_TargetEntity); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; - - if (m_TargetOffset.IsZero()) + if (m_MoveMetadata.currentPos == m_MoveMetadata.initialPos) { - // No offset, just return the position directly - out = cmpPosition->GetPosition2D(); - } - else - { - // 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; + // We have stopped moving. Dispatch this to components. + if (m_StartedMoving) + { + StopMoving(); + // Warn others we've stopped. We may actually be arrived + MoveHasPaused(); + } + // Note: we won't turn if we didn't move. + return; } - 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; - - // 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; + CmpPtr cmpPosition(GetEntityHandle()); + // We shouldn't be sending messages between TryMoving and this so if we actually moved, + // we must assume we had a valid position to start with. + // If this fails, we ended up in an impossible state somewhere before TryMoving. + ENSURE (cmpPosition); - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; + CFixedVector2D offset = m_MoveMetadata.currentPos - m_MoveMetadata.initialPos; - // 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; + // 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); - // 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 }); + // 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); - return true; + // Tell other components (obstructionManager,...) we are now moving. + if (!m_StartedMoving) + StartMoving(); } -bool CCmpUnitMotion::TryGoingStraightToTargetEntity(const CFixedVector2D& from) +void CCmpUnitMotion::HandleMoveFailures() { - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return false; - - // Fail if the target is too far away - if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) - return false; - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; + CmpPtr cmpPosition(GetEntityHandle()); - // 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) + // We can't really handle a failure if we're not really trying to move. + if (!IsTryingToMove() || !cmpPosition || !cmpPosition->IsInWorld()) + return; - // Find the point on the goal shape that we should head towards - CFixedVector2D goalPos = goal.NearestPointOnGoal(from); + // TODO: ping others that we are probably arrived. + if (ShouldConsiderOurselvesAtDestination(m_CurrentGoal)) + return; - // 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; + // Move went OK, so we're probably just moving. Carry on. + if (m_MoveMetadata.moveWentOK) + return; - // 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 }); + // Oops, we have a problem. We were obstructed, ran out of path and so on. + // Handle it. + // Failure to handle it will result in stuckness and players complaining. - return true; -} + // Reset our fail-counter on 0. + if (m_WaitingTurns == 0) + m_WaitingTurns = MAX_PATH_REATTEMPTS; -bool CCmpUnitMotion::CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta) -{ - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return false; + --m_WaitingTurns; - // Fail unless the target has moved enough - CFixedVector2D oldTargetPos(m_FinalGoal.x, m_FinalGoal.z); - if ((targetPos - oldTargetPos).CompareLength(minDelta) < 0) - return false; + // MAX_PATH_REATTEMPTS and above: we wait. + if (m_WaitingTurns >= MAX_PATH_REATTEMPTS) + return; - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; - 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; + // No path or it looks like the short-path requests below are not working. + if (!HasValidPath() || m_WaitingTurns == PATH_REATTEMPS_RETRY_TRESHOLD) + { + RequestNewPath(); + return; + } - // 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) + if (m_WaitingTurns > PATH_REATTEMPS_RETRY_TRESHOLD) { - CmpPtr cmpRangeManager(GetSystemEntity()); - if (cmpRangeManager && cmpRangeManager->GetLosVisibility(m_TargetEntity, cmpOwnership->GetOwner()) == ICmpRangeManager::VIS_HIDDEN) - return false; + /** + * Here we have a path and ran into some issue nonetheless. This calls for a new path. + * Distinguish 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 seem to 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; + + CFixedVector2D pos = cmpPosition->GetPosition2D(); + 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) + { + // Case (1), try dropping some waypoints near us. + DropCloseWaypoints(pos, m_Path.m_Waypoints); + + if (!m_Path.m_Waypoints.empty()) + { + redraw = false; + goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z }; + } + } + // Case (2) unless case (1) dropped all our waypoints. + if (redraw) + { + goal = CreatePathGoalFromMotionGoal(m_CurrentGoal); + m_PathRequest.dumpExistingPath = true; + } + + RequestShortPath(pos, goal, true); + 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; + // If we're here, we're completely stuck. We tried short paths and long-paths and still we are stuck. + // TODO: figure out the best course of action. + MoveWillFail(); - return true; + // Since we don't want to abort our movement to avoid getting stuck, start a new loop but give us some waiting time + // to relieve performance a little. + m_WaitingTurns = MAX_PATH_REATTEMPTS + 4; } -void CCmpUnitMotion::UpdateFinalGoal() +/** + * This probably should not send messages. + */ +void CCmpUnitMotion::AnticipatePathingNeeds() { - if (m_TargetEntity == INVALID_ENTITY) - return; - CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity); - if (!cmpUnitMotion) + CmpPtr cmpPathfinder(GetSystemEntity()); + CmpPtr cmpPosition(GetEntityHandle()); + + // Do nothing if we're not trying to move. + if (!IsTryingToMove() || !cmpPosition || !cmpPosition->IsInWorld()) return; - if (IsFormationMember()) + + // Also do nothing if we already have a path computing. + if (m_PathRequest.expectedPathTicket != 0) return; - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) + + bool pathWillBeObstructed = false; + + // Walk our path up to LOOKAHEAD_DISTANCE. If it looks like we'll be obstructed, + // ask for a new path up to that waypoint. + // In principle we should already be recomputing a path if we have none, so this functions does nothing then. + ssize_t index = m_Path.m_Waypoints.size() - 1; + CFixedVector2D checkPos = cmpPosition->GetPosition2D(); + fixed distance = (CFixedVector2D(m_Path.m_Waypoints[index].x,m_Path.m_Waypoints[index].z) - checkPos).Length(); + while (index >= 0 && distance < LOOKAHEAD_DISTANCE) + { + 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; - m_FinalGoal.x = targetPos.X; - m_FinalGoal.z = targetPos.Y; + + // Don't rely on the move metadata, our position may well have changed by now. + // (We could have garrisoned...) + CFixedVector2D pos = cmpPosition->GetPosition2D(); + + m_Path.m_Waypoints.erase(m_Path.m_Waypoints.begin() + index, m_Path.m_Waypoints.end()); + + PathGoal goal; + if (!m_Path.m_Waypoints.empty()) + goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z }; + else + { + goal = CreatePathGoalFromMotionGoal(m_CurrentGoal); + m_PathRequest.dumpExistingPath = true; + } + // request short path. + RequestShortPath(pos, goal, true); } -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 @@ -1249,6 +1188,20 @@ return true; } +void CCmpUnitMotion::AdjustSpeed() +{ + CmpPtr cmpValueModificationManager(GetSystemEntity()); + if (!cmpValueModificationManager) + return; + + m_WalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId()); + m_RunSpeedMultiplier = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateRunSpeedMultiplier, GetEntityId()); + + // Adjust our speed. UnitMotion cannot know if this speed is on purpose or not so always adjust and let unitAI and such adapt. + m_SpeedRatio = std::min(m_SpeedRatio, m_RunSpeedMultiplier); + m_Speed = m_SpeedRatio.Multiply(GetWalkSpeed()); +} + void CCmpUnitMotion::FaceTowardsPoint(entity_pos_t x, entity_pos_t z) { CmpPtr cmpPosition(GetEntityHandle()); @@ -1274,81 +1227,212 @@ } } -ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter(bool noTarget) const +void CCmpUnitMotion::FaceTowardsEntity(entity_id_t ent) { - entity_id_t group = noTarget ? m_TargetEntity : GetGroup(); - return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), group); -} + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; + CmpPtr cmpTargetPosition(GetSimContext(), ent); + if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) + return; + CFixedVector2D pos = cmpPosition->GetPosition2D(); + CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); -void CCmpUnitMotion::BeginPathing(const CFixedVector2D& from, const PathGoal& goal) + CFixedVector2D offset = targetPos - pos; + if (!offset.IsZero()) + { + entity_angle_t angle = atan2_approx(offset.X, offset.Y); + cmpPosition->TurnTo(angle); + } + +} + +ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter(bool evenMovingUnits) const { - // reset our state for sanity. - m_ExpectedPathTicket = 0; + if (IsFormationMember()) + return ControlGroupMovementObstructionFilter(evenMovingUnits, m_Destination.GetEntity()); + else + return ControlGroupMovementObstructionFilter(evenMovingUnits, GetEntityId()); +} - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); +// 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) +{ + ENSURE(CurrentGoalHasValidPosition()); + + CmpPtr cmpPosition(GetEntityHandle()); + ENSURE (cmpPosition); - m_Moving = false; + CFixedVector2D position = cmpPosition->GetPosition2D(); - m_PathState = PATHSTATE_NONE; + m_PathRequest.dumpExistingPath = true; -#if DISABLE_PATHFINDER + bool reachable = RecomputeGoalPosition(m_Goal); + + if (!reachable && !evenUnreachable) { - 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; - return; + // Do not submit a path request if we've been told it's not going to be used anyhow. + DiscardMove(); + return false; } -#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)) + 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 still use the long range pathfinder first? + * + * 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. + */ + + RequestLongPath(position, m_Goal); + + return reachable; +} + +PathGoal CCmpUnitMotion::CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal) +{ + PathGoal goal = PathGoal(); + goal.x = fixed::FromInt(-1); // to figure out whether it's false-unreachable or false-buggy + + CmpPtr cmpPosition(GetEntityHandle()); + ENSURE(cmpPosition); + + CFixedVector2D pos = cmpPosition->GetPosition2D(); + + // 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. + + // defaut to point at position + goal.type = PathGoal::POINT; + goal.x = GetGoalPosition(motionGoal).X; + goal.z = GetGoalPosition(motionGoal).Y; + + // few cases to consider. + if (motionGoal.IsEntity()) { - if (!HasValidPath()) - StartSucceeded(); - m_PathState = PATHSTATE_FOLLOWING; - return; - } + 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); - // Same thing applies to non-entity points - if (TryGoingStraightToGoalPoint(from)) + goal.type = PathGoal::CIRCLE; + goal.x = obstruction.x; + goal.z = obstruction.z; + goal.hw = obstruction.hw + motionGoal.Range() + m_Clearance; + + // 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()) { - if (!HasValidPath()) - StartSucceeded(); - m_PathState = PATHSTATE_FOLLOWING; - return; + goal.type = PathGoal::CIRCLE; + goal.hw = motionGoal.Range(); + if ((pos - CFixedVector2D(goal.x,goal.z)).CompareLength(goal.hw) <= 0) + goal.type = PathGoal::INVERTED_CIRCLE; } - // Otherwise we need to compute a path. + return goal; +} + +bool CCmpUnitMotion::RecomputeGoalPosition(PathGoal& goal) +{ + if (!CurrentGoalHasValidPosition()) + return false; // we're not going anywhere + + goal = CreatePathGoalFromMotionGoal(m_CurrentGoal); + + // We now have a correct goal. + // Make it reachable + + CmpPtr cmpPathfinder(GetSystemEntity()); + ENSURE(cmpPathfinder); + + CmpPtr cmpPosition(GetEntityHandle()); + ENSURE(cmpPosition); + + CFixedVector2D pos = cmpPosition->GetPosition2D(); - // 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) + // TODO: implement rounded rectangles. + bool reachable = false; + if (goal.type == PathGoal::SQUARE) { - // 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); + PathGoal ogoal = goal; + goal.hw -= m_Clearance + m_CurrentGoal.Range(); + reachable = cmpPathfinder->MakeGoalReachable(pos.X, pos.Y, goal, m_PassClass); + + PathGoal goal2 = ogoal; + goal2.hh -= m_Clearance + m_CurrentGoal.Range(); + reachable |= cmpPathfinder->MakeGoalReachable(pos.X, pos.Y, goal2, m_PassClass); + + // Pick the best + if ((CFixedVector2D(goal.x, goal.z) - pos).CompareLength(CFixedVector2D(goal2.x, goal2.z) - pos) == 1) + goal = goal2; } else { - m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; - RequestLongPath(from, goal); + reachable = cmpPathfinder->MakeGoalReachable(pos.X, pos.Y, goal, m_PassClass); } + + return reachable; } void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal) @@ -1357,6 +1441,8 @@ if (!cmpPathfinder) return; + m_PathRequest.runShortPathValidation = false; + // this is by how much our waypoints will be apart at most. // this value here seems sensible enough. PathGoal improvedGoal = goal; @@ -1364,7 +1450,7 @@ cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass); - m_ExpectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId()); + m_PathRequest.expectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId()); } void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits) @@ -1373,286 +1459,130 @@ if (!cmpPathfinder) return; - // 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; - - m_ExpectedPathTicket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetGroup(), GetEntityId()); -} - -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); -} - -bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange, entity_id_t target) -{ - PROFILE("MoveToPointRange"); + // 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); - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; - - CFixedVector2D pos = cmpPosition->GetPosition2D(); - - PathGoal goal; - goal.x = x; - goal.z = z; + // 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 (minRange.IsZero() && maxRange.IsZero()) - { - // Non-ranged movement: + if (searchRange > upperBound) + searchRange = upperBound; - // Head directly for the goal - goal.type = PathGoal::POINT; - } - else + if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE) { - // Ranged movement: - - entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length(); + // this is too far away, trash our path + if (m_PathRequest.expectedPathTicket != 0) + return; - 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; - } + RequestNewPath(); + return; } - 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; -} + m_PathRequest.runShortPathValidation = true; -bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const -{ - // 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); + m_PathRequest.expectedPathTicket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetEntityId(), GetEntityId()); } -bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) +void CCmpUnitMotion::DropCloseWaypoints(const CFixedVector2D& position, std::vector& waypoints) { - PROFILE("MoveToTargetRange"); - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; - - CFixedVector2D pos = cmpPosition->GetPosition2D(); - - CmpPtr cmpObstructionManager(GetSystemEntity()); - if (!cmpObstructionManager) - return false; + CmpPtr cmpPathfinder(GetSystemEntity()); - bool hasObstruction = false; - ICmpObstructionManager::ObstructionSquare obstruction; - CmpPtr cmpObstruction(GetSimContext(), target); - if (cmpObstruction) - hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); + // 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(); - if (!hasObstruction) + // 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) { - // The target didn't have an obstruction or obstruction shape, so treat it as a point instead - - CmpPtr cmpTargetPosition(GetSimContext(), target); - if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) - return false; - - CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); - - return MoveToPointRange(targetPos.X, targetPos.Y, minRange, maxRange); + 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(); } +} - /* - * 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; - - entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize, true); +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(); - // 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_Destination = SMotionGoal(CFixedVector2D(x, z), range); + m_CurrentGoal = m_Destination; - 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 + bool reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition - // Circumscribe the square - entity_pos_t circleRadius = halfSize.Length(); + return reachable; +} - entity_pos_t goalDistance = minRange + Pathfinding::GOAL_DELTA; +bool CCmpUnitMotion::SetNewDestinationAsEntity(entity_id_t ent, entity_pos_t range, bool evenUnreachable) +{ + // This sets up a new destination, scrap whatever came before. + DiscardMove(); - 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) - { - // We're already in range - no need to move anywhere - FaceTowardsPointFromPos(pos, goal.x, goal.z); + // validate entity's existence. + CmpPtr cmpPosition(GetSimContext(), ent); + if (!cmpPosition || !cmpPosition->IsInWorld()) return false; - } - else - { - // We might need to move closer: - // Circumscribe the square - entity_pos_t circleRadius = halfSize.Length(); + m_Destination = SMotionGoal(ent, range); + m_CurrentGoal = m_Destination; - if (ShouldTreatTargetAsCircle(maxRange, circleRadius)) - { - // The target is small relative to our range, so pretend it's a circle - - // 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; - - 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; - } - - entity_pos_t goalDistance = maxRange - Pathfinding::GOAL_DELTA; - - 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 reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition - entity_pos_t goalDistance = (maxRange - Pathfinding::GOAL_DELTA)*2 / 3; // multiply by slightly less than 1/sqrt(2) - - 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; - } - } - - 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 reachable; +} - return true; +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 +1603,33 @@ 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); + } + + if (CurrentGoalHasValidPosition() && m_CurrentGoal.IsEntity()) + { + float x = cmpPosition->GetPosition2D().X.ToFloat(); + float z = cmpPosition->GetPosition2D().Y.ToFloat(); + lines.push_back(SOverlayLine()); + lines.back().m_Color = CColor(1.0f, 1.0f, 1.0f, 1.0f); + SimRender::ConstructCircleOnGround(GetSimContext(), x, z, m_CurrentGoal.Range().ToFloat() + m_Clearance.ToFloat(), 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 +1643,10 @@ 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); - 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,134 @@ +/* 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); + cmpUnitMotion->PerformActualMovement(dt); + } + for (auto entity : m_Units) + { + CmpPtr cmpUnitMotion(GetSimContext(), entity.first); + 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/ICmpPathfinder.h =================================================================== --- source/simulation2/components/ICmpPathfinder.h +++ source/simulation2/components/ICmpPathfinder.h @@ -106,6 +106,11 @@ */ virtual bool NavcellIsReachable(u16 i0, u16 j0, u16 i1, u16 j1, pass_class_t passClass) = 0; + /** + * @returns true if these two points are on the same passable navcell. + */ + virtual bool OnSameNavcell(entity_pos_t xa, entity_pos_t za, entity_pos_t xb, entity_pos_t zb) const = 0; + /** * Compute a tile-based path from the given point to the goal, and return the set of waypoints. * The waypoints correspond to the centers of horizontally/vertically adjacent tiles Index: source/simulation2/components/ICmpUnitMotion.h =================================================================== --- source/simulation2/components/ICmpUnitMotion.h +++ source/simulation2/components/ICmpUnitMotion.h @@ -27,62 +27,77 @@ * 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; + + /** + * 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 +115,20 @@ */ virtual void SetSpeedRatio(fixed ratio) = 0; + /** + * Get whether the unit is actually moving on the map this turn. + * NB: this doesn't mean we are actually trying to get somewhere, + * that's IsTryingToMove. + */ + virtual bool IsActuallyMoving() = 0; + + /** + * Get whether a unit is trying to go somewhere, i.e. has a destination. + * NB: this does not mean its position is actually changing right now, + * nor does it mean that its position isn't changing (that's "isActuallyMoving"). + */ + virtual bool IsTryingToMove() = 0; + /** * Get the unit theoretical speed in metres per second. * This is affected by SetSpeedRatio. @@ -111,10 +140,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 +161,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,54 @@ #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_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 +78,19 @@ 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 fixed GetActualSpeed() { - return m_Script.Call("GetCurrentSpeed"); + return m_Script.Call("GetActualSpeed"); } virtual void SetSpeedRatio(fixed ratio) @@ -85,9 +103,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("HasValidPath"); + } + + virtual bool IsActuallyMoving() + { + return m_Script.Call("IsActuallyMoving"); + } + + virtual bool IsTryingToMove() { - return m_Script.Call("IsMoving"); + return m_Script.Call("IsTryingToMove"); } virtual fixed GetSpeed() const @@ -100,11 +133,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 +158,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/components/tests/test_Pathfinder.h =================================================================== --- source/simulation2/components/tests/test_Pathfinder.h +++ source/simulation2/components/tests/test_Pathfinder.h @@ -170,6 +170,7 @@ hierarchical_globalRegions_testmap(t); #endif } + void test_reachability_sanity_test() { // Goal of this test: validate that we stay on the same cell. This isn't 100% coverage but if the point goal gets broken, things will be _bad_. 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: