Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -308,8 +308,9 @@ ret.speed = { "walk": getEntityValue("UnitMotion/WalkSpeed"), }; - if (template.UnitMotion.Run) - ret.speed.run = getEntityValue("UnitMotion/Run/Speed"); + ret.speed.run = getEntityValue("UnitMotion/WalkSpeed"); + if (template.UnitMotion.RunMultiplier) + ret.speed.run *= getEntityValue("UnitMotion/RunMultiplier"); } if (template.ProductionQueue) Index: binaries/data/mods/public/gui/session/unit_actions.js =================================================================== --- binaries/data/mods/public/gui/session/unit_actions.js +++ binaries/data/mods/public/gui/session/unit_actions.js @@ -947,10 +947,31 @@ return false; } + // if the target has a static obstruction, move the rallypoint position closer to us + // keep this in sync with Rallypoint.js + let position = {}; + let template = GetTemplateData(targetState.template); + if (template.obstruction && template.obstruction.shape && template.obstruction.shape.type == "static") + { + let size = Math.min(+template.obstruction.shape.width, +template.obstruction.shape.depth); + let vector = new Vector2D(targetState.position.x-entState.position.x,targetState.position.z-entState.position.z); + let pos = new Vector2D(targetState.position.x, targetState.position.z); + pos = pos.sub(vector.normalize().mult(size * 0.49)); + position.x = pos.x; + position.z = pos.y; + position.y = targetState.position.y; + } + else + { + position.x = targetState.position.x; + position.z = targetState.position.z; + position.y = targetState.position.y; + } + return { "possible": true, "data": data, - "position": targetState.position, + "position": position, "cursor": cursor, "tooltip": tooltip }; Index: binaries/data/mods/public/maps/scenarios/Pathfinding_integrated_testmap.js =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/scenarios/Pathfinding_integrated_testmap.js @@ -0,0 +1,201 @@ +warn("Setting up test"); + +var tests = {}; +var start = 0; + +var failedTests = 0; + +let cmpTechMgr = QueryPlayerIDInterface(1, IID_TechnologyManager); + +//XXXtreme hack: create a fake technology to drastically limit the range of everybody in place. +cmpTechMgr.modifications = {}; +cmpTechMgr.modifications['Attack/Ranged/MaxRange'] = [ {"affects":[["Unit"]], "replace":1.5} ]; + +cmpTechMgr = QueryPlayerIDInterface(2, IID_TechnologyManager); +cmpTechMgr.modifications = {}; +cmpTechMgr.modifications['Attack/Ranged/MaxRange'] = [ {"affects":[["Unit"]], "replace":0} ]; + +Trigger.prototype.setupTests = function() +{ + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + start = cmpTimer.GetTime(); + + let cmpFoundation = Engine.QueryInterface(391, IID_Foundation); + cmpFoundation.InitialiseConstruction(1, "structures/maur_house"); + + // Ready to order people around. + + tests = { + "21" : {"target":353, "continuous":true}, // inbetween house: allies present + "200" : {"target":352, "continuous":true, "expectfail":true}, // Inbetweenhouse- should fail + "24" : {"target":361}, // basic labyrinth + "23" : {"target":356}, // corner house + "22" : {"target":354}, // between house + around + "49" : {"target":355}, // straight + "210" : {"target":357}, // formation - sparse + "211" : {"target":358}, // formation - wall + "186" : {"target":359, "expectfail" : true}, // inside forest - dense + "372" : {"target":359, "continuous":true, "expectfail" : true}, // inside forest - dense + "187" : {"target":360}, // inside forest - sparse + "50" : {"target":352}, // super long trip + "46" : {"target":363}, // trip inside hills + "53" : {"target":362}, // labyrinth: with hole for small + "54" : {"target":365}, // labyrinth: with hole for small - this is the elephant + "85" : {"target":362}, // labyrinth: with hole for small - this is the ram + "390" : {"target":391, "type" : "build"}, // build a house + "393" : {"target":392, "type" : "hunt", "becomes":440}, + "428" : {"target":426, "expectfail":true}, // try to reach unreachable obelisk. + "422" : {"target":363}, // Get out of impassable house + }; + + // order units to move + for (let test in tests) + { + let cmpTesterAI = Engine.QueryInterface(+test, IID_UnitAI); + let cmpPos = Engine.QueryInterface(tests[test].target, IID_Position); + + if (tests[test].type && tests[test].type === "build") + { + cmpTesterAI.Repair(tests[test].target, false, false); + continue; + } + else if (tests[test].type && tests[test].type === "hunt") + { + cmpTesterAI.Gather(tests[test].target, false); + continue; + } + + if (!tests[test].continuous) + cmpTesterAI.Walk(cmpPos.GetPosition2D().x, cmpPos.GetPosition2D().y, false); + else + { + let TgPos = new Vector2D(cmpPos.GetPosition2D().x,cmpPos.GetPosition2D().y); + let MyCmpPos = Engine.QueryInterface(+test, IID_Position); + let MyPos = new Vector2D(MyCmpPos.GetPosition2D().x,MyCmpPos.GetPosition2D().y); + + // must be below C++ constant for "short pathfinder only" + let vector = new Vector2D(TgPos.x,TgPos.y); + vector = vector.sub(MyPos).normalize().mult(3.4); // 2 happened to put a waypoint inside a unit, which is unreachable. + + let position = new Vector2D(MyPos.x,MyPos.y); + while (position.distanceToSquared(TgPos) > 12) + { + position.add(vector); + cmpTesterAI.Walk(position.x, position.y, true); + } + } + } + + // set up traders, they're not tested but their behavior should be looked at. + let traders = Engine.GetEntitiesWithInterface(IID_Trader); + for (let tID of traders) + { + let cmpTraderAI = Engine.QueryInterface(+tID, IID_UnitAI); + cmpTraderAI.SetupTradeRoute(401, 402, undefined, false); + } + + let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); + + cmpTrigger.RegisterTrigger("OnInterval", "CheckUnits", { + "enabled": true, + "delay": 2 * 1000, + "interval": 1 * 1000, + }); +} + +let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); +cmpTrigger.DoAfterDelay(4000, "setupTests", {}); + +function Success(test) +{ + warn("SUCCESS : test " + test + " succeeded"); + delete(tests[test]); +} + +function Fail(test) +{ + error("ERROR : test " + test + " failed"); + delete(tests[test]); + failedTests++; +} + +function testBuild(test) +{ + let cmpFoundation = Engine.QueryInterface(tests[test].target, IID_Foundation); + if (cmpFoundation.GetBuildProgress() > 0.1) + Success(test); +} + +function testHunt(test) +{ + let cmpResourceSupply = Engine.QueryInterface(tests[test].becomes, IID_ResourceSupply); + if (cmpResourceSupply && cmpResourceSupply.GetCurrentAmount() < cmpResourceSupply.GetMaxAmount()) + Success(test); +} + +function testWalk(test) +{ + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + let time = cmpTimer.GetTime(); + + let cmpTesterAI = Engine.QueryInterface(+test, IID_UnitAI); + let cmpTesterUM = Engine.QueryInterface(+test, IID_UnitMotion); + if (cmpTesterUM.IsTryingToMove()) + return; + + let cmpPos = Engine.QueryInterface(tests[test].target, IID_Position); + let TgPos = new Vector2D(cmpPos.GetPosition2D().x,cmpPos.GetPosition2D().y); + let MyCmpPos = Engine.QueryInterface(+test, IID_Position); + let MyPos = new Vector2D(MyCmpPos.GetPosition2D().x,MyCmpPos.GetPosition2D().y); + + cmpTesterAI.Stop(); + + if (MyPos.distanceTo(TgPos) > 10 || (tests[test].underTime && time > tests[test].underTime)) + if (!tests[test].expectfail) + { + Fail(test); + return; + } + else if (tests[test].expectfail && MyPos.distanceTo(TgPos) <= 8 && (!tests[test].underTime || time <= tests[test].underTime)) + { + Fail(test); + return; + } + Success(test); +} + + +Trigger.prototype.CheckUnits = function(data) +{ + // Check all tests + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + let time = cmpTimer.GetTime(); + + let testsleft = 0; + for (let test in tests) + { + testsleft++; + let cmpTesterAI = Engine.QueryInterface(+test, IID_UnitAI); + + if (!tests[test].type) + testWalk(test); + else if (tests[test].type === "build") + testBuild(test); + else if (tests[test].type === "hunt") + testHunt(test); + } + if (time > 120000) + for (let test in tests) + { + let cmpTesterAI = Engine.QueryInterface(+test, IID_UnitAI); + cmpTesterAI.Stop(); + Fail(test); + } + if (testsleft === 0) + { + if (failedTests > 0) + QueryPlayerIDInterface(1, IID_Player).SetState("defeated"); + else + QueryPlayerIDInterface(1, IID_Player).SetState("won"); + } +} Index: binaries/data/mods/public/maps/scenarios/Pathfinding_integrated_testmap.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/scenarios/Pathfinding_integrated_testmap.xml @@ -0,0 +1,3060 @@ + + + + + default + + + + + + + 0 + 0.5 + + + + + ocean + + + 177.809 + 4 + 0.45 + 0 + + + + 0 + 1 + 0.99 + 0.1999 + default + + + + + + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + \ No newline at end of file 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 @@ -865,7 +865,7 @@ { var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) - minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed()); + minSpeed = Math.min(minSpeed, cmpUnitMotion.GetSpeed()); } minSpeed *= this.GetSpeedMultiplier(); 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 @@ -596,8 +596,10 @@ let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { - "walk": cmpUnitMotion.GetWalkSpeed(), - "run": cmpUnitMotion.GetRunSpeed() + "walk": cmpUnitMotion.GetBaseSpeed(), + "run": cmpUnitMotion.GetBaseSpeed() * cmpUnitMotion.GetTopSpeedRatio(), + "current": cmpUnitMotion.GetSpeed(), + "tryingToMove" : cmpUnitMotion.IsTryingToMove() }; return ret; Index: binaries/data/mods/public/simulation/components/RallyPoint.js =================================================================== --- binaries/data/mods/public/simulation/components/RallyPoint.js +++ binaries/data/mods/public/simulation/components/RallyPoint.js @@ -23,6 +23,8 @@ var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + let cmpOurPosition = Engine.QueryInterface(this.entity, IID_Position); // We must not affect the simulation state here (modifications of the // RallyPointRenderer are allowed though), so copy the state @@ -49,13 +51,31 @@ if (!targetPosition) continue; - if (this.pos[i].x == targetPosition.x && this.pos[i].z == targetPosition.y) + // if the target has a static obstruction, move the rallypoint position closer to us + // keep this in sync with unit_actions.js + let position = {}; + let entityTemplateName = cmpTemplateManager.GetCurrentTemplateName(this.data[i].target); + let targetTemplate = cmpTemplateManager.GetTemplate(entityTemplateName); + if (targetTemplate.Obstruction && targetTemplate.Obstruction.Static) + { + let ourPosition = cmpOurPosition.GetPosition2D(); + let size = Math.min(+targetTemplate.Obstruction.Static["@width"], +targetTemplate.Obstruction.Static["@depth"]); + let vector = new Vector2D(targetPosition.x-ourPosition.x,targetPosition.y-ourPosition.y); + let pos = new Vector2D(targetPosition.x, targetPosition.y); + pos = pos.sub(vector.normalize().mult(size * 0.49)); + position.x = pos.x; + position.y = pos.y; + } + else + position = targetPosition; + + if (this.pos[i].x == position.x && this.pos[i].z == position.y) continue; - ret[i] = { "x": targetPosition.x, "z": targetPosition.y }; + ret[i] = { "x": position.x, "z": position.y }; var cmpRallyPointRenderer = Engine.QueryInterface(this.entity, IID_RallyPointRenderer); if (cmpRallyPointRenderer) - cmpRallyPointRenderer.UpdatePosition(i, targetPosition); + cmpRallyPointRenderer.UpdatePosition(i, position); } 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 @@ -1,3 +1,5 @@ +const WALKING_SPEED = 1.0 + function UnitAI() {} UnitAI.prototype.Schema = @@ -219,14 +221,14 @@ } // Move a tile 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.SetNextState("INDIVIDUAL.WALKING"); } else { - // We are already at the target, or can't move at all + // We can't reach the target this.FinishOrder(); } }, @@ -276,7 +278,7 @@ if (!this.order.data.max) this.MoveToPoint(this.order.data.x, this.order.data.z); else - this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max); + this.MoveToPointRange(this.order.data.x, this.order.data.z, (this.order.data.min + this.order.data.max) / 2.0); if (this.IsAnimal()) this.SetNextState("ANIMAL.WALKING"); else @@ -339,7 +341,7 @@ } else { - // We are already at the target, or can't move at all + // We can't reach the target this.StopMoving(); this.FinishOrder(); } @@ -372,7 +374,7 @@ } else { - // We are already at the target, or can't move at all + // We can't reach the target this.StopMoving(); this.SetNextState("INDIVIDUAL.PICKUP.LOADING"); } @@ -385,7 +387,7 @@ return; } - if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) + if (this.MoveToTargetRangeExplicit(this.isGuardOf, this.guardRange, true)) this.SetNextState("INDIVIDUAL.GUARD.ESCORTING"); else this.SetNextState("INDIVIDUAL.GUARD.GUARDING"); @@ -395,7 +397,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()) @@ -405,7 +407,7 @@ } else { - // We are already at the target, or can't move at all + // We can't reach the target this.StopMoving(); this.FinishOrder(); } @@ -617,7 +619,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(); @@ -643,7 +645,7 @@ Engine.QueryInterface(this.entity, IID_ResourceGatherer).CommitResources(dropsiteTypes); // Stop showing the carried resource animation. - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); // Our next order should always be a Gather, // so just switch back to that order @@ -694,7 +696,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(); @@ -827,7 +829,7 @@ // Only used by other orders to walk there in formation "Order.WalkToTargetRange": function(msg) { - if (this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max)) + if (this.MoveToTargetRangeExplicit(this.order.data.target, (this.order.data.min+this.order.data.max)/2.0)) this.SetNextState("WALKING"); else this.FinishOrder(); @@ -841,7 +843,7 @@ }, "Order.WalkToPointRange": function(msg) { - if (this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max)) + if (this.MoveToPointRange(this.order.data.x, this.order.data.z, (this.order.data.min + this.order.data.max)/2.0)) this.SetNextState("WALKING"); else this.FinishOrder(); @@ -1071,7 +1073,7 @@ cmpFormation.MoveMembersIntoFormation(true, true); }, - "MoveCompleted": function(msg) { + "MoveSucess": function(msg) { if (this.FinishOrder()) this.CallMemberFunction("ResetFinishOrder", []); }, @@ -1364,14 +1366,14 @@ } // Move a tile 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.SetNextState("WALKINGTOPOINT"); } else { - // We are already at the target, or can't move at all + // We can't reach the target. this.FinishOrder(); } }, @@ -1391,12 +1393,12 @@ "enter": function () { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); - if (cmpFormation && cmpVisual) +/* TOREPLACE if (cmpFormation && cmpVisual) { cmpVisual.ReplaceMoveAnimation("walk", cmpFormation.GetFormationAnimation(this.entity, "walk")); cmpVisual.ReplaceMoveAnimation("run", cmpFormation.GetFormationAnimation(this.entity, "run")); } - this.SelectAnimation("move"); + */ }, // Occurs when the unit has reached its destination and the controller @@ -1405,13 +1407,13 @@ // We can only finish this order if the move was really completed. if (!msg.data.error && this.FinishOrder()) return; - var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); +/* TOREPLACE var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) { cmpVisual.ResetMoveAnimation("walk"); cmpVisual.ResetMoveAnimation("run"); } - +*/ var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.SetInPosition(this.entity); @@ -1424,7 +1426,6 @@ var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.UnsetInPosition(this.entity); - this.SelectAnimation("move"); }, "MoveCompleted": function() { @@ -1585,7 +1586,6 @@ }, "MoveStarted": function() { - this.SelectAnimation("move"); }, "MoveCompleted": function() { @@ -1593,6 +1593,11 @@ }, "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; @@ -1603,10 +1608,10 @@ "WALKING": { "enter": function () { - this.SelectAnimation("move"); }, "MoveCompleted": function() { + this.StopMoving(); this.FinishOrder(); }, }, @@ -1614,10 +1619,9 @@ "WALKINGANDFIGHTING": { "enter": function () { // Show weapons rather than carried resources. - this.SetGathererAnimationOverride(true); + this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); - this.SelectAnimation("move"); }, "Timer": function(msg) { @@ -1629,6 +1633,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); this.FinishOrder(); }, }, @@ -1649,7 +1654,6 @@ } this.StartTimer(0, 1000); - this.SelectAnimation("move"); }, "leave": function() { @@ -1662,6 +1666,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); if (this.orderQueue.length == 1) this.PushOrder("Patrol",this.patrolStartPosOrder); @@ -1679,10 +1684,9 @@ "ESCORTING": { "enter": function () { // Show weapons rather than carried resources. - this.SetGathererAnimationOverride(true); + this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); - this.SelectAnimation("move"); this.SetHeldPositionOnEntity(this.isGuardOf); return false; }, @@ -1696,31 +1700,33 @@ return; } this.SetHeldPositionOnEntity(this.isGuardOf); - }, - "leave": function(msg) { - this.SetMoveSpeed(this.GetWalkSpeed()); - this.StopTimer(); - }, - - "MoveStarted": function(msg) { // Adapt the speed to the one of the target if needed - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (cmpUnitMotion.IsInTargetRange(this.isGuardOf, 0, 3*this.guardRange)) + 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)) { - var cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI); - if (cmpUnitAI) + var cmpOtherMotion = Engine.QueryInterface(this.isGuardOf, IID_UnitMotion); + if (cmpOtherMotion) { - var speed = cmpUnitAI.GetWalkSpeed(); - if (speed < this.GetWalkSpeed()) + let otherSpeed = cmpOtherMotion.GetSpeed(); + let mySpeed = cmpUnitMotion.GetSpeed(); + let speed = otherSpeed / mySpeed; + if (speed < WALKING_SPEED) this.SetMoveSpeed(speed); } } }, + "leave": function(msg) { + this.SetMoveSpeed(WALKING_SPEED); + this.StopTimer(); + }, + "MoveCompleted": function() { - this.SetMoveSpeed(this.GetWalkSpeed()); - if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) + this.StopMoving(); + this.SetMoveSpeed(WALKING_SPEED); + if (!this.MoveToTargetRangeExplicit(this.isGuardOf, this.guardRange)) this.SetNextState("GUARDING"); }, }, @@ -1747,7 +1753,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 { @@ -1774,23 +1781,17 @@ this.PlaySound("panic"); // Run quickly - var speed = this.GetRunSpeed(); - this.SelectAnimation("move"); - this.SetMoveSpeed(speed); - }, - - "HealthChanged": function() { - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); + this.SetMoveSpeed(this.GetRunSpeed()); }, "leave": function() { // Reset normal speed - this.SetMoveSpeed(this.GetWalkSpeed()); + this.SetMoveSpeed(WALKING_SPEED); }, "MoveCompleted": function() { // When we've run far enough, stop fleeing + this.StopMoving(); this.FinishOrder(); }, @@ -1811,15 +1812,14 @@ "APPROACHING": { "enter": function () { // Show weapons rather than carried resources. - this.SetGathererAnimationOverride(true); + this.SetAnimationVariant("combat"); - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function() { // Show carried resources when walking. - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); this.StopTimer(); }, @@ -1834,10 +1834,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 @@ -1970,6 +1979,7 @@ if (cmpBuildingAI) cmpBuildingAI.SetUnitAITarget(0); this.StopTimer(); + this.SelectAnimation("idle"); }, "Timer": function(msg) { @@ -2096,16 +2106,12 @@ "CHASING": { "enter": function () { // Show weapons rather than carried resources. - this.SetGathererAnimationOverride(true); + this.SetAnimationVariant("combat"); - this.SelectAnimation("move"); var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); + // Run after a fleeing target if (cmpUnitAI && cmpUnitAI.IsFleeing()) - { - // Run after a fleeing target - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); - } + this.SetMoveSpeed(this.GetRunSpeed()); this.StartTimer(1000, 1000); }, @@ -2113,15 +2119,16 @@ var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsFleeing()) return; - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); + // TODO: figure out what to do with fleeing + //var speed = this.GetRunSpeed(); + //this.SetMoveSpeed(speed); }, "leave": function() { // Reset normal speed in case it was changed - this.SetMoveSpeed(this.GetWalkSpeed()); + this.SetMoveSpeed(WALKING_SPEED); // Show carried resources when walking. - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); this.StopTimer(); }, @@ -2136,9 +2143,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"); }, }, @@ -2147,7 +2164,6 @@ "GATHER": { "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave". @@ -2207,10 +2223,12 @@ } return true; } + this.StartTimer(0, 500); return false; }, "MoveCompleted": function(msg) { + this.StopMoving(); if (msg.data.error) { // We failed to reach the target @@ -2258,7 +2276,16 @@ 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); @@ -2271,10 +2298,10 @@ // Walking to a good place to gather resources near, used by GatherNearPosition "WALKING": { "enter": function() { - this.SelectAnimation("move"); }, "MoveCompleted": function(msg) { + this.StopMoving(); var resourceType = this.order.data.type; var resourceTemplate = this.order.data.template; @@ -2368,6 +2395,7 @@ // off to a different target.) if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) { + this.FaceTowardsTarget(this.gatheringTarget); var typename = "gather_" + this.order.data.type.specific; this.SelectAnimation(typename, false, 1.0, typename); } @@ -2385,7 +2413,8 @@ delete this.gatheringTarget; // Show the carried resource, if we've gathered anything. - this.SetGathererAnimationOverride(); + this.SelectAnimation("idle"); + this.SetAnimationVariant(); }, "Timer": function(msg) { @@ -2455,10 +2484,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; @@ -2533,7 +2561,6 @@ "APPROACHING": { "enter": function () { - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, @@ -2554,6 +2581,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); this.SetNextState("HEALING"); }, }, @@ -2584,6 +2612,7 @@ }, "leave": function() { + this.SelectAnimation("idle"); this.StopTimer(); }, @@ -2634,7 +2663,6 @@ }, "CHASING": { "enter": function () { - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, @@ -2653,6 +2681,7 @@ } }, "MoveCompleted": function () { + this.StopMoving(); this.SetNextState("HEALING"); }, }, @@ -2662,19 +2691,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 @@ -2684,7 +2735,7 @@ cmpResourceGatherer.CommitResources(dropsiteTypes); // Stop showing the carried resource animation. - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); // Our next order should always be a Gather, // so just switch back to that order @@ -2692,23 +2743,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", }, }, @@ -2720,10 +2757,10 @@ "APPROACHINGMARKET": { "enter": function () { - this.SelectAnimation("move"); }, "MoveCompleted": function() { + this.StopMoving(); if (this.waypoints && this.waypoints.length) { if (!this.MoveToMarket(this.order.data.target)) @@ -2748,10 +2785,23 @@ "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"); }, }, @@ -2796,6 +2846,8 @@ if (cmpBuilderList) cmpBuilderList.AddBuilder(this.entity); + this.FaceTowardsTarget(this.repairTarget); + this.SelectAnimation("build", false, 1.0, "build"); this.StartTimer(1000, 1000); return false; @@ -2806,6 +2858,7 @@ if (cmpBuilderList) cmpBuilderList.RemoveBuilder(this.entity); delete this.repairTarget; + this.SelectAnimation("idle"); this.StopTimer(); }, @@ -2825,10 +2878,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 + } }, }, @@ -2852,7 +2908,7 @@ { let dropsiteTypes = cmpResourceDropsite.GetTypes(); cmpResourceGatherer.CommitResources(dropsiteTypes); - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); } // We finished building it. @@ -2861,7 +2917,7 @@ { if (this.CanReturnResource(msg.data.newentity, true)) { - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false }); } return; @@ -2880,7 +2936,7 @@ { if (this.CanReturnResource(msg.data.newentity, true)) { - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); this.PushOrder("ReturnResource", { "target": msg.data.newentity, "force": false }); } this.PerformGather(msg.data.newentity, true, false); @@ -2945,10 +3001,10 @@ "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); }, "MoveCompleted": function() { + this.StopMoving(); if (this.IsUnderAlert() && this.alertGarrisoningTarget) { // check that we can garrison in the building we're supposed to garrison in @@ -3026,7 +3082,7 @@ if (cmpResourceGatherer) { cmpResourceGatherer.CommitResources(dropsiteTypes); - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); } } @@ -3102,6 +3158,7 @@ this.StopTimer(); var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(false); + this.SelectAnimation("idle"); }, "Timer": function(msg) { @@ -3148,10 +3205,10 @@ "PICKUP": { "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); }, "MoveCompleted": function() { + this.StopMoving(); this.SetNextState("LOADING"); }, @@ -3202,14 +3259,14 @@ "Order.LeaveFoundation": function(msg) { // Move a tile 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(); } }, @@ -3227,7 +3284,6 @@ "ROAMING": { "enter": function() { // Walk in a random direction - this.SelectAnimation("walk", false, this.GetWalkSpeed()); this.MoveRandomly(+this.template.RoamDistance); // Set a random timer to switch to feeding state this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); @@ -3266,6 +3322,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); this.MoveRandomly(+this.template.RoamDistance); }, }, @@ -3298,7 +3355,7 @@ } }, - "MoveCompleted": function() { }, + "MoveCompleted": function() { this.StopMoving(); }, "Timer": function(msg) { this.SetNextState("ROAMING"); @@ -3683,6 +3740,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]; @@ -4010,12 +4070,14 @@ //// Message handlers ///// -UnitAI.prototype.OnMotionChanged = function(msg) +UnitAI.prototype.OnMoveSuccess = 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" : false }}); +}; + +UnitAI.prototype.OnMoveFailure = function(msg) +{ + this.UnitFsm.ProcessMessage(this, { "type": "MoveCompleted", "data": { "error" : true }}); }; UnitAI.prototype.OnGlobalConstructionFinished = function(msg) @@ -4076,22 +4138,10 @@ //// Helper functions to be called by the FSM //// -UnitAI.prototype.GetWalkSpeed = function() -{ - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.GetWalkSpeed(); -}; - UnitAI.prototype.GetRunSpeed = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - var runSpeed = cmpUnitMotion.GetRunSpeed(); - var walkSpeed = cmpUnitMotion.GetWalkSpeed(); - if (runSpeed <= walkSpeed) - return runSpeed; - var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); - var health = cmpHealth.GetHitpoints()/cmpHealth.GetMaxHitpoints(); - return (health*runSpeed + (1-health)*walkSpeed); + return cmpUnitMotion.GetTopSpeedRatio(); }; /** @@ -4277,37 +4327,42 @@ } }; -UnitAI.prototype.SetGathererAnimationOverride = function(disable) +// Select a visual actor variant for the purpose of animation +// This allows changing the walk animation for normal stance, combat stances, carrying stances… +// without using a hack of replacing the animation in code like we used to. +UnitAI.prototype.SetAnimationVariant = function(type) { - var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); - if (!cmpResourceGatherer) - return; - - var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; - // Remove the animation override, so that weapons are shown again. - if (disable) + if (!type || type == "normal") { - cmpVisual.ResetMoveAnimation("walk"); - return; - } + // switch between default and carrying resources depending. - // Work out what we're carrying, in order to select an appropriate animation - var type = cmpResourceGatherer.GetLastCarriedType(); - if (type) - { - var typename = "carry_" + type.generic; + let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + if (!cmpResourceGatherer) + { + cmpVisual.SetVariant("animationVariant", ""); + return; + } - // Special case for meat - if (type.specific == "meat") - typename = "carry_" + type.specific; + let type = cmpResourceGatherer.GetLastCarriedType(); + if (type) + { + let typename = "carry_" + type.generic; + + // Special case for meat + if (type.specific == "meat") + typename = "carry_" + type.specific; - cmpVisual.ReplaceMoveAnimation("walk", typename); + cmpVisual.SetVariant("animationVariant", typename); + } + else + cmpVisual.SetVariant("animationVariant", ""); } - else - cmpVisual.ResetMoveAnimation("walk"); + else if (type === "combat") + cmpVisual.SetVariant("animationVariant", "combat"); }; UnitAI.prototype.SelectAnimation = function(name, once, speed, sound) @@ -4316,17 +4371,6 @@ if (!cmpVisual) return; - // Special case: the "move" animation gets turned into a special - // movement mode that deals with speeds and walk/run automatically - if (name == "move") - { - // Speed to switch from walking to running animations - var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2; - - cmpVisual.SelectMovementAnimation(runThreshold); - return; - } - var soundgroup; if (sound) { @@ -4359,31 +4403,34 @@ UnitAI.prototype.StopMoving = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpUnitMotion.StopMoving(); + cmpUnitMotion.DiscardMove(); }; UnitAI.prototype.MoveToPoint = function(x, z) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToPointRange(x, z, 0, 0); + cmpUnitMotion.SetAbortIfStuck(3); + return cmpUnitMotion.SetNewDestinationAsPosition(x, z, 0, true); }; -UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) +UnitAI.prototype.MoveToPointRange = function(x, z, range) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); + cmpUnitMotion.SetAbortIfStuck(3); + return cmpUnitMotion.SetNewDestinationAsPosition(x, z, range, true); }; -UnitAI.prototype.MoveToTarget = function(target) +UnitAI.prototype.MoveToTarget = function(target, evenUnreachable = false) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, 0, 0); + cmpUnitMotion.SetAbortIfStuck(5); + return cmpUnitMotion.SetNewDestinationAsEntity(target, 0, evenUnreachable); }; -UnitAI.prototype.MoveToTargetRange = function(target, iid, type) +UnitAI.prototype.MoveToTargetRange = function(target, iid, type, evenUnreachable = false) { if (!this.CheckTargetVisible(target) || this.IsTurret()) return false; @@ -4394,7 +4441,9 @@ var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); + cmpUnitMotion.SetAbortIfStuck(5); + // generally speaking, try to aim for the middle of a range. + return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + range.max)/2.0, evenUnreachable); }; /** @@ -4402,7 +4451,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()) @@ -4417,7 +4466,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; @@ -4449,21 +4498,24 @@ // the parabole changes while walking, take something in the middle var guessedMaxRange = (range.max + parabolicMaxRange)/2; +// TODO: here we should give the desired range based on unit speed, our own desire to walk, and so on. var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange)) + cmpUnitMotion.SetAbortIfStuck(9); + if (cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + guessedMaxRange)/2.0, false)) return true; // if that failed, try closer - return cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange)); + return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + Math.min(range.max, parabolicMaxRange))/2.0, evenUnreachable); }; -UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) +UnitAI.prototype.MoveToTargetRangeExplicit = function(target, range, evenUnreachable = false) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, min, max); + cmpUnitMotion.SetAbortIfStuck(5); + return cmpUnitMotion.SetNewDestinationAsEntity(target, range, evenUnreachable); }; UnitAI.prototype.MoveToGarrisonRange = function(target) @@ -4477,13 +4529,14 @@ var range = cmpGarrisonHolder.GetLoadingRange(); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); + cmpUnitMotion.SetAbortIfStuck(5); + return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + range.max)/2.0, false); // presumably we always want to know if we can't garrison here }; UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max) { - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.IsInPointRange(x, z, min, max); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max); }; UnitAI.prototype.CheckTargetRange = function(target, iid, type) @@ -4492,9 +4545,8 @@ if (!cmpRanged) return false; var range = cmpRanged.GetRange(type); - - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.IsInTargetRange(target, range.min, range.max); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max); }; /** @@ -4542,14 +4594,14 @@ if (maxRangeSq < 0) return false; - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.IsInTargetRange(target, range.min, Math.sqrt(maxRangeSq)); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, Math.sqrt(maxRangeSq)); }; UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max) { - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.IsInTargetRange(target, min, max); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max); }; UnitAI.prototype.CheckGarrisonRange = function(target) @@ -4563,8 +4615,8 @@ if (cmpObstruction) range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2) - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.IsInTargetRange(target, range.min, range.max); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max); }; /** @@ -5122,7 +5174,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; @@ -5693,8 +5745,8 @@ UnitAI.prototype.SetMoveSpeed = function(speed) { - var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpMotion.SetSpeed(speed); + var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetSpeed(speed); }; UnitAI.prototype.SetHeldPosition = function(x, z) @@ -5999,7 +6051,7 @@ var tz = pos.z + (2*Math.random()-1)*jitter; var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpMotion.MoveToPointRange(tx, tz, distance, distance); + cmpMotion.SetNewDestinationAsPosition(tx, tz, distance, true); }; UnitAI.prototype.SetFacePointAfterMove = function(val) 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 @@ -241,33 +241,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 = range; + 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 = range; + this.targetMaxRange = range; return true; }; @@ -295,7 +305,7 @@ return this.IsInPointRange(targetPos.x, targetPos.y, minRange, maxRange); }; -UnitMotionFlying.prototype.GetWalkSpeed = function() +UnitMotionFlying.prototype.GetSpeed = function() { return +this.template.MaxSpeed; }; @@ -325,7 +335,7 @@ // 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 @@ -1,322 +1,172 @@ Engine.LoadHelperScript("FSM.js"); -Engine.LoadHelperScript("Entity.js"); -Engine.LoadHelperScript("Player.js"); -Engine.LoadComponentScript("interfaces/Attack.js"); -Engine.LoadComponentScript("interfaces/Auras.js"); -Engine.LoadComponentScript("interfaces/BuildingAI.js"); -Engine.LoadComponentScript("interfaces/Capturable.js"); -Engine.LoadComponentScript("interfaces/DamageReceiver.js"); -Engine.LoadComponentScript("interfaces/Formation.js"); -Engine.LoadComponentScript("interfaces/Heal.js"); -Engine.LoadComponentScript("interfaces/Health.js"); -Engine.LoadComponentScript("interfaces/Pack.js"); -Engine.LoadComponentScript("interfaces/ResourceSupply.js"); -Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); -Engine.LoadComponentScript("Formation.js"); Engine.LoadComponentScript("UnitAI.js"); -/* Regression test. - * Tests the FSM behaviour of a unit when walking as part of a formation, - * then exiting the formation. - * mode == 0: There is no enemy unit nearby. - * mode == 1: There is a live enemy unit nearby. - * mode == 2: There is a dead enemy unit nearby. - */ -function TestFormationExiting(mode) -{ - ResetState(); +Engine.LoadComponentScript("interfaces/Timer.js"); +Engine.LoadComponentScript("interfaces/Heal.js"); +Engine.LoadComponentScript("interfaces/Sound.js"); +Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); +Engine.LoadComponentScript("interfaces/DamageReceiver.js"); +Engine.LoadComponentScript("interfaces/Pack.js"); + +Engine.LoadHelperScript("Sound.js"); - var playerEntity = 5; - var unit = 10; - var enemy = 20; - var controller = 30; +const PLAYER_ENTITY = 2; +const UNIT_ID = 3; +const TARGET_ENTITY = 4; +var lastAnimationSet = ""; +function SetupMocks() +{ AddMock(SYSTEM_ENTITY, IID_Timer, { SetInterval: function() { }, SetTimeout: function() { }, }); - AddMock(SYSTEM_ENTITY, IID_RangeManager, { - CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) { - return 1; - }, - EnableActiveQuery: function(id) { }, - ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; }, - DisableActiveQuery: function(id) { }, - GetEntityFlagMask: function(identifier) { }, - }); - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - GetCurrentTemplateName: function(ent) { return "formations/line_closed"; }, + GetCurrentTemplateName: function(ent) { return "units/gaul_infantry_spearman_b"; }, }); - AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - GetPlayerByID: function(id) { return playerEntity; }, - GetNumPlayers: function() { return 2; }, - }); - - AddMock(playerEntity, IID_Player, { - IsAlly: function() { return false; }, - IsEnemy: function() { return true; }, - GetEnemies: function() { return []; }, + AddMock(UNIT_ID, IID_Sound, { + PlaySoundGroup: function() {}, }); - - var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); - - AddMock(unit, IID_Identity, { - GetClassesList: function() { return []; }, + AddMock(UNIT_ID, IID_Position, { + "IsInWorld" : function() { return true; }, + "GetPosition" : function() { return new Vector2D(0,0); } }); - AddMock(unit, IID_Ownership, { - GetOwner: function() { return 1; }, + AddMock(UNIT_ID, IID_UnitMotion, { + GetTopSpeedRatio : function() { return 0; }, + SetSpeed: function() {}, }); - AddMock(unit, IID_Position, { - GetTurretParent: function() { return INVALID_ENTITY; }, - GetPosition: function() { return new Vector3D(); }, - GetPosition2D: function() { return new Vector2D(); }, - GetRotation: function() { return { "y": 0 }; }, - IsInWorld: function() { return true; }, + AddMock(UNIT_ID, IID_DamageReceiver, { + SetInvulnerability : function() {}, }); - AddMock(unit, IID_UnitMotion, { - GetWalkSpeed: function() { return 1; }, - MoveToFormationOffset: function(target, x, z) { }, - IsInTargetRange: function(target, min, max) { return true; }, - MoveToTargetRange: function(target, min, max) { }, - StopMoving: function() { }, - GetPassabilityClassName: function() { return "default"; }, + AddMock(UNIT_ID, IID_Pack, { + Pack : function() {}, + Unpack : function() { }, }); - AddMock(unit, IID_Vision, { - GetRange: function() { return 10; }, - }); - - AddMock(unit, IID_Attack, { - GetRange: function() { return { "max": 10, "min": 0}; }, - GetFullAttackRange: function() { return { "max": 40, "min": 0}; }, - GetBestAttackAgainst: function(t) { return "melee"; }, - GetPreference: function(t) { return 0; }, - GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; }, - CanAttack: function(v) { return true; }, - CompareEntitiesByPreference: function(a, b) { return 0; }, - }); - - unitAI.OnCreate(); - - unitAI.SetupRangeQuery(1); - - - if (mode == 1) - { - AddMock(enemy, IID_Health, { - GetHitpoints: function() { return 10; }, - }); - AddMock(enemy, IID_UnitAI, { - IsAnimal: function() { return false; } - }); - } - else if (mode == 2) - AddMock(enemy, IID_Health, { - GetHitpoints: function() { return 0; }, - }); - - var controllerFormation = ConstructComponent(controller, "Formation", {"FormationName": "Line Closed", "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppyness": 0}); - var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" }); - - AddMock(controller, IID_Position, { - JumpTo: function(x, z) { this.x = x; this.z = z; }, - GetTurretParent: function() { return INVALID_ENTITY; }, - GetPosition: function() { return new Vector3D(this.x, 0, this.z); }, - GetPosition2D: function() { return new Vector2D(this.x, this.z); }, - GetRotation: function() { return { "y": 0 }; }, - IsInWorld: function() { return true; }, - }); - - AddMock(controller, IID_UnitMotion, { - SetSpeed: function(speed) { }, - MoveToPointRange: function(x, z, minRange, maxRange) { }, - GetPassabilityClassName: function() { return "default"; }, - }); - - controllerAI.OnCreate(); - - - TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.IDLE"); - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); - - 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"); - - controllerFormation.Disband(); - - if (mode == 0) - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); - else if (mode == 1) - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); - else if (mode == 2) - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); - else - TS_FAIL("invalid mode"); -} - -function TestMoveIntoFormationWhileAttacking() -{ - ResetState(); - - var playerEntity = 5; - var controller = 10; - var enemy = 20; - var unit = 30; - var units = []; - var unitCount = 8; - var unitAIs = []; - - AddMock(SYSTEM_ENTITY, IID_Timer, { - SetInterval: function() { }, - SetTimeout: function() { }, + AddMock(UNIT_ID, IID_Visual, { + SelectAnimation : function(name) { lastAnimationSet = name; }, + SetVariant : function(key, name) { }, }); - - +/* AddMock(SYSTEM_ENTITY, IID_RangeManager, { CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) { return 1; }, EnableActiveQuery: function(id) { }, - ResetActiveQuery: function(id) { return [enemy]; }, + ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; }, DisableActiveQuery: function(id) { }, GetEntityFlagMask: function(identifier) { }, }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - GetCurrentTemplateName: function(ent) { return "formations/line_closed"; }, + GetCurrentTemplateName: function(ent) { return "units/gaul_infantry_spearman_b"; }, }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - GetPlayerByID: function(id) { return playerEntity; }, - GetNumPlayers: function() { return 2; }, - }); - - AddMock(playerEntity, IID_Player, { - IsAlly: function() { return false; }, - IsEnemy: function() { return true; }, - GetEnemies: function() { return []; }, - }); - - // create units - for (var i = 0; i < unitCount; i++) { - - units.push(unit + i); - - var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); - - AddMock(unit + i, IID_Identity, { - GetClassesList: function() { return []; }, - }); - - AddMock(unit + i, IID_Ownership, { - GetOwner: function() { return 1; }, - }); - - AddMock(unit + i, IID_Position, { - GetTurretParent: function() { return INVALID_ENTITY; }, - GetPosition: function() { return new Vector3D(); }, - GetPosition2D: function() { return new Vector2D(); }, - GetRotation: function() { return { "y": 0 }; }, - IsInWorld: function() { return true; }, - }); - - AddMock(unit + i, IID_UnitMotion, { - GetWalkSpeed: function() { return 1; }, - MoveToFormationOffset: function(target, x, z) { }, - IsInTargetRange: function(target, min, max) { return true; }, - MoveToTargetRange: function(target, min, max) { }, - StopMoving: function() { }, - GetPassabilityClassName: function() { return "default"; }, - }); - - AddMock(unit + i, IID_Vision, { - GetRange: function() { return 10; }, - }); - - AddMock(unit + i, IID_Attack, { - GetRange: function() { return {"max":10, "min": 0}; }, - GetFullAttackRange: function() { return { "max": 40, "min": 0}; }, - GetBestAttackAgainst: function(t) { return "melee"; }, - GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; }, - CanAttack: function(v) { return true; }, - CompareEntitiesByPreference: function(a, b) { return 0; }, - }); - - unitAI.OnCreate(); - - unitAI.SetupRangeQuery(1); - - unitAIs.push(unitAI); - } - - // create enemy - AddMock(enemy, IID_Health, { - GetHitpoints: function() { return 40; }, - }); - - var controllerFormation = ConstructComponent(controller, "Formation", {"FormationName": "Line Closed", "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppyness": 0}); - var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" }); - - AddMock(controller, IID_Position, { - GetTurretParent: function() { return INVALID_ENTITY; }, - JumpTo: function(x, z) { this.x = x; this.z = z; }, - GetPosition: function() { return new Vector3D(this.x, 0, this.z); }, - GetPosition2D: function() { return new Vector2D(this.x, this.z); }, - GetRotation: function() { return { "y": 0 }; }, - IsInWorld: function() { return true; }, - }); - - AddMock(controller, IID_UnitMotion, { - SetSpeed: function(speed) { }, - MoveToPointRange: function(x, z, minRange, maxRange) { }, - IsInTargetRange: function(target, min, max) { return true; }, - GetPassabilityClassName: function() { return "default"; }, - }); - - AddMock(controller, IID_Attack, { - GetRange: function() { return {"max":10, "min": 0}; }, - CanAttackAsFormation: function() { return false; }, - }); - - controllerAI.OnCreate(); + GetPlayerByID: function(id) { return PLAYER_ENTITY; }, + GetNumPlayers: function() { return 1; }, + });*/ +} - controllerFormation.SetMembers(units); +// The intention of this test is to validate that unitAI states that select an animation correctly reset it when leaving +// This tests on "unevaled" FSM state instead of trying to get every state because it's basically a nightmare to get 100% coverage in UnitAI +// And this seems to be good enough to actually detect the bugs. +function testAnimationsAreReset() +{ + ResetState(); + SetupMocks(); - controllerAI.Attack(enemy, []); + let cmpUnitAI = ConstructComponent(UNIT_ID, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); - for each (var ent in unitAIs) { - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); - } + cmpUnitAI.OnCreate(); + TS_ASSERT_EQUALS(cmpUnitAI.UnitFsm.GetCurrentState(cmpUnitAI), "INDIVIDUAL.IDLE"); - controllerAI.MoveIntoFormation({"name": "Circle"}); + cmpUnitAI.order = {"data" : { "targetClasses" : [], "target" : TARGET_ENTITY }}; - // let all units be in position - for each (var ent in unitAIs) { - controllerFormation.SetInPosition(ent); + let TestForReset = function(cmpUnitAI, totest) + { + let shouldReset = false; + for (let fc in totest) + { + if (fc === "leave") + continue; + + let stringified = uneval(totest[fc]); + let pos = stringified.search("SelectAnimation"); + if (pos !== -1) + { + let animation = stringified.substr(pos, stringified.indexOf(")", pos) - pos) + ")"; + if (animation.search("idle") === -1 && animation.search(", true") === -1) + shouldReset = true; + } + } + if (shouldReset) + { + if (!totest.leave) + { + TS_FAIL("No leave"); + return false; + } + + let doesReset = false; + let stringified = uneval(totest.leave); + let pos = stringified.search("SelectAnimation"); + if (pos !== -1) + { + let animation = stringified.substr(pos, stringified.indexOf(")", pos) - pos) + ")"; + if (animation.search("idle") !== -1) + doesReset = true; + } + if (!doesReset) + { + TS_FAIL("No reset in the leave"); + return false; + } + } + return true; } - for each (var ent in unitAIs) { - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); + for (let i in cmpUnitAI.UnitFsmSpec.INDIVIDUAL) + { + // skip the default "Enter" states and such. + if (typeof cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i] === "function") + continue; + + // skip IDLE because the following dumb test doesn't detect it properly. + if (i === "IDLE") + continue; + + // check if this state has 2 levels or 3 levels + // eg INDIVIDUAL.FLEEING or INDIVIDUAL.COMBAT.SOMETHING + let hasChildren = false; + for (let child in cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i]) + if (typeof cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i][child] !== "function") + { + hasChildren = true; + break; + } + if (hasChildren) + { + for (let child in cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i]) + { + if (!TestForReset(cmpUnitAI, cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i][child])) + warn("Failed in " + i + " substate " + child); + } + } + else + if (!TestForReset(cmpUnitAI, cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i])) + warn("Failed in " + i); } - controllerFormation.Disband(); +// TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); } -TestFormationExiting(0); -TestFormationExiting(1); -TestFormationExiting(2); - -TestMoveIntoFormationWhileAttacking(); +testAnimationsAreReset(); \ No newline at end of file Index: binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_1.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_1.json +++ binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_1.json @@ -5,8 +5,7 @@ { "value": "Armour/Pierce", "add": 1 }, { "value": "Armour/Hack", "add": 1 }, { "value": "Armour/Crush", "add": 1 }, - { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.15 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 } ], "auraName": "Formation Reforms", "auraDescription": "All soldiers in his formation +15% speed and +1 armor." Index: binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_2.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_2.json +++ binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_2.json @@ -2,8 +2,7 @@ "type": "global", "affects": ["Javelin Infantry"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.15 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 } ], "auraName": "Peltast Reforms", "auraDescription": "All javelin infantry +15% speed." Index: binaries/data/mods/public/simulation/data/auras/athen_hero_themistocles_1.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/athen_hero_themistocles_1.json +++ binaries/data/mods/public/simulation/data/auras/athen_hero_themistocles_1.json @@ -3,8 +3,7 @@ "affects": ["Ship"], "affectedPlayers": ["MutualAlly"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.5 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.5 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.5 } ], "auraName": "Naval Commander Aura", "auraDescription": "When garrisoned in a ship, his ship is +50% faster." Index: binaries/data/mods/public/simulation/data/auras/brit_hero_boudicca.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/brit_hero_boudicca.json +++ binaries/data/mods/public/simulation/data/auras/brit_hero_boudicca.json @@ -4,7 +4,6 @@ "affects": ["Champion"], "modifications": [ { "value": "UnitMotion/WalkSpeed", "multiply": 1.25 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.25 }, { "value": "Attack/Melee/Hack", "multiply": 1.2 }, { "value": "Attack/Melee/Pierce", "multiply": 1.2 }, { "value": "Attack/Melee/Crush", "multiply": 1.2 }, Index: binaries/data/mods/public/simulation/data/auras/brit_hero_caratacos.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/brit_hero_caratacos.json +++ binaries/data/mods/public/simulation/data/auras/brit_hero_caratacos.json @@ -2,8 +2,7 @@ "type": "global", "affects": ["Soldier", "Siege"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.15 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 } ], "auraName": "Hero Aura", "auraDescription": "All soldiers and siege engines +15% speed." Index: binaries/data/mods/public/simulation/data/auras/cart_hero_hamilcar.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/cart_hero_hamilcar.json +++ binaries/data/mods/public/simulation/data/auras/cart_hero_hamilcar.json @@ -2,8 +2,7 @@ "type": "global", "affects": ["Soldier", "Siege"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.15 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 } ], "auraName": "Lightning Aura", "auraDescription": "All soldiers and siege engines +15% speed." Index: binaries/data/mods/public/simulation/data/auras/maur_pillar.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/maur_pillar.json +++ binaries/data/mods/public/simulation/data/auras/maur_pillar.json @@ -3,8 +3,7 @@ "radius": 75, "affects": ["Trader"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.20 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.20 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.20 } ], "auraDescription": "All traders in range +20% speed.", "overlayIcon": "art/textures/ui/session/auras/build_bonus.png" Index: binaries/data/mods/public/simulation/data/auras/pers_hero_darius.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/pers_hero_darius.json +++ binaries/data/mods/public/simulation/data/auras/pers_hero_darius.json @@ -2,8 +2,7 @@ "type": "global", "affects": ["Soldier", "Siege"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.15 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 } ], "auraName": "Leadership Aura", "auraDescription": "+15% movement speed for all soldiers and siege engines." Index: binaries/data/mods/public/simulation/data/auras/sele_hero_seleucus_victor.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/sele_hero_seleucus_victor.json +++ binaries/data/mods/public/simulation/data/auras/sele_hero_seleucus_victor.json @@ -4,7 +4,6 @@ "affects": ["Elephant Champion"], "modifications": [ { "value": "UnitMotion/WalkSpeed", "multiply": 1.2 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.2 }, { "value": "Attack/Melee/Hack", "multiply": 1.2 }, { "value": "Attack/Melee/Crush", "multiply": 1.2 } ], Index: binaries/data/mods/public/simulation/helpers/FSM.js =================================================================== --- binaries/data/mods/public/simulation/helpers/FSM.js +++ binaries/data/mods/public/simulation/helpers/FSM.js @@ -69,6 +69,10 @@ "leave": function() { // Called when transitioning out of this state. }, + + // Define a message handler that is an exact copy of another + // message handler defined in this Substate + "SomeMessageName": "AnotherMessageName", }, // Define a new state which is an exact copy of another @@ -194,16 +198,38 @@ { state[key] = node[key]; } - else if (key.match(/^[A-Z]+$/)) + else if (typeof node[key] === "function") + { + // New message handler + newhandlers[key] = node[key]; + } + else if (typeof node[key] === "object") { + // new substate state._refs[key] = (state._name ? state._name + "." : "") + key; // (the rest of this will be handled later once we've grabbed // all the event handlers) } - else + else if (typeof node[key] === "string") { - newhandlers[key] = node[key]; + // this can be either a reference to a message handler, or to a state. + if (!node[node[key]] && !fsm.states[node[key]]) + { + error("FSM node " + path.join(".") + " node " + key + " referring to unknown node/state " + node[key]); + return {}; + } + else if (!!node[node[key]] && !!fsm.states[node[key]]) + { + error("FSM node " + path.join(".") + " node " + key + " ambiguously referring to message handler or state " + node[key]); + return {}; + } + if (!node[node[key]]) + // new substate + state._refs[key] = (state._name ? state._name + "." : "") + key; + else + // New message handler + newhandlers[key] = node[node[key]]; } } Index: binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml @@ -32,11 +32,6 @@ 3.0 - - 9.0 - 600.0 - 5.0 - fauna/camel.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml @@ -40,9 +40,6 @@ 1.0 - - 6.0 - fauna/chicken.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_crocodile.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_crocodile.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_crocodile.xml @@ -39,9 +39,6 @@ 2.0 - - 18.0 - 20 Index: binaries/data/mods/public/simulation/templates/gaia/fauna_deer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_deer.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_deer.xml @@ -14,11 +14,6 @@ 2.0 - - 10.0 - 600.0 - 5.0 - fauna/deer.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml @@ -60,9 +60,6 @@ 3.0 - - 10.0 - fauna/elephant_african_forest.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_gazelle.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_gazelle.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_gazelle.xml @@ -12,12 +12,6 @@ pitch - - - 600.0 - 5.0 - - fauna/gazelle.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml @@ -32,11 +32,6 @@ 4.0 - - 12.0 - 600.0 - 5.0 - fauna/giraffe_adult.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml @@ -21,11 +21,6 @@ 4.0 - - 10.0 - 600.0 - 5.0 - fauna/giraffe_baby.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_hawk.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_hawk.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_hawk.xml @@ -7,7 +7,7 @@ 1.0 - 1000.0 + 1000.0 Index: binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml @@ -27,11 +27,6 @@ 5.0 - - 12.0 - 600.0 - 5.0 - fauna/horse_a.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_lion.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_lion.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_lion.xml @@ -30,9 +30,6 @@ 3.0 - - 15.0 - fauna/lion.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_lioness.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_lioness.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_lioness.xml @@ -30,9 +30,6 @@ 3.0 - - 15.0 - fauna/lioness.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_shark.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_shark.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_shark.xml @@ -44,9 +44,6 @@ ship-small 4.0 - - 35.0 - false Index: binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml @@ -19,11 +19,6 @@ 6.0 - - 15.0 - 600.0 - 5.0 - fauna/wildebeest.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml @@ -18,11 +18,6 @@ 6.0 - - 15.0 - 600.0 - 5.0 - fauna/zebra.xml Index: binaries/data/mods/public/simulation/templates/template_unit.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit.xml +++ binaries/data/mods/public/simulation/templates/template_unit.xml @@ -66,7 +66,7 @@ - 2.0 + 1.0 1.0 1 @@ -106,13 +106,6 @@ false 9 - - 15.0 - 50.0 - 0.0 - 0.1 - 0.2 - default Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml @@ -94,11 +94,7 @@ 16.5 - - 26.0 - 600.0 - 5.0 - + 2.5 92 Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_spearman.xml @@ -19,10 +19,5 @@ 22.0 - - 40.0 - 600.0 - 5.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_swordsman.xml @@ -29,10 +29,5 @@ 20.0 - - 28.75 - 600.0 - 5.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml @@ -25,8 +25,5 @@ 17.5 - - 28.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelinist.xml @@ -25,8 +25,5 @@ 17.5 - - 28.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml @@ -59,11 +59,6 @@ 16.5 - - 26.0 - 1000.0 - 10.0 - 96 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml @@ -34,10 +34,5 @@ 20.5 - - 28.0 - 1000.0 - 10.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml @@ -34,10 +34,5 @@ 20.5 - - 28.0 - 1000.0 - 10.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml @@ -28,8 +28,5 @@ 25.0 - - 40.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml @@ -29,8 +29,5 @@ 23.0 - - 40.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_elephant.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_elephant.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_elephant.xml @@ -59,11 +59,6 @@ large 8.5 - - 14.0 - 1000.0 - 10.0 - 100 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry.xml @@ -46,11 +46,6 @@ 8.5 - - 17.5 - 600.0 - 5.0 - 84 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml @@ -36,8 +36,5 @@ 11.0 - - 18.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml @@ -36,8 +36,5 @@ 16.0 - - 18.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml @@ -42,10 +42,5 @@ 7.0 - - 13.0 - 600.0 - 5.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_spearman.xml @@ -39,10 +39,5 @@ 11.5 - - 23.0 - 600.0 - 5.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_swordsman.xml @@ -33,10 +33,5 @@ 12.5 - - 16.0 - 600.0 - 5.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_dog.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_dog.xml +++ binaries/data/mods/public/simulation/templates/template_unit_dog.xml @@ -86,11 +86,7 @@ 14.5 - - 26.0 - 600.0 - 5.0 - + 2.5 30 Index: binaries/data/mods/public/simulation/templates/template_unit_fauna.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna.xml @@ -32,9 +32,7 @@ 6.5 - - 15.0 - + 2.0 true Index: binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml @@ -49,8 +49,6 @@ ship-small 11.5 - - 15.0 - + 1.0 Index: binaries/data/mods/public/simulation/templates/template_unit_hero.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero.xml @@ -60,9 +60,6 @@ 10.5 - - 22.5 - 88 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry.xml @@ -50,11 +50,7 @@ 16.5 - - 26.0 - 1000.0 - 16.0 - + 2.5 100 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml @@ -39,10 +39,5 @@ 17.0 - - 28.0 - 1000.0 - 16.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml @@ -50,11 +50,6 @@ 8.5 - - 14.0 - 1000.0 - 10.0 - 80 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry.xml @@ -38,4 +38,7 @@ actor/human/death/{gender}_death.xml + + 1.6 + Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_pikeman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_pikeman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_pikeman.xml @@ -32,8 +32,5 @@ 8.5 - - 17.5 - Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_spearman.xml @@ -31,8 +31,5 @@ 9 - - 18.75 - Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_swordsman.xml @@ -20,11 +20,6 @@ 9.5 - - 20.0 - 600.0 - 8.0 - 80 Index: binaries/data/mods/public/simulation/templates/template_unit_infantry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry.xml @@ -74,7 +74,7 @@ 100 - 2.0 + 1.0 1.0 0.5 @@ -119,9 +119,7 @@ 9 - - 18.75 - + 1.6 80 Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_pikeman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_pikeman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_pikeman.xml @@ -37,8 +37,5 @@ 6.0 - - 8.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_spearman.xml @@ -32,8 +32,5 @@ 8.5 - - 15.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_swordsman.xml @@ -29,8 +29,5 @@ 9.5 - - 16.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml @@ -34,4 +34,7 @@ attack/weapon/arrowfly.xml + + 2.0 + Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_archer.xml @@ -38,8 +38,5 @@ 8.0 - - 18.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_javelinist.xml @@ -32,8 +32,5 @@ 13.5 - - 24.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_slinger.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_slinger.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_slinger.xml @@ -32,8 +32,5 @@ 11.0 - - 24.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml @@ -22,4 +22,7 @@ 4.0 + + 1.0 + Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml @@ -69,8 +69,5 @@ 14 - - 18.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml @@ -45,9 +45,6 @@ ship-small 17.5 - - 22.0 - 60 Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml @@ -77,9 +77,6 @@ 16 - - 20 - 110 Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml @@ -69,8 +69,5 @@ 16 - - 20.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ballista.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ballista.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ballista.xml @@ -53,9 +53,6 @@ 8 - - 12.0 - 120 Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml @@ -69,9 +69,6 @@ 7 - - 10.0 - 120 Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ram.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ram.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ram.xml @@ -48,9 +48,6 @@ 8 - - 11.0 - 80 Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml @@ -71,9 +71,6 @@ 6.5 - - 10.0 - 80 Index: binaries/data/mods/public/simulation/templates/template_unit_support.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_support.xml +++ binaries/data/mods/public/simulation/templates/template_unit_support.xml @@ -31,4 +31,7 @@ passive + + 2.0 + Index: binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml +++ binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml @@ -48,7 +48,7 @@ - 2.0 + 1.0 1.0 1 @@ -87,11 +87,6 @@ 9.5 - - 16.0 - 200.0 - 0.0 - 32 Index: binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml @@ -41,11 +41,6 @@ 9 - - 12.0 - 200.0 - 0.0 - 30 Index: binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml +++ binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml @@ -49,7 +49,7 @@ 1 - 2.0 + 1.0 1.0 0.5 @@ -85,10 +85,5 @@ 8 - - 15.0 - 1000.0 - 8.0 - Index: binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_lithobolos_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_lithobolos_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_lithobolos_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/athenians/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_oxybeles_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_oxybeles_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_oxybeles_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/athenians/siege_spear.xml Index: binaries/data/mods/public/simulation/templates/units/cart_cavalry_swordsman_gaul_b.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/cart_cavalry_swordsman_gaul_b.xml +++ binaries/data/mods/public/simulation/templates/units/cart_cavalry_swordsman_gaul_b.xml @@ -21,9 +21,6 @@ 15.5 - - 24.0 - units/celts/cavalry_swordsman_b.xml Index: binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_ballista_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_ballista_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_ballista_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/carthaginians/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_oxybeles_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_oxybeles_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_oxybeles_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/carthaginians/siege_spear.xml Index: binaries/data/mods/public/simulation/templates/units/gaul_champion_fanatic.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/gaul_champion_fanatic.xml +++ binaries/data/mods/public/simulation/templates/units/gaul_champion_fanatic.xml @@ -24,11 +24,6 @@ 5 - - 1.5 - 600.0 - 5.0 - units/celts/fanatic.xml Index: binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_lithobolos_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_lithobolos_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_lithobolos_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/hellenes/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_oxybeles_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_oxybeles_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_oxybeles_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/hellenes/siege_spear.xml Index: binaries/data/mods/public/simulation/templates/units/maur_elephant_archer_b.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/maur_elephant_archer_b.xml +++ binaries/data/mods/public/simulation/templates/units/maur_elephant_archer_b.xml @@ -41,11 +41,6 @@ large 8.5 - - 14.0 - 1000.0 - 10.0 - units/mauryans/elephant_archer_b.xml Index: binaries/data/mods/public/simulation/templates/units/maur_hero_chanakya.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/maur_hero_chanakya.xml +++ binaries/data/mods/public/simulation/templates/units/maur_hero_chanakya.xml @@ -50,11 +50,6 @@ 9 - - 12.0 - 200.0 - 0.0 - 30 Index: binaries/data/mods/public/simulation/templates/units/maur_support_elephant.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/maur_support_elephant.xml +++ binaries/data/mods/public/simulation/templates/units/maur_support_elephant.xml @@ -66,9 +66,6 @@ large 5.5 - - 10.0 - 50 Index: binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_lithobolos_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_lithobolos_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_lithobolos_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/hellenes/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_polybolos_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_polybolos_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_polybolos_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/ptolemies/siege_spear.xml Index: binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_ballista_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_ballista_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_ballista_unpacked.xml @@ -15,9 +15,6 @@ 0.001 - - 0.001 - units/romans/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_onager_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_onager_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_onager_unpacked.xml @@ -24,9 +24,6 @@ 0.001 - - 0.001 - 88 Index: binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_scorpio_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_scorpio_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_scorpio_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/romans/siege_scorpio.xml Index: binaries/data/mods/public/simulation/templates/units/sele_mechanical_siege_lithobolos_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/sele_mechanical_siege_lithobolos_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/sele_mechanical_siege_lithobolos_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/hellenes/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/spart_mechanical_siege_oxybeles_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/spart_mechanical_siege_oxybeles_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/spart_mechanical_siege_oxybeles_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/athenians/siege_spear.xml Index: source/simulation2/MessageTypes.h =================================================================== --- source/simulation2/MessageTypes.h +++ source/simulation2/MessageTypes.h @@ -317,21 +317,83 @@ }; /** - * Sent by CCmpUnitMotion during Update, whenever the motion status has changed - * since the previous update. + * Sent by CCmpUnitMotion during Update, + * whenever we have started actually moving and were not moving before. + * We may or may not already have been trying to move */ -class CMessageMotionChanged : public CMessage +class CMessageBeginMove : public CMessage { public: - DEFAULT_MESSAGE_IMPL(MotionChanged) + DEFAULT_MESSAGE_IMPL(BeginMove) - CMessageMotionChanged(bool starting, bool error) : - starting(starting), error(error) + CMessageBeginMove() { } +}; + +/** + * Sent by CCmpUnitMotion during Update, + * whenever we were actually moving before, and cannot continue + * this can be because we've arrived (failed=false) or we failed moving (failed=true) + * After this message is sent, the unit won't remove/repath without orders. + * Will never be sent on the same turn as MT_BeginMove. + */ +class CMessageFinishedMove : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(FinishedMove) + + CMessageFinishedMove(bool fail) : failed(fail) + { + } + + bool failed; // move failed +}; + +/** + * Sent by CCmpUnitMotion as a hint when we have reached our destination + * The unit may start moving again on its own despite this message being sent. + */ +class CMessageMoveSuccess : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(MoveSuccess) + + CMessageMoveSuccess() + { + } +}; + +/** + * 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) + + CMessageMoveFailure() + { + } +}; - bool starting; // whether this is a start or end of movement - bool error; // whether we failed to start moving (couldn't find any path) +/** + * Sent by CCmpUnitMotion during Update, + * whenever we were actually moving before, and now stopped + * In this case, we will retry moving/pathing in the future on our own + * Unless ordered otherwise. + * We are just possibly stuck short-term, or must repath. + * Will never be sent on the same turn as MT_BeginMove. + */ +class CMessagePausedMove : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(PausedMove) + + CMessagePausedMove() + { + } }; /** Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -45,7 +45,11 @@ MESSAGE(PositionChanged) MESSAGE(InterpolatedPositionChanged) MESSAGE(TerritoryPositionChanged) -MESSAGE(MotionChanged) +MESSAGE(BeginMove) +MESSAGE(FinishedMove) +MESSAGE(MoveSuccess) +MESSAGE(MoveFailure) +MESSAGE(PausedMove) MESSAGE(RangeUpdate) MESSAGE(TerrainChanged) MESSAGE(VisibilityChanged) Index: source/simulation2/components/CCmpObstructionManager.cpp =================================================================== --- source/simulation2/components/CCmpObstructionManager.cpp +++ source/simulation2/components/CCmpObstructionManager.cpp @@ -20,6 +20,7 @@ #include "simulation2/system/Component.h" #include "ICmpObstructionManager.h" +#include "ICmpPosition.h" #include "ICmpTerrain.h" #include "simulation2/MessageTypes.h" @@ -313,7 +314,7 @@ { CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero()); CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1)); - ObstructionSquare o = { x, z, u, v, clearance, clearance }; + ObstructionSquare o = { x, z, u, v, clearance, fixed::Zero() }; return o; } @@ -465,6 +466,13 @@ } } + virtual bool IsInPointRange(entity_id_t ent, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange); + virtual bool IsInTargetRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); + virtual bool IsPointInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange); + virtual bool IsPointInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); + + bool AreShapesInRange(const ObstructionSquare& source, const ObstructionSquare& target, entity_pos_t minRange, entity_pos_t maxRange); + virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits = false); virtual bool TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, std::vector* out); virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, std::vector* out); @@ -658,6 +666,138 @@ REGISTER_COMPONENT_TYPE(ObstructionManager) +//////////////////////////////////////////////////////// +//////////////////////////////////////////////////////// +// Is In Range family of functions. Those either end up in IsPointInPointRange or AreShapesInRange + +bool CCmpObstructionManager::IsInPointRange(entity_id_t ent, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) +{ + CmpPtr cmpPosition(GetSimContext(), ent); + if (!cmpPosition) + return false; + + ObstructionSquare obstruction; + CmpPtr cmpObstruction(GetSimContext(), ent); + if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction)) + return IsPointInPointRange(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y, px, pz, minRange, maxRange); + + ObstructionSquare point; + point.x = px; + point.z = pz; + return AreShapesInRange(obstruction, point, minRange, maxRange); +} + +bool CCmpObstructionManager::IsInTargetRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) +{ + CmpPtr cmpPosition(GetSimContext(), ent); + if (!cmpPosition) + return false; + + CmpPtr cmpPositionTarget(GetSimContext(), target); + if (!cmpPositionTarget) + return false; + + ObstructionSquare obstruction; + CmpPtr cmpObstruction(GetSimContext(), ent); + if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction)) + return IsPointInTargetRange(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y, target, minRange, maxRange); + + ObstructionSquare target_obstruction; + CmpPtr cmpObstructionTarget(GetSimContext(), target); + if (!cmpObstructionTarget || !cmpObstructionTarget->GetObstructionSquare(target_obstruction)) + return IsInPointRange(ent, cmpPositionTarget->GetPosition2D().X, cmpPositionTarget->GetPosition2D().Y, minRange, maxRange); + + return AreShapesInRange(obstruction, target_obstruction, minRange, maxRange); +} + +bool CCmpObstructionManager::IsPointInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) +{ + ObstructionSquare point; + point.x = x; + point.z = z; + + CmpPtr cmpPositionTarget(GetSimContext(), target); + if (!cmpPositionTarget) + return false; + + ObstructionSquare target_obstruction; + CmpPtr cmpObstructionTarget(GetSimContext(), target); + if (!cmpObstructionTarget || !cmpObstructionTarget->GetObstructionSquare(target_obstruction)) + return IsPointInPointRange(x, z, cmpPositionTarget->GetPosition2D().X, cmpPositionTarget->GetPosition2D().Y, minRange, maxRange); + + return AreShapesInRange(point, target_obstruction, minRange, maxRange); +} + +// trivial case +bool CCmpObstructionManager::IsPointInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) +{ + CFixedVector2D pos(x, z); + + entity_pos_t distance = (pos - CFixedVector2D(px, pz)).Length() - Pathfinding::NAVCELL_SIZE / 2; // be a little permissive + + if (distance < minRange) + return false; + else if (maxRange >= entity_pos_t::Zero() && distance > maxRange) + return false; + else + return true; +} + +// hard case +bool CCmpObstructionManager::AreShapesInRange(const ObstructionSquare& source, const ObstructionSquare& target, entity_pos_t minRange, entity_pos_t maxRange) +{ + // In this function, we will give about a navcell worth of leeway to avoid weirdness + fixed navcellFix = Pathfinding::NAVCELL_SIZE * 3 / 2; // a little above √2, it's not important. + + if (source.hh == fixed::Zero() && target.hh == fixed::Zero()) + { + // sphere-sphere collision. + // Source is in range if the edge to edge distance is inferior to maxRange + // and the opposite edge to opposite edge distance is bigger than minRange + // TODO: figure out whether we actually want that + fixed distance = (CFixedVector2D(target.x, target.z) - CFixedVector2D(source.x, source.z)).Length(); + + if (distance - source.hw - target.hw - navcellFix > maxRange) + return false; + if (distance + source.hw + target.hw + navcellFix < minRange) + return false; + return true; + } + else if (source.hh != fixed::Zero() && target.hh != fixed::Zero()) + { + // square to square + // TODO: implement this. + LOGWARNING("square-square range tests not yet implemented"); + return false; + } + else + { + // to cover both remaining cases, shape a is the square one, shape b is the circular one. + const ObstructionSquare& a = source.hh == fixed::Zero() ? target : source; + const ObstructionSquare& b = source.hh == fixed::Zero() ? source : target; + + CFixedVector2D relativePosition = CFixedVector2D(b.x, b.z) - CFixedVector2D(a.x, a.z); + fixed distance = Geometry::DistanceToSquare(relativePosition, a.u, a.v, CFixedVector2D(a.hw, a.hh), true); + + // in range if the edge to edge distance is inferior to maxRange + // and if the opposite edge to opposite edge distance is more than Minrange + // This means for example that a unit is in range of a building if it is farther than clearance-buildingsize, + // which is generally going to be negative (and thus this returns true). + // NB: since calculating the opposite-edge distance of a square is annoying, we'll add min(hw,hh) instead which is OK enough I guess + // NB: from a game POV, this means units can easily fire on buildings, which is good, but it also means that buildings can easily fire on units + // Buildings are usually meant to fire from the edge, not the opposite edge, so this looks odd. + // I don't really see an easy way to fix this tbh. Depending on the case, the JS code should call + // IsPointInTargetRange with the center/correct position (so the real distance is counted) + // or just add the min(hw,hh) to the minRange of the building. + if (distance - b.hw - navcellFix > maxRange) + return false; + if (distance + b.hw + std::min(a.hw, a.hh) < minRange) + return false; + return true; + } +} + + bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits) { PROFILE("TestLine"); Index: source/simulation2/components/CCmpPathfinder.cpp =================================================================== --- source/simulation2/components/CCmpPathfinder.cpp +++ source/simulation2/components/CCmpPathfinder.cpp @@ -668,6 +668,35 @@ return m_ObstructionsDirty; } +bool CCmpPathfinder::MakeGoalReachable(entity_pos_t x0, entity_pos_t z0, PathGoal &goal, pass_class_t passClass) +{ + u16 i0, j0; + Pathfinding::NearestNavcell(x0, z0, i0, j0, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE); + if (!IS_PASSABLE(m_Grid->get(i0, j0), passClass)) + FindNearestPassableNavcell(i0, j0, passClass); + + return m_LongPathfinder.MakeGoalReachable(i0, j0, goal, passClass); +} + +u32 CCmpPathfinder::FindNearestPassableNavcell(entity_pos_t x, entity_pos_t z, u16& outI, u16& outJ, pass_class_t passClass) +{ + Pathfinding::NearestNavcell(x, z, outI, outJ, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE); + u16 i0 = outI; + u16 j0 = outJ; + FindNearestPassableNavcell(outI, outJ, passClass); + return abs(i0 - outI) + abs(j0 - outJ); +} + +void CCmpPathfinder::FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass) +{ + m_LongPathfinder.FindNearestPassableNavcell(i, j, passClass); +} + +bool CCmpPathfinder::NavcellIsReachable(u16 i0, u16 j0, u16 i1, u16 j1, pass_class_t passClass) +{ + return m_LongPathfinder.NavcellIsReachable(i0, j0, i1, j1, passClass); +} + ////////////////////////////////////////////////////////// // Async path requests: Index: source/simulation2/components/CCmpPathfinder_Common.h =================================================================== --- source/simulation2/components/CCmpPathfinder_Common.h +++ source/simulation2/components/CCmpPathfinder_Common.h @@ -237,6 +237,13 @@ virtual Grid ComputeShoreGrid(bool expandOnWater = false); + virtual bool MakeGoalReachable(entity_pos_t x0, entity_pos_t z0, PathGoal &goal, pass_class_t passClass); + + virtual u32 FindNearestPassableNavcell(entity_pos_t x, entity_pos_t z, u16& outI, u16& outJ, pass_class_t passClass); + void FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass); + + virtual bool NavcellIsReachable(u16 i0, u16 j0, u16 i1, u16 j1, pass_class_t passClass); + 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/CCmpPathfinder_Vertex.cpp =================================================================== --- source/simulation2/components/CCmpPathfinder_Vertex.cpp +++ source/simulation2/components/CCmpPathfinder_Vertex.cpp @@ -556,8 +556,13 @@ fixed rangeZMin = z0 - range; fixed rangeZMax = z0 + range; - // we don't actually add the "search space" edges as edges, since we may want to cross them - // in some cases (such as if we need to go around an obstruction that's partly out of the search range) + // add domain edges + // (The edges are the opposite direction to usual, so it's an inside-out square) + edges.emplace_back(Edge{ CFixedVector2D(rangeXMin, rangeZMin), CFixedVector2D(rangeXMin, rangeZMax) }); + edges.emplace_back(Edge{ CFixedVector2D(rangeXMin, rangeZMax), CFixedVector2D(rangeXMax, rangeZMax) }); + edges.emplace_back(Edge{ CFixedVector2D(rangeXMax, rangeZMax), CFixedVector2D(rangeXMax, rangeZMin) }); + edges.emplace_back(Edge{ CFixedVector2D(rangeXMax, rangeZMin), CFixedVector2D(rangeXMin, rangeZMin) }); + // List of obstruction vertexes (plus start/end points); we'll try to find paths through // the graph defined by these vertexes @@ -614,26 +619,23 @@ vert.status = Vertex::UNEXPLORED; vert.quadInward = QUADRANT_NONE; vert.quadOutward = QUADRANT_ALL; - vert.p.X = center.X - hd0.Dot(u); vert.p.Y = center.Y + hd0.Dot(v); if (aa) vert.quadInward = QUADRANT_BR; vertexes.push_back(vert); - if (vert.p.X < rangeXMin) rangeXMin = vert.p.X; - if (vert.p.Y < rangeZMin) rangeZMin = vert.p.Y; - if (vert.p.X > rangeXMax) rangeXMax = vert.p.X; - if (vert.p.Y > rangeZMax) rangeZMax = vert.p.Y; - vert.p.X = center.X - hd1.Dot(u); vert.p.Y = center.Y + hd1.Dot(v); if (aa) vert.quadInward = QUADRANT_TR; vertexes.push_back(vert); - if (vert.p.X < rangeXMin) rangeXMin = vert.p.X; - if (vert.p.Y < rangeZMin) rangeZMin = vert.p.Y; - if (vert.p.X > rangeXMax) rangeXMax = vert.p.X; - if (vert.p.Y > rangeZMax) rangeZMax = vert.p.Y; - vert.p.X = center.X + hd0.Dot(u); vert.p.Y = center.Y - hd0.Dot(v); if (aa) vert.quadInward = QUADRANT_TL; vertexes.push_back(vert); - if (vert.p.X < rangeXMin) rangeXMin = vert.p.X; - if (vert.p.Y < rangeZMin) rangeZMin = vert.p.Y; - if (vert.p.X > rangeXMax) rangeXMax = vert.p.X; - if (vert.p.Y > rangeZMax) rangeZMax = vert.p.Y; - vert.p.X = center.X + hd1.Dot(u); vert.p.Y = center.Y - hd1.Dot(v); if (aa) vert.quadInward = QUADRANT_BL; vertexes.push_back(vert); - if (vert.p.X < rangeXMin) rangeXMin = vert.p.X; - if (vert.p.Y < rangeZMin) rangeZMin = vert.p.Y; - if (vert.p.X > rangeXMax) rangeXMax = vert.p.X; - if (vert.p.Y > rangeZMax) rangeZMax = vert.p.Y; + + vert.p.X = center.X - hd0.Dot(u); vert.p.Y = center.Y + hd0.Dot(v); + if (aa) vert.quadInward = QUADRANT_BR; + if (vert.p.X >= rangeXMin && vert.p.Y >= rangeZMin && vert.p.X <= rangeXMax && vert.p.Y <= rangeZMax) + vertexes.push_back(vert); + vert.p.X = center.X - hd1.Dot(u); vert.p.Y = center.Y + hd1.Dot(v); + if (aa) vert.quadInward = QUADRANT_BR; + if (vert.p.X >= rangeXMin && vert.p.Y >= rangeZMin && vert.p.X <= rangeXMax && vert.p.Y <= rangeZMax) + vertexes.push_back(vert); + vert.p.X = center.X + hd0.Dot(u); vert.p.Y = center.Y - hd0.Dot(v); + if (aa) vert.quadInward = QUADRANT_TL; + if (vert.p.X >= rangeXMin && vert.p.Y >= rangeZMin && vert.p.X <= rangeXMax && vert.p.Y <= rangeZMax) + vertexes.push_back(vert); + vert.p.X = center.X + hd1.Dot(u); vert.p.Y = center.Y - hd1.Dot(v); + if (aa) vert.quadInward = QUADRANT_BL; + if (vert.p.X >= rangeXMin && vert.p.Y >= rangeZMin && vert.p.X <= rangeXMax && vert.p.Y <= rangeZMax) + vertexes.push_back(vert); // Add the edges: @@ -654,8 +656,6 @@ edges.emplace_back(Edge{ ev3, ev0 }); } - // TODO: should clip out vertexes and edges that are outside the range, - // to reduce the search space } // Add terrain obstructions Index: source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion.cpp +++ source/simulation2/components/CCmpUnitMotion.cpp @@ -27,6 +27,7 @@ #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpValueModificationManager.h" +#include "simulation2/components/ICmpVisual.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Render.h" #include "simulation2/MessageTypes.h" @@ -39,17 +40,6 @@ #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.) @@ -58,191 +48,154 @@ static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*9); /** - * Minimum distance to goal for a long path request + * 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*2); /** - * 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; + * Minimum distance to goal for a long path request + * Disabled, see note in RequestNewPath. */ -static const entity_pos_t SHORT_PATH_GOAL_RADIUS = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2); +static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*0); /** * 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 DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); /** - * 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. - */ -static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); - -/** - * 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. - */ -static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA_FORMATION = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1); - -/** - * 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_REATTEMPS = 8; -/** - * 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. - */ -static const fixed CHECK_TARGET_MOVEMENT_MIN_COS = fixed::FromInt(866)/1000; - -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); - } + private: + bool m_Valid = false; - DEFAULT_COMPONENT_ALLOCATOR(UnitMotion) - - 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; - fixed m_WalkSpeed, m_OriginalWalkSpeed; // in metres per second - fixed m_RunSpeed, m_OriginalRunSpeed; - pass_class_t m_PassClass; - std::string m_PassClassName; + SMotionGoal(CFixedVector2D position, entity_pos_t range) + { + m_Entity = INVALID_ENTITY; + m_Range = range; + m_Position = position; - // Dynamic state: + m_Valid = true; + } - entity_pos_t m_Clearance; - bool m_Moving; - bool m_FacePointAfterMove; + SMotionGoal(entity_id_t target, entity_pos_t range) + { + m_Entity = target; + m_Range = range; - 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, + 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 { ENSURE(!m_Entity); 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_Update_MotionUnit); + 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; - u32 m_ExpectedPathTicket; // asynchronous request ID we're waiting for, or 0 if none + // Template state, never changed after init. + fixed m_TemplateWalkSpeed, m_TemplateTopSpeedRatio; + pass_class_t m_PassClass; + std::string m_PassClassName; + entity_pos_t m_Clearance; - 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_TechModifiedWalkSpeed, m_TechModifiedTopSpeedRatio; + // 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. + // Will also be used whenever I implemented step-by-step long paths using the hierarchical pathfinder. + SMotionGoal m_CurrentGoal; + + // Pathfinder-compliant goal. Should be reachable (at least when it's recomputed). + PathGoal m_Goal; + + // MOTION PLANNING + // We will abort if we are stuck after X tries. + u8 m_AbortIfStuck; + // turn towards our target at the end + bool m_FacePointAfterMove; + // actual unit speed, after technology and ratio fixed m_Speed; + // cached for convenience + fixed m_SpeedRatio; - // Current mean speed (over the last turn). - fixed m_CurSpeed; + // asynchronous request ID we're waiting for, or 0 if none + u32 m_ExpectedPathTicket; + bool m_DumpPathOnResult; + bool m_RunShortPathValidation; // Currently active paths (storing waypoints in reverse order). // The last item in each path is the point we're currently heading towards. - WaypointPath m_LongPath; - WaypointPath m_ShortPath; - - // Motion planning - u8 m_Tries; // how many tries we've done to get to our current Final Goal. - - PathGoal m_FinalGoal; + WaypointPath m_Path; + // 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; + + // Speed over the last turn + // cached so we can tell the visual actor when it changes + fixed m_ActualSpeed; static std::string GetSchema() { @@ -259,14 +212,8 @@ "" "" "" - "" - "" - "" - "" - "" - "" - "" - "" + "" + "" "" "" "" @@ -276,19 +223,15 @@ virtual void Init(const CParamNode& paramNode) { - m_FormationController = paramNode.GetChild("FormationController").ToBool(); - - m_Moving = false; m_FacePointAfterMove = true; - m_WalkSpeed = m_OriginalWalkSpeed = paramNode.GetChild("WalkSpeed").ToFixed(); - m_Speed = m_WalkSpeed; - m_CurSpeed = fixed::Zero(); - - if (paramNode.GetChild("Run").IsOk()) - m_RunSpeed = m_OriginalRunSpeed = paramNode.GetChild("Run").GetChild("Speed").ToFixed(); - else - m_RunSpeed = m_OriginalRunSpeed = m_WalkSpeed; + m_TechModifiedWalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); + m_ActualSpeed = fixed::Zero(); + m_SpeedRatio = fixed::FromInt(1); + + m_TechModifiedTopSpeedRatio = m_TemplateTopSpeedRatio = fixed::FromInt(1); + if (paramNode.GetChild("RunMultiplier").IsOk()) + m_TechModifiedTopSpeedRatio = m_TemplateTopSpeedRatio = paramNode.GetChild("RunMultiplier").ToFixed(); CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) @@ -302,18 +245,16 @@ cmpObstruction->SetUnitClearance(m_Clearance); } - m_State = STATE_IDLE; - m_PathState = PATHSTATE_NONE; - m_ExpectedPathTicket = 0; + m_DumpPathOnResult = false; + m_RunShortPathValidation = false; m_Tries = 0; - - m_TargetEntity = INVALID_ENTITY; - - m_FinalGoal.type = PathGoal::POINT; + m_WaitingTurns = 0; m_DebugOverlayEnabled = false; + m_AbortIfStuck = 0; + } virtual void Deinit() @@ -323,33 +264,28 @@ 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.NumberU8("abort if stuck", m_AbortIfStuck, 0, 255); + serialize.Bool("face point after move", m_FacePointAfterMove); + serialize.NumberFixed_Unbounded("speed", m_Speed); + serialize.NumberFixed_Unbounded("speed ratio", m_SpeedRatio); serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket); + serialize.Bool("dump path on result", m_DumpPathOnResult); + serialize.Bool("short path validation", m_RunShortPathValidation); - 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", m_Speed); - serialize.NumberFixed_Unbounded("current speed", m_CurSpeed); - - 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); + + serialize.Bool("started moving", m_StartedMoving); - SerializeVector()(serialize, "long path", m_LongPath.m_Waypoints); - SerializeVector()(serialize, "short path", m_ShortPath.m_Waypoints); + // 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); - SerializeGoal()(serialize, "goal", m_FinalGoal); + m_Destination.SerializeCommon(serialize); + m_CurrentGoal.SerializeCommon(serialize); + SerializeGoal()(serialize, "goal", m_Goal); } virtual void Serialize(ISerializer& serialize) @@ -368,26 +304,15 @@ 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; + Move(dt); break; } case MT_RenderSubmit: @@ -410,24 +335,25 @@ break; } // fall-through - case MT_OwnershipChanged: case MT_Deserialized: { + // tell the visual actor our speed. + // don't call setactualspeed since the if check will return immediately. + CmpPtr cmpVisualActor(GetEntityHandle()); + if (cmpVisualActor) + cmpVisualActor->SetMovingSpeed(m_ActualSpeed); + } + case MT_OwnershipChanged: + { CmpPtr cmpValueModificationManager(GetSystemEntity()); if (!cmpValueModificationManager) break; - fixed newWalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_OriginalWalkSpeed, GetEntityId()); - fixed newRunSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/Run/Speed", m_OriginalRunSpeed, GetEntityId()); + m_TechModifiedWalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId()); + m_TechModifiedTopSpeedRatio = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateTopSpeedRatio, GetEntityId()); - // update m_Speed (the actual speed) if set to one of the variables - if (m_Speed == m_WalkSpeed) - m_Speed = newWalkSpeed; - else if (m_Speed == m_RunSpeed) - m_Speed = newRunSpeed; + m_Speed = m_SpeedRatio.Multiply(GetBaseSpeed()); - m_WalkSpeed = newWalkSpeed; - m_RunSpeed = newRunSpeed; break; } } @@ -439,19 +365,52 @@ GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender); } - virtual bool IsMoving() + virtual bool IsActuallyMoving() + { + return m_StartedMoving; + } + + virtual bool IsTryingToMove() { - return m_Moving; + return m_Destination.Valid(); } - virtual fixed GetWalkSpeed() + virtual fixed GetBaseSpeed() { - return m_WalkSpeed; + return m_TechModifiedWalkSpeed; } - virtual fixed GetRunSpeed() + virtual fixed GetSpeed() { - return m_RunSpeed; + return m_Speed; + } + + virtual fixed GetSpeedRatio() + { + return m_SpeedRatio; + } + + virtual fixed GetTopSpeedRatio() + { + return m_TechModifiedTopSpeedRatio; + } + + virtual void SetSpeed(fixed ratio) + { + m_SpeedRatio = std::min(ratio, GetTopSpeedRatio()); + m_Speed = m_SpeedRatio.Multiply(GetBaseSpeed()); + } + + // 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() @@ -472,16 +431,6 @@ m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName); } - virtual fixed GetCurrentSpeed() - { - return m_CurSpeed; - } - - virtual void SetSpeed(fixed speed) - { - m_Speed = speed; - } - virtual void SetFacePointAfterMove(bool facePointAfterMove) { m_FacePointAfterMove = facePointAfterMove; @@ -493,105 +442,121 @@ UpdateMessageSubscriptions(); } - virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange); - virtual bool IsInPointRange(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 bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); - virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z); - - virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z); - - virtual void StopMoving() - { - m_Moving = false; - m_ExpectedPathTicket = 0; - m_State = STATE_STOPPING; - m_PathState = PATHSTATE_NONE; - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); - } - virtual entity_pos_t GetUnitClearance() { return m_Clearance; } -private: - bool ShouldAvoidMovingUnits() const - { - return !m_FormationController; - } + 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); - bool IsFormationMember() const - { - return m_State == STATE_FORMATIONMEMBER_PATH; - } + // transform a motion goal into a corresponding PathGoal + // called by RecomputeGoalPosition + PathGoal CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal); - entity_id_t GetGroup() const - { - return IsFormationMember() ? m_TargetEntity : GetEntityId(); - } + // 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); - bool HasValidPath() const + virtual void SetAbortIfStuck(u8 shouldAbort) { - return m_PathState == PATHSTATE_FOLLOWING - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT; + m_AbortIfStuck = shouldAbort; } - void StartFailed() + void StartMoving() { - StopMoving(); - m_State = STATE_IDLE; // don't go through the STOPPING state since we never even started + m_StartedMoving = true; CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - CMessageMotionChanged msg(true, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + cmpObstruction->SetMovingFlag(true); } - void MoveFailed() + virtual void StopMoving() { - StopMoving(); + m_StartedMoving = false; CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->SetMovingFlag(false); - - CMessageMotionChanged msg(false, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } - void StartSucceeded() + virtual void DiscardMove() { - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); + StopMoving(); + + m_Tries = 0; + m_WaitingTurns = 0; + + m_ExpectedPathTicket = 0; + m_Path.m_Waypoints.clear(); - m_Moving = true; + m_CurrentGoal.Clear(); + m_Destination.Clear(); + } - CMessageMotionChanged msg(true, false); + void MoveWillFail() + { + CMessageMoveFailure msg; GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } - void MoveSucceeded() + void MoveHasSucceeded() { - m_Moving = false; + CMessageMoveSuccess msg; + GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + } - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + virtual bool HasValidPath() + { + return !m_Path.m_Waypoints.empty(); + } - // No longer moving, so speed is 0. - m_CurSpeed = fixed::Zero(); +private: + CFixedVector2D GetGoalPosition(const SMotionGoal& goal) const + { + ENSURE (goal.Valid()); - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + 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(); + } + else + return goal.GetPosition(); } - bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange, entity_id_t target); + bool CurrentGoalHasValidPosition() + { + if (!m_CurrentGoal.Valid()) + return false; + + if (m_CurrentGoal.IsEntity()) + { + CmpPtr cmpPosition(GetSimContext(), m_CurrentGoal.GetEntity()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return false; + return true; + } + else + return true; + } +/* + TODO: reimplement + bool IsFormationMember() const + { + return m_State == STATE_FORMATIONMEMBER_PATH; + } +*/ + entity_id_t GetGroup() const + { + //return IsFormationMember() ? m_TargetEntity : GetEntityId(); + return GetEntityId(); + } /** * Handle the result of an asynchronous path query. @@ -604,45 +569,26 @@ void Move(fixed dt); /** - * Decide whether to approximate the given range from a square target as a circle, - * rather than as a square. - */ - bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const; - - /** - * Computes the current location of our target entity (plus offset). - * Returns false if no target entity or no valid position. - */ - bool ComputeTargetPosition(CFixedVector2D& out); - - /** - * 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. + * Check that our current waypoints are sensible or whether we should recompute + * + * 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. */ - bool TryGoingStraightToGoalPoint(const CFixedVector2D& from); + void ValidateCurrentPath(); /** - * 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. + * take a 2D position and return an updated one based on estimated target velocity. */ - bool TryGoingStraightToTargetEntity(const CFixedVector2D& from); - - /** - * Returns whether the target entity has moved more than minDelta since our - * last path computations, and we're close enough to it to care. - */ - bool CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta); - - /** - * Update goal position if moving target - */ - void UpdateFinalGoal(); + inline void UpdatePositionForTargetVelocity(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 @@ -657,17 +603,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() 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. @@ -691,539 +634,428 @@ 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) return; m_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_DumpPathOnResult) { - 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_DumpPathOnResult = 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_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_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 > 1 || !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; } + } + } + + // 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) + { + m_Path.m_Waypoints.insert(m_Path.m_Waypoints.end(), path.m_Waypoints.begin(), path.m_Waypoints.end()-1); + return; + } + } + m_Path.m_Waypoints.insert(m_Path.m_Waypoints.end(), path.m_Waypoints.begin(), path.m_Waypoints.end()); +} - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); +void CCmpUnitMotion::ValidateCurrentPath() +{ + // this should be kept in sync with RequestNewPath otherwise we'll spend our whole life repathing. - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; + // don't validate points, they never change + if (!m_CurrentGoal.IsEntity()) + return; - CFixedVector2D pos = cmpPosition->GetPosition2D(); + // TODO: figure out what to do when the goal dies. + // for now we'll keep on keeping on, but reset as if our goal was a position + // and send a failure message to UnitAI in case it wants to do something + // use position as a proxy for existence + CmpPtr cmpTargetPosition(GetSimContext(), m_CurrentGoal.GetEntity()); + if (!cmpTargetPosition) + { + // TODO: this should call a custom function for this + SMotionGoal newGoal(CFixedVector2D(m_Goal.x, m_Goal.z), m_CurrentGoal.Range()); + m_Destination = newGoal; + m_CurrentGoal = newGoal; + RequestNewPath(); + MoveWillFail(); + return; + } - if (ShouldConsiderOurselvesAtDestination(pos)) - return; + // don't validate if no path. + if (!HasValidPath()) + return; - UpdateFinalGoal(); - RequestLongPath(pos, m_FinalGoal); - m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; - return; - } + // TODO: check LOS here (instead of in UnitAI like we do now). - // else we could, so reset our number of tries. - m_Tries = 0; + // if our goal can move, then perhaps it has. + CmpPtr cmpTargetUnitMotion(GetSimContext(), m_CurrentGoal.GetEntity()); + if (!cmpTargetUnitMotion) + return; - // Now we've got a short path that we can follow - if (!HasValidPath()) - StartSucceeded(); + // Check if our current Goal's position (ie m_Goal, not m_CurrentGoal) is sensible. - m_PathState = PATHSTATE_FOLLOWING; + // 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. - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); + CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); + fixed certainty = m_Clearance*2; + UpdatePositionForTargetVelocity(m_CurrentGoal.GetEntity(), targetPos.X, targetPos.Y, certainty); - m_Moving = true; - } - else - LOGWARNING("unexpected PathResult (%u %d %d)", GetEntityId(), m_State, m_PathState); + CmpPtr cmpObstructionManager(GetSystemEntity()); + if (!cmpObstructionManager->IsPointInPointRange(m_Goal.x, m_Goal.z, targetPos.X, targetPos.Y, m_CurrentGoal.Range() - certainty, m_CurrentGoal.Range() + certainty)) + RequestNewPath(); +} + +void CCmpUnitMotion::UpdatePositionForTargetVelocity(entity_id_t ent, entity_pos_t& x, entity_pos_t& z, fixed& certainty) +{ + CmpPtr cmpTargetPosition(GetSimContext(), ent); + CmpPtr cmpTargetUnitMotion(GetSimContext(), ent); + if (!cmpTargetPosition || !cmpTargetUnitMotion || !cmpTargetUnitMotion->IsActuallyMoving()) + return; + + // 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()); + + // try to estimate in how much time we'll reach it. + fixed distance = (cmpTargetPosition->GetPosition2D() - cmpPosition->GetPosition2D()).Length(); + fixed time = std::min(distance / GetSpeed(), fixed::FromInt(5)); // don't try from too far away or this is just dumb. + + CFixedVector2D travelVector = (cmpTargetPosition->GetPosition2D() - cmpTargetPosition->GetPreviousPosition2D()).Multiply(time) * 2; + x += travelVector.X; + z += travelVector.Y; + + certainty += time * 2; } +// TODO: this can probably be split in a few functions efficiently and it'd be cleaner. void CCmpUnitMotion::Move(fixed dt) { PROFILE("Move"); - if (m_State == STATE_STOPPING) + // early out + if (!IsTryingToMove()) { - m_State = STATE_IDLE; - MoveSucceeded(); + SetActualSpeed(fixed::Zero()); return; } - if (m_State == STATE_IDLE) - return; + // TODO: units will look at each other's position in an arbitrary order that must be the same for any simulation + // In particular this means no threading. Maybe we should update this someday if it's possible. - switch (m_PathState) - { - case PATHSTATE_NONE: - { - // If we're not pathing, do nothing + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) return; - } - case PATHSTATE_WAITING_REQUESTING_LONG: - case PATHSTATE_WAITING_REQUESTING_SHORT: - { - // If we're waiting for a path and don't have one yet, do nothing + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) return; - } - - case PATHSTATE_FOLLOWING: - case PATHSTATE_FOLLOWING_REQUESTING_SHORT: - case PATHSTATE_FOLLOWING_REQUESTING_LONG: - { - // TODO: there's some asymmetry here when units look at other - // units' positions - the result will depend on the order of execution. - // Maybe we should split the updates into multiple phases to minimise - // that problem. - - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return; - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; + CFixedVector2D initialPos = cmpPosition->GetPosition2D(); - CFixedVector2D initialPos = cmpPosition->GetPosition2D(); + // NB: unitMotion has been changed such that unitAI should NOT rely on "movecompleted" messages but do its own range checks on a timer basis. + // To streamline some common interactions, such as gathering from a static entity, we'll send a "hint" when we're done moving (and thus presumably arrived) + // but this should NOT be relied upon. + if (ShouldConsiderOurselvesAtDestination(m_CurrentGoal)) + { + if (m_FacePointAfterMove && CurrentGoalHasValidPosition()) + FaceTowardsPoint(GetGoalPosition(m_CurrentGoal).X, GetGoalPosition(m_CurrentGoal).Y); - // 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); - - // Keep track of the current unit's position during the update - CFixedVector2D pos = initialPos; - - // If in formation, run to keep up; otherwise just walk - fixed basicSpeed; - if (IsFormationMember()) - basicSpeed = GetRunSpeed(); - else - basicSpeed = m_Speed; // (typically but not always WalkSpeed) + bool sendMessage = false; + if (ShouldConsiderOurselvesAtDestination(m_Destination)) + // send a hint to unitAI to maintain compatibility. + sendMessage = true; - // 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 (sendMessage) + MoveHasSucceeded(); + } - fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); + // All path updates/checks should go here, before the moving loop. + ValidateCurrentPath(); - bool wasObstructed = false; + ////////////////////////////////////////////////////////////////////////////////////// + //// AFTER THIS POINT, NO MESSAGES SHOULD BE SENT OR HORRIBLE THINGS WILL HAPPEN. //// + //// YOU HAVE BEEN WARNED. //// + ////////////////////////////////////////////////////////////////////////////////////// - // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint + if (!IsTryingToMove()) + { + // One of the messages we sent UnitAI caused us to stop moving entirely. + // Tell the visual actor we're not moving this turn to avoid gliding. + SetActualSpeed(fixed::Zero()); + return; + } - fixed timeLeft = dt; - fixed zero = fixed::Zero(); + // Keep track of the current unit's position during the update + CFixedVector2D pos = initialPos; - 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; + // 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 + // TODO: note that this is also linked to pathfinding so maybe never supported + // fixed terrainSpeed = fixed::FromInt(1); - 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); - - CFixedVector2D offset = target - pos; - - // Work out how far we can travel in timeLeft - fixed maxdist = maxSpeed.Multiply(timeLeft); - - // 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; + bool wasObstructed = false; - // Spend the rest of the time heading towards the next waypoint - timeLeft = timeLeft - (offsetLength / maxSpeed); + // We want to move (at most) m_Speed*dt units from pos towards the next waypoint - if (m_ShortPath.m_Waypoints.empty()) - m_LongPath.m_Waypoints.pop_back(); - else - m_ShortPath.m_Waypoints.pop_back(); + fixed timeLeft = dt; - 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; - - 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 + // TODO: I think this may be a little buggy if we want to compute it several times per turn. + while (timeLeft > fixed::Zero()) + { + // If we ran out of path, we have to stop + if (!HasValidPath()) + break; - break; - } - } + CFixedVector2D target; + target = CFixedVector2D(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z); - // Update the Position component after our movement (if we actually moved anywhere) - if (pos != initialPos) + CFixedVector2D offset = target - pos; + fixed offsetLength = offset.Length(); + // Work out how far we can travel in timeLeft + fixed maxdist = m_Speed.Multiply(timeLeft); + + CFixedVector2D destination; + if (offsetLength <= maxdist) + destination = target; + else { - CFixedVector2D offset = pos - initialPos; - - // Face towards the target - entity_angle_t angle = atan2_approx(offset.X, offset.Y); - cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle); - - // Calculate the mean speed over this past turn. - m_CurSpeed = cmpPosition->GetDistanceTravelled() / dt; + offset.Normalize(maxdist); + destination = pos + offset; } - if (wasObstructed) + // TODO: try moving as much as we can still? + // TODO: get more information about what blocked us. + if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.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. + pos = 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 + wasObstructed = true; + break; } } - } -} -bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out) -{ - if (m_TargetEntity == INVALID_ENTITY) - return false; + if (!m_StartedMoving && wasObstructed) + // If this is the turn we start moving, and we're already obstructed, + // fail the move entirely to avoid weirdness. + // TODO: figure out if this is actually necessary with the other changes + pos = initialPos; + + // Update the Position component after our movement (if we actually moved anywhere) + if (pos != initialPos) + { + CFixedVector2D offset = pos - initialPos; + + // tell other components and visual actor we are moving. + if (!m_StartedMoving) + StartMoving(); - CmpPtr cmpPosition(GetSimContext(), m_TargetEntity); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + // Face towards the target + entity_angle_t angle = atan2_approx(offset.X, offset.Y); + cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle); - if (m_TargetOffset.IsZero()) - { - // No offset, just return the position directly - out = cmpPosition->GetPosition2D(); + // 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); + + if (!wasObstructed) + { + // everything is going smoothly, return. + m_Tries = 0; + m_WaitingTurns = 0; + return; + } } 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; - } - return true; -} + // TODO: this and the same call in the if above could probably be moved before the if entirely, check rounding. + SetActualSpeed(fixed::Zero()); -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 cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; - - // 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; + // we've had to stop at the end of the turn. + StopMoving(); - // 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 }); + //////////////////////////////////////////////////////////////////// + //// From this point onwards messages are "safe" to send again. //// + //////////////////////////////////////////////////////////////////// + + if (ShouldConsiderOurselvesAtDestination(m_CurrentGoal)) + // If we're out of path (ie not moving) but have a valid destination (IsTryingToMove()), we'll end up here every turn. + // We should not repath if we actually are where we want to be (ie at destination). + return; - return true; -} + // Oops, we've had a problem. Either we were obstructed, or we ran out of path (but still have a goal). + // Handle it. + // Failure to handle it will result in stuckness and players complaining. -bool CCmpUnitMotion::TryGoingStraightToTargetEntity(const CFixedVector2D& from) -{ - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return false; + if (m_ExpectedPathTicket != 0) + // wait until we get our path to see where that leads us. + return; - // Fail if the target is too far away - if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) - return false; + // give us some turns to recover. + // TODO: only do this if we ran into a moving unit and not something else, because something else won't move + // specifically: if we ran into a moving unit, we should wait a turn and see what happens + // if we ran into a static unit, recompute a short-path directly + // if we ran into a static obstruction, recompute long-path directly + // And then maybe we could add some finetuning based on target. + if (m_WaitingTurns == 0) + { + if (HasValidPath()) + m_WaitingTurns = MAX_PATH_REATTEMPS; // currently we won't wait at all + else + m_WaitingTurns = 3; + } - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; + --m_WaitingTurns; - // 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) + // Try again next turn, no changes + if (m_WaitingTurns >= MAX_PATH_REATTEMPS) + return; - // Find the point on the goal shape that we should head towards - CFixedVector2D goalPos = goal.NearestPointOnGoal(from); + // already waited one turn, no changes, so try computing a short path. + if (m_WaitingTurns >= 3) + { + if (m_Path.m_Waypoints.empty()) + { + RequestNewPath(); + return; + } + /** + * Here there are two cases: + * 1) We are somewhat far away from the goal, in which case proceed as usual + * 2) We're really close to the goal. + * If it's (2) it's likely that we are running into units that are currently doing the same thing we want to do (gathering from the same tree…) + * Since the initial call to MakeGoalReachable gave us a specific 2D coordinate, and we can't reach it, + * We have a relatively high chance of never being able to reach that particular point. + * So we need to recreate the actual goal for this entity. This is a little dangerous in terms of short/long pathfinder compatibility + * So we'll run sanity checks on the output to try and not get stuck/go where we shouldn't. + */ + PathGoal goal; - // Check if there's any collisions on that route - if (!cmpPathfinder->CheckMovement(GetObstructionFilter(true), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) - return false; + CFixedVector2D nextWptPos(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z); + if ((nextWptPos - pos).CompareLength(SHORT_PATH_GOAL_REDUX_DIST) > 0) + { + goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z }; + m_Path.m_Waypoints.pop_back(); + } + else + { + goal = CreatePathGoalFromMotionGoal(m_CurrentGoal); + m_DumpPathOnResult = true; + } - // 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 }); + RequestShortPath(pos, goal, true); + return; + } - return true; -} + // Last resort, compute a long path + if (m_WaitingTurns == 2) + { + if (m_Path.m_Waypoints.empty()) + { + RequestNewPath(); + return; + } + PathGoal goal; + goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z }; + m_Path.m_Waypoints.pop_back(); -bool CCmpUnitMotion::CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta) -{ - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return false; + RequestLongPath(pos, goal); + return; + } - // Fail unless the target has moved enough - CFixedVector2D oldTargetPos(m_FinalGoal.x, m_FinalGoal.z); - if ((targetPos - oldTargetPos).CompareLength(minDelta) < 0) - return false; - 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; + // m_waitingTurns == 1 here - // 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) + // we tried getting a renewed path and still got stuck + if (m_AbortIfStuck == 0) { - CmpPtr cmpRangeManager(GetSystemEntity()); - if (cmpRangeManager && cmpRangeManager->GetLosVisibility(m_TargetEntity, cmpOwnership->GetOwner()) == ICmpRangeManager::VIS_HIDDEN) - return false; + DiscardMove(); + MoveWillFail(); + 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; + --m_AbortIfStuck; - return true; -} + // Recompute a new path, but wait a few turns first + m_WaitingTurns = 4 + MAX_PATH_REATTEMPS; -void CCmpUnitMotion::UpdateFinalGoal() -{ - if (m_TargetEntity == INVALID_ENTITY) - return; - CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity); - if (!cmpUnitMotion) - return; - if (IsFormationMember()) - return; - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return; - m_FinalGoal.x = targetPos.X; - m_FinalGoal.z = targetPos.Y; + return; } -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()); + else + return cmpObstructionManager->IsInPointRange(GetEntityId(), goal.GetPosition().X, goal.GetPosition().Y, goal.Range(), goal.Range()); } bool CCmpUnitMotion::PathIsShort(const WaypointPath& path, const CFixedVector2D& from, entity_pos_t minDistance) const @@ -1273,498 +1105,269 @@ } } -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); -} - - - -void CCmpUnitMotion::BeginPathing(const CFixedVector2D& from, const PathGoal& goal) -{ - // reset our state for sanity. - m_ExpectedPathTicket = 0; - - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - m_Moving = false; - - m_PathState = PATHSTATE_NONE; - -#if DISABLE_PATHFINDER - { - CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY); - CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from); - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); - m_PathState = PATHSTATE_FOLLOWING; - return; - } -#endif - - // If we're aiming at a target entity and it's close and we can reach - // it in a straight line, then we'll just go along the straight line - // instead of computing a path. - if (TryGoingStraightToTargetEntity(from)) - { - if (!HasValidPath()) - StartSucceeded(); - m_PathState = PATHSTATE_FOLLOWING; + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) return; - } - // Same thing applies to non-entity points - if (TryGoingStraightToGoalPoint(from)) - { - if (!HasValidPath()) - StartSucceeded(); - m_PathState = PATHSTATE_FOLLOWING; + CmpPtr cmpTargetPosition(GetSimContext(), ent); + if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) return; - } - // Otherwise we need to compute a path. + CFixedVector2D pos = cmpPosition->GetPosition2D(); + CFixedVector2D targetPos = cmpTargetPosition->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) - { - // 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); - } - else + CFixedVector2D offset = targetPos - pos; + if (!offset.IsZero()) { - m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; - RequestLongPath(from, goal); + entity_angle_t angle = atan2_approx(offset.X, offset.Y); + cmpPosition->TurnTo(angle); } -} - -void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal) -{ - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return; - // this is by how much our waypoints will be apart at most. - // this value here seems sensible enough. - PathGoal improvedGoal = goal; - improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1); - - cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass); - - m_ExpectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId()); } -void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits) +ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter() const { - CmpPtr cmpPathfinder(GetSystemEntity()); - 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()); + // TODO: if we sometimes want to consider moving units, change here. + return ControlGroupMovementObstructionFilter(true, GetGroup()); } -bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) +// 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) { - return MoveToPointRange(x, z, minRange, maxRange, INVALID_ENTITY); -} + ENSURE(m_ExpectedPathTicket == 0); -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"); + ENSURE(CurrentGoalHasValidPosition()); CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + ENSURE (cmpPosition); - CFixedVector2D pos = cmpPosition->GetPosition2D(); + CFixedVector2D position = cmpPosition->GetPosition2D(); - PathGoal goal; - goal.x = x; - goal.z = z; + m_DumpPathOnResult = true; - if (minRange.IsZero() && maxRange.IsZero()) - { - // Non-ranged movement: + bool reachable = RecomputeGoalPosition(m_Goal); - // Head directly for the goal - goal.type = PathGoal::POINT; - } - else + if (!reachable && !evenUnreachable) { - // Ranged movement: - - entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length(); - - 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; - } - } - - 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; -} - -bool CCmpUnitMotion::IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) -{ - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) + // Do not submit a path request if we've been told it's not going to be used anyhow. + DiscardMove(); return false; + } - CFixedVector2D pos = cmpPosition->GetPosition2D(); - - bool hasObstruction = false; - CmpPtr cmpObstructionManager(GetSystemEntity()); - ICmpObstructionManager::ObstructionSquare obstruction; -//TODO if (cmpObstructionManager) -// hasObstruction = cmpObstructionManager->FindMostImportantObstruction(GetObstructionFilter(), x, z, m_Radius, obstruction); - - if (minRange.IsZero() && maxRange.IsZero() && hasObstruction) - { - // Handle the non-ranged mode: - CFixedVector2D halfSize(obstruction.hw, obstruction.hh); - entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize); - - // See if we're too close to the target square - if (distance < minRange) - return false; + ENSURE(m_Goal.x >= fixed::Zero()); - // See if we're close enough to the target square - if (maxRange < entity_pos_t::Zero() || distance <= maxRange) - return true; + /** + * A (long) note on short vs long range pathfinder, their conflict, and stuck units. + * A long-standing issue with 0 A.D.'s pathfinding has been that the short-range pathfinder is "better" than the long-range + * Indeed it can find paths that the long-range one cannot, since the grid is coarser than the real vector representation. + * This leads to units going where they shouldn't go, notably impassable and "unreachable" areas. + * This has been a -real- plague. Made worse by the facts that groups of trees tended to trigger it, leading to stuck gatherers… + * Thus, in general, we'd want the short-range and the long-range pathfinder to coincide. But making the short-range pathfinder + * register all impassable navcells as edges would just be way too slow, so we can't do that, so we -cannot- fix the issue + * by just changing the pathfinders' behavior. + * + * All hope is not lost, however. + * + * A big part of the problem is that before the unitMotion rewrite, UnitMotion requested a path to the goal, and then the pathfinder + * made that goal "reachable" by calling MakeGoalReachable, which uses the same grid as the long-range pathfinder. Thus, over short ranges, + * the pathfinder entirely short-circuited this. Since UnitMotion now calls MakeGoalReachable on its own, it only ever requests + * paths to points that are indeed supposed to be reachable. This does fix a number of cases. + * + * But then, why set LONG_PATH_MIN_DIST to 0 and disable the use of short paths here? Well it turns out you still had a few edge cases. + * + * Imagine two houses next to each other, with a space between them just wide enough that there are no passable navcells, + * but enough space for the short-range pathfinder to return a path through them (make them in a test map if you have to). + * If you ask a unit to cross there, the goal won't change: it's reachable by the long-range pathfinder by going around the house. + * However, the distance is < LONG_PATH_MIN_DIST, so the short-range pathfinder is called, so it goes through the house. Edge case. + * There's a variety of similar cases that can be imagined around the idea that there exists a shorter path visible only by the short-range pathfinder. + * If we never use the short-pathfinder in RequestNewPath, we can safely avoid those edge cases. + * + * However, we still call the short-pathfinder when running into an obstruction to avoid units. Can't that get us stuck too? + * Well, it probably can. But there's a few things to consider: + * -It's harder to trigger it if you actually have to run into a unit + * -In those cases, UnitMotion requests a path to the next existing waypoint (if there are none, it calls requestnewPath to get those) + * and the next existing waypoint has -necessarily- been given to us by the long-range pathfinder since we're using it here + * -We are running sanity checks on the output (see PathResult). + * Thus it's far less likely that the short-range pathfinder will return us an impassable path. + * It -is- not entirely impossible. A freak construction with many units strategically positionned could probably reveal the bug. + * But it's in my opinion rare enough that this discrepancy can be considered fixed. + */ - return false; - } + if (m_Goal.DistanceToPoint(position) < LONG_PATH_MIN_DIST) + RequestShortPath(position, m_Goal, true); else - { - entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length(); + RequestLongPath(position, m_Goal); - if (distance < minRange) - return false; - else if (maxRange >= entity_pos_t::Zero() && distance > maxRange) - return false; - else - return 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); + return reachable; } -bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) +PathGoal CCmpUnitMotion::CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal) { - PROFILE("MoveToTargetRange"); + PathGoal goal = PathGoal(); + goal.x = fixed::FromInt(-1); // to figure out whether it's false-unreachable or false-buggy CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + ENSURE(cmpPosition); CFixedVector2D pos = cmpPosition->GetPosition2D(); - CmpPtr cmpObstructionManager(GetSystemEntity()); - if (!cmpObstructionManager) - return false; + // 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. - bool hasObstruction = false; - ICmpObstructionManager::ObstructionSquare obstruction; - CmpPtr cmpObstruction(GetSimContext(), target); - if (cmpObstruction) - hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); + // defaut to point at position + goal.type = PathGoal::POINT; + goal.x = GetGoalPosition(motionGoal).X; + goal.z = GetGoalPosition(motionGoal).Y; - if (!hasObstruction) + // few cases to consider. + if (motionGoal.IsEntity()) { - // 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); - } - - /* - * 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); - - // 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); - - 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 - - // Circumscribe the square - entity_pos_t circleRadius = halfSize.Length(); + CmpPtr cmpObstruction(GetSimContext(), motionGoal.GetEntity()); + if (cmpObstruction) + { + ICmpObstructionManager::ObstructionSquare obstruction; + bool hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); + if (hasObstruction) + { + fixed certainty; + UpdatePositionForTargetVelocity(motionGoal.GetEntity(), obstruction.x, obstruction.z, certainty); - entity_pos_t goalDistance = minRange + Pathfinding::GOAL_DELTA; + goal.type = PathGoal::CIRCLE; + goal.x = obstruction.x; + goal.z = obstruction.z; + goal.hw = obstruction.hw + motionGoal.Range() + m_Clearance; - 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; + // 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 } - else if (maxRange < entity_pos_t::Zero() || distance < maxRange || previousDistance < maxRange) + if (goal.type == PathGoal::POINT && motionGoal.Range() > fixed::Zero()) { - // We're already in range - no need to move anywhere - FaceTowardsPointFromPos(pos, goal.x, goal.z); - return false; + goal.type = PathGoal::CIRCLE; + goal.hw = motionGoal.Range(); + if ((pos - CFixedVector2D(goal.x,goal.z)).CompareLength(goal.hw) <= 0) + goal.type = PathGoal::INVERTED_CIRCLE; } - else - { - // We might need to move closer: - // Circumscribe the square - entity_pos_t circleRadius = halfSize.Length(); + return goal; +} - if (ShouldTreatTargetAsCircle(maxRange, circleRadius)) - { - // The target is small relative to our range, so pretend it's a circle +bool CCmpUnitMotion::RecomputeGoalPosition(PathGoal& goal) +{ + if (!CurrentGoalHasValidPosition()) + return false; // we're not going anywhere - // 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; + goal = CreatePathGoalFromMotionGoal(m_CurrentGoal); - if (circleDistance < maxRange || previousCircleDistance < maxRange) - { - // We're already in range - no need to move anywhere - if (m_FacePointAfterMove) - FaceTowardsPointFromPos(pos, goal.x, goal.z); - return false; - } + // We now have a correct goal. + // Make it reachable - entity_pos_t goalDistance = maxRange - Pathfinding::GOAL_DELTA; + CmpPtr cmpPathfinder(GetSystemEntity()); + ENSURE(cmpPathfinder); - 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 + CmpPtr cmpPosition(GetEntityHandle()); + ENSURE(cmpPosition); - entity_pos_t goalDistance = (maxRange - Pathfinding::GOAL_DELTA)*2 / 3; // multiply by slightly less than 1/sqrt(2) + CFixedVector2D pos = cmpPosition->GetPosition2D(); - goal.type = PathGoal::SQUARE; - goal.u = obstruction.u; - goal.v = obstruction.v; - entity_pos_t delta = std::max(goalDistance, m_Clearance + entity_pos_t::FromInt(TERRAIN_TILE_SIZE)/16); // ensure it's far enough to not intersect the building itself - goal.hw = obstruction.hw + delta; - goal.hh = obstruction.hh + delta; - } - } + bool reachable = cmpPathfinder->MakeGoalReachable(pos.X, pos.Y, goal, m_PassClass); - m_State = STATE_INDIVIDUAL_PATH; - m_TargetEntity = target; - m_TargetOffset = CFixedVector2D(); - m_TargetMinRange = minRange; - m_TargetMaxRange = maxRange; - m_FinalGoal = goal; - m_Tries = 0; + // TODO: ought to verify that the returned navcell is in range if it's reachable as a sanity check - BeginPathing(pos, goal); + m_Goal = goal; - return true; + return reachable; } -bool CCmpUnitMotion::IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) +void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal) { - // This function closely mirrors MoveToTargetRange - it needs to return true - // after that Move has completed - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; - - CFixedVector2D pos = cmpPosition->GetPosition2D(); - - CmpPtr cmpObstructionManager(GetSystemEntity()); - if (!cmpObstructionManager) - return false; + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) + return; - bool hasObstruction = false; - ICmpObstructionManager::ObstructionSquare obstruction; - CmpPtr cmpObstruction(GetSimContext(), target); - if (cmpObstruction) - hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); - - if (hasObstruction) - { - CFixedVector2D halfSize(obstruction.hw, obstruction.hh); - entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize, true); - - // 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); - - // See if we're too close to the target square - bool inside = distance.IsZero() && !Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize).IsZero(); - if ((distance < minRange && previousDistance < minRange) || inside) - return false; + m_RunShortPathValidation = false; - // See if we're close enough to the target square - if (maxRange < entity_pos_t::Zero() || distance <= maxRange || previousDistance <= maxRange) - return true; + // this is by how much our waypoints will be apart at most. + // this value here seems sensible enough. + PathGoal improvedGoal = goal; + improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1); - entity_pos_t circleRadius = halfSize.Length(); + cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass); - if (ShouldTreatTargetAsCircle(maxRange, circleRadius)) - { - // The target is small relative to our range, so pretend it's a circle - // and see if we're close enough to that. - // Also check circle around previous position. - entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius; - entity_pos_t previousCircleDistance = (pos - CFixedVector2D(previousObstruction.x, previousObstruction.z)).Length() - circleRadius; + m_ExpectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId()); +} - return circleDistance <= maxRange || previousCircleDistance <= maxRange; - } +void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits) +{ + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) + return; - // take minimal clearance required in MoveToTargetRange into account, multiplying by 3/2 for diagonals - entity_pos_t maxDist = std::max(maxRange, (m_Clearance + entity_pos_t::FromInt(TERRAIN_TILE_SIZE)/16)*3/2); - return distance <= maxDist || previousDistance <= maxDist; - } - else - { - CmpPtr cmpTargetPosition(GetSimContext(), target); - if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) - return false; + m_RunShortPathValidation = true; - CFixedVector2D targetPos = cmpTargetPosition->GetPreviousPosition2D(); - entity_pos_t distance = (pos - targetPos).Length(); + // 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), 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; - return minRange <= distance && (maxRange < entity_pos_t::Zero() || distance <= maxRange); - } + m_ExpectedPathTicket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetGroup(), GetEntityId()); } -void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) -{ - CmpPtr cmpPosition(GetSimContext(), target); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - CFixedVector2D pos = cmpPosition->GetPosition2D(); +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(); - PathGoal goal; - goal.type = PathGoal::POINT; - goal.x = pos.X; - goal.z = pos.Y; + m_Destination = SMotionGoal(CFixedVector2D(x, z), range); + m_CurrentGoal = m_Destination; - 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; + bool reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition - BeginPathing(pos, goal); + return reachable; } +bool CCmpUnitMotion::SetNewDestinationAsEntity(entity_id_t ent, entity_pos_t range, bool evenUnreachable) +{ + // This sets up a new destination, scrap whatever came before. + DiscardMove(); + // validate entity's existence. + CmpPtr cmpPosition(GetSimContext(), ent); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return false; + m_Destination = SMotionGoal(ent, range); + m_CurrentGoal = m_Destination; + bool reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition + + return reachable; +} void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color) { @@ -1785,8 +1388,24 @@ lines.back().m_Color = color; SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating); } - float x = cmpPosition->GetPosition2D().X.ToFloat(); - float z = cmpPosition->GetPosition2D().Y.ToFloat(); + + if (CurrentGoalHasValidPosition()) + { + float x = GetGoalPosition(m_CurrentGoal).X.ToFloat(); + float z = GetGoalPosition(m_CurrentGoal).Y.ToFloat(); + lines.push_back(SOverlayLine()); + lines.back().m_Color = CColor(0.0f, 1.0f, 0.0f, 1.0f); + SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.4f, lines.back(), floating); + } + + float x = m_Goal.x.ToFloat(); + float z = m_Goal.z.ToFloat(); + lines.push_back(SOverlayLine()); + lines.back().m_Color = CColor(0.0f, 1.0f, 1.0f, 1.0f); + SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating); + + x = cmpPosition->GetPosition2D().X.ToFloat(); + z = cmpPosition->GetPosition2D().Y.ToFloat(); waypointCoords.push_back(x); waypointCoords.push_back(z); lines.push_back(SOverlayLine()); @@ -1800,12 +1419,8 @@ 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_DebugOverlayPathLines.size(); ++i) + collector.Submit(&m_DebugOverlayPathLines[i]); } Index: source/simulation2/components/CCmpVisualActor.cpp =================================================================== --- source/simulation2/components/CCmpVisualActor.cpp +++ source/simulation2/components/CCmpVisualActor.cpp @@ -54,7 +54,6 @@ public: static void ClassInit(CComponentManager& componentManager) { - componentManager.SubscribeToMessageType(MT_Update_Final); componentManager.SubscribeToMessageType(MT_InterpolatedPositionChanged); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_ValueModification); @@ -71,10 +70,7 @@ 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; @@ -83,6 +79,9 @@ fixed m_AnimSyncRepeatTime; // 0.0 if not synced fixed m_AnimSyncOffsetTime; + std::string m_MovingPrefix; + fixed m_MovingSpeed; + std::map m_VariantSelections; u32 m_Seed; // seed used for random variations @@ -190,6 +189,7 @@ { m_Unit = NULL; m_R = m_G = m_B = fixed::FromInt(1); + m_MovingSpeed = fixed::FromInt(1); m_ConstructionPreview = paramNode.GetChild("ConstructionPreview").IsOk(); @@ -225,9 +225,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); @@ -282,12 +279,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) @@ -423,7 +414,6 @@ virtual void SelectAnimation(const std::string& name, bool once, fixed speed, const std::wstring& soundgroup) { - m_AnimRunThreshold = fixed::Zero(); m_AnimName = name; m_AnimOnce = once; m_AnimSpeed = speed; @@ -432,28 +422,36 @@ 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->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) - { - SelectAnimation("walk", false, fixed::FromFloat(1.f), L""); - m_AnimRunThreshold = runThreshold; + SelectAnimation(m_AnimName, m_AnimOnce, m_AnimSpeed, m_SoundGroup); } virtual void SetAnimationSyncRepeat(fixed repeattime) @@ -534,8 +532,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) @@ -741,45 +737,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, L""); - m_AnimRunThreshold = runThreshold; -} Index: source/simulation2/components/ICmpObstructionManager.h =================================================================== --- source/simulation2/components/ICmpObstructionManager.h +++ source/simulation2/components/ICmpObstructionManager.h @@ -159,6 +159,26 @@ virtual void RemoveShape(tag_t tag) = 0; /** + * Check if the given point is in range of the other point given those parameters + */ + virtual bool IsPointInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) = 0; + + /** + * Check if the given point is in range of the target given those parameters + */ + virtual bool IsPointInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; + + /** + * Check if the given entity is in range of the other point given those parameters + */ + virtual bool IsInPointRange(entity_id_t ent, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) = 0; + + /** + * Check if the given entity is in range of the target given those parameters + */ + virtual bool IsInTargetRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; + + /** * Collision test a flat-ended thick line against the current set of shapes. * The line caps extend by @p r beyond the end points. * Only intersections going from outside to inside a shape are counted. Index: source/simulation2/components/ICmpObstructionManager.cpp =================================================================== --- source/simulation2/components/ICmpObstructionManager.cpp +++ source/simulation2/components/ICmpObstructionManager.cpp @@ -24,4 +24,6 @@ BEGIN_INTERFACE_WRAPPER(ObstructionManager) DEFINE_INTERFACE_METHOD_1("SetPassabilityCircular", void, ICmpObstructionManager, SetPassabilityCircular, bool) DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpObstructionManager, SetDebugOverlay, bool) +DEFINE_INTERFACE_METHOD_5("IsInPointRange", bool, ICmpObstructionManager, IsInPointRange, entity_id_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t) +DEFINE_INTERFACE_METHOD_4("IsInTargetRange", bool, ICmpObstructionManager, IsInTargetRange, entity_id_t, entity_id_t, entity_pos_t, entity_pos_t) END_INTERFACE_WRAPPER(ObstructionManager) Index: source/simulation2/components/ICmpPathfinder.h =================================================================== --- source/simulation2/components/ICmpPathfinder.h +++ source/simulation2/components/ICmpPathfinder.h @@ -89,6 +89,24 @@ virtual Grid ComputeShoreGrid(bool expandOnWater = false) = 0; /** + * Transform an arbitrary PathGoal into a reachable Point PathGoal, see Hierarchical Pathfinder for details + * Return true if the goal was reachable originally, false otherwise. + */ + virtual bool MakeGoalReachable(entity_pos_t x0, entity_pos_t z0, PathGoal &goal, pass_class_t passClass) = 0; + + /** + * Gives the closest passable navcell from the given position. + * Returns how many navcells away (manhattan) that navcell is. + */ + virtual u32 FindNearestPassableNavcell(entity_pos_t x, entity_pos_t z, u16& outI, u16& outJ, pass_class_t passClass) = 0; + + /** + * Returns true if navcell (i0, j0) has the same global region ID as navcell (i1, j1). + * i.e. you can reach one from the other. + */ + virtual bool NavcellIsReachable(u16 i0, u16 j0, u16 i1, u16 j1, pass_class_t passClass) = 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 * along the path. Index: source/simulation2/components/ICmpUnitMotion.h =================================================================== --- source/simulation2/components/ICmpUnitMotion.h +++ source/simulation2/components/ICmpUnitMotion.h @@ -36,80 +36,97 @@ 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 SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool evenUnreachable) = 0; + + /** + * 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 MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0; + virtual bool SetNewDestinationAsEntity(entity_id_t target, entity_pos_t range, bool evenUnreachable) = 0; /** - * Determine wether the givven point is within the given range, using the same measurement - * as MoveToPointRange. + * Turn to look towards the given point. */ - virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0; + virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; /** - * Determine whether the target is within the given range, using the same measurement - * as MoveToTargetRange. + * Turn to look towards the given entity. */ - virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; + virtual void FaceTowardsEntity(entity_id_t ent) = 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. + * Determine whether to abort or retry X times if pathing fails. + * Generally safer to let it abort and inform us. */ - virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; + virtual void SetAbortIfStuck(u8 shouldAbort) = 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. + * Stops the unit. Does not clear the destination + * so the unit may start moving again next turn. + * Mostly used internally but exposed if anybody wants to stop for whatever reason. */ - virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0; + virtual void StopMoving() = 0; /** - * Turn to look towards the given point. + * Stop moving, clear any destination, path, and ticket pending. + * Basically resets the unit's motion. + * Won't send any message. */ - virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; + virtual void DiscardMove() = 0; /** - * Stop moving immediately. + * Asks wether the unit has a path to follow */ - virtual void StopMoving() = 0; + virtual bool HasValidPath() = 0; /** - * Get the current movement speed. + * Get how much faster/slower we are at than normal. */ - virtual fixed GetCurrentSpeed() = 0; + virtual fixed GetSpeedRatio() = 0; + + /** + * Get how much faster than our regular speed we can go. + */ + virtual fixed GetTopSpeedRatio() = 0; /** * Set the current movement speed. + * 'speed' in % of top speed (ie 3.0 will be 3 times top speed). */ virtual void SetSpeed(fixed speed) = 0; /** - * Get whether the unit is moving. + * Get whether the unit is actually moving on the map this turn. + */ + virtual bool IsActuallyMoving() = 0; + + /** + * Get whether a unit is trying to go somewhere + * NB: this does not mean its position is actually changing right now. */ - virtual bool IsMoving() = 0; + virtual bool IsTryingToMove() = 0; /** - * Get the default speed that this unit will have when walking, in metres per second. + * Get the unit theoretical speed in metres per second. + * GetActualSpeed will return historical speed + * This is affected by SetSpeed. */ - virtual fixed GetWalkSpeed() = 0; + virtual fixed GetSpeed() = 0; /** - * Get the default speed that this unit will have when running, in metres per second. + * Get the unit base/walk speed in metres per second. + * This is NOT affected by SetSpeed. */ - virtual fixed GetRunSpeed() = 0; + virtual fixed GetBaseSpeed() = 0; /** * Set whether the unit will turn to face the target point after finishing moving. Index: source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- source/simulation2/components/ICmpUnitMotion.cpp +++ source/simulation2/components/ICmpUnitMotion.cpp @@ -23,67 +23,70 @@ #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_4("IsInPointRange", bool, ICmpUnitMotion, IsInPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t) -DEFINE_INTERFACE_METHOD_3("IsInTargetRange", bool, ICmpUnitMotion, IsInTargetRange, entity_id_t, entity_pos_t, entity_pos_t) -DEFINE_INTERFACE_METHOD_3("MoveToTargetRange", bool, ICmpUnitMotion, MoveToTargetRange, entity_id_t, entity_pos_t, entity_pos_t) -DEFINE_INTERFACE_METHOD_3("MoveToFormationOffset", void, ICmpUnitMotion, MoveToFormationOffset, entity_id_t, entity_pos_t, entity_pos_t) +DEFINE_INTERFACE_METHOD_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_2("FaceTowardsPoint", void, ICmpUnitMotion, FaceTowardsPoint, entity_pos_t, entity_pos_t) +DEFINE_INTERFACE_METHOD_1("FaceTowardsEntity", void, ICmpUnitMotion, FaceTowardsEntity, entity_id_t) +DEFINE_INTERFACE_METHOD_1("SetAbortIfStuck", void, ICmpUnitMotion, SetAbortIfStuck, u8) DEFINE_INTERFACE_METHOD_0("StopMoving", void, ICmpUnitMotion, StopMoving) -DEFINE_INTERFACE_METHOD_0("GetCurrentSpeed", fixed, ICmpUnitMotion, GetCurrentSpeed) +DEFINE_INTERFACE_METHOD_0("DiscardMove", void, ICmpUnitMotion, DiscardMove) +DEFINE_INTERFACE_METHOD_0("HasValidPath", bool, ICmpUnitMotion, HasValidPath) +DEFINE_INTERFACE_METHOD_0("GetTopSpeedRatio", fixed, ICmpUnitMotion, GetTopSpeedRatio) DEFINE_INTERFACE_METHOD_1("SetSpeed", void, ICmpUnitMotion, SetSpeed, fixed) -DEFINE_INTERFACE_METHOD_0("IsMoving", bool, ICmpUnitMotion, IsMoving) -DEFINE_INTERFACE_METHOD_0("GetWalkSpeed", fixed, ICmpUnitMotion, GetWalkSpeed) -DEFINE_INTERFACE_METHOD_0("GetRunSpeed", fixed, ICmpUnitMotion, GetRunSpeed) +DEFINE_INTERFACE_METHOD_0("IsActuallyMoving", bool, ICmpUnitMotion, IsActuallyMoving) +DEFINE_INTERFACE_METHOD_0("IsTryingToMove", bool, ICmpUnitMotion, IsTryingToMove) +DEFINE_INTERFACE_METHOD_0("GetSpeed", fixed, ICmpUnitMotion, GetSpeed) +DEFINE_INTERFACE_METHOD_0("GetBaseSpeed", fixed, ICmpUnitMotion, GetBaseSpeed) DEFINE_INTERFACE_METHOD_0("GetPassabilityClassName", std::string, ICmpUnitMotion, GetPassabilityClassName) DEFINE_INTERFACE_METHOD_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 IsInPointRange(entity_pos_t x, entity_pos_t z, 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("IsInPointRange", x, z, minRange, maxRange); + return m_Script.Call("SetNewDestinationAsEntity", target, range, true); } - virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) + virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) { - return m_Script.Call("IsInTargetRange", target, minRange, maxRange); + m_Script.CallVoid("FaceTowardsPoint", x, z); } - virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) + virtual void FaceTowardsEntity(entity_id_t ent) { - return m_Script.Call("MoveToTargetRange", target, minRange, maxRange); + m_Script.CallVoid("FaceTowardsEntity", ent); } - virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) + virtual void DiscardMove() { - m_Script.CallVoid("MoveToFormationOffset", target, x, z); + m_Script.CallVoid("DiscardMove"); } - virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) + virtual void StopMoving() { - m_Script.CallVoid("FaceTowardsPoint", x, z); + m_Script.CallVoid("CompleteMove"); } - virtual void StopMoving() + virtual void SetAbortIfStuck(u8 shouldAbort) { - m_Script.CallVoid("StopMoving"); + m_Script.CallVoid("SetAbortIfStuck", shouldAbort); } - virtual fixed GetCurrentSpeed() + virtual fixed GetActualSpeed() { - return m_Script.Call("GetCurrentSpeed"); + return m_Script.Call("GetActualSpeed"); } virtual void SetSpeed(fixed speed) @@ -91,19 +94,34 @@ m_Script.CallVoid("SetSpeed", speed); } - virtual bool IsMoving() + virtual fixed GetTopSpeedRatio() + { + return m_Script.Call("GetTopSpeedRatio"); + } + + virtual bool HasValidPath() { - return m_Script.Call("IsMoving"); + return m_Script.Call("HasValidPath"); } - virtual fixed GetWalkSpeed() + virtual bool IsActuallyMoving() { - return m_Script.Call("GetWalkSpeed"); + return m_Script.Call("IsActuallyMoving"); } - virtual fixed GetRunSpeed() + virtual bool IsTryingToMove() { - return m_Script.Call("GetRunSpeed"); + return m_Script.Call("IsTryingToMove"); + } + + virtual fixed GetSpeed() + { + return m_Script.Call("GetSpeed"); + } + + virtual fixed GetBaseSpeed() + { + return m_Script.Call("GetBaseSpeed"); } virtual void SetFacePointAfterMove(bool facePointAfterMove) @@ -116,6 +134,11 @@ return m_Script.Call("GetPassabilityClass"); } + virtual fixed GetSpeedRatio() + { + return fixed::FromInt(1); + } + virtual std::string GetPassabilityClassName() { return m_Script.Call("GetPassabilityClassName"); Index: source/simulation2/components/ICmpVisual.h =================================================================== --- source/simulation2/components/ICmpVisual.h +++ source/simulation2/components/ICmpVisual.h @@ -99,25 +99,10 @@ virtual void SelectAnimation(const std::string& name, bool once, fixed speed, const std::wstring& soundgroup) = 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 @@ -24,13 +24,11 @@ BEGIN_INTERFACE_WRAPPER(Visual) DEFINE_INTERFACE_METHOD_2("SetVariant", void, ICmpVisual, SetVariant, CStr, CStr) DEFINE_INTERFACE_METHOD_4("SelectAnimation", void, ICmpVisual, SelectAnimation, std::string, bool, fixed, std::wstring) -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_0("GetActorSeed", u32, ICmpVisual, GetActorSeed) DEFINE_INTERFACE_METHOD_1("SetActorSeed", void, ICmpVisual, SetActorSeed, u32) DEFINE_INTERFACE_METHOD_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 @@ -17,8 +17,11 @@ #include "simulation2/system/ComponentTest.h" +#define TEST + #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpPathfinder.h" +#include "simulation2/components/CCmpPathfinder_Common.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" @@ -64,6 +67,129 @@ TS_ASSERT_EQUALS((Pathfinding::NAVCELL_SIZE >> 1).ToInt_RoundToZero(), Pathfinding::NAVCELL_SIZE_LOG2); } + void hierarchical_globalRegions_testmap(std::wstring map) + { + CTerrain terrain; + + CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain); + sim2.LoadDefaultScripts(); + sim2.ResetState(); + + CMapReader* mapReader = new CMapReader(); // it'll call "delete this" itself + + LDR_BeginRegistering(); + mapReader->LoadMap(map, + sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue, + &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + &sim2, &sim2.GetSimContext(), -1, false); + LDR_EndRegistering(); + TS_ASSERT_OK(LDR_NonprogressiveLoad()); + + sim2.Update(0); + + CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); + + pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default"); + HierarchicalPathfinder& hier = ((CCmpPathfinder*)cmpPathfinder.operator->())->m_LongPathfinder.GetHierarchicalPathfinder(); + + std::map globalRegions = hier.m_GlobalRegions[obstructionsMask]; + + for (u8 cj = 0; cj < hier.m_ChunksH; cj += 2) + for (u8 ci = 0; ci < hier.m_ChunksW; ci += 2) + for(u16 i : hier.GetChunk(ci, cj, obstructionsMask).m_RegionsID) + { + std::set reachables; + hier.FindReachableRegions(HierarchicalPathfinder::RegionID{ci, cj, i}, reachables, obstructionsMask); + HierarchicalPathfinder::GlobalRegionID ID = globalRegions[HierarchicalPathfinder::RegionID{ci, cj, i}]; + for (HierarchicalPathfinder::RegionID region : reachables) + TS_ASSERT_EQUALS(ID, globalRegions[region]); + } + } + + void test_hierarchical_globalRegions() + { + // This test validates that the hierarchical's pathfinder global regions are in accordance with its regions + // IE it asserts that, for any two regions A and B of the hierarchical pathfinder, if one can find a path from A to B + // then A and B have the same global region. + std::vector maps = { L"maps/scenarios/Peloponnese.pmp", L"maps/skirmishes/Corinthian Isthmus (2).pmp", L"maps/skirmishes/Greek Acropolis (2).pmp" }; + +// disable in debug mode, creating the simulation and running the initial turn is too slow and tends to OOM in debug mode. +#ifndef DEBUG + for (std::wstring t : maps) + hierarchical_globalRegions_testmap(t); +#endif + } + + void hierarchical_update_testmap(std::wstring map) + { + CTerrain terrain; + + CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain); + sim2.LoadDefaultScripts(); + sim2.ResetState(); + + CMapReader* mapReader = new CMapReader(); // it'll call "delete this" itself + + LDR_BeginRegistering(); + mapReader->LoadMap(map, + sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue, + &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + &sim2, &sim2.GetSimContext(), -1, false); + LDR_EndRegistering(); + TS_ASSERT_OK(LDR_NonprogressiveLoad()); + + sim2.Update(0); + + CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); + + pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default"); + HierarchicalPathfinder& hier = ((CCmpPathfinder*)cmpPathfinder.operator->())->m_LongPathfinder.GetHierarchicalPathfinder(); + + // make copies + const auto pristine_GR = hier.m_GlobalRegions; + const auto pristine_Chunks = hier.m_Chunks; + const HierarchicalPathfinder::EdgesMap pristine_Edges = hier.m_Edges.at(obstructionsMask); + + Grid* pathfinderGrid = ((CCmpPathfinder*)cmpPathfinder.operator->())->m_LongPathfinder.m_Grid; + + Grid dirtyGrid(hier.m_ChunksW * HierarchicalPathfinder::CHUNK_SIZE,hier.m_ChunksH * HierarchicalPathfinder::CHUNK_SIZE); + srand(1234); + + size_t tries = 20; + for (size_t i = 0; i < tries; ++i) + { + // Dirty a random one + dirtyGrid.reset(); + u8 ci = rand() % (hier.m_ChunksW-10) + 8; + u8 cj = rand() % (hier.m_ChunksH-10) + 8; + dirtyGrid.set(ci * HierarchicalPathfinder::CHUNK_SIZE + 4, cj * HierarchicalPathfinder::CHUNK_SIZE + 4, 1); + + hier.Update(pathfinderGrid, dirtyGrid); + + // Formally speaking we should rather validate that regions exist with the same pixels, but so far + // re-initing regions will keep the same IDs for the same pixels so this is OK. + TS_ASSERT_EQUALS(hier.m_Chunks.at(obstructionsMask), pristine_Chunks.at(obstructionsMask)); + // same here + TS_ASSERT_EQUALS(pristine_Edges, hier.m_Edges.at(obstructionsMask)); + + // TODO: ought to test global regions, but those should be OK if the connections are OK + // and glboal regions ID can change on update making it annoying. + } + } + + void test_hierarchical_update() + { + // This test validates that the "Update" function of the hierarchical pathfinder + // ends up in a correct state (by comparing it with the clean, "Recompute"-d state). + std::vector maps = { L"maps/scenarios/Peloponnese.pmp", L"maps/skirmishes/Corinthian Isthmus (2).pmp", L"maps/skirmishes/Greek Acropolis (2).pmp" }; + +// disable in debug mode, creating the simulation and running the initial turn is too slow and tends to OOM in debug mode. +#ifndef DEBUG + for (std::wstring t : maps) + hierarchical_update_testmap(t); +#endif + } + void test_performance_DISABLED() { CTerrain terrain; @@ -285,6 +411,333 @@ stream << "\n"; } + static const size_t scale = 1; + + void MakeGoalReachable_testIteration(CStr& map, u16 sx, u16 sz, u16 gx, u16 gz) + { + int colors[26][3] = { + { 255, 0, 0 }, + { 0, 255, 0 }, + { 0, 0, 255 }, + { 255, 255, 0 }, + { 255, 0, 255 }, + { 0, 255, 255 }, + { 255, 255, 255 }, + + { 127, 0, 0 }, + { 0, 127, 0 }, + { 0, 0, 127 }, + { 127, 127, 0 }, + { 127, 0, 127 }, + { 0, 127, 127 }, + { 127, 127, 127}, + + { 255, 127, 0 }, + { 127, 255, 0 }, + { 255, 0, 127 }, + { 127, 0, 255}, + { 0, 255, 127 }, + { 0, 127, 255}, + { 255, 127, 127}, + { 127, 255, 127}, + { 127, 127, 255}, + + { 127, 255, 255 }, + { 255, 127, 255 }, + { 255, 255, 127 }, + }; + + // Load up a map, dump hierarchical regions + // From a few positions test making a few positions reachable. + // Check performance and output results as svg files so user can verify sanity. + + CTerrain terrain; + + CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain); + sim2.LoadDefaultScripts(); + sim2.ResetState(); + + CMapReader* mapReader = new CMapReader(); // it'll call "delete this" itself + + LDR_BeginRegistering(); + mapReader->LoadMap(map.FromUTF8(), + sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue, + &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + &sim2, &sim2.GetSimContext(), -1, false); + LDR_EndRegistering(); + TS_ASSERT_OK(LDR_NonprogressiveLoad()); + + sim2.Update(0); + + map.Replace(".pmp",""); map.Replace("/",""); + CStr path("MGR_" + map + "_" + CStr::FromUInt(sx) + "_" + CStr::FromUInt(sz) + "_" + CStr::FromUInt(gx) + "_" + CStr::FromUInt(gz) + ".html"); + std::cout << path << std::endl; + std::ofstream stream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); + + CmpPtr cmpObstructionManager(sim2, SYSTEM_ENTITY); + CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); + + pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default"); + const Grid& obstructions = cmpPathfinder->GetPassabilityGrid(); + + // Dump as canvas. This is terrible code but who cares. + stream << "\n"; + stream << "\n" + "\n" + "\n" + ""; + + stream << "

\n"; + stream << "Display Grid"; + stream << "Display Hierarchical grid "; + stream << "Display Global Regions "; + stream << "Display Path search "; + stream << "Display path lookups "; + stream << ""; + stream << "\n"; + stream << "

"; + + stream << ""; + + // set up grid + stream << "\n"; + + // Dump hierarchical regions on another one. + stream << ""; + stream << ""; + + HierarchicalPathfinder& hier = ((CCmpPathfinder*)cmpPathfinder.operator->())->m_LongPathfinder.GetHierarchicalPathfinder(); + + stream << "\n"; + + // Ok let's check out MakeGoalReachable + // pick a point + fixed X,Z; + X = fixed::FromInt(sx); + Z = fixed::FromInt(sz); + u16 gridSize = obstructions.m_W; + // Convert the start coordinates to tile indexes + u16 i0, j0; + Pathfinding::NearestNavcell(X, Z, i0, j0, gridSize, gridSize); + + // Dump as HTML so that it's on top and add fancy shadows so it's easy to see. + stream << "

"; + + hier.FindNearestPassableNavcell(i0, j0, obstructionsMask); + stream << "

"; + + // Make the goal reachable. This includes shortening the path if the goal is in a non-passable + // region, transforming non-point goals to reachable point goals, etc. + + PathGoal goal; + goal.type = PathGoal::POINT; + goal.x = fixed::FromInt(gx); + goal.z = fixed::FromInt(gz); + goal.u = CFixedVector2D(fixed::FromInt(1), fixed::Zero()); + goal.v = CFixedVector2D(fixed::Zero(),fixed::FromInt(1)); + goal.hh = fixed::FromInt(0); + goal.hw = fixed::FromInt(0); + + u16 i1, j1; + Pathfinding::NearestNavcell(goal.x, goal.z, i1, j1, gridSize, gridSize); + stream << "

"; + + stream << ""; + stream << ""; + + stream << "\n"; + + Pathfinding::NearestNavcell(goalCopy.x, goalCopy.z, i1, j1, gridSize, gridSize); + stream << "

"; + stream << "\n"; + stream.close(); + + // Perf test. This is a little primitive, but should work well enough to give an idea of the algo. + double t = timer_Time(); + + srand(1234); + for (size_t j = 0; j < 10000; ++j) + { + PathGoal oldGoal = goal; + hier.MakeGoalReachable(i0, j0, goal, obstructionsMask); + goal = oldGoal; + } + + t = timer_Time() - t; + printf("\nPoint Goal: [%f]\n", t); + + goal.type = PathGoal::CIRCLE; + goal.hh = fixed::FromInt(40); + goal.hw = fixed::FromInt(40); + + t = timer_Time(); + + srand(1234); + for (size_t j = 0; j < 10000; ++j) + { + PathGoal oldGoal = goal; + hier.MakeGoalReachable(i0, j0, goal, obstructionsMask); + goal = oldGoal; + } + + t = timer_Time() - t; + printf("\nCircle Goal: [%f]\n", t); + } + + void test_MakeGoalReachable_performance_DISABLED() + { + struct test + { + CStr map; + u16 sx; + u16 sz; + u16 gx; + u16 gz; + }; + /* + * Initially this was done to compare A* to the earlier flood-fill method, which has since been removed. + * Compare performance on a few cases: + * - short path, good case for the flood fill (it finds immediately the point/circle and stops) + * - short path, bad case for the flood fill (it will not find the correct region right away, so it's literally about 100x slower than the former) + * - long path around the bend, close to worst-case for A* + * - Long unreachable path, but the "closest point" is reachable in almost a straight direction. + * - Inverse of the former (the region to fill is much smaller) + * - large island, A* still has a lot to fill here + * - straight with obstructions + * - straight, fewer obstructions + * - bad case (U shape around the start containing A*) + * - bad case: U shape + unreachable. We need to return something reasonably close, not in the first U + * - bad calse: two U shapes tripping A* + */ + std::vector maps = { + { "maps/scenarios/Peloponnese.pmp", 600, 800, 800, 800 }, + { "maps/scenarios/Peloponnese.pmp", 600, 800, 600, 900 }, + { "maps/scenarios/Peloponnese.pmp", 600, 800, 770, 1400 }, + { "maps/scenarios/Peloponnese.pmp", 1000, 300, 1500, 1450 }, + { "maps/scenarios/Peloponnese.pmp", 1500, 1450, 1000, 300 }, + { "maps/skirmishes/Corsica and Sardinia (4).pmp", 300, 1300, 1300, 300 }, + { "maps/skirmishes/Alpine_Mountains_(3).pmp", 200, 200, 800, 800 }, + { "maps/skirmishes/Corinthian Isthmus (2).pmp", 200, 200, 800, 800 }, + { "maps/skirmishes/Mediterranean Cove (2).pmp", 200, 200, 800, 800 }, + { "maps/skirmishes/Dueling Cliffs (3v3).pmp", 200, 200, 800, 800 }, + { "maps/skirmishes/Dueling Cliffs (3v3).pmp", 350, 200, 900, 900 }, + { "maps/skirmishes/Dueling Cliffs (3v3).pmp", 200, 200, 950, 950 }, + }; + + for (auto t : maps) + { + MakeGoalReachable_testIteration(t.map, t.sx, t.sz, t.gx, t.gz); + } + } + void DumpPath(std::ostream& stream, int i0, int j0, int i1, int j1, CmpPtr& cmpPathfinder) { entity_pos_t x0 = entity_pos_t::FromInt(i0); Index: source/simulation2/helpers/Geometry.h =================================================================== --- source/simulation2/helpers/Geometry.h +++ source/simulation2/helpers/Geometry.h @@ -30,6 +30,21 @@ namespace Geometry { +/* + * Check if we should treat a square as a circle, given the radius + * of the resulting circle and a distance to it + * used by UnitMotion and ObstructionManager + */ +inline bool ShouldTreatTargetAsCircle(const fixed& range, const fixed& circleRadius) +{ + // 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); +} + /** * Checks if a point is inside the given rotated rectangle. * Points precisely on an edge are considered to be inside. Index: source/simulation2/helpers/HierarchicalPathfinder.h =================================================================== --- source/simulation2/helpers/HierarchicalPathfinder.h +++ source/simulation2/helpers/HierarchicalPathfinder.h @@ -24,10 +24,13 @@ #include "Render.h" #include "graphics/SColor.h" +#include +#include + /** * Hierarchical pathfinder. * - * It doesn't find shortest paths, but deals with connectivity. + * Deals with connectivity (can point A reach point B?) * * The navcell-grid representation of the map is split into fixed-size chunks. * Within a chunk, each maximal set of adjacently-connected passable navcells @@ -35,18 +38,31 @@ * Each region is a vertex in the hierarchical pathfinder's graph. * When two regions in adjacent chunks are connected by passable navcells, * the graph contains an edge between the corresponding two vertexes. - * (There will never be an edge between two regions in the same chunk.) + * (by design, there can never be an edge between two regions in the same chunk.) + * + * Those fixed-size chunks are used to efficiently compute "global regions" by effectively flood-filling. + * Those can then be used to immediately determine if two reachables points are connected + * + * The main use of this class is to convert an arbitrary PathGoal to a reachable navcell + * This happens in MakeGoalReachable, which implements A* over the chunks. + * Currently, the resulting path is unused. * - * Since regions are typically fairly large, it is possible to determine - * connectivity between any two navcells by mapping them onto their appropriate - * region and then doing a relatively small graph search. */ +#ifdef TEST +class TestCmpPathfinder; +#endif + class HierarchicalOverlay; class HierarchicalPathfinder { +#ifdef TEST + friend class TestCmpPathfinder; +#endif public: + typedef u32 GlobalRegionID; + struct RegionID { u8 ci, cj; // chunk ID @@ -54,7 +70,7 @@ RegionID(u8 ci, u8 cj, u16 r) : ci(ci), cj(cj), r(r) { } - bool operator<(RegionID b) const + bool operator<(const RegionID& b) const { // Sort by chunk ID, then by per-chunk region ID if (ci < b.ci) @@ -68,7 +84,7 @@ return r < b.r; } - bool operator==(RegionID b) const + bool operator==(const RegionID& b) const { return ((ci == b.ci) && (cj == b.cj) && (r == b.r)); } @@ -89,18 +105,25 @@ bool IsChunkDirty(int ci, int cj, const Grid& dirtinessGrid) const; RegionID Get(u16 i, u16 j, pass_class_t passClass); + GlobalRegionID GetGlobalRegion(u16 i, u16 j, pass_class_t passClass); /** - * Updates @p goal so that it's guaranteed to be reachable from the navcell + * Updates @p goal to a point goal guaranteed to be reachable from the original navcell * @p i0, @p j0 (which is assumed to be on a passable navcell). * - * If the goal is not reachable, it is replaced with a point goal nearest to - * the goal center. + * If the goal is not reachable, it is replaced with an acceptable point goal + * This function does not necessarily return the closest navcell to the goal + * but the one with the lowest f score of the A* algorithm. + * This means it is usually a tradeoff between walking time and distance to the goal. * * In the case of a non-point reachable goal, it is replaced with a point goal * at the reachable navcell of the goal which is nearest to the starting navcell. + * TODO: since A* is used, it could return the reachable navcell nearest to the penultimate region visited. + * which is probably better (imagine a path that must bend around). + * + * @returns true if the goal was reachable, false otherwise. */ - void MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass); + bool MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass); /** * Updates @p i, @p j (which is assumed to be an impassable navcell) @@ -125,12 +148,12 @@ private: static const u8 CHUNK_SIZE = 96; // number of navcells per side - // TODO PATHFINDER: figure out best number. Probably 64 < n < 128 + // TODO: figure out best number. Probably 64 < n < 128 struct Chunk { u8 m_ChunkI, m_ChunkJ; // chunk ID - u16 m_NumRegions; // number of local region IDs (starting from 1) + std::vector m_RegionsID; // IDs of local region, without 0 u16 m_Regions[CHUNK_SIZE][CHUNK_SIZE]; // local region ID per navcell cassert(CHUNK_SIZE*CHUNK_SIZE/2 < 65536); // otherwise we could overflow m_NumRegions with a checkerboard pattern @@ -144,16 +167,41 @@ void RegionNavcellNearest(u16 r, int iGoal, int jGoal, int& iBest, int& jBest, u32& dist2Best) const; bool RegionNearestNavcellInGoal(u16 r, u16 i0, u16 j0, const PathGoal& goal, u16& iOut, u16& jOut, u32& dist2Best) const; + +#ifdef TEST + bool operator==(const Chunk& b) const + { + return (m_ChunkI == b.m_ChunkI && m_ChunkJ == b.m_ChunkJ && m_RegionsID == b.m_RegionsID && memcmp(&m_Regions, &b.m_Regions, sizeof(u16) * CHUNK_SIZE * CHUNK_SIZE) == 0); + } +#endif }; typedef std::map > EdgesMap; - void FindEdges(u8 ci, u8 cj, pass_class_t passClass, EdgesMap& edges); + void RecomputeAllEdges(pass_class_t passClass, EdgesMap& edges); + void UpdateEdges(u8 ci, u8 cj, pass_class_t passClass, EdgesMap& edges); void FindReachableRegions(RegionID from, std::set& reachable, pass_class_t passClass); void FindPassableRegions(std::set& regions, pass_class_t passClass); + void FindGoalRegions(u16 gi, u16 gj, const PathGoal& goal, std::set& regions, pass_class_t passClass); + + /* + * Helpers for the A* implementation of MakeGoalReachable. + * We reuse flat_XX containers to have good cache locality and avoid the cost of allocating memory. Flat_XX implementa map/set as a sorted vector + */ + boost::container::flat_map m_Astar_Predecessor; + boost::container::flat_map m_Astar_GScore; + boost::container::flat_map m_Astar_FScore; + boost::container::flat_set m_Astar_ClosedNodes; + boost::container::flat_set m_Astar_OpenNodes; + + inline int DistBetween(const RegionID& a, const RegionID& b) + { + return (abs(a.ci - b.ci) + abs(a.cj - b.cj)) * CHUNK_SIZE - 30; + }; + /** * Updates @p iGoal and @p jGoal to the navcell that is the nearest to the * initial goal coordinates, in one of the given @p regions. @@ -174,6 +222,9 @@ std::map m_Edges; + std::map> m_GlobalRegions; + std::vector m_AvailableGlobalRegionIDs; // TODO: actually push back deleted global regions here. + // Passability classes for which grids will be updated when calling Update std::map m_PassClassMasks; Index: source/simulation2/helpers/HierarchicalPathfinder.cpp =================================================================== --- source/simulation2/helpers/HierarchicalPathfinder.cpp +++ source/simulation2/helpers/HierarchicalPathfinder.cpp @@ -111,13 +111,12 @@ } // Directly point the root ID - m_NumRegions = 0; for (u16 i = 1; i < regionID+1; ++i) { - if (connect[i] == i) - ++m_NumRegions; - else + if (connect[i] != i) connect[i] = RootID(i, connect); + if (std::find(m_RegionsID.begin(),m_RegionsID.end(), connect[i]) == m_RegionsID.end()) + m_RegionsID.push_back(connect[i]); } // Replace IDs by the root ID @@ -224,6 +223,8 @@ { case PathGoal::POINT: { + if (gi/CHUNK_SIZE != m_ChunkI || gj/CHUNK_SIZE != m_ChunkJ) + return false; if (m_Regions[gj-m_ChunkJ * CHUNK_SIZE][gi-m_ChunkI * CHUNK_SIZE] == r) { iOut = gi; @@ -366,6 +367,10 @@ m_Chunks.clear(); m_Edges.clear(); + // reset global regions + m_AvailableGlobalRegionIDs.clear(); + m_AvailableGlobalRegionIDs.push_back(1); + for (auto& passClassMask : allPassClasses) { pass_class_t passClass = passClassMask.second; @@ -381,16 +386,35 @@ } // Construct the search graph over the regions - EdgesMap& edges = m_Edges[passClass]; + RecomputeAllEdges(passClass, edges); - for (int cj = 0; cj < m_ChunksH; ++cj) - { - for (int ci = 0; ci < m_ChunksW; ++ci) - { - FindEdges(ci, cj, passClass, edges); - } - } + // Spread global regions. + std::map& globalRegion = m_GlobalRegions[passClass]; + globalRegion.clear(); + for (u8 cj = 0; cj < m_ChunksH; ++cj) + for (u8 ci = 0; ci < m_ChunksW; ++ci) + for (u16 rid : GetChunk(ci, cj, passClass).m_RegionsID) + { + RegionID reg{ci,cj,rid}; + if (globalRegion.find(reg) == globalRegion.end()) + { + GlobalRegionID ID = m_AvailableGlobalRegionIDs.back(); + m_AvailableGlobalRegionIDs.pop_back(); + if (m_AvailableGlobalRegionIDs.empty()) + m_AvailableGlobalRegionIDs.push_back(ID+1); + + globalRegion.insert({ reg, ID }); + // avoid creating an empty link if possible, FindReachableRegions uses [] which calls the default constructor + if (edges.find(reg) != edges.end()) + { + std::set reachable; + FindReachableRegions(reg, reachable, passClass); + for (const RegionID& region : reachable) + globalRegion.insert({ region, ID }); + } + } + } } if (m_DebugOverlay) @@ -405,9 +429,10 @@ { PROFILE3("Hierarchical Update"); - for (int cj = 0; cj < m_ChunksH; ++cj) + std::vector updated; + for (u8 cj = 0; cj < m_ChunksH; ++cj) { - for (int ci = 0; ci < m_ChunksW; ++ci) + for (u8 ci = 0; ci < m_ChunksW; ++ci) { if (!IsChunkDirty(ci, cj, dirtinessGrid)) continue; @@ -415,25 +440,79 @@ { pass_class_t passClass = passClassMask.second; Chunk& a = m_Chunks[passClass].at(ci + cj*m_ChunksW); + + // Clean up edges and global region ID + EdgesMap& edgeMap = m_Edges[passClass]; + for (u16 i : a.m_RegionsID) + { + RegionID reg{ci, cj, i}; + m_GlobalRegions[passClass].erase(reg); + for (const RegionID& neighbor : edgeMap[reg]) + { + edgeMap[neighbor].erase(reg); + if (edgeMap[neighbor].empty()) + edgeMap.erase(neighbor); + } + edgeMap.erase(reg); + } + + // recompute a.InitRegions(ci, cj, grid, passClass); + + for (u16 i : a.m_RegionsID) + updated.push_back(RegionID{ci, cj, i}); + + // add back edges + UpdateEdges(ci, cj, passClass, edgeMap); } } } - // TODO: Also be clever with edges - m_Edges.clear(); - for (const std::pair& passClassMask : m_PassClassMasks) - { - pass_class_t passClass = passClassMask.second; - EdgesMap& edges = m_Edges[passClass]; + // Add back global region ID + // To try and be clever we'll run a custom flood-fill that stops as soon as it runs into something we know, + // and if nothing then we'll create a new global region. + // It also keeps track of all connected regions with no IDs in case of contiguous dirtiness (likely) to be faster if possible. + // This probably scales poorly with a large enough update? - for (int cj = 0; cj < m_ChunksH; ++cj) + for (const RegionID& reg : updated) + for (const std::pair& passClassMask : m_PassClassMasks) { - for (int ci = 0; ci < m_ChunksW; ++ci) + std::set visited; + std::vector open; + std::vector updating = { reg }; + open.push_back(reg); + + GlobalRegionID ID = 0; + std::map& globalRegion = m_GlobalRegions[passClassMask.second]; + EdgesMap& edgeMap = m_Edges[passClassMask.second]; + // avoid creating empty edges. + bool unlinked = edgeMap.find(reg) == edgeMap.end(); + while (!open.empty() && ID == 0 && !unlinked) { - FindEdges(ci, cj, passClass, edges); + RegionID curr = open.back(); + open.pop_back(); + for (const RegionID& region : edgeMap[curr]) + if (visited.insert(region).second) + { + open.push_back(region); + if (globalRegion.find(region) != globalRegion.end()) + { + ID = globalRegion.at(region); + break; + } + else + updating.push_back(region); + } } - } + if (ID == 0) + { + ID = m_AvailableGlobalRegionIDs.back(); + m_AvailableGlobalRegionIDs.pop_back(); + if (m_AvailableGlobalRegionIDs.empty()) + m_AvailableGlobalRegionIDs.push_back(ID+1); + } + for (const RegionID& reg : updating) + globalRegion[reg] = ID; } if (m_DebugOverlay) @@ -462,22 +541,15 @@ } /** - * Find edges between regions in this chunk and the adjacent below/left chunks. + * Connect a chunk's regions to their neighbors. Not optimised for global recomputing. + * TODO: reduce code duplication with below */ -void HierarchicalPathfinder::FindEdges(u8 ci, u8 cj, pass_class_t passClass, EdgesMap& edges) +void HierarchicalPathfinder::UpdateEdges(u8 ci, u8 cj, pass_class_t passClass, EdgesMap& edges) { std::vector& chunks = m_Chunks[passClass]; Chunk& a = chunks.at(cj*m_ChunksW + ci); - // For each edge between chunks, we loop over every adjacent pair of - // navcells in the two chunks. If they are both in valid regions - // (i.e. are passable navcells) then add a graph edge between those regions. - // (We don't need to test for duplicates since EdgesMap already uses a - // std::set which will drop duplicate entries.) - // But as set.insert can be quite slow on large collection, and that we usually - // try to insert the same values, we cache the previous one for a fast test. - if (ci > 0) { Chunk& b = chunks.at(cj*m_ChunksW + (ci-1)); @@ -499,6 +571,27 @@ } } + if (ci < m_ChunksW-1) + { + Chunk& b = chunks.at(cj*m_ChunksW + (ci+1)); + RegionID raPrev(0,0,0); + RegionID rbPrev(0,0,0); + for (int j = 0; j < CHUNK_SIZE; ++j) + { + RegionID ra = a.Get(CHUNK_SIZE-1, j); + RegionID rb = b.Get(0, j); + if (ra.r && rb.r) + { + if (ra == raPrev && rb == rbPrev) + continue; + edges[ra].insert(rb); + edges[rb].insert(ra); + raPrev = ra; + rbPrev = rb; + } + } + } + if (cj > 0) { Chunk& b = chunks.at((cj-1)*m_ChunksW + ci); @@ -520,6 +613,94 @@ } } + if (cj < m_ChunksH - 1) + { + Chunk& b = chunks.at((cj+1)*m_ChunksW + ci); + RegionID raPrev(0,0,0); + RegionID rbPrev(0,0,0); + for (int i = 0; i < CHUNK_SIZE; ++i) + { + RegionID ra = a.Get(i, CHUNK_SIZE-1); + RegionID rb = b.Get(i, 0); + if (ra.r && rb.r) + { + if (ra == raPrev && rb == rbPrev) + continue; + edges[ra].insert(rb); + edges[rb].insert(ra); + raPrev = ra; + rbPrev = rb; + } + } + } +} + +/** + * Find edges between regions in all chunks, in an optimised manner (only look at top/left) + */ +void HierarchicalPathfinder::RecomputeAllEdges(pass_class_t passClass, EdgesMap& edges) +{ + std::vector& chunks = m_Chunks[passClass]; + + edges.clear(); + + for (int cj = 0; cj < m_ChunksH; ++cj) + { + for (int ci = 0; ci < m_ChunksW; ++ci) + { + Chunk& a = chunks.at(cj*m_ChunksW + ci); + + // For each edge between chunks, we loop over every adjacent pair of + // navcells in the two chunks. If they are both in valid regions + // (i.e. are passable navcells) then add a graph edge between those regions. + // (We don't need to test for duplicates since EdgesMap already uses a + // std::set which will drop duplicate entries.) + // But as set.insert can be quite slow on large collection, and that we usually + // try to insert the same values, we cache the previous one for a fast test. + + if (ci > 0) + { + Chunk& b = chunks.at(cj*m_ChunksW + (ci-1)); + RegionID raPrev(0,0,0); + RegionID rbPrev(0,0,0); + for (int j = 0; j < CHUNK_SIZE; ++j) + { + RegionID ra = a.Get(0, j); + RegionID rb = b.Get(CHUNK_SIZE-1, j); + if (ra.r && rb.r) + { + if (ra == raPrev && rb == rbPrev) + continue; + edges[ra].insert(rb); + edges[rb].insert(ra); + raPrev = ra; + rbPrev = rb; + } + } + } + + if (cj > 0) + { + Chunk& b = chunks.at((cj-1)*m_ChunksW + ci); + RegionID raPrev(0,0,0); + RegionID rbPrev(0,0,0); + for (int i = 0; i < CHUNK_SIZE; ++i) + { + RegionID ra = a.Get(i, 0); + RegionID rb = b.Get(i, CHUNK_SIZE-1); + if (ra.r && rb.r) + { + if (ra == raPrev && rb == rbPrev) + continue; + edges[ra].insert(rb); + edges[rb].insert(ra); + raPrev = ra; + rbPrev = rb; + } + } + } + } + } } /** @@ -557,7 +738,7 @@ xz.push_back(b.Y.ToFloat()); m_DebugOverlayLines.emplace_back(); - m_DebugOverlayLines.back().m_Color = CColor(1.0, 1.0, 1.0, 1.0); + m_DebugOverlayLines.back().m_Color = CColor(1.0, 0.0, 0.0, 1.0); SimRender::ConstructLineOnGround(*m_SimContext, xz, m_DebugOverlayLines.back(), true); } } @@ -571,75 +752,283 @@ return m_Chunks[passClass][cj*m_ChunksW + ci].Get(i % CHUNK_SIZE, j % CHUNK_SIZE); } -void HierarchicalPathfinder::MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass) +HierarchicalPathfinder::GlobalRegionID HierarchicalPathfinder::GetGlobalRegion(u16 i, u16 j, pass_class_t passClass) { - PROFILE2("MakeGoalReachable"); + RegionID region = Get(i, j, passClass); + if (region.r == 0) + return (GlobalRegionID)0; + return m_GlobalRegions[passClass][region]; +} + +#define OUTPUT 0 + +#if OUTPUT + #include +#endif + +#if OUTPUT +// also change the header +bool HierarchicalPathfinder::MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass, std::ofstream& stream) +#else +bool HierarchicalPathfinder::MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass) +#endif +{ + /* + * Relatively straightforward implementation of A* on the hierarchical pathfinder graph. + * Since this isn't a grid, we cannot use JPS (though I'm fairly sure it could sort of be extended to work, but it's probably not trivial/worth it) + * Uses flat_set and flat_map over std::set and std::map since testing proves that reusing the memory ends up being more efficient + * The main optimisations are: + * - picking the best item directly from the open list when we can be sure we know which one it is (see fasttrack) + * - checking whether the goal is reachable or not, and if it isn't stopping early to avoid slowly flood-filling everything + * + * Since we'd like to return the best possible navcell, if the goal is reachable, we'll stop A* once we've reached any goal region + * since then presumably other reachable goal-regions would be reachable following roughtly the same path. + * Then we'll loop over goal regions to get the best navcell and reconstruct the path from there. + * + * NB: since the path is currently unused, I skip the A* part for reachable goals. + */ RegionID source = Get(i0, j0, passClass); - // Find everywhere that's reachable - std::set reachableRegions; - FindReachableRegions(source, reachableRegions, passClass); + u16 gi, gj; + Pathfinding::NearestNavcell(goal.x, goal.z, gi, gj, m_W, m_H); - // Check whether any reachable region contains the goal - // And get the navcell that's the closest to the point + // determine if we will be able to reach the goal. + // If not, we can stop A* earlier by being clever. + std::set goalRegions; + FindGoalRegions(gi, gj, goal, goalRegions, passClass); + + std::vector reachableGoalRegions; + + GlobalRegionID startID = GetGlobalRegion(i0, j0, passClass); + bool reachable = false; + for (const RegionID& r : goalRegions) + if (m_GlobalRegions[passClass][r] == startID) + { + reachable = true; + reachableGoalRegions.push_back(r); + } - u16 bestI = 0; - u16 bestJ = 0; - u32 dist2Best = std::numeric_limits::max(); +#if OUTPUT + stream << "context.fillStyle = 'rgba(1,0,1,1)';\n"; + for (const RegionID& r : goalRegions) + { + entity_pos_t x0 = Pathfinding::NAVCELL_SIZE * (r.ci * CHUNK_SIZE); + entity_pos_t z0 = Pathfinding::NAVCELL_SIZE * (r.cj * CHUNK_SIZE); + stream << "context2.fillRect(" << x0.ToInt_RoundToZero() << " * scale," << z0.ToInt_RoundToZero() << " * scale," << (int)CHUNK_SIZE << " * scale," << (int)CHUNK_SIZE << " * scale);\n"; + } +#endif + + // In general, our maps are relatively open, so it's usually a better bet to be biaised towards minimal distance over path length. + int (*DistEstimate)(u16, u16, u16, u16) = [](u16 regI, u16 regJ, u16 gi, u16 gj) -> int { return (regI - gi)*(regI - gi) + (regJ - gj)*(regJ - gj); }; + // However, run unbiaised euclidian if we know the goal is unreachable, since we want to get as close as possible efficiently. + // multiply by 20 because we want distance to goal to matter a lot + if (!reachable) + DistEstimate = [](u16 regI, u16 regJ, u16 gi, u16 gj) -> int { + return 20 * isqrt64((regI - gi)*(regI - gi) + (regJ - gj)*(regJ - gj)); + }; + + m_Astar_ClosedNodes.clear(); + m_Astar_OpenNodes.clear(); + m_Astar_OpenNodes.insert(source); + + m_Astar_Predecessor.clear(); + + m_Astar_GScore.clear(); + m_Astar_GScore[source] = 0; + + m_Astar_FScore.clear(); + m_Astar_FScore[source] = DistEstimate(source.ci * CHUNK_SIZE + CHUNK_SIZE/2, source.cj * CHUNK_SIZE + CHUNK_SIZE/2, gi, gj); + + RegionID current {0,0,0}; + + u16 bestI, bestJ; + u32 dist2; + + u32 timeSinceLastFScoreImprovement = 0; +#if OUTPUT + int step = 0; +#endif + + RegionID fastTrack = source; + int currentFScore = m_Astar_FScore[source]; + int secondBestFScore = currentFScore; + int globalBestFScore = currentFScore; + + EdgesMap& edgeMap = m_Edges[passClass]; + + // NB: to re-enable A* for the reachable case (if you want to use the path), remove the "!reachable" part of this check + while (!reachable && !m_Astar_OpenNodes.empty()) + { + // Since we are not using a fancy open list, we have to go through all nodes each time + // So when we are sure that we know the best node (because the last run gave us a node better than us, which was already the best + // we can fast-track and not sort but just pick that one instead. + // In cases where the obvious path is the best, we hardly ever sort and it's a little faster + if (fastTrack.r) + { + current = fastTrack; + currentFScore = m_Astar_FScore[current]; + secondBestFScore = currentFScore; + } + else + { + auto iter = m_Astar_OpenNodes.begin(); + current = *iter; + currentFScore = m_Astar_FScore[current]; + secondBestFScore = currentFScore; + while (++iter != m_Astar_OpenNodes.end()) + { + int score = m_Astar_FScore[*iter]; + if (score < currentFScore) + { + current = *iter; + secondBestFScore = currentFScore; + currentFScore = score; + } + } + } - for (const RegionID& region : reachableRegions) - { - // Skip region if its chunk doesn't contain the goal area - entity_pos_t x0 = Pathfinding::NAVCELL_SIZE * (region.ci * CHUNK_SIZE); - entity_pos_t z0 = Pathfinding::NAVCELL_SIZE * (region.cj * CHUNK_SIZE); + m_Astar_OpenNodes.erase(current); + m_Astar_ClosedNodes.emplace(current); + if (reachable) + m_Astar_FScore.erase(current); + m_Astar_GScore.erase(current); + + // Stop heuristic in case we know we cannot reach the goal. + // Indeed this would cause A* to become an inacceptably slow flood fill. + // We keep track of our best fScore, we'll early-exit if we're too far from it + // or we haven't found a better path in a while. + // This will cause us to return largely suboptimal paths now and then, + // but then again those should be rare and the player can just re-order a move. + if (!reachable) + { + if (currentFScore < globalBestFScore) + { + globalBestFScore = currentFScore; + timeSinceLastFScoreImprovement = 0; + } + else if ( (++timeSinceLastFScoreImprovement > 3 && currentFScore > globalBestFScore * 2) || timeSinceLastFScoreImprovement > m_ChunksW) + break; + } + + entity_pos_t x0 = Pathfinding::NAVCELL_SIZE * (current.ci * CHUNK_SIZE); + entity_pos_t z0 = Pathfinding::NAVCELL_SIZE * (current.cj * CHUNK_SIZE); entity_pos_t x1 = x0 + Pathfinding::NAVCELL_SIZE * CHUNK_SIZE; entity_pos_t z1 = z0 + Pathfinding::NAVCELL_SIZE * CHUNK_SIZE; - if (!goal.RectContainsGoal(x0, z0, x1, z1)) - continue; - u16 i,j; - u32 dist2; +#if OUTPUT + stream << "context.fillStyle = 'rgba(" < 0 ? 255 : 0) <<",0.8)';\n maxStep = " << step+1 << ";\n"; + stream << "if (step >= " << step << ") context.fillRect(" << x0.ToInt_RoundToZero() << " * scale," << z0.ToInt_RoundToZero() << " * scale," << (int)CHUNK_SIZE << " * scale," << (int)CHUNK_SIZE << " * scale);\n"; +#endif + + fastTrack = RegionID{0,0,0}; + + // TODO: we should get those first and then validate here, instead of recomputing for each. + if (goal.RectContainsGoal(x0, z0, x1, z1)) + if (GetChunk(current.ci, current.cj, passClass).RegionNearestNavcellInGoal(current.r, i0, j0, goal, bestI, bestJ, dist2)) + break; - // If the region contains the goal area, the goal is reachable - // Remember the best point for optimization. - if (GetChunk(region.ci, region.cj, passClass).RegionNearestNavcellInGoal(region.r, i0, j0, goal, i, j, dist2)) - { - // If it's a point, no need to move it, we're done - if (goal.type == PathGoal::POINT) - return; - if (dist2 < dist2Best) + int currScore = m_Astar_GScore[current]; + for (const RegionID& neighbor : edgeMap[current]) + { + if (m_Astar_ClosedNodes.find(neighbor) != m_Astar_ClosedNodes.end()) + continue; + int temp_m_Astar_GScore = currScore + DistBetween(neighbor, current); + auto iter = m_Astar_OpenNodes.emplace(neighbor); + if (!iter.second && temp_m_Astar_GScore >= m_Astar_GScore[neighbor]) + continue; +#if OUTPUT + x0 = Pathfinding::NAVCELL_SIZE * (neighbor.ci * CHUNK_SIZE); + z0 = Pathfinding::NAVCELL_SIZE * (neighbor.cj * CHUNK_SIZE); + stream << "context2.fillStyle = 'rgba(255,255,0,0.3)';\n"; + stream << "if (step >= " << step << ") context2.fillRect(" << x0.ToInt_RoundToZero() << " * scale," << z0.ToInt_RoundToZero() << " * scale," << (int)CHUNK_SIZE << " * scale," << (int)CHUNK_SIZE << " * scale);\n"; +#endif + + m_Astar_GScore[neighbor] = temp_m_Astar_GScore; + // no default constructor so we'll use this hack in the meantime + auto alreadyThere = m_Astar_Predecessor.emplace( boost::container::flat_map::value_type{ neighbor, current }); + alreadyThere.first->second = current; + int score; + // if the goal is reachable, we don't care much about fscore precision so pick the center + if (reachable) + score = temp_m_Astar_GScore + DistEstimate(neighbor.ci * CHUNK_SIZE + CHUNK_SIZE/2, neighbor.cj * CHUNK_SIZE + CHUNK_SIZE/2, gi, gj); + else { - bestI = i; - bestJ = j; - dist2Best = dist2; + // if it's unreachable, it's more important however. So when we're close, get the "region center". + entity_pos_t x0 = Pathfinding::NAVCELL_SIZE * (neighbor.ci * CHUNK_SIZE); + entity_pos_t z0 = Pathfinding::NAVCELL_SIZE * (neighbor.cj * CHUNK_SIZE); + entity_pos_t x1 = x0 + Pathfinding::NAVCELL_SIZE * CHUNK_SIZE; + entity_pos_t z1 = z0 + Pathfinding::NAVCELL_SIZE * CHUNK_SIZE; + if (goal.RectContainsGoal(x0, z0, x1, z1)) + { + int ri, rj; + GetChunk(neighbor.ci, neighbor.cj, passClass).RegionCenter(neighbor.r, ri, rj); + score = temp_m_Astar_GScore + DistEstimate((u16)ri, (u16)rj, gi, gj); + } + else + score = temp_m_Astar_GScore + DistEstimate(neighbor.ci * CHUNK_SIZE + CHUNK_SIZE/2, neighbor.cj * CHUNK_SIZE + CHUNK_SIZE/2, gi, gj); } + if (score < secondBestFScore) + { + secondBestFScore = score; + fastTrack = neighbor; + } + m_Astar_FScore[neighbor] = score; } +#if OUTPUT + step++; +#endif } - // If the goal area wasn't reachable, - // find the navcell that's nearest to the goal's center - if (dist2Best == std::numeric_limits::max()) - { - u16 iGoal, jGoal; - Pathfinding::NearestNavcell(goal.x, goal.z, iGoal, jGoal, m_W, m_H); - - FindNearestNavcellInRegions(reachableRegions, iGoal, jGoal, passClass); - - // Construct a new point goal at the nearest reachable navcell - PathGoal newGoal; - newGoal.type = PathGoal::POINT; - Pathfinding::NavcellCenter(iGoal, jGoal, newGoal.x, newGoal.z); - goal = newGoal; - return; +#if OUTPUT + fixed x0 = Pathfinding::NAVCELL_SIZE * (current.ci * CHUNK_SIZE); + fixed z0 = Pathfinding::NAVCELL_SIZE * (current.cj * CHUNK_SIZE); + stream << "context.fillStyle = 'rgba(255,0,0,0.6)';\n"; + stream << "if (step >= " << step << ") context.fillRect(" << x0.ToInt_RoundToZero() << " * scale," << z0.ToInt_RoundToZero() << " * scale," << (int)CHUNK_SIZE << " * scale," << (int)CHUNK_SIZE << " * scale);\n"; +#endif + + if (!reachable) + { + // I don't believe this is possible, nor should it. + ENSURE(!m_Astar_ClosedNodes.empty()); + + // Pick best and roll with that. + current = *std::min_element(m_Astar_ClosedNodes.begin(), m_Astar_ClosedNodes.end(), + [this](const RegionID& a, const RegionID& b) -> bool { return m_Astar_FScore[a] < m_Astar_FScore[b]; }); + + std::set set = { current }; + Pathfinding::NearestNavcell(goal.x, goal.z, bestI, bestJ, m_W, m_H); + + FindNearestNavcellInRegions(set, bestI, bestJ, passClass); + } + else + { + u32 bestDist = std::numeric_limits::max(); + // loop through reachable goal regions and get the best navcell. + // TODO: we probably could skip some of those if our gScore/fScore were good enough. + for (const RegionID& region : reachableGoalRegions) + { + u16 ri, rj; + u32 dist; + // TODO: using the A* path, we should consider from predecessor and not from source + GetChunk(region.ci, region.cj, passClass).RegionNearestNavcellInGoal(region.r, i0, j0, goal, ri, rj, dist); + if (dist < bestDist) + { + bestI = ri; + bestJ = rj; + bestDist = dist; + } + } } - ENSURE(dist2Best != std::numeric_limits::max()); PathGoal newGoal; newGoal.type = PathGoal::POINT; Pathfinding::NavcellCenter(bestI, bestJ, newGoal.x, newGoal.z); goal = newGoal; + + return reachable; } + void HierarchicalPathfinder::FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass) { std::set regions; @@ -710,15 +1099,16 @@ // collecting all the regions that are reachable via edges std::vector open; + open.reserve(64); open.push_back(from); reachable.insert(from); + EdgesMap& edgeMap = m_Edges[passClass]; while (!open.empty()) { RegionID curr = open.back(); open.pop_back(); - - for (const RegionID& region : m_Edges[passClass][curr]) + for (const RegionID& region : edgeMap[curr]) // Add to the reachable set; if this is the first time we added // it then also add it to the open list if (reachable.insert(region).second) @@ -731,12 +1121,43 @@ // Construct a set of all regions of all chunks for this pass class for (const Chunk& chunk : m_Chunks[passClass]) { - // region 0 is impassable tiles - for (int r = 1; r <= chunk.m_NumRegions; ++r) + for (u16 r : chunk.m_RegionsID) regions.insert(RegionID(chunk.m_ChunkI, chunk.m_ChunkJ, r)); } } +void HierarchicalPathfinder::FindGoalRegions(u16 gi, u16 gj, const PathGoal& goal, std::set& regions, pass_class_t passClass) +{ + if (goal.type == PathGoal::POINT) + { + RegionID region = Get(gi, gj, passClass); + if (region.r > 0) + regions.insert(region); + return; + } + + // For non-point cases, we'll test each region inside the bounds of the goal. + // we might occasionally test a few too many for circles but it's not too bad. + // Note that this also works in the Inverse-circle / Inverse-square case + // Since our ranges are inclusive, we will necessarily test at least the perimeter/outer bound of the goal. + // If we find a navcell, great, if not, well then we'll be surrounded by an impassable barrier. + // Since in the Inverse-XX case we're supposed to start inside, then we can't ever reach the goal so it's good enough. + // It's not worth it to skip the "inner" regions since we'd need ranges above CHUNK_SIZE for that to start mattering + // (and even then not always) and that just doesn't happen for Inverse-XX goals + int size = (std::max(goal.hh, goal.hw) * 3 / 2).ToInt_RoundToInfinity(); + + u16 a,b; u32 c; // unused params for RegionNearestNavcellInGoal + + for (u8 sz = (gj - size) / CHUNK_SIZE; sz <= (gj + size) / CHUNK_SIZE; ++sz) + for (u8 sx = (gi - size) / CHUNK_SIZE; sx <= (gi + size) / CHUNK_SIZE; ++sx) + { + Chunk& chunk = GetChunk(sx, sz, passClass); + for (u16 i : chunk.m_RegionsID) + if (chunk.RegionNearestNavcellInGoal(i, 0, 0, goal, a, b, c)) + regions.insert(RegionID{sx, sz, i}); + } +} + void HierarchicalPathfinder::FillRegionOnGrid(const RegionID& region, pass_class_t passClass, u16 value, Grid& grid) { ENSURE(grid.m_W == m_W && grid.m_H == m_H); Index: source/simulation2/helpers/LongPathfinder.h =================================================================== --- source/simulation2/helpers/LongPathfinder.h +++ source/simulation2/helpers/LongPathfinder.h @@ -164,6 +164,10 @@ LongPathfinder(); ~LongPathfinder(); +#ifdef TEST + HierarchicalPathfinder& GetHierarchicalPathfinder() { return m_PathfinderHier; } +#endif + void SetDebugOverlay(bool enabled); void SetHierDebugOverlay(bool enabled, const CSimContext *simContext) @@ -239,6 +243,16 @@ void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& origGoal, pass_class_t passClass, std::vector excludedRegions, WaypointPath& path); + bool MakeGoalReachable(u16 i0, u16 j0, PathGoal &goal, pass_class_t passClass) { return m_PathfinderHier.MakeGoalReachable(i0, j0, goal, passClass); }; + + void FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass) { return m_PathfinderHier.FindNearestPassableNavcell(i, j, passClass); }; + + bool NavcellIsReachable(u16 i0, u16 j0, u16 i1, u16 j1, pass_class_t passClass) + { + return m_PathfinderHier.GetGlobalRegion(i0, j0, passClass) == m_PathfinderHier.GetGlobalRegion(i1, j1, passClass); + }; + + Grid GetConnectivityGrid(pass_class_t passClass) { return m_PathfinderHier.GetConnectivityGrid(passClass); Index: source/simulation2/helpers/PathGoal.h =================================================================== --- source/simulation2/helpers/PathGoal.h +++ source/simulation2/helpers/PathGoal.h @@ -28,6 +28,7 @@ * part of the goal. * Also, it can be an 'inverted' circle/square, where any point outside * the shape is part of the goal. + * In both cases, points on the range (ie at the frontier) are considered inside. */ class PathGoal { 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/scripting/MessageTypeConversions.cpp =================================================================== --- source/simulation2/scripting/MessageTypeConversions.cpp +++ source/simulation2/scripting/MessageTypeConversions.cpp @@ -267,20 +267,74 @@ //////////////////////////////// -JS::Value CMessageMotionChanged::ToJSVal(ScriptInterface& scriptInterface) const +JS::Value CMessageBeginMove::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); - SET_MSG_PROPERTY(starting); - SET_MSG_PROPERTY(error); return JS::ObjectValue(*obj); } -CMessage* CMessageMotionChanged::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +CMessage* CMessageBeginMove::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) { FROMJSVAL_SETUP(); - GET_MSG_PROPERTY(bool, starting); - GET_MSG_PROPERTY(bool, error); - return new CMessageMotionChanged(starting, error); + return new CMessageBeginMove(); +} + +//////////////////////////////// + +JS::Value CMessagePausedMove::ToJSVal(ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + return JS::ObjectValue(*obj); +} + +CMessage* CMessagePausedMove::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + return new CMessagePausedMove(); +} + +//////////////////////////////// + +JS::Value CMessageFinishedMove::ToJSVal(ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + SET_MSG_PROPERTY(failed); + return JS::ObjectValue(*obj); +} + +CMessage* CMessageFinishedMove::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + GET_MSG_PROPERTY(bool, failed); + return new CMessageFinishedMove(failed); +} + +//////////////////////////////// + +JS::Value CMessageMoveSuccess::ToJSVal(ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + return JS::ObjectValue(*obj); +} + +CMessage* CMessageMoveSuccess::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + return new CMessageMoveSuccess(); +} + +//////////////////////////////// + +JS::Value CMessageMoveFailure::ToJSVal(ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + return JS::ObjectValue(*obj); +} + +CMessage* CMessageMoveFailure::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + return new CMessageMoveFailure(); } //////////////////////////////// Index: source/tools/atlas/GameInterface/ActorViewer.cpp =================================================================== --- source/tools/atlas/GameInterface/ActorViewer.cpp +++ source/tools/atlas/GameInterface/ActorViewer.cpp @@ -375,7 +375,7 @@ { CmpPtr cmpUnitMotion(m.Simulation2, m.Entity); if (cmpUnitMotion) - speed = cmpUnitMotion->GetWalkSpeed().ToFloat(); + speed = cmpUnitMotion->GetBaseSpeed().ToFloat(); else speed = 7.f; // typical unit speed @@ -385,7 +385,7 @@ { CmpPtr cmpUnitMotion(m.Simulation2, m.Entity); if (cmpUnitMotion) - speed = cmpUnitMotion->GetRunSpeed().ToFloat(); + speed = cmpUnitMotion->GetBaseSpeed().ToFloat() * cmpUnitMotion->GetTopSpeedRatio().ToFloat(); else speed = 12.f; // typical unit speed