Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -309,8 +309,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/input.js =================================================================== --- binaries/data/mods/public/gui/session/input.js +++ binaries/data/mods/public/gui/session/input.js @@ -1517,12 +1517,11 @@ function performFormation(entities, formationTemplate) { - if (entities) - Engine.PostNetworkCommand({ - "type": "formation", - "entities": entities, - "name": formationTemplate - }); + Engine.PostNetworkCommand({ + "type": "formation", + "entities": entities, + "name": formationTemplate + }); } function performGroup(action, groupId) Index: binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- binaries/data/mods/public/gui/session/selection_panels.js +++ binaries/data/mods/public/gui/session/selection_panels.js @@ -338,11 +338,8 @@ g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item })); let formationInfo = g_FormationsInfo.get(data.item); - let formationOk = canMoveSelectionIntoFormation(data.item); - let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", { - "ents": data.unitEntStates.map(state => state.id), - "formationTemplate": data.item - }); + let formationOk = true;//TODO: reenable? canMoveSelectionIntoFormation(data.item); + let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", { "entities" : data.unitEntStates.map(state => state.id), "formationTemplate": data.item }); data.button.onPress = function() { performFormation(data.unitEntStates.map(state => state.id), data.item); 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/random/pathfinder_pertest.js =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/random/pathfinder_pertest.js @@ -0,0 +1,15 @@ +RMS.LoadLibrary("rmgen"); + +// initialize map +log("Initializing map..."); +InitMap(); + + +// General map setup +let mapSize = getMapSize(); +let mapCenterX = mapSize/2; +let mapCenterY = mapSize/2; + +// Nothing to create really, it'll all be done in triggers. + +ExportMap(); Index: binaries/data/mods/public/maps/random/pathfinder_pertest.json =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/random/pathfinder_pertest.json @@ -0,0 +1,15 @@ +{ + "settings" : { + "Name" : "Pathfinder Perf test", + "Script" : "pathfinder_pertest.js", + "Description" : "Test the performance of the pathfinder in a bunch of situations", + "BaseTerrain" : ["desert_sand_dunes_100"], + "BaseHeight" : 1, + "Keywords" : ["demo"], + "CircularMap" : true, + "TriggerScripts": [ + "scripts/TriggerHelper.js", + "random/pathfinder_pertest_triggers.js" + ] + } +} \ No newline at end of file Index: binaries/data/mods/public/maps/random/pathfinder_pertest_triggers.js =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/random/pathfinder_pertest_triggers.js @@ -0,0 +1,134 @@ +warn("Setting up test"); + +let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); +cmpTrigger.DoAfterDelay(1000, "TestUnobstructedShortDistance", {}); +cmpTrigger.DoAfterDelay(10000, "KillAll", {}); +cmpTrigger.DoAfterDelay(11000, "TestShortRunIntoEachOther", {}); +cmpTrigger.DoAfterDelay(20000, "KillAll", {}); +cmpTrigger.DoAfterDelay(21000, "TestLongDiagonalpath", {}); + +const TEST_TEMPLATE_REGULAR = "units/cart_support_female_citizen"; +const TEST_TEMPLATE_OBELISK = "other/obelisk"; + +Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetLosRevealAll(1, true); + + +Trigger.prototype.KillAll = function() +{ + let ents = Engine.GetEntitiesWithInterface(IID_Position); + for (let ent of ents) + Engine.DestroyEntity(ent); +} + +Trigger.prototype.TestUnobstructedShortDistance = function() +{ + warn("Starting TestUnobstructedShortDistance"); + // here we will plop units and make them walk in a straight, unobstructed, short line. + // in principle the largest cost here is the unit move order + let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); + let mapSize = 4*cmpTerrain.GetTilesPerSide(); + + let mapCenter = new Vector2D(mapSize/2.0, mapSize/2.0); + let testSquareSize = mapSize / 5; + + // create units + for (let z = mapCenter.y - testSquareSize; z <= mapCenter.y + testSquareSize; z += 20) + for (let x = mapCenter.x - testSquareSize; x <= mapCenter.x + testSquareSize;x += 4) + { + let ent = Engine.AddEntity(TEST_TEMPLATE_REGULAR); + + let component = Engine.QueryInterface(ent, IID_Position); + component.JumpTo(x, z); + + component = Engine.QueryInterface(ent, IID_Ownership); + component.SetOwnerQuiet(1); + + component = Engine.QueryInterface(ent, IID_UnitAI); + for (let i = 0; i < 10; ++i) + { + component.Walk(x, z + 15, true); + component.Walk(x, z, true); + } + } +} + +Trigger.prototype.TestShortRunIntoEachOther = function() +{ + warn("Starting TestShortRunIntoEachOther"); + // Same principle but units will run into each other. + // highlights slowness of hort-range pathfinder + let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); + let mapSize = 4*cmpTerrain.GetTilesPerSide(); + + let mapCenter = new Vector2D(mapSize/2.0, mapSize/2.0); + let testSquareSize = mapSize / 5; + + let dir = false; + // create units + for (let z = mapCenter.y - testSquareSize; z <= mapCenter.y + testSquareSize; z += 20) + { + for (let x = mapCenter.x - testSquareSize; x <= mapCenter.x + testSquareSize;x += 4) + { + let ent = Engine.AddEntity(TEST_TEMPLATE_REGULAR); + + let component = Engine.QueryInterface(ent, IID_Position); + component.JumpTo(x, z); + + component = Engine.QueryInterface(ent, IID_Ownership); + component.SetOwnerQuiet(1); + + component = Engine.QueryInterface(ent, IID_UnitAI); + for (let i = 0; i < 10; ++i) + { + component.Walk(x, z + (dir ? 40 : -40), true); + component.Walk(x, z, true); + } + } + dir = !dir; + } +} + +Trigger.prototype.TestLongDiagonalpath = function() +{ + warn("Starting TestLongDiagonalpath"); + + // stress-test long-range pathfinder. This is a diagonal move in the wrong direction on an largely empty map + // which means that JPS will basically loop over the whole map but also run in a ton of jump points. + // and generally speaking this is a worst case of some kind. + // units will then start moving. + // this scales poorly, on a medium map it's OK, but on a large map with more units and stuff it's just horribly slow. + + // the movement will stress-test TestLine() but it's a bit of an atypical case to say the least. + + let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); + let mapSize = 4*cmpTerrain.GetTilesPerSide(); + + let mapCenter = new Vector2D(mapSize/2.0, mapSize/2.0); + let testSquareSize = mapSize / 5; + + // create blocking obelisks + for (let z = mapCenter.y-testSquareSize; z <= mapCenter.y + testSquareSize*2; z += 8) + for (let x = mapCenter.x-testSquareSize; x <= mapCenter.x + testSquareSize*2;x += 8) + { + let ent = Engine.AddEntity(TEST_TEMPLATE_OBELISK); + + let component = Engine.QueryInterface(ent, IID_Position); + component.JumpTo(x, z); + } + + // create units + for (let z = mapCenter.y - testSquareSize*2; z <= mapCenter.y; z += 8) + for (let x = mapCenter.x - testSquareSize*2; x <= mapCenter.x;x += 8) + { + let ent = Engine.AddEntity(TEST_TEMPLATE_REGULAR); + + let component = Engine.QueryInterface(ent, IID_Position); + component.JumpTo(x, z); + + component = Engine.QueryInterface(ent, IID_Ownership); + component.SetOwnerQuiet(1); + + component = Engine.QueryInterface(ent, IID_UnitAI); + component.Walk(x + testSquareSize*2, z + testSquareSize*2); + } +} \ No newline at end of file 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/Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/Attack.js +++ binaries/data/mods/public/simulation/components/Attack.js @@ -210,10 +210,6 @@ Attack.prototype.CanAttack = function(target) { - let cmpFormation = Engine.QueryInterface(target, IID_Formation); - if (cmpFormation) - return true; - let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) @@ -293,14 +289,6 @@ Attack.prototype.GetBestAttackAgainst = function(target, allowCapture) { - let cmpFormation = Engine.QueryInterface(target, IID_Formation); - if (cmpFormation) - { - // TODO: Formation against formation needs review - let types = this.GetAttackTypes(); - return ["Ranged", "Melee", "Capture"].find(attack => types.indexOf(attack) != -1); - } - let cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; Index: binaries/data/mods/public/simulation/components/Builder.js =================================================================== --- binaries/data/mods/public/simulation/components/Builder.js +++ binaries/data/mods/public/simulation/components/Builder.js @@ -49,12 +49,7 @@ Builder.prototype.GetRange = function() { - var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); - var max = 2; - if (cmpObstruction) - max += cmpObstruction.GetUnitRadius(); - - return { "max": max, "min": 0 }; + return { "max": 1, "min": 0 }; }; /** 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 @@ -861,7 +861,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/GroupWalkManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/GroupWalkManager.js @@ -0,0 +1,266 @@ +function GroupWalkManager() {} + +GroupWalkManager.prototype.Schema = + ""; + +// This is a simple system component that keeps track of existing "groups" +// when a "walk together" order is issued as well as necessary information. + +GroupWalkManager.prototype.Init = function() +{ + this.groups = new Map(); + this.nextGroupID = 0; +}; + +GroupWalkManager.prototype.Serialize = function() +{ +}; + +GroupWalkManager.prototype.CreateGroup = function(formableEntsID, x, z, range, formationTemplate) +{ + let speed = 50.0; // TODO: put formation max speed? + for (let ent of formableEntsID) + { + let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); + let sp = cmpUnitMotion.GetBaseSpeed(); + if (sp < speed) + speed = sp; + } + let group = { + "entities" : formableEntsID, + "x" : x, + "z" : z, + "range": range, + "formationTemplate":formationTemplate, + "maxSpeed": speed, + "state": "waiting", + "readyForNextStep" : [], + "entHasObstructedPath" : [], + "step":0, + "waypoints": [], + "offsets": {} + }; + this.groups.set(this.nextGroupID++, group); + + return this.nextGroupID - 1; +}; + +GroupWalkManager.prototype.GetMaxSpeed = function(ID) +{ + // undefined if undefined + return !!this.groups.get(ID) ? this.groups.get(ID).maxSpeed : 0; +} + +GroupWalkManager.prototype.GetGroup = function(ID) +{ + // undefined if undefined + return this.groups.get(ID); +} + +// An entity is telling us it won't be able to reach the intended position +// if enough entities have told us this, skip the waypoint unless it's the last +GroupWalkManager.prototype.SetBlockedPath = function(ID, ent) +{ + if (!this.groups.get(ID)) + { + error("Entity " + ent + " blocked for unkown group " + ID); + return; + } + let group = this.groups.get(ID); + if (group.entities.indexOf(ent) === -1) + { + error("Entity " + ent + " blocked for group " + ID + " it is not a part of."); + return; + } + if (group.entHasObstructedPath.indexOf(ent) === -1) + group.entHasObstructedPath.push(ent); + + if (group.entHasObstructedPath.length < group.entities.length * 0.1) + return; + + if (group.waypoints.length <= 1) + return; + + this.AdvanceGroupState(ID); +} + +// An entity is telling us it's ready for the next waypoint +// if enough entities have told us, shift the whole group. +GroupWalkManager.prototype.SetReady = function(ID, ent) +{ + if (!this.groups.get(ID)) + { + error("Entity " + ent + " ready for unkown group " + ID); + return; + } + let group = this.groups.get(ID); + if (group.entities.indexOf(ent) === -1) + { + error("Entity " + ent + " ready for group " + ID + " it is not a part of."); + return; + } + if (group.readyForNextStep.indexOf(ent) === -1) + group.readyForNextStep.push(ent); + + if (group.state == "waiting" && group.readyForNextStep.length !== group.entities.length + || group.readyForNextStep.length < group.entities.length * 0.85) + return; + + this.AdvanceGroupState(ID); +} + +GroupWalkManager.prototype.AdvanceGroupState = function(ID) +{ + let group = this.groups.get(ID); + if (!this.groups.get(ID)) + { + error("Trying to advance unknown group " + ID); + return; + } + + group.readyForNextStep = []; + if (group.state == "waiting") + { + group.rallyPoint = { "x":0, "z": 0 }; + + // TODO: find the algo to find the initial grouping position. + + let x = 0; + let z = 0; + let bestDist = Number.POSITIVE_INFINITY; + for (let ent of group.entities) + { + let cmpPosition = Engine.QueryInterface(ent, IID_Position); + let ex = cmpPosition.GetPosition2D().x; + let ez = cmpPosition.GetPosition2D().y; + + let entPos = new Vector2D(ex, ez); + let goal = new Vector2D(group.x, group.z); + + let dist = entPos.distanceToSquared(goal); + if (dist < bestDist) + { + x = ex; + z = ez; + bestDist = dist; + } + } + group.rallyPoint.x = x; + group.rallyPoint.z = z; + + let p1 = new Vector2D(group.rallyPoint.x, group.rallyPoint.z); + + let cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder); + let path = cmpPathfinder.ComputePath(group.rallyPoint.x, group.rallyPoint.z, group.x, group.z, "large"); + group.waypoints = path; + group.step = group.waypoints.length; + if (group.waypoints.length > 1) + { + group.rallyPoint = { "x":group.waypoints[group.step-1].x, "z":group.waypoints[group.step-1].y }; + group.step--; + } + + // compute offsets. + let p2 = new Vector2D(group.rallyPoint.x, group.rallyPoint.z); + p1.sub(p2).mult(-1); + + let angle = Math.atan2(p1.x, p1.y); + group.offsets = this.ComputeOffsetsForWaypoint(angle, p1, group.entities); + + group.state = "walking"; + } + else if (group.state == "walking") + { + if (group.step === 0) + { + group.state = "arrived"; + return; + } + let p1 = new Vector2D(group.rallyPoint.x, group.rallyPoint.z); + + group.rallyPoint = { "x":group.waypoints[group.step-1].x, "z":group.waypoints[group.step-1].y }; + group.step--; + + // compute offsets. + let p2 = new Vector2D(group.rallyPoint.x, group.rallyPoint.z); + p1.sub(p2).mult(-1); + + let angle = Math.atan2(p1.x, p1.y); + group.offsets = this.ComputeOffsetsForWaypoint(angle, p1, group.entities); + + group.entHasObstructedPath = []; + + group.state = "walking"; + + for (let ent of group.entities) + { + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + cmpUnitAI.SetNextStateAlwaysEntering("WALKING"); + } + } +} + +GroupWalkManager.prototype.ComputeOffsetsForWaypoint = function(angle, center, entities) +{ + // for now we'll do a simple rectangular formations + // TODO: support more stuff. + let ret = {}; + + let xW = Math.round(Math.min(6, Math.max(2, entities.length/4))); + let y = -1; + let maxYOffset = 0; + let largeEntities = []; + for (let i = 0; i < entities.length; i++) + { + let ent = entities[i]; + let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); + if (cmpUnitMotion.GetUnitClearance() > 1) + { + largeEntities.push(ent); + continue; + } + let x = i % xW; + if (x == 0) + y++; + let offsetX = 3 * (x-xW/2.0); + let offsetY = -4 * y; + maxYOffset = -offsetY; + let vector = new Vector2D(offsetX, offsetY); + ret[ent] = vector.rotate(angle); + } + let baseY = y * -4; + y = 0; + xW = Math.round(Math.min(3, Math.max(1, largeEntities.length/2))); + for (let i = 0; i < largeEntities.length; i++) + { + let ent = largeEntities[i]; + let x = i % xW; + if (x == 0) + y++; + let offsetX = 9 * (x-xW/2.0); + let offsetY = baseY -10 * y; + maxYOffset = -offsetY; + let vector = new Vector2D(offsetX, offsetY); + ret[ent] = vector.rotate(angle); + } + + for (let ent in ret) + ret[ent].y += maxYOffset / 2.0; + + return ret; +} + +GroupWalkManager.prototype.ResignFromGroup = function(ID, ent) +{ + let group = this.groups.get(ID); + if (!group) + return; + group.entities.splice(group.entities.indexOf(ent), 1); + if (group.readyForNextStep.indexOf(ent) !== -1) + group.readyForNextStep.splice(group.readyForNextStep.indexOf(ent), 1); + + if (!group.entities.length) + this.groups.delete(ID); +} + +Engine.RegisterSystemComponentType(IID_GroupWalkManager, "GroupWalkManager", GroupWalkManager); 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 @@ -377,6 +377,7 @@ let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) ret.unitAI = { + "formation": cmpUnitAI.GetFormationTemplate(), "state": cmpUnitAI.GetCurrentState(), "orders": cmpUnitAI.GetOrders(), "hasWorkOrders": cmpUnitAI.HasWorkOrders(), @@ -589,8 +590,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; @@ -817,12 +820,10 @@ GuiInterface.prototype.IsFormationSelected = function(player, data) { - for (let ent of data.ents) + for (let ent of data.entities) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); - // GetLastFormationName is named in a strange way as it (also) is - // the value of the current formation (see Formation.js LoadFormation) - if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate) + if (cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate) return true; } return false; Index: binaries/data/mods/public/simulation/components/ProductionQueue.js =================================================================== --- binaries/data/mods/public/simulation/components/ProductionQueue.js +++ binaries/data/mods/public/simulation/components/ProductionQueue.js @@ -640,7 +640,7 @@ if (spawnedEnts.length > 0 && !cmpAutoGarrison) { - // If a rally point is set, walk towards it (in formation) using a suitable command based on where the + // If a rally point is set, walk towards it using a suitable command based on where the // rally point is placed. if (cmpRallyPoint) { 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; @@ -117,10 +137,6 @@ */ RallyPoint.prototype.TargetIsAlive = function(ent) { - var cmpFormation = Engine.QueryInterface(ent, IID_Formation); - if (cmpFormation) - return true; - var cmpHealth = QueryMiragedInterface(ent, IID_Health); return cmpHealth && cmpHealth.GetHitpoints() != 0; }; 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 = @@ -15,9 +17,6 @@ "standground" + "" + "" + - "" + - "" + - "" + "" + "" + "" + @@ -174,37 +173,6 @@ // ignore }, - // Formation handlers: - - "FormationLeave": function(msg) { - // ignore when we're not in FORMATIONMEMBER - }, - - // Called when being told to walk as part of a formation - "Order.FormationWalk": function(msg) { - // Let players move captured domestic animals around - if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) - { - this.FinishOrder(); - return; - } - - // For packable units: - // 1. If packed, we can move. - // 2. If unpacked, we first need to pack, then follow case 1. - if (this.CanPack()) - { - // Case 2: pack - this.PushOrderFront("Pack", { "force": true }); - return; - } - - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z); - - this.SetNextStateAlwaysEntering("FORMATIONMEMBER.WALKING"); - }, - // Special orders: // (these will be overridden by various states) @@ -217,22 +185,21 @@ this.FinishOrder(); return; } - // Move a tile outside the building - let range = 4; - if (this.MoveToTargetRangeExplicit(msg.data.target, range, range)) + // Move outside the building. Since INDIVIDUAL.WALKING checks for a distance in [0,1], move to 0.5 + let range = 0.5; + if (this.MoveToTargetRangeExplicit(msg.data.target, range)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.WALKING"); } else { - // We are already at the target, or can't move at all + // We can't reach the target this.FinishOrder(); } }, // Individual orders: - // (these will switch the unit out of formation mode) "Order.Stop": function(msg) { // We have no control over non-domestic animals. @@ -254,6 +221,34 @@ }, + "Order.GroupWalk": function(msg) { + if (this.IsAnimal() || this.IsTurret()) + { + this.FinishOrder(); + return; + } + + // For packable units: + // 1. If packed, we can move. + // 2. If unpacked, we first need to pack, then follow case 1. + if (this.CanPack()) + { + // Case 2: pack + this.PushOrderFront("Pack", { "force": true }); + return; + } + + let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager); + let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID); + if (!group || group.state != "waiting") + { + this.FinishOrder(); + return; + } + + this.SetNextStateAlwaysEntering("INDIVIDUAL.GROUPWALKING"); + }, + "Order.Walk": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) @@ -276,7 +271,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 +334,7 @@ } else { - // We are already at the target, or can't move at all + // We can't reach the target this.StopMoving(); this.FinishOrder(); } @@ -366,16 +361,8 @@ // TODO: what if the units are on a cliff ? the ship will go below the cliff // and the units won't be able to garrison. Should go to the nearest (accessible) shore - if (needToMove && this.MoveToTarget(this.order.data.target)) - { - this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING"); - } - else - { - // We are already at the target, or can't move at all - this.StopMoving(); - this.SetNextState("INDIVIDUAL.PICKUP.LOADING"); - } + this.MoveToTarget(this.order.data.target, true); + this.SetNextState("INDIVIDUAL.PICKUP"); }, "Order.Guard": function(msg) { @@ -385,7 +372,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 +382,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 +392,7 @@ } else { - // We are already at the target, or can't move at all + // We can't reach the target this.StopMoving(); this.FinishOrder(); } @@ -603,8 +590,13 @@ } return; } - - this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true, "allowCapture": false }); + if (this.order.data.triedAttacking) + { + this.FinishOrder(); + return; + } + this.order.data.triedAttacking = true; + this.PushOrderFront("Attack", { "target": this.order.data.target, "force": this.order.data.force, "hunting": true, "allowCapture": false }); return; } @@ -616,7 +608,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(); @@ -642,7 +634,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 @@ -693,7 +685,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(); @@ -723,16 +715,8 @@ return; } - if (this.MoveToGarrisonRange(this.order.data.target)) - { - this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING"); - } - else - { - // We do a range check before actually garrisoning - this.StopMoving(); - this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED"); - } + this.MoveToGarrisonRange(this.order.data.target, true) + this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING"); }, "Order.Autogarrison": function(msg) { @@ -750,690 +734,56 @@ this.isGarrisoned = false; }, - "Order.Alert": function(msg) { - this.alertRaiser = this.order.data.raiser; - - // Find a target to garrison into, if we don't already have one - if (!this.alertGarrisoningTarget) - this.alertGarrisoningTarget = this.FindNearbyGarrisonHolder(); - - if (this.alertGarrisoningTarget) - this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget}); - else - { - this.StopMoving(); - this.FinishOrder(); - } - }, - - "Order.Cheering": function(msg) { - this.SetNextState("INDIVIDUAL.CHEERING"); - }, - - "Order.Pack": function(msg) { - if (this.CanPack()) - { - this.StopMoving(); - this.SetNextState("INDIVIDUAL.PACKING"); - } - }, - - "Order.Unpack": function(msg) { - if (this.CanUnpack()) - { - this.StopMoving(); - this.SetNextState("INDIVIDUAL.UNPACKING"); - } - }, - - "Order.CancelPack": function(msg) { - var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); - if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked()) - cmpPack.CancelPack(); - this.FinishOrder(); - }, - - "Order.CancelUnpack": function(msg) { - var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); - if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked()) - cmpPack.CancelPack(); - this.FinishOrder(); - }, - - // States for the special entity representing a group of units moving in formation: - "FORMATIONCONTROLLER": { - - "Order.Walk": function(msg) { - this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); - - this.MoveToPoint(this.order.data.x, this.order.data.z); - this.SetNextState("WALKING"); - }, - - "Order.WalkAndFight": function(msg) { - this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); - - this.MoveToPoint(this.order.data.x, this.order.data.z); - this.SetNextState("WALKINGANDFIGHTING"); - }, - - "Order.MoveIntoFormation": function(msg) { - this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); - - this.MoveToPoint(this.order.data.x, this.order.data.z); - this.SetNextState("FORMING"); - }, - - // 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)) - this.SetNextState("WALKING"); - else - this.FinishOrder(); - }, - - "Order.WalkToTarget": function(msg) { - if (this.MoveToTarget(this.order.data.target)) - this.SetNextState("WALKING"); - else - this.FinishOrder(); - }, - - "Order.WalkToPointRange": function(msg) { - if (this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max)) - this.SetNextState("WALKING"); - else - this.FinishOrder(); - }, - - "Order.Patrol": function(msg) { - this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); - - this.MoveToPoint(this.order.data.x, this.order.data.z); - this.SetNextState("PATROL"); - }, - - "Order.Guard": function(msg) { - this.CallMemberFunction("Guard", [msg.data.target, false]); - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.Disband(); - }, - - "Order.Stop": function(msg) { - if (!this.IsAttackingAsFormation()) - this.CallMemberFunction("Stop", [false]); - this.FinishOrder(); - }, - - "Order.Attack": function(msg) { - var target = msg.data.target; - var allowCapture = msg.data.allowCapture; - var cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI); - if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember()) - target = cmpTargetUnitAI.GetFormationController(); - - var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - // Check if we are already in range, otherwise walk there - if (!this.CheckTargetAttackRange(target, target)) - { - if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) - { - if (this.MoveToTargetAttackRange(target, target)) - { - this.SetNextState("COMBAT.APPROACHING"); - return; - } - } - this.FinishOrder(); - return; - } - this.CallMemberFunction("Attack", [target, false, allowCapture]); - if (cmpAttack.CanAttackAsFormation()) - this.SetNextState("COMBAT.ATTACKING"); - else - this.SetNextState("MEMBER"); - }, - - "Order.Garrison": function(msg) { - if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder)) - { - this.FinishOrder(); - return; - } - // Check if we are already in range, otherwise walk there - if (!this.CheckGarrisonRange(msg.data.target)) - { - if (!this.CheckTargetVisible(msg.data.target)) - { - this.FinishOrder(); - return; - } - else - { - // Out of range; move there in formation - if (this.MoveToGarrisonRange(msg.data.target)) - { - this.SetNextState("GARRISON.APPROACHING"); - return; - } - } - } - - this.SetNextState("GARRISON.GARRISONING"); - }, - - "Order.Gather": function(msg) { - if (this.MustKillGatherTarget(msg.data.target)) - { - // The target was visible when this order was given, - // but could now be invisible. - if (!this.CheckTargetVisible(msg.data.target)) - { - if (msg.data.secondTry === undefined) - { - msg.data.secondTry = true; - this.PushOrderFront("Walk", msg.data.lastPos); - } - else - { - // We couldn't move there, or the target moved away - this.FinishOrder(); - } - return; - } - - this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, "allowCapture": false }); - return; - } - - // TODO: on what should we base this range? - // Check if we are already in range, otherwise walk there - if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) - { - if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) - // The target isn't gatherable or not visible any more. - this.FinishOrder(); - // TODO: Should we issue a gather-near-position order - // if the target isn't gatherable/doesn't exist anymore? - else - // Out of range; move there in formation - this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); - return; - } - - this.CallMemberFunction("Gather", [msg.data.target, false]); - - this.SetNextStateAlwaysEntering("MEMBER"); - }, - - "Order.GatherNearPosition": function(msg) { - // TODO: on what should we base this range? - // Check if we are already in range, otherwise walk there - if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20)) - { - // Out of range; move there in formation - this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 }); - return; - } - - this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]); - - this.SetNextStateAlwaysEntering("MEMBER"); - }, - - "Order.Heal": function(msg) { - // TODO: on what should we base this range? - // Check if we are already in range, otherwise walk there - if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) - { - if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) - // The target was destroyed - this.FinishOrder(); - else - // Out of range; move there in formation - this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); - return; - } - - this.CallMemberFunction("Heal", [msg.data.target, false]); - - this.SetNextStateAlwaysEntering("MEMBER"); - }, - - "Order.Repair": function(msg) { - // TODO: on what should we base this range? - // Check if we are already in range, otherwise walk there - if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) - { - if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) - // The building was finished or destroyed - this.FinishOrder(); - else - // Out of range move there in formation - this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); - return; - } - - this.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]); - - this.SetNextStateAlwaysEntering("MEMBER"); - }, - - "Order.ReturnResource": function(msg) { - // TODO: on what should we base this range? - // Check if we are already in range, otherwise walk there - if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) - { - if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) - // The target was destroyed - this.FinishOrder(); - else - // Out of range; move there in formation - this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); - return; - } - - this.CallMemberFunction("ReturnResource", [msg.data.target, false]); - - this.SetNextStateAlwaysEntering("MEMBER"); - }, - - "Order.Pack": function(msg) { - this.CallMemberFunction("Pack", [false]); - - this.SetNextStateAlwaysEntering("MEMBER"); - }, - - "Order.Unpack": function(msg) { - this.CallMemberFunction("Unpack", [false]); - - this.SetNextStateAlwaysEntering("MEMBER"); - }, - - "IDLE": { - "enter": function(msg) { - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.SetRearrange(false); - }, - - "MoveStarted": function() { - let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.SetRearrange(true); - cmpFormation.MoveMembersIntoFormation(true, true); - } - }, - - "WALKING": { - "MoveStarted": function(msg) { - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.SetRearrange(true); - cmpFormation.MoveMembersIntoFormation(true, true); - }, - - "MoveCompleted": function(msg) { - if (this.FinishOrder()) - this.CallMemberFunction("ResetFinishOrder", []); - }, - }, - - "WALKINGANDFIGHTING": { - "enter": function(msg) { - this.StartTimer(0, 1000); - }, - - "Timer": function(msg) { - // check if there are no enemies to attack - this.FindWalkAndFightTargets(); - }, - - "leave": function(msg) { - this.StopTimer(); - }, - - "MoveStarted": function(msg) { - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.SetRearrange(true); - cmpFormation.MoveMembersIntoFormation(true, true); - }, - - "MoveCompleted": function(msg) { - if (this.FinishOrder()) - this.CallMemberFunction("ResetFinishOrder", []); - }, - }, - - "PATROL": { - "enter": function(msg) { - // Memorize the origin position in case that we want to go back - let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); - if (!cmpPosition || !cmpPosition.IsInWorld()) - { - this.FinishOrder(); - return; - } - if (!this.patrolStartPosOrder) - { - this.patrolStartPosOrder = cmpPosition.GetPosition(); - this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses; - } - - this.StartTimer(0, 1000); - }, - - "Timer": function(msg) { - // Check if there are no enemies to attack - this.FindWalkAndFightTargets(); - }, - - "leave": function(msg) { - this.StopTimer(); - delete this.patrolStartPosOrder; - }, - - "MoveStarted": function(msg) { - let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.SetRearrange(true); - cmpFormation.MoveMembersIntoFormation(true, true); - }, - - "MoveCompleted": function() { - /** - * A-B-A-B-..: - * if the user only commands one patrol order, the patrol will be between - * the last position and the defined waypoint - * A-B-C-..-A-B-..: - * otherwise, the patrol is only between the given patrol commands and the - * last position is not included (last position = the position where the unit - * is located at the time of the first patrol order) - */ - - if (this.orderQueue.length == 1) - this.PushOrder("Patrol", this.patrolStartPosOrder); - - this.PushOrder(this.order.type, this.order.data); - this.FinishOrder(); - }, - }, - - "GARRISON":{ - "enter": function() { - // If the garrisonholder should pickup, warn it so it can take needed action - var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder); - if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity)) - { - this.pickup = this.order.data.target; // temporary, deleted in "leave" - Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity }); - } - }, - - "leave": function() { - // If a pickup has been requested and not yet canceled, cancel it - if (this.pickup) - { - Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); - delete this.pickup; - } - }, - - - "APPROACHING": { - "MoveStarted": function(msg) { - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.SetRearrange(true); - cmpFormation.MoveMembersIntoFormation(true, true); - }, - - "MoveCompleted": function(msg) { - this.SetNextState("GARRISONING"); - }, - }, - - "GARRISONING": { - "enter": function() { - // If a pickup has been requested, cancel it as it will be requested by members - if (this.pickup) - { - Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); - delete this.pickup; - } - this.CallMemberFunction("Garrison", [this.order.data.target, false]); - this.SetNextStateAlwaysEntering("MEMBER"); - }, - }, - }, - - "FORMING": { - "MoveStarted": function(msg) { - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.SetRearrange(true); - cmpFormation.MoveMembersIntoFormation(true, false); - }, - - "MoveCompleted": function(msg) { - - if (this.FinishOrder()) - { - this.CallMemberFunction("ResetFinishOrder", []); - return; - } - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - - cmpFormation.FindInPosition(); - } - }, - - "COMBAT": { - "APPROACHING": { - "MoveStarted": function(msg) { - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.SetRearrange(true); - cmpFormation.MoveMembersIntoFormation(true, true); - }, - - "MoveCompleted": function(msg) { - var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - this.CallMemberFunction("Attack", [this.order.data.target, false, this.order.data.allowCapture]); - if (cmpAttack.CanAttackAsFormation()) - this.SetNextState("COMBAT.ATTACKING"); - else - this.SetNextState("MEMBER"); - }, - }, - - "ATTACKING": { - // Wait for individual members to finish - "enter": function(msg) { - var target = this.order.data.target; - var allowCapture = this.order.data.allowCapture; - // Check if we are already in range, otherwise walk there - if (!this.CheckTargetAttackRange(target, target)) - { - if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) - { - this.FinishOrder(); - this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture }); - return true; - } - this.FinishOrder(); - return true; - } - - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - // TODO fix the rearranging while attacking as formation - cmpFormation.SetRearrange(!this.IsAttackingAsFormation()); - cmpFormation.MoveMembersIntoFormation(false, false); - this.StartTimer(200, 200); - return false; - }, - - "Timer": function(msg) { - var target = this.order.data.target; - var allowCapture = this.order.data.allowCapture; - // Check if we are already in range, otherwise walk there - if (!this.CheckTargetAttackRange(target, target)) - { - if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) - { - this.FinishOrder(); - this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture }); - return; - } - this.FinishOrder(); - return; - } - }, - - "leave": function(msg) { - this.StopTimer(); - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - if (cmpFormation) - cmpFormation.SetRearrange(true); - }, - }, - }, - - "MEMBER": { - // Wait for individual members to finish - "enter": function(msg) { - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.SetRearrange(false); - this.StartTimer(1000, 1000); - }, - - "Timer": function(msg) { - // Have all members finished the task? - if (!this.TestAllMemberFunction("HasFinishedOrder", [])) - return; - - this.CallMemberFunction("ResetFinishOrder", []); - - // Execute the next order - if (this.FinishOrder()) - { - // if WalkAndFight order, look for new target before moving again - if (this.IsWalkingAndFighting()) - this.FindWalkAndFightTargets(); - return; - } - }, - - "leave": function(msg) { - this.StopTimer(); - }, - }, - }, - - - // States for entities moving as part of a formation: - "FORMATIONMEMBER": { - "FormationLeave": function(msg) { - // We're not in a formation anymore, so no need to track this. - this.finishedOrder = false; - - // Stop moving as soon as the formation disbands - this.StopMoving(); - - // If the controller handled an order but some members rejected it, - // they will have no orders and be in the FORMATIONMEMBER.IDLE state. - if (this.orderQueue.length) - { - // We're leaving the formation, so stop our FormationWalk order - if (this.FinishOrder()) - return; - } - - // No orders left, we're an individual now - if (this.IsAnimal()) - this.SetNextState("ANIMAL.IDLE"); - else - this.SetNextState("INDIVIDUAL.IDLE"); - }, - - // Override the LeaveFoundation order since we're not doing - // anything more important (and we might be stuck in the WALKING - // state forever and need to get out of foundations in that case) - "Order.LeaveFoundation": function(msg) { - // If foundation is not ally of entity, or if entity is unpacked siege, - // ignore the order - if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) && !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() || - this.IsPacking() || this.CanPack() || this.IsTurret()) - { - this.FinishOrder(); - return; - } - // Move a tile outside the building - let range = 4; - if (this.MoveToTargetRangeExplicit(msg.data.target, range, 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 - this.FinishOrder(); - } - }, - + "Order.Alert": function(msg) { + this.alertRaiser = this.order.data.raiser; - "IDLE": { - "enter": function() { - if (this.IsAnimal()) - this.SetNextState("ANIMAL.IDLE"); - else - this.SetNextState("INDIVIDUAL.IDLE"); - return true; - }, - }, + // Find a target to garrison into, if we don't already have one + if (!this.alertGarrisoningTarget) + this.alertGarrisoningTarget = this.FindNearbyGarrisonHolder(); - "WALKING": { - "enter": function () { - var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); - var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); - if (cmpFormation && cmpVisual) - { - cmpVisual.ReplaceMoveAnimation("walk", cmpFormation.GetFormationAnimation(this.entity, "walk")); - cmpVisual.ReplaceMoveAnimation("run", cmpFormation.GetFormationAnimation(this.entity, "run")); - } - this.SelectAnimation("move"); - }, + if (this.alertGarrisoningTarget) + this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget}); + else + { + this.StopMoving(); + this.FinishOrder(); + } + }, - // Occurs when the unit has reached its destination and the controller - // is done moving. The controller is notified. - "MoveCompleted": function(msg) { - // We can only finish this order if the move was really completed. - if (!msg.data.error && this.FinishOrder()) - return; - var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); - if (cmpVisual) - { - cmpVisual.ResetMoveAnimation("walk"); - cmpVisual.ResetMoveAnimation("run"); - } + "Order.Cheering": function(msg) { + this.SetNextState("INDIVIDUAL.CHEERING"); + }, - var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); - if (cmpFormation) - cmpFormation.SetInPosition(this.entity); - }, - }, + "Order.Pack": function(msg) { + if (this.CanPack()) + { + this.StopMoving(); + this.SetNextState("INDIVIDUAL.PACKING"); + } + }, - // Special case used by Order.LeaveFoundation - "WALKINGTOPOINT": { - "enter": function() { - var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); - if (cmpFormation) - cmpFormation.UnsetInPosition(this.entity); - this.SelectAnimation("move"); - }, + "Order.Unpack": function(msg) { + if (this.CanUnpack()) + { + this.StopMoving(); + this.SetNextState("INDIVIDUAL.UNPACKING"); + } + }, - "MoveCompleted": function() { - this.FinishOrder(); - }, - }, + "Order.CancelPack": function(msg) { + var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); + if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked()) + cmpPack.CancelPack(); + this.FinishOrder(); }, + "Order.CancelUnpack": function(msg) { + var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); + if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked()) + cmpPack.CancelPack(); + this.FinishOrder(); + }, - // States for entities not part of a formation: "INDIVIDUAL": { "enter": function() { @@ -1445,6 +795,7 @@ "Attacked": function(msg) { // Respond to attack if we always target attackers, or if we target attackers // during passive orders (e.g. gathering/repairing are never forced) + // TODO: handle group-walking order. if (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && (!this.order || !this.order.data || !this.order.data.force))) { this.RespondToTargetedEntities([msg.data.attacker]); @@ -1508,14 +859,7 @@ "enter": function() { // Switch back to idle animation to guarantee we won't // get stuck with an incorrect animation - var animationName = "idle"; - if (this.IsFormationMember()) - { - var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); - if (cmpFormation) - animationName = cmpFormation.GetFormationAnimation(this.entity, animationName); - } - this.SelectAnimation(animationName); + this.SelectAnimation("idle"); // If we have some orders, it is because we are in an intermediary state // from FinishOrder (SetNextState("IDLE") is only executed when we get @@ -1584,7 +928,6 @@ }, "MoveStarted": function() { - this.SelectAnimation("move"); }, "MoveCompleted": function() { @@ -1592,6 +935,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; @@ -1600,23 +948,179 @@ }, }, + "GROUPWALKING": { + "enter": function() { + this.group = this.order.data.groupID; + this.SetNextState("WAITING"); + }, + + "leave": function() { + let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager); + let group = cmpGroupWalkManager.GetGroup(this.group); + if (group) + cmpGroupWalkManager.ResignFromGroup(this.group, this.entity); + this.group = undefined; + }, + + "WAITING" : { + "enter": function() { + let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager); + let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID); + if (!group || group.state != "waiting") + { + this.FinishOrder(); + return true; + } + cmpGroupWalkManager.SetReady(this.order.data.groupID, this.entity); + this.StartTimer(200, 400); + }, + + "leave": function() { + this.StopTimer(); + }, + + "Timer": function() { + let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager); + let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID); + if (!group || group.state == "arrived") + { + this.FinishOrder(); + return true; + } + if (group.state == "walking") + this.SetNextState("WALKING"); + }, + }, + "WALKING" : { + "enter": function() { + let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager); + let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID); + if (!group || group.state != "walking") + { + this.FinishOrder(); + return true; + } + let offset = group.offsets[this.entity]; + this.MoveToPointRange(group.rallyPoint.x + offset.x, group.rallyPoint.z + offset.y, 0, true); + + // TODO: for the first "grouping" order it'd be nice to skip this + let maxSpeed = cmpGroupWalkManager.GetMaxSpeed(this.order.data.groupID); + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + let ratio = maxSpeed / cmpUnitMotion.GetBaseSpeed(); + cmpUnitMotion.SetSpeed(ratio); + this.StartTimer(200, 500); + this.step = group.step; // temporary, deleted in leave + }, + + "leave": function() { + this.StopTimer(); + this.ready = undefined; + this.step = undefined; + this.validatedOffset = undefined; + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetSpeed(WALKING_SPEED); + }, + + "Timer": function() { + let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager); + let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID); + if (!group) + { + this.FinishOrder(); + return true; + } + if (group.state == "arrived") + { + // TODO: should probably handle stances and do different things depending on the order here. + + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + let offset = group.offsets[this.entity]; + if (!cmpUnitMotion.IsActuallyMoving() + && cmpObstructionManager.IsInPointRange(this.entity, group.rallyPoint.x + offset.x, group.rallyPoint.z + offset.y, 0, 0)) + this.FinishOrder(); + return; + } + if (group.step < this.step) + { + // jump straight to the next rallypoint. + this.SetNextStateAlwaysEntering("WALKING"); + return; + } + if (this.ready) + return; + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + let range = group.step !== 0 ? 10 : group.range; + let offset = group.offsets[this.entity]; + if (cmpObstructionManager.IsInPointRange(this.entity, group.rallyPoint.x + offset.x, group.rallyPoint.z + offset.y, 0, range)) + { + this.ready = true; + cmpGroupWalkManager.SetReady(this.order.data.groupID, this.entity); + } + else if (!this.validatedOffset) + { + this.validatedOffset = true; + // check whether our reachable goal is close enough to our intended offset. + // TODO: ideally we'd also check the actual-path-distance to the rallypoint, and figure if we've been placed somewhere wrong. + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + let goal = cmpUnitMotion.GetReachableGoalPosition(); + let distance = (goal.x - group.rallyPoint.x - offset.x) * (goal.x - group.rallyPoint.x - offset.x) + + (goal.y - group.rallyPoint.z - offset.y) * (goal.y - group.rallyPoint.z - offset.y) + if (distance > 3) + cmpGroupWalkManager.SetBlockedPath(this.order.data.groupID, this.entity); + } + }, + + "MoveCompleted": function(msg) { + let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager); + let group = cmpGroupWalkManager.GetGroup(this.order.data.groupID); + if (!group) + { + this.FinishOrder(); + return; + } + + if (!msg.data.error) + return; + + // UnitMotion has told us we were unlikely to reach our destination. + // if we're way out of position we should exit the group + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + let offset = group.offsets[this.entity]; + if (!cmpObstructionManager.IsInPointRange(this.entity, group.rallyPoint.x + offset.x, group.rallyPoint.z + offset.y, 0, 60)) + { + this.FinishOrder(); + return; + } + // tell our group we're ready, it's probably just that our waypoint is impassable right now + this.ready = true; + cmpGroupWalkManager.SetReady(this.order.data.groupID, this.entity); + } + } + }, + "WALKING": { "enter": function () { - this.SelectAnimation("move"); }, - "MoveCompleted": function() { - this.FinishOrder(); + "MoveCompleted": function(msg) { + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (msg.data.error + || !this.order.data.target && cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, 0, 1) + || this.order.data.target && cmpObstructionManager.IsInTargetRange(this.entity, this.order.data.target, 0, 1)) + { + this.StopMoving(); + this.FinishOrder(); + } }, }, "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) { @@ -1628,7 +1132,12 @@ }, "MoveCompleted": function() { - this.FinishOrder(); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + if (cmpObstructionManager.IsInPointRange(this.entity, this.order.data.x, this.order.data.z, 0, 1)) + { + this.StopMoving(); + this.FinishOrder(); + } }, }, @@ -1648,7 +1157,6 @@ } this.StartTimer(0, 1000); - this.SelectAnimation("move"); }, "leave": function() { @@ -1661,6 +1169,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); if (this.orderQueue.length == 1) this.PushOrder("Patrol",this.patrolStartPosOrder); @@ -1678,10 +1187,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; }, @@ -1695,31 +1203,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"); }, }, @@ -1746,7 +1256,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 { @@ -1771,25 +1282,14 @@ "FLEEING": { "enter": function() { this.PlaySound("panic"); - - // Run quickly - var speed = this.GetRunSpeed(); - this.SelectAnimation("move"); - this.SetMoveSpeed(speed); - }, - - "HealthChanged": function() { - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); }, "leave": function() { - // Reset normal speed - this.SetMoveSpeed(this.GetWalkSpeed()); }, "MoveCompleted": function() { // When we've run far enough, stop fleeing + this.StopMoving(); this.FinishOrder(); }, @@ -1810,15 +1310,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(); }, @@ -1833,10 +1332,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 @@ -1903,14 +1411,6 @@ "ATTACKING": { "enter": function() { var target = this.order.data.target; - var cmpFormation = Engine.QueryInterface(target, IID_Formation); - // if the target is a formation, save the attacking formation, and pick a member - if (cmpFormation) - { - this.order.data.formationTarget = target; - target = cmpFormation.GetClosestMember(this.entity); - this.order.data.target = target; - } // Check the target is still alive and attackable if (this.TargetIsAlive(target) && this.CanAttack(target, this.order.data.forceResponse || null) && @@ -1943,12 +1443,6 @@ this.oldAttackType = this.order.data.attackType; // add prefix + no capital first letter for attackType var animationName = "attack_" + this.order.data.attackType.toLowerCase(); - if (this.IsFormationMember()) - { - var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); - if (cmpFormation) - animationName = cmpFormation.GetFormationAnimation(this.entity, animationName); - } this.SelectAnimation(animationName, false, 1.0, "attack"); this.SetAnimationSync(prepare, this.attackTimers.repeat); this.StartTimer(prepare, this.attackTimers.repeat); @@ -1969,22 +1463,11 @@ if (cmpBuildingAI) cmpBuildingAI.SetUnitAITarget(0); this.StopTimer(); + this.SelectAnimation("idle"); }, "Timer": function(msg) { var target = this.order.data.target; - var cmpFormation = Engine.QueryInterface(target, IID_Formation); - // if the target is a formation, save the attacking formation, and pick a member - if (cmpFormation) - { - var thisObject = this; - var filter = function(t) { - return thisObject.TargetIsAlive(t) && thisObject.CanAttack(t, thisObject.order.data.forceResponse || null); - }; - this.order.data.formationTarget = target; - target = cmpFormation.GetClosestMember(this.entity, filter); - this.order.data.target = target; - } // Check the target is still alive and attackable if (this.TargetIsAlive(target) && this.CanAttack(target, this.order.data.forceResponse || null)) { @@ -2038,16 +1521,6 @@ } } - // if we're targetting a formation, find a new member of that formation - var cmpTargetFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation); - // if there is no target, it means previously searching for the target inside the target formation failed, so don't repeat the search - if (target && cmpTargetFormation) - { - this.order.data.target = this.order.data.formationTarget; - this.TimerHandler(msg.data, msg.lateness); - return; - } - // Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up // Except if in WalkAndFight mode where we look for more ennemies around before moving again if (this.FinishOrder()) @@ -2095,16 +1568,10 @@ "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); - if (cmpUnitAI && cmpUnitAI.IsFleeing()) - { - // Run after a fleeing target - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); - } + this.StartTimer(1000, 1000); }, @@ -2112,15 +1579,11 @@ var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsFleeing()) return; - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); }, "leave": function() { - // Reset normal speed in case it was changed - this.SetMoveSpeed(this.GetWalkSpeed()); // Show carried resources when walking. - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); this.StopTimer(); }, @@ -2135,9 +1598,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"); }, }, @@ -2146,7 +1619,6 @@ "GATHER": { "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave". @@ -2206,12 +1678,14 @@ } return true; } + this.StartTimer(0, 500); return false; }, "MoveCompleted": function(msg) { if (msg.data.error) { + this.StopMoving(); // We failed to reach the target // remove us from the list of entities gathering from Resource. @@ -2252,12 +1726,24 @@ this.PerformGather(oldTarget, false, false); return; } + else if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) + { + this.StopMoving(); + // We reached the target - start gathering from it now + this.SetNextState("GATHERING"); + } + }, - // We reached the target - start gathering from it now - this.SetNextState("GATHERING"); + "Timer": function() { + if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) + { + this.StopMoving(); + this.SetNextState("GATHERING"); + } }, "leave": function() { + this.StopTimer(); // don't use ownership because this is called after a conversion/resignation // and the ownership would be invalid then. var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); @@ -2270,10 +1756,12 @@ // Walking to a good place to gather resources near, used by GatherNearPosition "WALKING": { "enter": function() { - this.SelectAnimation("move"); }, "MoveCompleted": function(msg) { + if (msg.data.error) + this.StopMoving(); + var resourceType = this.order.data.type; var resourceTemplate = this.order.data.template; @@ -2305,7 +1793,6 @@ this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } - // No dropsites, just give up }, }, @@ -2354,22 +1841,22 @@ return true; } - // Scale timing interval based on rate, and start timer - // The offset should be at least as long as the repeat time so we use the same value for both. - var offset = 1000/rate; - var repeat = offset; - this.StartTimer(offset, repeat); - - // We want to start the gather animation as soon as possible, - // but only if we're actually at the target and it's still alive - // (else it'll look like we're chopping empty air). - // (If it's not alive, the Timer handler will deal with sending us - // off to a different target.) + // Range check: if we are in-range, start the gathering animation and set the timer + // If we are not in-range, we'll set a different timer to avoid waiting an inordinate amount of time. if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) { + this.FaceTowardsTarget(this.gatheringTarget); var typename = "gather_" + this.order.data.type.specific; this.SelectAnimation(typename, false, 1.0, typename); + + // Scale timing interval based on rate, and start timer + // The offset should be at least as long as the repeat time so we use the same value for both. + var offset = 1000/rate; + var repeat = offset; + this.StartTimer(offset, repeat); } + else + this.StartTimer(0, 1000); return false; }, @@ -2384,7 +1871,8 @@ delete this.gatheringTarget; // Show the carried resource, if we've gathered anything. - this.SetGathererAnimationOverride(); + this.SelectAnimation("idle"); + this.SetAnimationVariant(); }, "Timer": function(msg) { @@ -2454,10 +1942,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; @@ -2532,7 +2019,6 @@ "APPROACHING": { "enter": function () { - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, @@ -2553,6 +2039,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); this.SetNextState("HEALING"); }, }, @@ -2583,6 +2070,7 @@ }, "leave": function() { + this.SelectAnimation("idle"); this.StopTimer(); }, @@ -2633,7 +2121,6 @@ }, "CHASING": { "enter": function () { - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, @@ -2652,6 +2139,7 @@ } }, "MoveCompleted": function () { + this.StopMoving(); this.SetNextState("HEALING"); }, }, @@ -2661,19 +2149,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 @@ -2683,7 +2193,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 @@ -2691,23 +2201,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", }, }, @@ -2718,19 +2214,29 @@ }, "APPROACHINGMARKET": { - "enter": function () { - this.SelectAnimation("move"); + "enter": function() { + this.StartTimer(1000, 1000); }, - "MoveCompleted": function() { - if (this.waypoints && this.waypoints.length) + "leave": function() { + this.StopTimer(); + }, + + "Timer": function() { + if (this.CheckTargetRange(this.order.data.target, IID_Trader)) { - if (!this.MoveToMarket(this.order.data.target)) - this.StopTrading(); + this.StopMoving(); + if (this.waypoints && this.waypoints.length) + { + if (!this.MoveToMarket(this.order.data.target)) + this.StopTrading(); + } + else + this.PerformTradeAndMoveToNextMarket(this.order.data.target); } - else - this.PerformTradeAndMoveToNextMarket(this.order.data.target); }, + + "MoveCompleted": "Timer", }, "TradingCanceled": function(msg) { @@ -2740,17 +2246,30 @@ let otherMarket = cmpTrader && cmpTrader.GetFirstMarket(); this.StopTrading(); if (otherMarket) - this.WalkToTarget(otherMarket); + this.g(otherMarket); }, }, "REPAIR": { "APPROACHING": { "enter": function () { - this.SelectAnimation("move"); + this.StartTimer(0, 1000); }, - "MoveCompleted": function() { + "leave": function() { + this.StopTimer(); + }, + + "Timer": function() { + if (this.CheckTargetRange(this.order.data.target, IID_Builder)) + { + this.StopMoving(); + this.SetNextState("REPAIRING"); + } + }, + // TODO: clean this up when MoveCompleted becomes MoveSuccesHint and MoveFailure or something + "MoveCompleted": function(msg) { + this.StopMoving(); this.SetNextState("REPAIRING"); }, }, @@ -2795,6 +2314,8 @@ if (cmpBuilderList) cmpBuilderList.AddBuilder(this.entity); + this.FaceTowardsTarget(this.repairTarget); + this.SelectAnimation("build", false, 1.0, "build"); this.StartTimer(1000, 1000); return false; @@ -2805,6 +2326,7 @@ if (cmpBuilderList) cmpBuilderList.RemoveBuilder(this.entity); delete this.repairTarget; + this.SelectAnimation("idle"); this.StopTimer(); }, @@ -2824,10 +2346,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 + } }, }, @@ -2851,7 +2376,7 @@ { let dropsiteTypes = cmpResourceDropsite.GetTypes(); cmpResourceGatherer.CommitResources(dropsiteTypes); - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); } // We finished building it. @@ -2860,7 +2385,7 @@ { if (this.CanReturnResource(msg.data.newentity, true)) { - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false }); } return; @@ -2879,7 +2404,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); @@ -2944,10 +2469,14 @@ "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); + this.StartTimer(1000,1000); }, - "MoveCompleted": function() { + "leave": function() { + this.StopTimer(); + }, + + "Timer": function() { if (this.IsUnderAlert() && this.alertGarrisoningTarget) { // check that we can garrison in the building we're supposed to garrison in @@ -2963,13 +2492,16 @@ } else this.FinishOrder(); + return; } - else - this.SetNextState("GARRISONED"); } - else + if (this.order.data.target && this.CheckGarrisonRange(this.order.data.target)) + this.SetNextState("GARRISONED"); + else if (this.alertGarrisoningTarget && this.CheckGarrisonRange(this.alertGarrisoningTarget)) this.SetNextState("GARRISONED"); }, + + "MoveCompleted": "Timer", }, "GARRISONED": { @@ -3000,21 +2532,6 @@ { this.isGarrisoned = true; - if (this.formationController) - { - var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); - if (cmpFormation) - { - // disable rearrange for this removal, - // but enable it again for the next - // move command - var rearrange = cmpFormation.rearrange; - cmpFormation.SetRearrange(false); - cmpFormation.RemoveMembers([this.entity]); - cmpFormation.SetRearrange(rearrange); - } - } - // Check if we are garrisoned in a dropsite var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (cmpResourceDropsite && this.CanReturnResource(target, true)) @@ -3025,7 +2542,7 @@ if (cmpResourceGatherer) { cmpResourceGatherer.CommitResources(dropsiteTypes); - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); } } @@ -3060,7 +2577,7 @@ } } - if (this.MoveToTarget(target)) + if (this.MoveToTarget(target, true)) { this.SetNextState("APPROACHING"); return false; @@ -3101,6 +2618,7 @@ this.StopTimer(); var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(false); + this.SelectAnimation("idle"); }, "Timer": function(msg) { @@ -3145,36 +2663,19 @@ }, "PICKUP": { - "APPROACHING": { - "enter": function() { - this.SelectAnimation("move"); - }, - - "MoveCompleted": function() { - this.SetNextState("LOADING"); - }, - - "PickupCanceled": function() { - this.StopMoving(); + "enter": function() { + var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); + if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) + { this.FinishOrder(); - }, + return true; + } + return false; }, - "LOADING": { - "enter": function() { - this.SelectAnimation("idle"); - var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); - if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) - { - this.FinishOrder(); - return true; - } - return false; - }, - - "PickupCanceled": function() { - this.FinishOrder(); - }, + "PickupCanceled": function() { + this.StopMoving(); + this.FinishOrder(); }, }, }, @@ -3199,16 +2700,16 @@ }, "Order.LeaveFoundation": function(msg) { - // Move a tile outside the building - var range = 4; - if (this.MoveToTargetRangeExplicit(msg.data.target, range, range)) + // Move outside the building. Since INDIVIDUAL.WALKING checks for a distance in [0,1], move to 0.5 + var range = 0.5; + if (this.MoveToTargetRangeExplicit(msg.data.target, range)) { // We've started walking to the given point this.SetNextState("WALKING"); } else { - // We are already at the target, or can't move at all + // We cannot reach the target. this.FinishOrder(); } }, @@ -3226,16 +2727,13 @@ "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(randIntInclusive(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); - this.SetFacePointAfterMove(false); }, "leave": function() { this.StopTimer(); - this.SetFacePointAfterMove(true); }, "LosRangeUpdate": function(msg) { @@ -3265,6 +2763,7 @@ }, "MoveCompleted": function() { + this.StopMoving(); this.MoveRandomly(+this.template.RoamDistance); }, }, @@ -3297,7 +2796,7 @@ } }, - "MoveCompleted": function() { }, + "MoveCompleted": function() { this.StopMoving(); }, "Timer": function(msg) { this.SetNextState("ROAMING"); @@ -3317,12 +2816,8 @@ { this.orderQueue = []; // current order is at the front of the list this.order = undefined; // always == this.orderQueue[0] - this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to this.isGarrisoned = false; this.isIdle = false; - // For A19, keep no formations as a default to help pathfinding. - this.lastFormationTemplate = "formations/null"; - this.finishedOrder = false; // used to find if all formation members finished the order this.heldPosition = undefined; @@ -3339,6 +2834,8 @@ this.lastAttacked = undefined; this.lastHealed = undefined; + this.formationTemplate = "formations/null"; + this.SetStance(this.template.DefaultStance); }; @@ -3371,16 +2868,6 @@ return this.alertRaiser; }; -UnitAI.prototype.IsFormationController = function() -{ - return (this.template.FormationController == "true"); -}; - -UnitAI.prototype.IsFormationMember = function() -{ - return (this.formationController != INVALID_ENTITY); -}; - UnitAI.prototype.HasFinishedOrder = function() { return this.finishedOrder; @@ -3445,12 +2932,6 @@ */ UnitAI.prototype.IsWalkingAndFighting = function() { - if (this.IsFormationMember()) - { - var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); - return (cmpUnitAI && cmpUnitAI.IsWalkingAndFighting()); - } - return (this.orderQueue.length > 0 && this.orderQueue[0].type == "WalkAndFight"); }; @@ -3458,8 +2939,6 @@ { if (this.IsAnimal()) this.UnitFsm.Init(this, "ANIMAL.FEEDING"); - else if (this.IsFormationController()) - this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE"); else this.UnitFsm.Init(this, "INDIVIDUAL.IDLE"); this.isIdle = true; @@ -3682,6 +3161,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]; @@ -3707,21 +3189,6 @@ Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); - // Check if there are queued formation orders - if (this.IsFormationMember()) - { - let cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); - if (cmpUnitAI) - { - // Inform the formation controller that we finished this task - this.finishedOrder = true; - // We don't want to carry out the default order - // if there are still queued formation orders left - if (cmpUnitAI.GetOrders().length > 1) - return true; - } - } - return false; } }; @@ -3824,12 +3291,9 @@ { // Remember the previous work orders to be able to go back to them later if required if (data && data.force) - { - if (this.IsFormationController()) - this.CallMemberFunction("UpdateWorkOrders", [type]); - else - this.UpdateWorkOrders(type); - } + this.UpdateWorkOrders(type); + + this.StopMoving(); // Special cases of orders that shouldn't be replaced: // 1. Cheering - we're invulnerable, add order after we finish @@ -3894,24 +3358,7 @@ if (this.workOrders.length) return; - // First if the unit is in a formation, get its workOrders from it - if (this.IsFormationMember()) - { - var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); - if (cmpUnitAI) - { - for (var i = 0; i < cmpUnitAI.orderQueue.length; ++i) - { - if (isWorkType(cmpUnitAI.orderQueue[i].type)) - { - this.workOrders = cmpUnitAI.orderQueue.slice(i); - return; - } - } - } - } - - // If nothing found, take the unit orders + // Take the unit orders for (var i = 0; i < this.orderQueue.length; ++i) { if (isWorkType(this.orderQueue[i].type)) @@ -3939,14 +3386,6 @@ this.AddOrders(this.workOrders); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); - // And if the unit is in a formation, remove it from the formation - if (this.IsFormationMember()) - { - var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); - if (cmpFormation) - cmpFormation.RemoveMembers([this.entity]); - } - this.workOrders = []; return true; }; @@ -4009,12 +3448,15 @@ //// Message handlers ///// -UnitAI.prototype.OnMotionChanged = function(msg) +UnitAI.prototype.OnMovePaused = function(msg) +{ + // TODO: change this. Doesn't matter if UnitAI thinks it's completed for now since anyways the states do range checks. + this.UnitFsm.ProcessMessage(this, { "type": "MoveCompleted", "data": { "error" : false }}); +}; + +UnitAI.prototype.OnMoveFailure = function(msg) { - if (msg.starting && !msg.error) - this.UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg}); - else if (!msg.starting || msg.error) - this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg}); + this.UnitFsm.ProcessMessage(this, { "type": "MoveCompleted", "data": { "error" : true }}); }; UnitAI.prototype.OnGlobalConstructionFinished = function(msg) @@ -4035,11 +3477,6 @@ changed = true; order.data.target = msg.newentity; } - if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity) - { - changed = true; - order.data.formationTarget = msg.newentity; - } } if (changed) Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); @@ -4075,22 +3512,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(); }; /** @@ -4098,10 +3523,6 @@ */ UnitAI.prototype.TargetIsAlive = function(ent) { - var cmpFormation = Engine.QueryInterface(ent, IID_Formation); - if (cmpFormation) - return true; - var cmpHealth = QueryMiragedInterface(ent, IID_Health); return cmpHealth && cmpHealth.GetHitpoints() != 0; }; @@ -4261,52 +3682,45 @@ */ UnitAI.prototype.PlaySound = function(name) { - // If we're a formation controller, use the sounds from our first member - if (this.IsFormationController()) - { - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - var member = cmpFormation.GetPrimaryMember(); - if (member) - PlaySound(name, member); - } - else - { - // Otherwise use our own sounds - PlaySound(name, this.entity); - } + PlaySound(name, this.entity); }; -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; + } + + let type = cmpResourceGatherer.GetLastCarriedType(); + if (type) + { + let typename = "carry_" + type.generic; - // Special case for meat - if (type.specific == "meat") - typename = "carry_" + type.specific; + // 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) @@ -4315,17 +3729,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) { @@ -4358,31 +3761,34 @@ UnitAI.prototype.StopMoving = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpUnitMotion.StopMoving(); + cmpUnitMotion.DiscardMove(); }; UnitAI.prototype.MoveToPoint = function(x, z) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToPointRange(x, z, 0, 0); + cmpUnitMotion.SetAbortIfStuck(30); + return cmpUnitMotion.SetNewDestinationAsPosition(x, z, 0, true); }; -UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) +UnitAI.prototype.MoveToPointRange = function(x, z, range, evenUnreachable = false) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); + cmpUnitMotion.SetAbortIfStuck(30); + return cmpUnitMotion.SetNewDestinationAsPosition(x, z, range, evenUnreachable); }; -UnitAI.prototype.MoveToTarget = function(target) +UnitAI.prototype.MoveToTarget = function(target, evenUnreachable = false) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, 0, 0); + cmpUnitMotion.SetAbortIfStuck(5); + return cmpUnitMotion.SetNewDestinationAsEntity(target, 0, evenUnreachable); }; -UnitAI.prototype.MoveToTargetRange = function(target, iid, type) +UnitAI.prototype.MoveToTargetRange = function(target, iid, type, evenUnreachable = false) { if (!this.CheckTargetVisible(target) || this.IsTurret()) return false; @@ -4393,7 +3799,10 @@ var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); + cmpUnitMotion.SetAbortIfStuck(5); + // generally speaking, try to aim for the middle of a range. + // + return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + range.max)/2.0, evenUnreachable); }; /** @@ -4401,22 +3810,10 @@ * 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()) - { - var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); - if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation()) - return false; - } - - var cmpFormation = Engine.QueryInterface(target, IID_Formation); - if (cmpFormation) - 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; @@ -4448,24 +3845,27 @@ // the parabole changes while walking, take something in the middle var guessedMaxRange = (range.max + parabolicMaxRange)/2; +// TODO: here we should give the desired range based on unit speed, our own desire to walk, and so on. var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange)) + cmpUnitMotion.SetAbortIfStuck(9); + if (cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + guessedMaxRange)/2.0, false)) return true; // if that failed, try closer - return cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange)); + return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + Math.min(range.max, parabolicMaxRange))/2.0, evenUnreachable); }; -UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) +UnitAI.prototype.MoveToTargetRangeExplicit = function(target, range, evenUnreachable = false) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.MoveToTargetRange(target, min, max); + cmpUnitMotion.SetAbortIfStuck(5); + return cmpUnitMotion.SetNewDestinationAsEntity(target, range, evenUnreachable); }; -UnitAI.prototype.MoveToGarrisonRange = function(target) +UnitAI.prototype.MoveToGarrisonRange = function(target, evenUnreachable = false) { if (!this.CheckTargetVisible(target)) return false; @@ -4476,13 +3876,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, evenUnreachable); }; 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) @@ -4491,9 +3892,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); }; /** @@ -4504,19 +3904,6 @@ */ UnitAI.prototype.CheckTargetAttackRange = function(target, type) { - // for formation members, the formation will take care of the range check - if (this.IsFormationMember()) - { - var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); - if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation() - && cmpFormationUnitAI.order.data.target == target) - return true; - } - - var cmpFormation = Engine.QueryInterface(target, IID_Formation); - if (cmpFormation) - target = cmpFormation.GetClosestMember(this.entity); - if (type != "Ranged") return this.CheckTargetRange(target, IID_Attack, type); @@ -4541,14 +3928,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) @@ -4562,8 +3949,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); }; /** @@ -4815,57 +4202,6 @@ //// External interface functions //// -UnitAI.prototype.SetFormationController = function(ent) -{ - this.formationController = ent; - - // Set obstruction group, so we can walk through members - // of our own formation (or ourself if not in formation) - var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); - if (cmpObstruction) - { - if (ent == INVALID_ENTITY) - cmpObstruction.SetControlGroup(this.entity); - else - cmpObstruction.SetControlGroup(ent); - } - - // If we were removed from a formation, let the FSM switch back to INDIVIDUAL - if (ent == INVALID_ENTITY) - this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" }); -}; - -UnitAI.prototype.GetFormationController = function() -{ - return this.formationController; -}; - -UnitAI.prototype.SetLastFormationTemplate = function(template) -{ - this.lastFormationTemplate = template; -}; - -UnitAI.prototype.GetLastFormationTemplate = function() -{ - return this.lastFormationTemplate; -}; - -UnitAI.prototype.MoveIntoFormation = function(cmd) -{ - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - if (!cmpFormation) - return; - - var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); - if (!cmpPosition || !cmpPosition.IsInWorld()) - return; - - var pos = cmpPosition.GetPosition(); - - // Add new order to move into formation at the current position - this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true }); -}; - UnitAI.prototype.GetTargetPositions = function() { var targetPositions = []; @@ -4877,7 +4213,6 @@ case "Walk": case "WalkAndFight": case "WalkToPointRange": - case "MoveIntoFormation": case "GatherNearPosition": case "Patrol": targetPositions.push(new Vector2D(order.data.x, order.data.z)); @@ -5031,11 +4366,6 @@ UnitAI.prototype.CanGuard = function() { - // Formation controllers should always respond to commands - // (then the individual units can make up their own minds) - if (this.IsFormationController()) - return true; - // Do not let a unit already guarded to guard. This would work in principle, // but would clutter the gui with too much buttons to take all cases into account var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard); @@ -5046,6 +4376,28 @@ }; /** + * Set the preferred formation for this entity. + */ +UnitAI.prototype.SetFormationTemplate = function(template) +{ + // TODO: validate this entity accepts this? + this.formationTemplate = template; +}; + +UnitAI.prototype.GetFormationTemplate = function() +{ + return this.formationTemplate; +}; + +/** + * Adds group-walk order to the queue, necessarily in front. + */ +UnitAI.prototype.GroupWalk = function(groupID) +{ + this.AddOrder("GroupWalk", { "groupID": groupID }, false); +}; + +/** * Adds walk order to queue, forced by the player. */ UnitAI.prototype.Walk = function(x, z, queued) @@ -5121,7 +4473,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; @@ -5221,7 +4573,7 @@ if (template.indexOf("resource|") != -1) template = template.slice(9); - if (this.IsFormationController() || Engine.QueryInterface(this.entity, IID_ResourceGatherer)) + if (Engine.QueryInterface(this.entity, IID_ResourceGatherer)) this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued); else this.AddOrder("Walk", { "x": x, "z": z, "force": false }, queued); @@ -5289,22 +4641,11 @@ this.expectedRoute = undefined; } - if (this.IsFormationController()) - { - this.CallMemberFunction("AddOrder", ["Trade", data, queued]); - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - if (cmpFormation) - cmpFormation.Disband(); - } - else - this.AddOrder("Trade", data, queued); + this.AddOrder("Trade", data, queued); } else { - if (this.IsFormationController()) - this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued]); - else - this.WalkToTarget(cmpTrader.GetFirstMarket(), queued); + this.WalkToTarget(cmpTrader.GetFirstMarket(), queued); this.expectedRoute = []; } }; @@ -5316,9 +4657,6 @@ return false; var marketsChanged = cmpTrader.SetTargetMarket(target, source); - if (this.IsFormationController()) - this.CallMemberFunction("SetTargetMarket", [target, source]); - return marketsChanged; }; @@ -5521,40 +4859,6 @@ UnitAI.prototype.FindWalkAndFightTargets = function() { - if (this.IsFormationController()) - { - var cmpUnitAI; - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - for (var ent of cmpFormation.members) - { - if (!(cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI))) - continue; - var targets = cmpUnitAI.GetTargetsFromUnit(); - for (var targ of targets) - { - if (!cmpUnitAI.CanAttack(targ)) - continue; - if (this.order.data.targetClasses) - { - var cmpIdentity = Engine.QueryInterface(targ, IID_Identity); - var targetClasses = this.order.data.targetClasses; - if (targetClasses.attack && cmpIdentity - && !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) - continue; - if (targetClasses.avoid && cmpIdentity - && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) - continue; - // Only used by the AIs to prevent some choices of targets - if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) - continue; - } - this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": true }); - return true; - } - } - return false; - } - var targets = this.GetTargetsFromUnit(); for (var targ of targets) { @@ -5691,8 +4995,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) @@ -5728,11 +5032,6 @@ UnitAI.prototype.CanAttack = function(target, forceResponse) { - // Formation controllers should always respond to commands - // (then the individual units can make up their own minds) - if (this.IsFormationController()) - return true; - // Verify that we're able to respond to Attack commands var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) @@ -5769,11 +5068,6 @@ UnitAI.prototype.CanGarrison = function(target) { - // Formation controllers should always respond to commands - // (then the individual units can make up their own minds) - if (this.IsFormationController()) - return true; - var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; @@ -5801,11 +5095,6 @@ if (!cmpResourceSupply) return false; - // Formation controllers should always respond to commands - // (then the individual units can make up their own minds) - if (this.IsFormationController()) - return true; - // Verify that we're able to respond to Gather commands var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) @@ -5825,11 +5114,6 @@ UnitAI.prototype.CanHeal = function(target) { - // Formation controllers should always respond to commands - // (then the individual units can make up their own minds) - if (this.IsFormationController()) - return true; - // Verify that we're able to respond to Heal commands var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); if (!cmpHeal) @@ -5868,10 +5152,6 @@ { if (this.IsTurret()) return false; - // Formation controllers should always respond to commands - // (then the individual units can make up their own minds) - if (this.IsFormationController()) - return true; // Verify that we're able to respond to ReturnResource commands var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); @@ -5905,10 +5185,6 @@ { if (this.IsTurret()) return false; - // Formation controllers should always respond to commands - // (then the individual units can make up their own minds) - if (this.IsFormationController()) - return true; // Verify that we're able to respond to Trade commands var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); @@ -5919,10 +5195,6 @@ { if (this.IsTurret()) return false; - // Formation controllers should always respond to commands - // (then the individual units can make up their own minds) - if (this.IsFormationController()) - return true; // Verify that we're able to respond to Repair (Builder) commands var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); @@ -5958,15 +5230,6 @@ return (cmpPack && cmpPack.IsPacking()); }; -//// Formation specific functions //// - -UnitAI.prototype.IsAttackingAsFormation = function() -{ - var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - return cmpAttack && cmpAttack.CanAttackAsFormation() - && this.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING"; -}; - //// Animal specific functions //// UnitAI.prototype.MoveRandomly = function(distance) @@ -5997,14 +5260,7 @@ var tz = pos.z + randFloat(-1, 1) * jitter; var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpMotion.MoveToPointRange(tx, tz, distance, distance); -}; - -UnitAI.prototype.SetFacePointAfterMove = function(val) -{ - var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (cmpMotion) - cmpMotion.SetFacePointAfterMove(val); + cmpMotion.SetNewDestinationAsPosition(tx, tz, distance, true); }; UnitAI.prototype.AttackEntitiesByPreference = function(ents) @@ -6054,37 +5310,6 @@ return this.RespondToTargetedEntities(entsWithoutPref); }; -/** - * Call obj.funcname(args) on UnitAI components of all formation members. - */ -UnitAI.prototype.CallMemberFunction = function(funcname, args) -{ - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - if (!cmpFormation) - return; - - cmpFormation.GetMembers().forEach(ent => { - var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); - cmpUnitAI[funcname].apply(cmpUnitAI, args); - }); -}; - -/** - * Call obj.functname(args) on UnitAI components of all formation members, - * and return true if all calls return true. - */ -UnitAI.prototype.TestAllMemberFunction = function(funcname, args) -{ - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - if (!cmpFormation) - return false; - - return cmpFormation.GetMembers().every(ent => { - var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); - return cmpUnitAI[funcname].apply(cmpUnitAI, args); - }); -}; - UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec); Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI); 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; }; @@ -315,17 +325,17 @@ return this.speed; }; -UnitMotionFlying.prototype.FaceTowardsPoint = function(x, z) +UnitMotionFlying.prototype.IsTryingToMove = function() { - // Ignore this - angle is controlled by the target-seeking code instead -}; + return false; +} -UnitMotionFlying.prototype.SetFacePointAfterMove = function() +UnitMotionFlying.prototype.FaceTowardsPoint = function(x, z) { // 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/interfaces/GroupWalkManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/interfaces/GroupWalkManager.js @@ -0,0 +1 @@ +Engine.RegisterInterface("GroupWalkManager"); Index: binaries/data/mods/public/simulation/components/interfaces/Messages.js =================================================================== --- binaries/data/mods/public/simulation/components/interfaces/Messages.js +++ binaries/data/mods/public/simulation/components/interfaces/Messages.js @@ -2,7 +2,6 @@ * Message of the form { "entity": number, "newentity": number } * sent when one entity is changed to another: * - from Foundation component when a building construction is done - * - from Formation component * - from Health component when an entity died and should remain as a resource * - from Promotion component when a unit is promoted * - from Mirage component when a fogged entity is re-discovered Index: binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -4,7 +4,6 @@ Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); -Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("Attack.js"); 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,319 +1,175 @@ 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 []; }, - }); - - - var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); - - AddMock(unit, IID_Identity, { - GetClassesList: function() { return []; }, - }); - - AddMock(unit, IID_Ownership, { - GetOwner: function() { return 1; }, - }); - - 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_Sound, { + PlaySoundGroup: 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_Position, { + "IsInWorld" : function() { return true; }, + "GetPosition" : function() { return new Vector2D(0,0); } }); - AddMock(unit, IID_Vision, { - GetRange: function() { return 10; }, + AddMock(UNIT_ID, IID_UnitMotion, { + GetTopSpeedRatio : function() { return 0; }, + SetSpeed: function() {}, }); - 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; }, + AddMock(UNIT_ID, IID_DamageReceiver, { + SetInvulnerability : function() {}, }); - 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(UNIT_ID, IID_Pack, { + Pack : function() {}, + Unpack : function() { }, }); - AddMock(controller, IID_UnitMotion, { - SetSpeed: function(speed) { }, - MoveToPointRange: function(x, z, minRange, maxRange) { }, - GetPassabilityClassName: function() { return "default"; }, + AddMock(UNIT_ID, IID_Visual, { + SelectAnimation : function(name) { lastAnimationSet = name; }, + SetVariant : function(key, name) { }, }); - - 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(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"; }, - }); - AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - GetPlayerByID: function(id) { return playerEntity; }, - GetNumPlayers: function() { return 2; }, +======= + GetCurrentTemplateName: function(ent) { return "units/gaul_infantry_spearman_b"; }, }); - 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); + AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + GetPlayerByID: function(id) { return PLAYER_ENTITY; }, + GetNumPlayers: function() { return 1; }, + });*/ +} - var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); +// 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(); - 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; }, - }); + let cmpUnitAI = ConstructComponent(UNIT_ID, "UnitAI", { "DefaultStance": "aggressive" }); - unitAI.OnCreate(); + cmpUnitAI.OnCreate(); + TS_ASSERT_EQUALS(cmpUnitAI.UnitFsm.GetCurrentState(cmpUnitAI), "INDIVIDUAL.IDLE"); - unitAI.SetupRangeQuery(1); + cmpUnitAI.order = {"data" : { "targetClasses" : [], "target" : TARGET_ENTITY }}; - unitAIs.push(unitAI); + 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; } - // 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(); - - controllerFormation.SetMembers(units); - - controllerAI.Attack(enemy, []); - - for (var ent of unitAIs) - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); - - controllerAI.MoveIntoFormation({"name": "Circle"}); - - // let all units be in position - for (var ent of unitAIs) - controllerFormation.SetInPosition(ent); - - for (var ent of 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(); 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 } ], "auraName": "Edict of Ashoka", "auraDescription": "All traders in range +20% speed.", 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/data/civs/athen.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/athen.json +++ binaries/data/mods/public/simulation/data/civs/athen.json @@ -133,16 +133,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line", - "formations/phalanx" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/data/civs/brit.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/brit.json +++ binaries/data/mods/public/simulation/data/civs/brit.json @@ -115,15 +115,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/data/civs/cart.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/cart.json +++ binaries/data/mods/public/simulation/data/civs/cart.json @@ -136,15 +136,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/data/civs/gaul.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/gaul.json +++ binaries/data/mods/public/simulation/data/civs/gaul.json @@ -111,15 +111,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/data/civs/iber.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/iber.json +++ binaries/data/mods/public/simulation/data/civs/iber.json @@ -109,15 +109,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/data/civs/mace.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/mace.json +++ binaries/data/mods/public/simulation/data/civs/mace.json @@ -138,17 +138,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line", - "formations/phalanx", - "formations/syntagma" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/data/civs/maur.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/maur.json +++ binaries/data/mods/public/simulation/data/civs/maur.json @@ -123,15 +123,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/data/civs/pers.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/pers.json +++ binaries/data/mods/public/simulation/data/civs/pers.json @@ -128,15 +128,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/data/civs/ptol.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/ptol.json +++ binaries/data/mods/public/simulation/data/civs/ptol.json @@ -137,17 +137,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line", - "formations/phalanx", - "formations/syntagma" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/data/civs/rome.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/rome.json +++ binaries/data/mods/public/simulation/data/civs/rome.json @@ -113,16 +113,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line", - "formations/testudo" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/data/civs/sele.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/sele.json +++ binaries/data/mods/public/simulation/data/civs/sele.json @@ -134,17 +134,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line", - "formations/phalanx", - "formations/syntagma" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/data/civs/spart.json =================================================================== --- binaries/data/mods/public/simulation/data/civs/spart.json +++ binaries/data/mods/public/simulation/data/civs/spart.json @@ -129,16 +129,7 @@ "Formations": [ "formations/null", - "formations/box", - "formations/column_closed", - "formations/line_closed", - "formations/column_open", - "formations/line_open", - "formations/flank", - "formations/skirmish", - "formations/wedge", - "formations/battle_line", - "formations/phalanx" + "formations/column_open" ], "AINames": [ Index: binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Commands.js +++ binaries/data/mods/public/simulation/helpers/Commands.js @@ -141,7 +141,9 @@ "walk": function(player, cmd, data) { - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { + let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued); + // the grouped entities will group-walk to the target, the non-grouped will just walk there. + unitAIs[0].forEach(cmpUnitAI => { cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued); }); }, @@ -159,9 +161,11 @@ "attack-walk": function(player, cmd, data) { - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { - cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued); - }); + let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued); + for (let i in unitAIs) + unitAIs[i].forEach(cmpUnitAI => { + cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued || +i); + }); }, "attack": function(player, cmd, data) @@ -171,16 +175,23 @@ let allowCapture = cmd.allowCapture || cmd.allowCapture == null; - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { - cmpUnitAI.Attack(cmd.target, cmd.queued, allowCapture); - }); + let pos = Engine.QueryInterface(cmd.target, IID_Position); + if (!pos) + return; + let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); + for (let i in unitAIs) + unitAIs[i].forEach(cmpUnitAI => { + cmpUnitAI.Attack(cmd.target, cmd.queued || +i, allowCapture); + }); }, "patrol": function(player, cmd, data) { - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => - cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.queued) - ); + let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued); + for (let i in unitAIs) + unitAIs[i].forEach(cmpUnitAI => { + cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.queued || +i) + }); }, "heal": function(player, cmd, data) @@ -188,9 +199,14 @@ if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target))) warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd)); - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { - cmpUnitAI.Heal(cmd.target, cmd.queued); - }); + let pos = Engine.QueryInterface(cmd.target, IID_Position); + if (!pos) + return; + let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); + for (let i in unitAIs) + unitAIs[i].forEach(cmpUnitAI => { + cmpUnitAI.Heal(cmd.target, cmd.queued || +i); + }); }, "repair": function(player, cmd, data) @@ -199,9 +215,14 @@ if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd)); - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { - cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued); - }); + let pos = Engine.QueryInterface(cmd.target, IID_Position); + if (!pos) + return; + let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); + for (let i in unitAIs) + unitAIs[i].forEach(cmpUnitAI => { + cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued || +i); + }); }, "gather": function(player, cmd, data) @@ -209,16 +230,23 @@ if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target))) warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd)); - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { - cmpUnitAI.Gather(cmd.target, cmd.queued); - }); + let pos = Engine.QueryInterface(cmd.target, IID_Position); + if (!pos) + return; + let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); + for (let i in unitAIs) + unitAIs[i].forEach(cmpUnitAI => { + cmpUnitAI.Gather(cmd.target, cmd.queued || +i); + }); }, "gather-near-position": function(player, cmd, data) { - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { - cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued); - }); + let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued); + for (let i in unitAIs) + unitAIs[i].forEach(cmpUnitAI => { + cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued || +i); + }); }, "returnresource": function(player, cmd, data) @@ -226,9 +254,14 @@ if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target)) warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd)); - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { - cmpUnitAI.ReturnResource(cmd.target, cmd.queued); - }); + let pos = Engine.QueryInterface(cmd.target, IID_Position); + if (!pos) + return; + let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); + for (let i in unitAIs) + unitAIs[i].forEach(cmpUnitAI => { + cmpUnitAI.ReturnResource(cmd.target, cmd.queued || +i); + }); }, "back-to-work": function(player, cmd, data) @@ -441,9 +474,14 @@ return; } - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { - cmpUnitAI.Garrison(cmd.target, cmd.queued); - }); + let pos = Engine.QueryInterface(cmd.target, IID_Position); + if (!pos) + return; + let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); + for (let i in unitAIs) + unitAIs[i].forEach(cmpUnitAI => { + cmpUnitAI.Garrison(cmd.target, cmd.queued || +i); + }); }, "guard": function(player, cmd, data) @@ -456,15 +494,22 @@ return; } - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { - cmpUnitAI.Guard(cmd.target, cmd.queued); - }); + let pos = Engine.QueryInterface(cmd.target, IID_Position); + if (!pos) + return; + let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); + for (let i in unitAIs) + unitAIs[i].forEach(cmpUnitAI => { + cmpUnitAI.Guard(cmd.target, cmd.queued || +i); + }); }, "stop": function(player, cmd, data) { - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { - cmpUnitAI.Stop(cmd.queued); + data.entities.forEach(ent => { + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (cmpUnitAI) + cmpUnitAI.Stop(cmd.queued); }); }, @@ -556,8 +601,10 @@ "formation": function(player, cmd, data) { - GetFormationUnitAIs(data.entities, player, cmd.name).forEach(cmpUnitAI => { - cmpUnitAI.MoveIntoFormation(cmd); + data.entities.forEach(ent => { + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (cmpUnitAI) + cmpUnitAI.SetFormationTemplate(cmd.name); }); }, @@ -606,8 +653,10 @@ "setup-trade-route": function(player, cmd, data) { - GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { - cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued); + data.entities.forEach(ent => { + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (cmpUnitAI) + cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued); }); }, @@ -835,32 +884,6 @@ } /** - * Get some information about the formations used by entities. - * The entities must have a UnitAI component. - */ -function ExtractFormations(ents) -{ - var entities = []; // subset of ents that have UnitAI - var members = {}; // { formationentity: [ent, ent, ...], ... } - for (let ent of ents) - { - var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); - var fid = cmpUnitAI.GetFormationController(); - if (fid != INVALID_ENTITY) - { - if (!members[fid]) - members[fid] = []; - members[fid].push(ent); - } - entities.push(ent); - } - - var ids = [ id for (id in members) ]; - - return { "entities": entities, "members": members, "ids": ids }; -} - -/** * Tries to find the best angle to put a dock at a given position * Taken from GuiInterface.js */ @@ -1360,226 +1383,67 @@ } } -/** - * Remove the given list of entities from their current formations. - */ -function RemoveFromFormation(ents) +function CreateGroupWalkOrderIfNecessary(ents, x, z, range, queued) { - var formation = ExtractFormations(ents); - for (var fid in formation.members) - { - var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); - if (cmpFormation) - cmpFormation.RemoveMembers(formation.members[fid]); - } -} - -/** - * Returns a list of UnitAI components, each belonging either to a - * selected unit or to a formation entity for groups of the selected units. - */ -function GetFormationUnitAIs(ents, player, formationTemplate) -{ - // If an individual was selected, remove it from any formation - // and command it individually - if (ents.length == 1) + // First, loop through units and check if all of them share a single non-null formation. + let formationTemplate = null; + for (let ent of ents) { - // Skip unit if it has no UnitAI - var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI); + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) - return []; + { + formationTemplate = "formations/null"; + break; + } + if (!formationTemplate) + formationTemplate = cmpUnitAI.GetFormationTemplate(); + if (formationTemplate == "formations/null") + break; + else if (cmpUnitAI.GetFormationTemplate() !== formationTemplate) + { + formationTemplate = "formations/null"; + break; + } + } - RemoveFromFormation(ents); + let nonFormableUnitAIs = []; + let formableEntsID = []; + let formableEntsAI = []; - return [ cmpUnitAI ]; - } + // don't create a walk together order if this is a queued order because that's just going to be weird + let createGroupOrder = queued === false && formationTemplate !== "formations/null"; - // Separate out the units that don't support the chosen formation - var formedEnts = []; - var nonformedUnitAIs = []; for (let ent of ents) { - // Skip units with no UnitAI or no position - var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); - var cmpPosition = Engine.QueryInterface(ent, IID_Position); + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld()) continue; - var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); - // TODO: We only check if the formation is usable by some units - // if we move them to it. We should check if we can use formations - // for the other cases. - var nullFormation = (formationTemplate || cmpUnitAI.GetLastFormationTemplate()) == "formations/null"; - if (!nullFormation && cmpIdentity && cmpIdentity.CanUseFormation(formationTemplate || "formations/null")) - formedEnts.push(ent); - else + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + if (createGroupOrder && cmpIdentity)// && cmpIdentity.CanUseFormation(formationTemplate)) { - if (nullFormation) - cmpUnitAI.SetLastFormationTemplate("formations/null"); - nonformedUnitAIs.push(cmpUnitAI); + formableEntsID.push(ent); + formableEntsAI.push(cmpUnitAI); } + else + nonFormableUnitAIs.push(cmpUnitAI); } - if (formedEnts.length == 0) - { - // No units support the foundation - return all the others - return nonformedUnitAIs; - } - - // Find what formations the formationable selected entities are currently in - var formation = ExtractFormations(formedEnts); - - var formationUnitAIs = []; - if (formation.ids.length == 1) + // TODO: validate formation + if (createGroupOrder && formableEntsAI.length > 1) { - // Selected units either belong to this formation or have no formation - // Check that all its members are selected - var fid = formation.ids[0]; - var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); - if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length - && cmpFormation.GetMemberCount() == formation.entities.length) - { - cmpFormation.DeleteTwinFormations(); - // The whole formation was selected, so reuse its controller for this command - formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)]; - if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate)) - cmpFormation.LoadFormation(formationTemplate); - } + // TODO: get position, get obstruction, that kind of stuff. + let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager); + let groupID = cmpGroupWalkManager.CreateGroup(formableEntsID, x, z, range, formationTemplate); + formableEntsAI.forEach(cmpUnitAI => { cmpUnitAI.GroupWalk(groupID); }); } - - if (!formationUnitAIs.length) + else { - // We need to give the selected units a new formation controller - - // Remove selected units from their current formation - for (var fid in formation.members) - { - var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); - if (cmpFormation) - cmpFormation.RemoveMembers(formation.members[fid]); - } - - // TODO replace the fixed 60 with something sensible, based on vision range f.e. - var formationSeparation = 60; - var clusters = ClusterEntities(formation.entities, formationSeparation); - var formationEnts = []; - for (let cluster of clusters) - { - if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate)) - { - // get the most recently used formation, or default to line closed - var lastFormationTemplate = undefined; - for (let ent of cluster) - { - var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); - if (cmpUnitAI) - { - var template = cmpUnitAI.GetLastFormationTemplate(); - if (lastFormationTemplate === undefined) - { - lastFormationTemplate = template; - } - else if (lastFormationTemplate != template) - { - lastFormationTemplate = undefined; - break; - } - } - } - if (lastFormationTemplate && CanMoveEntsIntoFormation(cluster, lastFormationTemplate)) - formationTemplate = lastFormationTemplate; - else - formationTemplate = "formations/null"; - } - - // Create the new controller - var formationEnt = Engine.AddEntity(formationTemplate); - var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); - formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI)); - cmpFormation.SetFormationSeparation(formationSeparation); - cmpFormation.SetMembers(cluster); - - for (let ent of formationEnts) - cmpFormation.RegisterTwinFormation(ent); - - formationEnts.push(formationEnt); - var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership); - cmpOwnership.SetOwner(player); - } - } - - return nonformedUnitAIs.concat(formationUnitAIs); -} - -/** - * Group a list of entities in clusters via single-links - */ -function ClusterEntities(ents, separationDistance) -{ - var clusters = []; - if (!ents.length) - return clusters; - - var distSq = separationDistance * separationDistance; - var positions = []; - // triangular matrix with the (squared) distances between the different clusters - // the other half is not initialised - var matrix = []; - for (let i = 0; i < ents.length; ++i) - { - matrix[i] = []; - clusters.push([ents[i]]); - var cmpPosition = Engine.QueryInterface(ents[i], IID_Position); - positions.push(cmpPosition.GetPosition2D()); - for (let j = 0; j < i; ++j) - matrix[i][j] = positions[i].distanceToSquared(positions[j]); - } - while (clusters.length > 1) - { - // search two clusters that are closer than the required distance - var closeClusters = undefined; - - for (var i = matrix.length - 1; i >= 0 && !closeClusters; --i) - for (var j = i - 1; j >= 0 && !closeClusters; --j) - if (matrix[i][j] < distSq) - closeClusters = [i,j]; - - // if no more close clusters found, just return all found clusters so far - if (!closeClusters) - return clusters; - - // make a new cluster with the entities from the two found clusters - var newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]); - - // calculate the minimum distance between the new cluster and all other remaining - // clusters by taking the minimum of the two distances. - var distances = []; - for (let i = 0; i < clusters.length; ++i) - { - if (i == closeClusters[1] || i == closeClusters[0]) - continue; - var dist1 = matrix[closeClusters[1]][i] || matrix[i][closeClusters[1]]; - var dist2 = matrix[closeClusters[0]][i] || matrix[i][closeClusters[0]]; - distances.push(Math.min(dist1, dist2)); - } - // remove the rows and columns in the matrix for the merged clusters, - // and the clusters themselves from the cluster list - clusters.splice(closeClusters[0],1); - clusters.splice(closeClusters[1],1); - matrix.splice(closeClusters[0],1); - matrix.splice(closeClusters[1],1); - for (let i = 0; i < matrix.length; ++i) - { - if (matrix[i].length > closeClusters[0]) - matrix[i].splice(closeClusters[0],1); - if (matrix[i].length > closeClusters[1]) - matrix[i].splice(closeClusters[1],1); - } - // add a new row of distances to the matrix and the new cluster - clusters.push(newCluster); - matrix.push(distances); + nonFormableUnitAIs = nonFormableUnitAIs.concat(formableEntsAI); + formableEntsAI = []; } - return clusters; + return [nonFormableUnitAIs, formableEntsAI]; } function GetFormationRequirements(formationTemplate) @@ -1591,7 +1455,6 @@ return { "minCount": +template.Formation.RequiredMemberCount }; } - function CanMoveEntsIntoFormation(ents, formationTemplate) { // TODO: should check the player's civ is allowed to use this formation 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_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_peacock.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml @@ -39,11 +39,6 @@ 2.0 - - 5.0 - 600.0 - 5.0 - fauna/peacock.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 @@ -45,9 +45,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_formation.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_formation.xml +++ binaries/data/mods/public/simulation/templates/template_formation.xml @@ -35,11 +35,9 @@ 2 aggressive 12.0 - true true - true 1.0 large 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 @@ -100,19 +100,10 @@ 2 aggressive 12.0 - false true - 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 @@ -55,7 +55,7 @@ 150 - 2.0 + 1.6 1.0 5 @@ -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_decorative.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna_decorative.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna_decorative.xml @@ -23,7 +23,6 @@ 0 passive passive - false false 20 10.0 @@ -33,7 +32,6 @@ 2 - false 8.0 unrestricted default Index: binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_elephant.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_elephant.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_defensive_elephant.xml @@ -52,9 +52,6 @@ large 3.0 - - 10.0 - fauna/elephant_african_forest.xml 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 @@ -53,8 +53,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 @@ large 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,6 @@ 100 - 2.0 1.0 0.5 @@ -119,9 +118,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 @@ -61,9 +61,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 @@ -45,10 +45,8 @@ Women in the ancient world took on a variety of roles - from leadership (Celts) to servant (Greeks). Women are hard workers, the economic backbone of any civilization. In history, it was typical when all the males (capable of fighting) were killed for the females, children, and elderly to be sold as slaves. FemaleCitizen Citizen Worker - - 2.0 1.0 1 @@ -87,11 +85,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,6 @@ 1 - 2.0 1.0 0.5 @@ -85,10 +84,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_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 @@ -40,11 +40,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: libraries/win32/boost/include/boost/container/detail/flat_tree.hpp =================================================================== --- /dev/null +++ libraries/win32/boost/include/boost/container/detail/flat_tree.hpp @@ -0,0 +1,982 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// (C) Copyright Ion Gaztanaga 2005-2015. Distributed under the Boost +// Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// See http://www.boost.org/libs/container for documentation. +// +//////////////////////////////////////////////////////////////////////////////// + +#ifndef BOOST_CONTAINER_FLAT_TREE_HPP +#define BOOST_CONTAINER_FLAT_TREE_HPP + +#ifndef BOOST_CONFIG_HPP +# include +#endif + +#if defined(BOOST_HAS_PRAGMA_ONCE) +# pragma once +#endif + +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include //algo_equal(), algo_lexicographical_compare +#include +#include +#ifdef BOOST_CONTAINER_VECTOR_ITERATOR_IS_POINTER +#include +#endif +#include +#include +#include +#if defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) +#include +#endif + +#include //pair + +namespace boost { +namespace container { +namespace container_detail { + +template +class flat_tree_value_compare + : private Compare +{ + typedef Value first_argument_type; + typedef Value second_argument_type; + typedef bool return_type; + public: + flat_tree_value_compare() + : Compare() + {} + + flat_tree_value_compare(const Compare &pred) + : Compare(pred) + {} + + bool operator()(const Value& lhs, const Value& rhs) const + { + KeyOfValue key_extract; + return Compare::operator()(key_extract(lhs), key_extract(rhs)); + } + + const Compare &get_comp() const + { return *this; } + + Compare &get_comp() + { return *this; } +}; + +template +struct get_flat_tree_iterators +{ + #ifdef BOOST_CONTAINER_VECTOR_ITERATOR_IS_POINTER + typedef Pointer iterator; + typedef typename boost::intrusive:: + pointer_traits::element_type iterator_element_type; + typedef typename boost::intrusive:: + pointer_traits:: template + rebind_pointer::type const_iterator; + #else //BOOST_CONTAINER_VECTOR_ITERATOR_IS_POINTER + typedef typename boost::container::container_detail:: + vec_iterator iterator; + typedef typename boost::container::container_detail:: + vec_iterator const_iterator; + #endif //BOOST_CONTAINER_VECTOR_ITERATOR_IS_POINTER + typedef boost::container::reverse_iterator reverse_iterator; + typedef boost::container::reverse_iterator const_reverse_iterator; +}; + +template +class flat_tree +{ + typedef boost::container::vector vector_t; + typedef Allocator allocator_t; + typedef allocator_traits allocator_traits_type; + + public: + typedef flat_tree_value_compare value_compare; + + private: + struct Data + //Inherit from value_compare to do EBO + : public value_compare + { + BOOST_COPYABLE_AND_MOVABLE(Data) + + public: + Data() + : value_compare(), m_vect() + {} + + explicit Data(const Data &d) + : value_compare(static_cast(d)), m_vect(d.m_vect) + {} + + Data(BOOST_RV_REF(Data) d) + : value_compare(boost::move(static_cast(d))), m_vect(boost::move(d.m_vect)) + {} + + Data(const Data &d, const Allocator &a) + : value_compare(static_cast(d)), m_vect(d.m_vect, a) + {} + + Data(BOOST_RV_REF(Data) d, const Allocator &a) + : value_compare(boost::move(static_cast(d))), m_vect(boost::move(d.m_vect), a) + {} + + explicit Data(const Compare &comp) + : value_compare(comp), m_vect() + {} + + Data(const Compare &comp, const allocator_t &alloc) + : value_compare(comp), m_vect(alloc) + {} + + explicit Data(const allocator_t &alloc) + : value_compare(), m_vect(alloc) + {} + + Data& operator=(BOOST_COPY_ASSIGN_REF(Data) d) + { + this->value_compare::operator=(d); + m_vect = d.m_vect; + return *this; + } + + Data& operator=(BOOST_RV_REF(Data) d) + { + this->value_compare::operator=(boost::move(static_cast(d))); + m_vect = boost::move(d.m_vect); + return *this; + } + + void swap(Data &d) + { + value_compare& mycomp = *this, & othercomp = d; + boost::adl_move_swap(mycomp, othercomp); + this->m_vect.swap(d.m_vect); + } + + vector_t m_vect; + }; + + Data m_data; + BOOST_COPYABLE_AND_MOVABLE(flat_tree) + + public: + + typedef typename vector_t::value_type value_type; + typedef typename vector_t::pointer pointer; + typedef typename vector_t::const_pointer const_pointer; + typedef typename vector_t::reference reference; + typedef typename vector_t::const_reference const_reference; + typedef Key key_type; + typedef Compare key_compare; + typedef typename vector_t::allocator_type allocator_type; + typedef typename vector_t::size_type size_type; + typedef typename vector_t::difference_type difference_type; + typedef typename vector_t::iterator iterator; + typedef typename vector_t::const_iterator const_iterator; + typedef typename vector_t::reverse_iterator reverse_iterator; + typedef typename vector_t::const_reverse_iterator const_reverse_iterator; + + //!Standard extension + typedef allocator_type stored_allocator_type; + + private: + typedef allocator_traits stored_allocator_traits; + + public: + flat_tree() + : m_data() + { } + + explicit flat_tree(const Compare& comp) + : m_data(comp) + { } + + flat_tree(const Compare& comp, const allocator_type& a) + : m_data(comp, a) + { } + + explicit flat_tree(const allocator_type& a) + : m_data(a) + { } + + flat_tree(const flat_tree& x) + : m_data(x.m_data) + { } + + flat_tree(BOOST_RV_REF(flat_tree) x) + : m_data(boost::move(x.m_data)) + { } + + flat_tree(const flat_tree& x, const allocator_type &a) + : m_data(x.m_data, a) + { } + + flat_tree(BOOST_RV_REF(flat_tree) x, const allocator_type &a) + : m_data(boost::move(x.m_data), a) + { } + + template + flat_tree( ordered_range_t, InputIterator first, InputIterator last + , const Compare& comp = Compare() + , const allocator_type& a = allocator_type()) + : m_data(comp, a) + { this->m_data.m_vect.insert(this->m_data.m_vect.end(), first, last); } + + template + flat_tree( bool unique_insertion + , InputIterator first, InputIterator last + , const Compare& comp = Compare() + , const allocator_type& a = allocator_type()) + : m_data(comp, a) + { + //Use cend() as hint to achieve linear time for + //ordered ranges as required by the standard + //for the constructor + //Call end() every iteration as reallocation might have invalidated iterators + if(unique_insertion){ + for ( ; first != last; ++first){ + this->insert_unique(this->cend(), *first); + } + } + else{ + for ( ; first != last; ++first){ + this->insert_equal(this->cend(), *first); + } + } + } + + ~flat_tree() + {} + + flat_tree& operator=(BOOST_COPY_ASSIGN_REF(flat_tree) x) + { m_data = x.m_data; return *this; } + + flat_tree& operator=(BOOST_RV_REF(flat_tree) x) + BOOST_NOEXCEPT_IF( allocator_traits_type::is_always_equal::value + && boost::container::container_detail::is_nothrow_move_assignable::value ) + { m_data = boost::move(x.m_data); return *this; } + + public: + // accessors: + Compare key_comp() const + { return this->m_data.get_comp(); } + + value_compare value_comp() const + { return this->m_data; } + + allocator_type get_allocator() const + { return this->m_data.m_vect.get_allocator(); } + + const stored_allocator_type &get_stored_allocator() const + { return this->m_data.m_vect.get_stored_allocator(); } + + stored_allocator_type &get_stored_allocator() + { return this->m_data.m_vect.get_stored_allocator(); } + + iterator begin() + { return this->m_data.m_vect.begin(); } + + const_iterator begin() const + { return this->cbegin(); } + + const_iterator cbegin() const + { return this->m_data.m_vect.begin(); } + + iterator end() + { return this->m_data.m_vect.end(); } + + const_iterator end() const + { return this->cend(); } + + const_iterator cend() const + { return this->m_data.m_vect.end(); } + + reverse_iterator rbegin() + { return reverse_iterator(this->end()); } + + const_reverse_iterator rbegin() const + { return this->crbegin(); } + + const_reverse_iterator crbegin() const + { return const_reverse_iterator(this->cend()); } + + reverse_iterator rend() + { return reverse_iterator(this->begin()); } + + const_reverse_iterator rend() const + { return this->crend(); } + + const_reverse_iterator crend() const + { return const_reverse_iterator(this->cbegin()); } + + bool empty() const + { return this->m_data.m_vect.empty(); } + + size_type size() const + { return this->m_data.m_vect.size(); } + + size_type max_size() const + { return this->m_data.m_vect.max_size(); } + + void swap(flat_tree& other) + BOOST_NOEXCEPT_IF( allocator_traits_type::is_always_equal::value + && boost::container::container_detail::is_nothrow_swappable::value ) + { this->m_data.swap(other.m_data); } + + public: + // insert/erase + std::pair insert_unique(const value_type& val) + { + std::pair ret; + insert_commit_data data; + ret.second = this->priv_insert_unique_prepare(val, data); + ret.first = ret.second ? this->priv_insert_commit(data, val) + : iterator(vector_iterator_get_ptr(data.position)); + return ret; + } + + std::pair insert_unique(BOOST_RV_REF(value_type) val) + { + std::pair ret; + insert_commit_data data; + ret.second = this->priv_insert_unique_prepare(val, data); + ret.first = ret.second ? this->priv_insert_commit(data, boost::move(val)) + : iterator(vector_iterator_get_ptr(data.position)); + return ret; + } + + iterator insert_equal(const value_type& val) + { + iterator i = this->upper_bound(KeyOfValue()(val)); + i = this->m_data.m_vect.insert(i, val); + return i; + } + + iterator insert_equal(BOOST_RV_REF(value_type) mval) + { + iterator i = this->upper_bound(KeyOfValue()(mval)); + i = this->m_data.m_vect.insert(i, boost::move(mval)); + return i; + } + + iterator insert_unique(const_iterator hint, const value_type& val) + { + BOOST_ASSERT(this->priv_in_range_or_end(hint)); + std::pair ret; + insert_commit_data data; + return this->priv_insert_unique_prepare(hint, val, data) + ? this->priv_insert_commit(data, val) + : iterator(vector_iterator_get_ptr(data.position)); + } + + iterator insert_unique(const_iterator hint, BOOST_RV_REF(value_type) val) + { + BOOST_ASSERT(this->priv_in_range_or_end(hint)); + std::pair ret; + insert_commit_data data; + return this->priv_insert_unique_prepare(hint, val, data) + ? this->priv_insert_commit(data, boost::move(val)) + : iterator(vector_iterator_get_ptr(data.position)); + } + + iterator insert_equal(const_iterator hint, const value_type& val) + { + BOOST_ASSERT(this->priv_in_range_or_end(hint)); + insert_commit_data data; + this->priv_insert_equal_prepare(hint, val, data); + return this->priv_insert_commit(data, val); + } + + iterator insert_equal(const_iterator hint, BOOST_RV_REF(value_type) mval) + { + BOOST_ASSERT(this->priv_in_range_or_end(hint)); + insert_commit_data data; + this->priv_insert_equal_prepare(hint, mval, data); + return this->priv_insert_commit(data, boost::move(mval)); + } + + template + void insert_unique(InIt first, InIt last) + { + for ( ; first != last; ++first){ + this->insert_unique(*first); + } + } + + template + void insert_equal(InIt first, InIt last + #if !defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + , typename container_detail::enable_if_c + < container_detail::is_input_iterator::value + >::type * = 0 + #endif + ) + { this->priv_insert_equal_loop(first, last); } + + template + void insert_equal(InIt first, InIt last + #if !defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + , typename container_detail::enable_if_c + < !container_detail::is_input_iterator::value + >::type * = 0 + #endif + ) + { + const size_type len = static_cast(boost::container::iterator_distance(first, last)); + this->reserve(this->size()+len); + this->priv_insert_equal_loop(first, last); + } + + //Ordered + + template + void insert_equal(ordered_range_t, InIt first, InIt last + #if !defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + , typename container_detail::enable_if_c + < container_detail::is_input_iterator::value + >::type * = 0 + #endif + ) + { this->priv_insert_equal_loop_ordered(first, last); } + + template + void insert_equal(ordered_range_t, FwdIt first, FwdIt last + #if !defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + , typename container_detail::enable_if_c + < !container_detail::is_input_iterator::value && + container_detail::is_forward_iterator::value + >::type * = 0 + #endif + ) + { + const size_type len = static_cast(boost::container::iterator_distance(first, last)); + this->reserve(this->size()+len); + this->priv_insert_equal_loop_ordered(first, last); + } + + template + void insert_equal(ordered_range_t, BidirIt first, BidirIt last + #if !defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + , typename container_detail::disable_if_or + < void + , container_detail::is_input_iterator + , container_detail::is_forward_iterator + >::type * = 0 + #endif + ) + { this->m_data.m_vect.merge(first, last); } + + template + void insert_unique(ordered_unique_range_t, InIt first, InIt last + #if !defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + , typename container_detail::enable_if_or + < void + , container_detail::is_input_iterator + , container_detail::is_forward_iterator + >::type * = 0 + #endif + ) + { + const_iterator pos(this->cend()); + for ( ; first != last; ++first){ + pos = this->insert_unique(pos, *first); + ++pos; + } + } + + template + void insert_unique(ordered_unique_range_t, BidirIt first, BidirIt last + #if !defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + , typename container_detail::enable_if_c + < !(container_detail::is_input_iterator::value || + container_detail::is_forward_iterator::value) + >::type * = 0 + #endif + ) + { this->m_data.m_vect.merge_unique(first, last, value_compare()); } + + #if !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) + + template + std::pair emplace_unique(BOOST_FWD_REF(Args)... args) + { + typename aligned_storage::value>::type v; + value_type &val = *static_cast(static_cast(&v)); + stored_allocator_type &a = this->get_stored_allocator(); + stored_allocator_traits::construct(a, &val, ::boost::forward(args)... ); + value_destructor d(a, val); + return this->insert_unique(::boost::move(val)); + } + + template + iterator emplace_hint_unique(const_iterator hint, BOOST_FWD_REF(Args)... args) + { + //hint checked in insert_unique + typename aligned_storage::value>::type v; + value_type &val = *static_cast(static_cast(&v)); + stored_allocator_type &a = this->get_stored_allocator(); + stored_allocator_traits::construct(a, &val, ::boost::forward(args)... ); + value_destructor d(a, val); + return this->insert_unique(hint, ::boost::move(val)); + } + + template + iterator emplace_equal(BOOST_FWD_REF(Args)... args) + { + typename aligned_storage::value>::type v; + value_type &val = *static_cast(static_cast(&v)); + stored_allocator_type &a = this->get_stored_allocator(); + stored_allocator_traits::construct(a, &val, ::boost::forward(args)... ); + value_destructor d(a, val); + return this->insert_equal(::boost::move(val)); + } + + template + iterator emplace_hint_equal(const_iterator hint, BOOST_FWD_REF(Args)... args) + { + //hint checked in insert_equal + typename aligned_storage::value>::type v; + value_type &val = *static_cast(static_cast(&v)); + stored_allocator_type &a = this->get_stored_allocator(); + stored_allocator_traits::construct(a, &val, ::boost::forward(args)... ); + value_destructor d(a, val); + return this->insert_equal(hint, ::boost::move(val)); + } + + #else // !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) + + #define BOOST_CONTAINER_FLAT_TREE_EMPLACE_CODE(N) \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + std::pair emplace_unique(BOOST_MOVE_UREF##N)\ + {\ + typename aligned_storage::value>::type v;\ + value_type &val = *static_cast(static_cast(&v));\ + stored_allocator_type &a = this->get_stored_allocator();\ + stored_allocator_traits::construct(a, &val BOOST_MOVE_I##N BOOST_MOVE_FWD##N);\ + value_destructor d(a, val);\ + return this->insert_unique(::boost::move(val));\ + }\ + \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + iterator emplace_hint_unique(const_iterator hint BOOST_MOVE_I##N BOOST_MOVE_UREF##N)\ + {\ + typename aligned_storage::value>::type v;\ + value_type &val = *static_cast(static_cast(&v));\ + stored_allocator_type &a = this->get_stored_allocator();\ + stored_allocator_traits::construct(a, &val BOOST_MOVE_I##N BOOST_MOVE_FWD##N);\ + value_destructor d(a, val);\ + return this->insert_unique(hint, ::boost::move(val));\ + }\ + \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + iterator emplace_equal(BOOST_MOVE_UREF##N)\ + {\ + typename aligned_storage::value>::type v;\ + value_type &val = *static_cast(static_cast(&v));\ + stored_allocator_type &a = this->get_stored_allocator();\ + stored_allocator_traits::construct(a, &val BOOST_MOVE_I##N BOOST_MOVE_FWD##N);\ + value_destructor d(a, val);\ + return this->insert_equal(::boost::move(val));\ + }\ + \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + iterator emplace_hint_equal(const_iterator hint BOOST_MOVE_I##N BOOST_MOVE_UREF##N)\ + {\ + typename aligned_storage ::value>::type v;\ + value_type &val = *static_cast(static_cast(&v));\ + stored_allocator_type &a = this->get_stored_allocator();\ + stored_allocator_traits::construct(a, &val BOOST_MOVE_I##N BOOST_MOVE_FWD##N);\ + value_destructor d(a, val);\ + return this->insert_equal(hint, ::boost::move(val));\ + }\ + // + BOOST_MOVE_ITERATE_0TO9(BOOST_CONTAINER_FLAT_TREE_EMPLACE_CODE) + #undef BOOST_CONTAINER_FLAT_TREE_EMPLACE_CODE + + #endif // !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) + + iterator erase(const_iterator position) + { return this->m_data.m_vect.erase(position); } + + size_type erase(const key_type& k) + { + std::pair itp = this->equal_range(k); + size_type ret = static_cast(itp.second-itp.first); + if (ret){ + this->m_data.m_vect.erase(itp.first, itp.second); + } + return ret; + } + + iterator erase(const_iterator first, const_iterator last) + { return this->m_data.m_vect.erase(first, last); } + + void clear() + { this->m_data.m_vect.clear(); } + + //! Effects: Tries to deallocate the excess of memory created + // with previous allocations. The size of the vector is unchanged + //! + //! Throws: If memory allocation throws, or T's copy constructor throws. + //! + //! Complexity: Linear to size(). + void shrink_to_fit() + { this->m_data.m_vect.shrink_to_fit(); } + + iterator nth(size_type n) BOOST_NOEXCEPT_OR_NOTHROW + { return this->m_data.m_vect.nth(n); } + + const_iterator nth(size_type n) const BOOST_NOEXCEPT_OR_NOTHROW + { return this->m_data.m_vect.nth(n); } + + size_type index_of(iterator p) BOOST_NOEXCEPT_OR_NOTHROW + { return this->m_data.m_vect.index_of(p); } + + size_type index_of(const_iterator p) const BOOST_NOEXCEPT_OR_NOTHROW + { return this->m_data.m_vect.index_of(p); } + + // set operations: + iterator find(const key_type& k) + { + iterator i = this->lower_bound(k); + iterator end_it = this->end(); + if (i != end_it && this->m_data.get_comp()(k, KeyOfValue()(*i))){ + i = end_it; + } + return i; + } + + const_iterator find(const key_type& k) const + { + const_iterator i = this->lower_bound(k); + + const_iterator end_it = this->cend(); + if (i != end_it && this->m_data.get_comp()(k, KeyOfValue()(*i))){ + i = end_it; + } + return i; + } + + // set operations: + size_type count(const key_type& k) const + { + std::pair p = this->equal_range(k); + size_type n = p.second - p.first; + return n; + } + + iterator lower_bound(const key_type& k) + { return this->priv_lower_bound(this->begin(), this->end(), k); } + + const_iterator lower_bound(const key_type& k) const + { return this->priv_lower_bound(this->cbegin(), this->cend(), k); } + + iterator upper_bound(const key_type& k) + { return this->priv_upper_bound(this->begin(), this->end(), k); } + + const_iterator upper_bound(const key_type& k) const + { return this->priv_upper_bound(this->cbegin(), this->cend(), k); } + + std::pair equal_range(const key_type& k) + { return this->priv_equal_range(this->begin(), this->end(), k); } + + std::pair equal_range(const key_type& k) const + { return this->priv_equal_range(this->cbegin(), this->cend(), k); } + + std::pair lower_bound_range(const key_type& k) + { return this->priv_lower_bound_range(this->begin(), this->end(), k); } + + std::pair lower_bound_range(const key_type& k) const + { return this->priv_lower_bound_range(this->cbegin(), this->cend(), k); } + + size_type capacity() const + { return this->m_data.m_vect.capacity(); } + + void reserve(size_type cnt) + { this->m_data.m_vect.reserve(cnt); } + + friend bool operator==(const flat_tree& x, const flat_tree& y) + { + return x.size() == y.size() && ::boost::container::algo_equal(x.begin(), x.end(), y.begin()); + } + + friend bool operator<(const flat_tree& x, const flat_tree& y) + { + return ::boost::container::algo_lexicographical_compare(x.begin(), x.end(), y.begin(), y.end()); + } + + friend bool operator!=(const flat_tree& x, const flat_tree& y) + { return !(x == y); } + + friend bool operator>(const flat_tree& x, const flat_tree& y) + { return y < x; } + + friend bool operator<=(const flat_tree& x, const flat_tree& y) + { return !(y < x); } + + friend bool operator>=(const flat_tree& x, const flat_tree& y) + { return !(x < y); } + + friend void swap(flat_tree& x, flat_tree& y) + { x.swap(y); } + + private: + + bool priv_in_range_or_end(const_iterator pos) const + { + return (this->begin() <= pos) && (pos <= this->end()); + } + + struct insert_commit_data + { + const_iterator position; + }; + + // insert/erase + void priv_insert_equal_prepare + (const_iterator pos, const value_type& val, insert_commit_data &data) + { + // N1780 + // To insert val at pos: + // if pos == end || val <= *pos + // if pos == begin || val >= *(pos-1) + // insert val before pos + // else + // insert val before upper_bound(val) + // else + // insert val before lower_bound(val) + const value_compare &val_cmp = this->m_data; + + if(pos == this->cend() || !val_cmp(*pos, val)){ + if (pos == this->cbegin() || !val_cmp(val, pos[-1])){ + data.position = pos; + } + else{ + data.position = + this->priv_upper_bound(this->cbegin(), pos, KeyOfValue()(val)); + } + } + else{ + data.position = + this->priv_lower_bound(pos, this->cend(), KeyOfValue()(val)); + } + } + + bool priv_insert_unique_prepare + (const_iterator b, const_iterator e, const value_type& val, insert_commit_data &commit_data) + { + const value_compare &val_cmp = this->m_data; + commit_data.position = this->priv_lower_bound(b, e, KeyOfValue()(val)); + return commit_data.position == e || val_cmp(val, *commit_data.position); + } + + bool priv_insert_unique_prepare + (const value_type& val, insert_commit_data &commit_data) + { return this->priv_insert_unique_prepare(this->cbegin(), this->cend(), val, commit_data); } + + bool priv_insert_unique_prepare + (const_iterator pos, const value_type& val, insert_commit_data &commit_data) + { + //N1780. Props to Howard Hinnant! + //To insert val at pos: + //if pos == end || val <= *pos + // if pos == begin || val >= *(pos-1) + // insert val before pos + // else + // insert val before upper_bound(val) + //else if pos+1 == end || val <= *(pos+1) + // insert val after pos + //else + // insert val before lower_bound(val) + const value_compare &val_cmp = this->m_data; + const const_iterator cend_it = this->cend(); + if(pos == cend_it || val_cmp(val, *pos)){ //Check if val should go before end + const const_iterator cbeg = this->cbegin(); + commit_data.position = pos; + if(pos == cbeg){ //If container is empty then insert it in the beginning + return true; + } + const_iterator prev(pos); + --prev; + if(val_cmp(*prev, val)){ //If previous element was less, then it should go between prev and pos + return true; + } + else if(!val_cmp(val, *prev)){ //If previous was equal then insertion should fail + commit_data.position = prev; + return false; + } + else{ //Previous was bigger so insertion hint was pointless, dispatch to hintless insertion + //but reduce the search between beg and prev as prev is bigger than val + return this->priv_insert_unique_prepare(cbeg, prev, val, commit_data); + } + } + else{ + //The hint is before the insertion position, so insert it + //in the remaining range [pos, end) + return this->priv_insert_unique_prepare(pos, cend_it, val, commit_data); + } + } + + template + iterator priv_insert_commit + (insert_commit_data &commit_data, BOOST_FWD_REF(Convertible) convertible) + { + return this->m_data.m_vect.insert + ( commit_data.position + , boost::forward(convertible)); + } + + template + RanIt priv_lower_bound(RanIt first, const RanIt last, + const key_type & key) const + { + const Compare &key_cmp = this->m_data.get_comp(); + KeyOfValue key_extract; + size_type len = static_cast(last - first); + RanIt middle; + + while (len) { + size_type step = len >> 1; + middle = first; + middle += step; + + if (key_cmp(key_extract(*middle), key)) { + first = ++middle; + len -= step + 1; + } + else{ + len = step; + } + } + return first; + } + + template + RanIt priv_upper_bound + (RanIt first, const RanIt last,const key_type & key) const + { + const Compare &key_cmp = this->m_data.get_comp(); + KeyOfValue key_extract; + size_type len = static_cast(last - first); + RanIt middle; + + while (len) { + size_type step = len >> 1; + middle = first; + middle += step; + + if (key_cmp(key, key_extract(*middle))) { + len = step; + } + else{ + first = ++middle; + len -= step + 1; + } + } + return first; + } + + template + std::pair + priv_equal_range(RanIt first, RanIt last, const key_type& key) const + { + const Compare &key_cmp = this->m_data.get_comp(); + KeyOfValue key_extract; + size_type len = static_cast(last - first); + RanIt middle; + + while (len) { + size_type step = len >> 1; + middle = first; + middle += step; + + if (key_cmp(key_extract(*middle), key)){ + first = ++middle; + len -= step + 1; + } + else if (key_cmp(key, key_extract(*middle))){ + len = step; + } + else { + //Middle is equal to key + last = first; + last += len; + RanIt const first_ret = this->priv_lower_bound(first, middle, key); + return std::pair + ( first_ret, this->priv_upper_bound(++middle, last, key)); + } + } + return std::pair(first, first); + } + + template + std::pair priv_lower_bound_range(RanIt first, RanIt last, const key_type& k) const + { + const Compare &key_cmp = this->m_data.get_comp(); + KeyOfValue key_extract; + RanIt lb(this->priv_lower_bound(first, last, k)), ub(lb); + if(lb != last && static_cast(!key_cmp(k, key_extract(*lb)))){ + ++ub; + } + return std::pair(lb, ub); + } + + template + void priv_insert_equal_loop(InIt first, InIt last) + { + for ( ; first != last; ++first){ + this->insert_equal(*first); + } + } + + template + void priv_insert_equal_loop_ordered(InIt first, InIt last) + { + const_iterator pos(this->cend()); + for ( ; first != last; ++first){ + //If ordered, then try hint version + //to achieve constant-time complexity per insertion + //in some cases + pos = this->insert_equal(pos, *first); + ++pos; + } + } +}; + +} //namespace container_detail { + +} //namespace container { + +//!has_trivial_destructor_after_move<> == true_type +//!specialization for optimizations +template +struct has_trivial_destructor_after_move > +{ + typedef typename ::boost::container::allocator_traits::pointer pointer; + static const bool value = ::boost::has_trivial_destructor_after_move::value && + ::boost::has_trivial_destructor_after_move::value; +}; + +} //namespace boost { + +#include + +#endif // BOOST_CONTAINER_FLAT_TREE_HPP Index: libraries/win32/boost/include/boost/container/flat_map.hpp =================================================================== --- /dev/null +++ libraries/win32/boost/include/boost/container/flat_map.hpp @@ -0,0 +1,2014 @@ +////////////////////////////////////////////////////////////////////////////// +// +// (C) Copyright Ion Gaztanaga 2005-2013. Distributed under the Boost +// Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// See http://www.boost.org/libs/container for documentation. +// +////////////////////////////////////////////////////////////////////////////// +#ifndef BOOST_CONTAINER_FLAT_MAP_HPP +#define BOOST_CONTAINER_FLAT_MAP_HPP + +#ifndef BOOST_CONFIG_HPP +# include +#endif + +#if defined(BOOST_HAS_PRAGMA_ONCE) +# pragma once +#endif + +#include +#include +// container +#include +#include +#include //new_allocator +#include +// container/detail +#include +#include +#include +#include //equal() +// move +#include +#include +// move/detail +#if defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) +#include +#endif +#include +// intrusive +#include //pair +#include //less, equal +//others +#include + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) +#include +#endif + +namespace boost { +namespace container { + +#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + +namespace container_detail{ + +template +static D &force(const S &s) +{ return *const_cast((reinterpret_cast(&s))); } + +template +static D force_copy(S s) +{ + D *vp = reinterpret_cast(&s); + return D(*vp); +} + +} //namespace container_detail{ + +#endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + +//! A flat_map is a kind of associative container that supports unique keys (contains at +//! most one of each key value) and provides for fast retrieval of values of another +//! type T based on the keys. The flat_map class supports random-access iterators. +//! +//! A flat_map satisfies all of the requirements of a container and of a reversible +//! container and of an associative container. A flat_map also provides +//! most operations described for unique keys. For a +//! flat_map the key_type is Key and the value_type is std::pair +//! (unlike std::map which value_type is std::pair<const Key, T>). +//! +//! Compare is the ordering function for Keys (e.g. std::less). +//! +//! Allocator is the allocator to allocate the value_types +//! (e.g. allocator< std::pair >). +//! +//! flat_map is similar to std::map but it's implemented like an ordered vector. +//! This means that inserting a new element into a flat_map invalidates +//! previous iterators and references +//! +//! Erasing an element invalidates iterators and references +//! pointing to elements that come after (their keys are bigger) the erased element. +//! +//! This container provides random-access iterators. +//! +//! \tparam Key is the key_type of the map +//! \tparam Value is the mapped_type +//! \tparam Compare is the ordering function for Keys (e.g. std::less). +//! \tparam Allocator is the allocator to allocate the value_types +//! (e.g. allocator< std::pair > ). +#ifdef BOOST_CONTAINER_DOXYGEN_INVOKED +template , class Allocator = new_allocator< std::pair< Key, T> > > +#else +template +#endif +class flat_map +{ + #ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + private: + BOOST_COPYABLE_AND_MOVABLE(flat_map) + //This is the tree that we should store if pair was movable + typedef container_detail::flat_tree, + container_detail::select1st< std::pair >, + Compare, + Allocator> tree_t; + + //This is the real tree stored here. It's based on a movable pair + typedef container_detail::flat_tree, + container_detail::select1st >, + Compare, + typename allocator_traits::template portable_rebind_alloc + >::type> impl_tree_t; + impl_tree_t m_flat_tree; // flat tree representing flat_map + + typedef typename impl_tree_t::value_type impl_value_type; + typedef typename impl_tree_t::const_iterator impl_const_iterator; + typedef typename impl_tree_t::iterator impl_iterator; + typedef typename impl_tree_t::allocator_type impl_allocator_type; + typedef container_detail::flat_tree_value_compare + < Compare + , container_detail::select1st< std::pair > + , std::pair > value_compare_impl; + typedef typename container_detail::get_flat_tree_iterators + ::pointer>::iterator iterator_impl; + typedef typename container_detail::get_flat_tree_iterators + ::pointer>::const_iterator const_iterator_impl; + typedef typename container_detail::get_flat_tree_iterators + ::pointer>::reverse_iterator reverse_iterator_impl; + typedef typename container_detail::get_flat_tree_iterators + ::pointer>::const_reverse_iterator const_reverse_iterator_impl; + public: + typedef typename impl_tree_t::stored_allocator_type impl_stored_allocator_type; + private: + #endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + + public: + + ////////////////////////////////////////////// + // + // types + // + ////////////////////////////////////////////// + typedef Key key_type; + typedef T mapped_type; + typedef std::pair value_type; + typedef ::boost::container::allocator_traits allocator_traits_type; + typedef typename boost::container::allocator_traits::pointer pointer; + typedef typename boost::container::allocator_traits::const_pointer const_pointer; + typedef typename boost::container::allocator_traits::reference reference; + typedef typename boost::container::allocator_traits::const_reference const_reference; + typedef typename boost::container::allocator_traits::size_type size_type; + typedef typename boost::container::allocator_traits::difference_type difference_type; + typedef Allocator allocator_type; + typedef BOOST_CONTAINER_IMPDEF(Allocator) stored_allocator_type; + typedef BOOST_CONTAINER_IMPDEF(value_compare_impl) value_compare; + typedef Compare key_compare; + typedef BOOST_CONTAINER_IMPDEF(iterator_impl) iterator; + typedef BOOST_CONTAINER_IMPDEF(const_iterator_impl) const_iterator; + typedef BOOST_CONTAINER_IMPDEF(reverse_iterator_impl) reverse_iterator; + typedef BOOST_CONTAINER_IMPDEF(const_reverse_iterator_impl) const_reverse_iterator; + typedef BOOST_CONTAINER_IMPDEF(impl_value_type) movable_value_type; + + public: + ////////////////////////////////////////////// + // + // construct/copy/destroy + // + ////////////////////////////////////////////// + + //! Effects: Default constructs an empty flat_map. + //! + //! Complexity: Constant. + flat_map() + : m_flat_tree() + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_map using the specified + //! comparison object and allocator. + //! + //! Complexity: Constant. + explicit flat_map(const Compare& comp, const allocator_type& a = allocator_type()) + : m_flat_tree(comp, container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_map using the specified allocator. + //! + //! Complexity: Constant. + explicit flat_map(const allocator_type& a) + : m_flat_tree(container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_map using the specified comparison object and + //! allocator, and inserts elements from the range [first ,last ). + //! + //! Complexity: Linear in N if the range [first ,last ) is already sorted using + //! comp and otherwise N logN, where N is last - first. + template + flat_map(InputIterator first, InputIterator last, const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : m_flat_tree(true, first, last, comp, container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_map using the specified + //! allocator, and inserts elements from the range [first ,last ). + //! + //! Complexity: Linear in N if the range [first ,last ) is already sorted using + //! comp and otherwise N logN, where N is last - first. + template + flat_map(InputIterator first, InputIterator last, const allocator_type& a) + : m_flat_tree(true, first, last, Compare(), container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_map using the specified comparison object and + //! allocator, and inserts elements from the ordered unique range [first ,last). This function + //! is more efficient than the normal range creation for ordered ranges. + //! + //! Requires: [first ,last) must be ordered according to the predicate and must be + //! unique values. + //! + //! Complexity: Linear in N. + //! + //! Note: Non-standard extension. + template + flat_map( ordered_unique_range_t, InputIterator first, InputIterator last + , const Compare& comp = Compare(), const allocator_type& a = allocator_type()) + : m_flat_tree(ordered_range, first, last, comp, a) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! Effects: Constructs an empty flat_map using the specified comparison object and + //! allocator, and inserts elements from the range [il.begin() ,il.end()). + //! + //! Complexity: Linear in N if the range [il.begin(), il.end()) is already sorted using + //! comp and otherwise N logN, where N is last - first. + flat_map(std::initializer_list il, const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : m_flat_tree(true, il.begin(), il.end(), comp, container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_map using the specified + //! allocator, and inserts elements from the range [il.begin() ,il.end()). + //! + //! Complexity: Linear in N if the range [il.begin(), il.end()) is already sorted using + //! comp and otherwise N logN, where N is last - first. + flat_map(std::initializer_list il, const allocator_type& a) + : m_flat_tree(true, il.begin(), il.end(), Compare(), container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_map using the specified comparison object and + //! allocator, and inserts elements from the ordered unique range [il.begin(), il.end()). This function + //! is more efficient than the normal range creation for ordered ranges. + //! + //! Requires: [il.begin(), il.end()) must be ordered according to the predicate and must be + //! unique values. + //! + //! Complexity: Linear in N. + //! + //! Note: Non-standard extension. + flat_map(ordered_unique_range_t, std::initializer_list il, const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : m_flat_tree(ordered_range, il.begin(), il.end(), comp, a) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } +#endif + + //! Effects: Copy constructs a flat_map. + //! + //! Complexity: Linear in x.size(). + flat_map(const flat_map& x) + : m_flat_tree(x.m_flat_tree) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Move constructs a flat_map. + //! Constructs *this using x's resources. + //! + //! Complexity: Constant. + //! + //! Postcondition: x is emptied. + flat_map(BOOST_RV_REF(flat_map) x) + : m_flat_tree(boost::move(x.m_flat_tree)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Copy constructs a flat_map using the specified allocator. + //! + //! Complexity: Linear in x.size(). + flat_map(const flat_map& x, const allocator_type &a) + : m_flat_tree(x.m_flat_tree, a) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Move constructs a flat_map using the specified allocator. + //! Constructs *this using x's resources. + //! + //! Complexity: Constant if x.get_allocator() == a, linear otherwise. + flat_map(BOOST_RV_REF(flat_map) x, const allocator_type &a) + : m_flat_tree(boost::move(x.m_flat_tree), a) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Makes *this a copy of x. + //! + //! Complexity: Linear in x.size(). + flat_map& operator=(BOOST_COPY_ASSIGN_REF(flat_map) x) + { m_flat_tree = x.m_flat_tree; return *this; } + + //! Effects: Move constructs a flat_map. + //! Constructs *this using x's resources. + //! + //! Throws: If allocator_traits_type::propagate_on_container_move_assignment + //! is false and (allocation throws or value_type's move constructor throws) + //! + //! Complexity: Constant if allocator_traits_type:: + //! propagate_on_container_move_assignment is true or + //! this->get>allocator() == x.get_allocator(). Linear otherwise. + flat_map& operator=(BOOST_RV_REF(flat_map) x) + BOOST_NOEXCEPT_IF( allocator_traits_type::is_always_equal::value + && boost::container::container_detail::is_nothrow_move_assignable::value ) + { m_flat_tree = boost::move(x.m_flat_tree); return *this; } + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! Effects: Assign elements from il to *this + flat_map& operator=(std::initializer_list il) + { + this->clear(); + this->insert(il.begin(), il.end()); + return *this; + } +#endif + + //! Effects: Returns a copy of the allocator that + //! was passed to the object's constructor. + //! + //! Complexity: Constant. + allocator_type get_allocator() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.get_allocator()); } + + //! Effects: Returns a reference to the internal allocator. + //! + //! Throws: Nothing + //! + //! Complexity: Constant. + //! + //! Note: Non-standard extension. + stored_allocator_type &get_stored_allocator() BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force(m_flat_tree.get_stored_allocator()); } + + //! Effects: Returns a reference to the internal allocator. + //! + //! Throws: Nothing + //! + //! Complexity: Constant. + //! + //! Note: Non-standard extension. + const stored_allocator_type &get_stored_allocator() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force(m_flat_tree.get_stored_allocator()); } + + ////////////////////////////////////////////// + // + // iterators + // + ////////////////////////////////////////////// + + //! Effects: Returns an iterator to the first element contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + iterator begin() BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.begin()); } + + //! Effects: Returns a const_iterator to the first element contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator begin() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.begin()); } + + //! Effects: Returns an iterator to the end of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + iterator end() BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.end()); } + + //! Effects: Returns a const_iterator to the end of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator end() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.end()); } + + //! Effects: Returns a reverse_iterator pointing to the beginning + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + reverse_iterator rbegin() BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.rbegin()); } + + //! Effects: Returns a const_reverse_iterator pointing to the beginning + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator rbegin() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.rbegin()); } + + //! Effects: Returns a reverse_iterator pointing to the end + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + reverse_iterator rend() BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.rend()); } + + //! Effects: Returns a const_reverse_iterator pointing to the end + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator rend() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.rend()); } + + //! Effects: Returns a const_iterator to the first element contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator cbegin() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.cbegin()); } + + //! Effects: Returns a const_iterator to the end of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator cend() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.cend()); } + + //! Effects: Returns a const_reverse_iterator pointing to the beginning + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator crbegin() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.crbegin()); } + + //! Effects: Returns a const_reverse_iterator pointing to the end + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator crend() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.crend()); } + + ////////////////////////////////////////////// + // + // capacity + // + ////////////////////////////////////////////// + + //! Effects: Returns true if the container contains no elements. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + bool empty() const BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.empty(); } + + //! Effects: Returns the number of the elements contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + size_type size() const BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.size(); } + + //! Effects: Returns the largest possible size of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + size_type max_size() const BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.max_size(); } + + //! Effects: Number of elements for which memory has been allocated. + //! capacity() is always greater than or equal to size(). + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + size_type capacity() const BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.capacity(); } + + //! Effects: If n is less than or equal to capacity(), this call has no + //! effect. Otherwise, it is a request for allocation of additional memory. + //! If the request is successful, then capacity() is greater than or equal to + //! n; otherwise, capacity() is unchanged. In either case, size() is unchanged. + //! + //! Throws: If memory allocation allocation throws or T's copy constructor throws. + //! + //! Note: If capacity() is less than "cnt", iterators and references to + //! to values might be invalidated. + void reserve(size_type cnt) + { m_flat_tree.reserve(cnt); } + + //! Effects: Tries to deallocate the excess of memory created + // with previous allocations. The size of the vector is unchanged + //! + //! Throws: If memory allocation throws, or T's copy constructor throws. + //! + //! Complexity: Linear to size(). + void shrink_to_fit() + { m_flat_tree.shrink_to_fit(); } + + ////////////////////////////////////////////// + // + // element access + // + ////////////////////////////////////////////// + + #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + //! Effects: If there is no key equivalent to x in the flat_map, inserts + //! value_type(x, T()) into the flat_map. + //! + //! Returns: A reference to the mapped_type corresponding to x in *this. + //! + //! Complexity: Logarithmic. + mapped_type &operator[](const key_type& k); + + //! Effects: If there is no key equivalent to x in the flat_map, inserts + //! value_type(move(x), T()) into the flat_map (the key is move-constructed) + //! + //! Returns: A reference to the mapped_type corresponding to x in *this. + //! + //! Complexity: Logarithmic. + mapped_type &operator[](key_type &&k) ; + #elif defined(BOOST_MOVE_HELPERS_RETURN_SFINAE_BROKEN) + //in compilers like GCC 3.4, we can't catch temporaries + mapped_type& operator[](const key_type &k) { return this->priv_subscript(k); } + mapped_type& operator[](BOOST_RV_REF(key_type) k) { return this->priv_subscript(::boost::move(k)); } + #else + BOOST_MOVE_CONVERSION_AWARE_CATCH( operator[] , key_type, mapped_type&, this->priv_subscript) + #endif + + //! @copydoc ::boost::container::flat_set::nth(size_type) + iterator nth(size_type n) BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.nth(n)); } + + //! @copydoc ::boost::container::flat_set::nth(size_type) const + const_iterator nth(size_type n) const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.nth(n)); } + + //! @copydoc ::boost::container::flat_set::index_of(iterator) + size_type index_of(iterator p) BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.index_of(container_detail::force_copy(p)); } + + //! @copydoc ::boost::container::flat_set::index_of(const_iterator) const + size_type index_of(const_iterator p) const BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.index_of(container_detail::force_copy(p)); } + + //! Returns: A reference to the element whose key is equivalent to x. + //! + //! Throws: An exception object of type out_of_range if no such element is present. + //! + //! Complexity: logarithmic. + T& at(const key_type& k) + { + iterator i = this->find(k); + if(i == this->end()){ + throw_out_of_range("flat_map::at key not found"); + } + return i->second; + } + + //! Returns: A reference to the element whose key is equivalent to x. + //! + //! Throws: An exception object of type out_of_range if no such element is present. + //! + //! Complexity: logarithmic. + const T& at(const key_type& k) const + { + const_iterator i = this->find(k); + if(i == this->end()){ + throw_out_of_range("flat_map::at key not found"); + } + return i->second; + } + + ////////////////////////////////////////////// + // + // modifiers + // + ////////////////////////////////////////////// + + #if !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) || defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + //! Effects: Inserts an object x of type T constructed with + //! std::forward(args)... if and only if there is no element in the container + //! with key equivalent to the key of x. + //! + //! Returns: The bool component of the returned pair is true if and only + //! if the insertion takes place, and the iterator component of the pair + //! points to the element with key equivalent to the key of x. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + template + std::pair emplace(BOOST_FWD_REF(Args)... args) + { return container_detail::force_copy< std::pair >(m_flat_tree.emplace_unique(boost::forward(args)...)); } + + //! Effects: Inserts an object of type T constructed with + //! std::forward(args)... in the container if and only if there is + //! no element in the container with key equivalent to the key of x. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent + //! to the key of x. + //! + //! Complexity: Logarithmic search time (constant if x is inserted + //! right before p) plus insertion linear to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + template + iterator emplace_hint(const_iterator hint, BOOST_FWD_REF(Args)... args) + { + return container_detail::force_copy + (m_flat_tree.emplace_hint_unique( container_detail::force_copy(hint) + , boost::forward(args)...)); + } + + #else // !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) + + #define BOOST_CONTAINER_FLAT_MAP_EMPLACE_CODE(N) \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + std::pair emplace(BOOST_MOVE_UREF##N)\ + {\ + return container_detail::force_copy< std::pair >\ + (m_flat_tree.emplace_unique(BOOST_MOVE_FWD##N));\ + }\ + \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + iterator emplace_hint(const_iterator hint BOOST_MOVE_I##N BOOST_MOVE_UREF##N)\ + {\ + return container_detail::force_copy(m_flat_tree.emplace_hint_unique\ + (container_detail::force_copy(hint) BOOST_MOVE_I##N BOOST_MOVE_FWD##N));\ + }\ + // + BOOST_MOVE_ITERATE_0TO9(BOOST_CONTAINER_FLAT_MAP_EMPLACE_CODE) + #undef BOOST_CONTAINER_FLAT_MAP_EMPLACE_CODE + + #endif // !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) + + //! Effects: Inserts x if and only if there is no element in the container + //! with key equivalent to the key of x. + //! + //! Returns: The bool component of the returned pair is true if and only + //! if the insertion takes place, and the iterator component of the pair + //! points to the element with key equivalent to the key of x. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + std::pair insert(const value_type& x) + { return container_detail::force_copy >( + m_flat_tree.insert_unique(container_detail::force(x))); } + + //! Effects: Inserts a new value_type move constructed from the pair if and + //! only if there is no element in the container with key equivalent to the key of x. + //! + //! Returns: The bool component of the returned pair is true if and only + //! if the insertion takes place, and the iterator component of the pair + //! points to the element with key equivalent to the key of x. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + std::pair insert(BOOST_RV_REF(value_type) x) + { return container_detail::force_copy >( + m_flat_tree.insert_unique(boost::move(container_detail::force(x)))); } + + //! Effects: Inserts a new value_type move constructed from the pair if and + //! only if there is no element in the container with key equivalent to the key of x. + //! + //! Returns: The bool component of the returned pair is true if and only + //! if the insertion takes place, and the iterator component of the pair + //! points to the element with key equivalent to the key of x. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + std::pair insert(BOOST_RV_REF(movable_value_type) x) + { + return container_detail::force_copy > + (m_flat_tree.insert_unique(boost::move(x))); + } + + //! Effects: Inserts a copy of x in the container if and only if there is + //! no element in the container with key equivalent to the key of x. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent + //! to the key of x. + //! + //! Complexity: Logarithmic search time (constant if x is inserted + //! right before p) plus insertion linear to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const_iterator p, const value_type& x) + { + return container_detail::force_copy( + m_flat_tree.insert_unique( container_detail::force_copy(p) + , container_detail::force(x))); + } + + //! Effects: Inserts an element move constructed from x in the container. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent to the key of x. + //! + //! Complexity: Logarithmic search time (constant if x is inserted + //! right before p) plus insertion linear to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const_iterator p, BOOST_RV_REF(value_type) x) + { + return container_detail::force_copy + (m_flat_tree.insert_unique( container_detail::force_copy(p) + , boost::move(container_detail::force(x)))); + } + + //! Effects: Inserts an element move constructed from x in the container. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent to the key of x. + //! + //! Complexity: Logarithmic search time (constant if x is inserted + //! right before p) plus insertion linear to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const_iterator p, BOOST_RV_REF(movable_value_type) x) + { + return container_detail::force_copy( + m_flat_tree.insert_unique(container_detail::force_copy(p), boost::move(x))); + } + + //! Requires: first, last are not iterators into *this. + //! + //! Effects: inserts each element from the range [first,last) if and only + //! if there is no element with key equivalent to the key of that element. + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + template + void insert(InputIterator first, InputIterator last) + { m_flat_tree.insert_unique(first, last); } + + //! Requires: first, last are not iterators into *this. + //! + //! Requires: [first ,last) must be ordered according to the predicate and must be + //! unique values. + //! + //! Effects: inserts each element from the range [first,last) if and only + //! if there is no element with key equivalent to the key of that element. This + //! function is more efficient than the normal range creation for ordered ranges. + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + //! + //! Note: Non-standard extension. + template + void insert(ordered_unique_range_t, InputIterator first, InputIterator last) + { m_flat_tree.insert_unique(ordered_unique_range, first, last); } + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! Effects: inserts each element from the range [il.begin(), il.end()) if and only + //! if there is no element with key equivalent to the key of that element. + //! + //! Complexity: At most N log(size()+N) (N is the distance from il.first() to il.end()) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + void insert(std::initializer_list il) + { m_flat_tree.insert_unique(il.begin(), il.end()); } + + //! Requires: [il.begin(), il.end()) must be ordered according to the predicate and must be + //! unique values. + //! + //! Effects: inserts each element from the range [il.begin(), il.end()) if and only + //! if there is no element with key equivalent to the key of that element. This + //! function is more efficient than the normal range creation for ordered ranges. + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + //! + //! Note: Non-standard extension. + void insert(ordered_unique_range_t, std::initializer_list il) + { m_flat_tree.insert_unique(ordered_unique_range, il.begin(), il.end()); } +#endif + + //! Effects: Erases the element pointed to by p. + //! + //! Returns: Returns an iterator pointing to the element immediately + //! following q prior to the element being erased. If no such element exists, + //! returns end(). + //! + //! Complexity: Linear to the elements with keys bigger than p + //! + //! Note: Invalidates elements with keys + //! not less than the erased element. + iterator erase(const_iterator p) + { + return container_detail::force_copy + (m_flat_tree.erase(container_detail::force_copy(p))); + } + + //! Effects: Erases all elements in the container with key equivalent to x. + //! + //! Returns: Returns the number of erased elements. + //! + //! Complexity: Logarithmic search time plus erasure time + //! linear to the elements with bigger keys. + size_type erase(const key_type& x) + { return m_flat_tree.erase(x); } + + //! Effects: Erases all the elements in the range [first, last). + //! + //! Returns: Returns last. + //! + //! Complexity: size()*N where N is the distance from first to last. + //! + //! Complexity: Logarithmic search time plus erasure time + //! linear to the elements with bigger keys. + iterator erase(const_iterator first, const_iterator last) + { + return container_detail::force_copy( + m_flat_tree.erase( container_detail::force_copy(first) + , container_detail::force_copy(last))); + } + + //! Effects: Swaps the contents of *this and x. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + void swap(flat_map& x) + BOOST_NOEXCEPT_IF( allocator_traits_type::is_always_equal::value + && boost::container::container_detail::is_nothrow_swappable::value ) + { m_flat_tree.swap(x.m_flat_tree); } + + //! Effects: erase(a.begin(),a.end()). + //! + //! Postcondition: size() == 0. + //! + //! Complexity: linear in size(). + void clear() BOOST_NOEXCEPT_OR_NOTHROW + { m_flat_tree.clear(); } + + ////////////////////////////////////////////// + // + // observers + // + ////////////////////////////////////////////// + + //! Effects: Returns the comparison object out + //! of which a was constructed. + //! + //! Complexity: Constant. + key_compare key_comp() const + { return container_detail::force_copy(m_flat_tree.key_comp()); } + + //! Effects: Returns an object of value_compare constructed out + //! of the comparison object. + //! + //! Complexity: Constant. + value_compare value_comp() const + { return value_compare(container_detail::force_copy(m_flat_tree.key_comp())); } + + ////////////////////////////////////////////// + // + // map operations + // + ////////////////////////////////////////////// + + //! Returns: An iterator pointing to an element with the key + //! equivalent to x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic. + iterator find(const key_type& x) + { return container_detail::force_copy(m_flat_tree.find(x)); } + + //! Returns: A const_iterator pointing to an element with the key + //! equivalent to x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic. + const_iterator find(const key_type& x) const + { return container_detail::force_copy(m_flat_tree.find(x)); } + + //! Returns: The number of elements with key equivalent to x. + //! + //! Complexity: log(size())+count(k) + size_type count(const key_type& x) const + { return static_cast(m_flat_tree.find(x) != m_flat_tree.end()); } + + //! Returns: An iterator pointing to the first element with key not less + //! than k, or a.end() if such an element is not found. + //! + //! Complexity: Logarithmic. + iterator lower_bound(const key_type& x) + { return container_detail::force_copy(m_flat_tree.lower_bound(x)); } + + //! Returns: A const iterator pointing to the first element with key not + //! less than k, or a.end() if such an element is not found. + //! + //! Complexity: Logarithmic. + const_iterator lower_bound(const key_type& x) const + { return container_detail::force_copy(m_flat_tree.lower_bound(x)); } + + //! Returns: An iterator pointing to the first element with key not less + //! than x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic. + iterator upper_bound(const key_type& x) + { return container_detail::force_copy(m_flat_tree.upper_bound(x)); } + + //! Returns: A const iterator pointing to the first element with key not + //! less than x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic. + const_iterator upper_bound(const key_type& x) const + { return container_detail::force_copy(m_flat_tree.upper_bound(x)); } + + //! Effects: Equivalent to std::make_pair(this->lower_bound(k), this->upper_bound(k)). + //! + //! Complexity: Logarithmic. + std::pair equal_range(const key_type& x) + { return container_detail::force_copy >(m_flat_tree.lower_bound_range(x)); } + + //! Effects: Equivalent to std::make_pair(this->lower_bound(k), this->upper_bound(k)). + //! + //! Complexity: Logarithmic. + std::pair equal_range(const key_type& x) const + { return container_detail::force_copy >(m_flat_tree.lower_bound_range(x)); } + + //! Effects: Returns true if x and y are equal + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator==(const flat_map& x, const flat_map& y) + { return x.size() == y.size() && ::boost::container::algo_equal(x.begin(), x.end(), y.begin()); } + + //! Effects: Returns true if x and y are unequal + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator!=(const flat_map& x, const flat_map& y) + { return !(x == y); } + + //! Effects: Returns true if x is less than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator<(const flat_map& x, const flat_map& y) + { return ::boost::container::algo_lexicographical_compare(x.begin(), x.end(), y.begin(), y.end()); } + + //! Effects: Returns true if x is greater than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator>(const flat_map& x, const flat_map& y) + { return y < x; } + + //! Effects: Returns true if x is equal or less than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator<=(const flat_map& x, const flat_map& y) + { return !(y < x); } + + //! Effects: Returns true if x is equal or greater than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator>=(const flat_map& x, const flat_map& y) + { return !(x < y); } + + //! Effects: x.swap(y) + //! + //! Complexity: Constant. + friend void swap(flat_map& x, flat_map& y) + { x.swap(y); } + + #ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + private: + mapped_type &priv_subscript(const key_type& k) + { + iterator i = lower_bound(k); + // i->first is greater than or equivalent to k. + if (i == end() || key_comp()(k, (*i).first)){ + container_detail::value_init m; + i = insert(i, impl_value_type(k, ::boost::move(m.m_t))); + } + return (*i).second; + } + mapped_type &priv_subscript(BOOST_RV_REF(key_type) mk) + { + key_type &k = mk; + iterator i = lower_bound(k); + // i->first is greater than or equivalent to k. + if (i == end() || key_comp()(k, (*i).first)){ + container_detail::value_init m; + i = insert(i, impl_value_type(boost::move(k), ::boost::move(m.m_t))); + } + return (*i).second; + } + #endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED +}; + +#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + +} //namespace container { + +//!has_trivial_destructor_after_move<> == true_type +//!specialization for optimizations +template +struct has_trivial_destructor_after_move > +{ + typedef typename ::boost::container::allocator_traits::pointer pointer; + static const bool value = ::boost::has_trivial_destructor_after_move::value && + ::boost::has_trivial_destructor_after_move::value && + ::boost::has_trivial_destructor_after_move::value; +}; + +namespace container { + +#endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + +//! A flat_multimap is a kind of associative container that supports equivalent keys +//! (possibly containing multiple copies of the same key value) and provides for +//! fast retrieval of values of another type T based on the keys. The flat_multimap +//! class supports random-access iterators. +//! +//! A flat_multimap satisfies all of the requirements of a container and of a reversible +//! container and of an associative container. For a +//! flat_multimap the key_type is Key and the value_type is std::pair +//! (unlike std::multimap which value_type is std::pair<const Key, T>). +//! +//! Compare is the ordering function for Keys (e.g. std::less). +//! +//! Allocator is the allocator to allocate the value_types +//! (e.g. allocator< std::pair >). +//! +//! flat_multimap is similar to std::multimap but it's implemented like an ordered vector. +//! This means that inserting a new element into a flat_map invalidates +//! previous iterators and references +//! +//! Erasing an element invalidates iterators and references +//! pointing to elements that come after (their keys are bigger) the erased element. +//! +//! This container provides random-access iterators. +//! +//! \tparam Key is the key_type of the map +//! \tparam Value is the mapped_type +//! \tparam Compare is the ordering function for Keys (e.g. std::less). +//! \tparam Allocator is the allocator to allocate the value_types +//! (e.g. allocator< std::pair > ). +#ifdef BOOST_CONTAINER_DOXYGEN_INVOKED +template , class Allocator = new_allocator< std::pair< Key, T> > > +#else +template +#endif +class flat_multimap +{ + #ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + private: + BOOST_COPYABLE_AND_MOVABLE(flat_multimap) + typedef container_detail::flat_tree, + container_detail::select1st< std::pair >, + Compare, + Allocator> tree_t; + //This is the real tree stored here. It's based on a movable pair + typedef container_detail::flat_tree, + container_detail::select1st >, + Compare, + typename allocator_traits::template portable_rebind_alloc + >::type> impl_tree_t; + impl_tree_t m_flat_tree; // flat tree representing flat_map + + typedef typename impl_tree_t::value_type impl_value_type; + typedef typename impl_tree_t::const_iterator impl_const_iterator; + typedef typename impl_tree_t::iterator impl_iterator; + typedef typename impl_tree_t::allocator_type impl_allocator_type; + typedef container_detail::flat_tree_value_compare + < Compare + , container_detail::select1st< std::pair > + , std::pair > value_compare_impl; + typedef typename container_detail::get_flat_tree_iterators + ::pointer>::iterator iterator_impl; + typedef typename container_detail::get_flat_tree_iterators + ::pointer>::const_iterator const_iterator_impl; + typedef typename container_detail::get_flat_tree_iterators + ::pointer>::reverse_iterator reverse_iterator_impl; + typedef typename container_detail::get_flat_tree_iterators + ::pointer>::const_reverse_iterator const_reverse_iterator_impl; + public: + typedef typename impl_tree_t::stored_allocator_type impl_stored_allocator_type; + private: + #endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + + public: + + ////////////////////////////////////////////// + // + // types + // + ////////////////////////////////////////////// + typedef Key key_type; + typedef T mapped_type; + typedef std::pair value_type; + typedef ::boost::container::allocator_traits allocator_traits_type; + typedef typename boost::container::allocator_traits::pointer pointer; + typedef typename boost::container::allocator_traits::const_pointer const_pointer; + typedef typename boost::container::allocator_traits::reference reference; + typedef typename boost::container::allocator_traits::const_reference const_reference; + typedef typename boost::container::allocator_traits::size_type size_type; + typedef typename boost::container::allocator_traits::difference_type difference_type; + typedef Allocator allocator_type; + typedef BOOST_CONTAINER_IMPDEF(Allocator) stored_allocator_type; + typedef BOOST_CONTAINER_IMPDEF(value_compare_impl) value_compare; + typedef Compare key_compare; + typedef BOOST_CONTAINER_IMPDEF(iterator_impl) iterator; + typedef BOOST_CONTAINER_IMPDEF(const_iterator_impl) const_iterator; + typedef BOOST_CONTAINER_IMPDEF(reverse_iterator_impl) reverse_iterator; + typedef BOOST_CONTAINER_IMPDEF(const_reverse_iterator_impl) const_reverse_iterator; + typedef BOOST_CONTAINER_IMPDEF(impl_value_type) movable_value_type; + + ////////////////////////////////////////////// + // + // construct/copy/destroy + // + ////////////////////////////////////////////// + + //! Effects: Default constructs an empty flat_map. + //! + //! Complexity: Constant. + flat_multimap() + : m_flat_tree() + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_multimap using the specified comparison + //! object and allocator. + //! + //! Complexity: Constant. + explicit flat_multimap(const Compare& comp, + const allocator_type& a = allocator_type()) + : m_flat_tree(comp, container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_multimap using the specified allocator. + //! + //! Complexity: Constant. + explicit flat_multimap(const allocator_type& a) + : m_flat_tree(container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_multimap using the specified comparison object + //! and allocator, and inserts elements from the range [first ,last ). + //! + //! Complexity: Linear in N if the range [first ,last ) is already sorted using + //! comp and otherwise N logN, where N is last - first. + template + flat_multimap(InputIterator first, InputIterator last, + const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : m_flat_tree(false, first, last, comp, container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_multimap using the specified + //! allocator, and inserts elements from the range [first ,last ). + //! + //! Complexity: Linear in N if the range [first ,last ) is already sorted using + //! comp and otherwise N logN, where N is last - first. + template + flat_multimap(InputIterator first, InputIterator last, const allocator_type& a) + : m_flat_tree(false, first, last, Compare(), container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_multimap using the specified comparison object and + //! allocator, and inserts elements from the ordered range [first ,last). This function + //! is more efficient than the normal range creation for ordered ranges. + //! + //! Requires: [first ,last) must be ordered according to the predicate. + //! + //! Complexity: Linear in N. + //! + //! Note: Non-standard extension. + template + flat_multimap(ordered_range_t, InputIterator first, InputIterator last, + const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : m_flat_tree(ordered_range, first, last, comp, a) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! Effects: Constructs an empty flat_map using the specified comparison object and + //! allocator, and inserts elements from the range [il.begin(), il.end()). + //! + //! Complexity: Linear in N if the range [il.begin(), il.end()) is already sorted using + //! comp and otherwise N logN, where N is last - first. + flat_multimap(std::initializer_list il, const Compare& comp = Compare(), const allocator_type& a = allocator_type()) + : m_flat_tree(false, il.begin(), il.end(), comp, container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_map using the specified + //! allocator, and inserts elements from the range [il.begin(), il.end()). + //! + //! Complexity: Linear in N if the range [il.begin(), il.end()) is already sorted using + //! comp and otherwise N logN, where N is last - first. + flat_multimap(std::initializer_list il, const allocator_type& a) + : m_flat_tree(false, il.begin(), il.end(), Compare(), container_detail::force(a)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Constructs an empty flat_multimap using the specified comparison object and + //! allocator, and inserts elements from the ordered range [il.begin(), il.end()). This function + //! is more efficient than the normal range creation for ordered ranges. + //! + //! Requires: [il.begin(), il.end()) must be ordered according to the predicate. + //! + //! Complexity: Linear in N. + //! + //! Note: Non-standard extension. + flat_multimap(ordered_range_t, std::initializer_list il, const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : m_flat_tree(ordered_range, il.begin(), il.end(), comp, a) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } +#endif + + //! Effects: Copy constructs a flat_multimap. + //! + //! Complexity: Linear in x.size(). + flat_multimap(const flat_multimap& x) + : m_flat_tree(x.m_flat_tree) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Move constructs a flat_multimap. Constructs *this using x's resources. + //! + //! Complexity: Constant. + //! + //! Postcondition: x is emptied. + flat_multimap(BOOST_RV_REF(flat_multimap) x) + : m_flat_tree(boost::move(x.m_flat_tree)) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Copy constructs a flat_multimap using the specified allocator. + //! + //! Complexity: Linear in x.size(). + flat_multimap(const flat_multimap& x, const allocator_type &a) + : m_flat_tree(x.m_flat_tree, a) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Move constructs a flat_multimap using the specified allocator. + //! Constructs *this using x's resources. + //! + //! Complexity: Constant if a == x.get_allocator(), linear otherwise. + flat_multimap(BOOST_RV_REF(flat_multimap) x, const allocator_type &a) + : m_flat_tree(boost::move(x.m_flat_tree), a) + { + //A type must be std::pair + BOOST_STATIC_ASSERT((container_detail::is_same, typename Allocator::value_type>::value)); + } + + //! Effects: Makes *this a copy of x. + //! + //! Complexity: Linear in x.size(). + flat_multimap& operator=(BOOST_COPY_ASSIGN_REF(flat_multimap) x) + { m_flat_tree = x.m_flat_tree; return *this; } + + //! Effects: this->swap(x.get()). + //! + //! Complexity: Constant. + flat_multimap& operator=(BOOST_RV_REF(flat_multimap) x) + BOOST_NOEXCEPT_IF( allocator_traits_type::is_always_equal::value + && boost::container::container_detail::is_nothrow_move_assignable::value ) + { m_flat_tree = boost::move(x.m_flat_tree); return *this; } + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! Effects: Assign content of il to *this + //! + //! Complexity: Linear in il.size(). + flat_multimap& operator=(std::initializer_list il) + { + this->clear(); + this->insert(il.begin(), il.end()); + return *this; + } +#endif + + //! Effects: Returns a copy of the allocator that + //! was passed to the object's constructor. + //! + //! Complexity: Constant. + allocator_type get_allocator() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.get_allocator()); } + + //! Effects: Returns a reference to the internal allocator. + //! + //! Throws: Nothing + //! + //! Complexity: Constant. + //! + //! Note: Non-standard extension. + stored_allocator_type &get_stored_allocator() BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force(m_flat_tree.get_stored_allocator()); } + + //! Effects: Returns a reference to the internal allocator. + //! + //! Throws: Nothing + //! + //! Complexity: Constant. + //! + //! Note: Non-standard extension. + const stored_allocator_type &get_stored_allocator() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force(m_flat_tree.get_stored_allocator()); } + + ////////////////////////////////////////////// + // + // iterators + // + ////////////////////////////////////////////// + + //! Effects: Returns an iterator to the first element contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + iterator begin() BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.begin()); } + + //! Effects: Returns a const_iterator to the first element contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator begin() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.begin()); } + + //! Effects: Returns an iterator to the end of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + iterator end() BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.end()); } + + //! Effects: Returns a const_iterator to the end of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator end() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.end()); } + + //! Effects: Returns a reverse_iterator pointing to the beginning + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + reverse_iterator rbegin() BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.rbegin()); } + + //! Effects: Returns a const_reverse_iterator pointing to the beginning + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator rbegin() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.rbegin()); } + + //! Effects: Returns a reverse_iterator pointing to the end + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + reverse_iterator rend() BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.rend()); } + + //! Effects: Returns a const_reverse_iterator pointing to the end + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator rend() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.rend()); } + + //! Effects: Returns a const_iterator to the first element contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator cbegin() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.cbegin()); } + + //! Effects: Returns a const_iterator to the end of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator cend() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.cend()); } + + //! Effects: Returns a const_reverse_iterator pointing to the beginning + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator crbegin() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.crbegin()); } + + //! Effects: Returns a const_reverse_iterator pointing to the end + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator crend() const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.crend()); } + + ////////////////////////////////////////////// + // + // capacity + // + ////////////////////////////////////////////// + + //! Effects: Returns true if the container contains no elements. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + bool empty() const BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.empty(); } + + //! Effects: Returns the number of the elements contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + size_type size() const BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.size(); } + + //! Effects: Returns the largest possible size of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + size_type max_size() const BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.max_size(); } + + //! Effects: Number of elements for which memory has been allocated. + //! capacity() is always greater than or equal to size(). + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + size_type capacity() const BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.capacity(); } + + //! Effects: If n is less than or equal to capacity(), this call has no + //! effect. Otherwise, it is a request for allocation of additional memory. + //! If the request is successful, then capacity() is greater than or equal to + //! n; otherwise, capacity() is unchanged. In either case, size() is unchanged. + //! + //! Throws: If memory allocation allocation throws or T's copy constructor throws. + //! + //! Note: If capacity() is less than "cnt", iterators and references to + //! to values might be invalidated. + void reserve(size_type cnt) + { m_flat_tree.reserve(cnt); } + + //! Effects: Tries to deallocate the excess of memory created + // with previous allocations. The size of the vector is unchanged + //! + //! Throws: If memory allocation throws, or T's copy constructor throws. + //! + //! Complexity: Linear to size(). + void shrink_to_fit() + { m_flat_tree.shrink_to_fit(); } + + //! @copydoc ::boost::container::flat_set::nth(size_type) + iterator nth(size_type n) BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.nth(n)); } + + //! @copydoc ::boost::container::flat_set::nth(size_type) const + const_iterator nth(size_type n) const BOOST_NOEXCEPT_OR_NOTHROW + { return container_detail::force_copy(m_flat_tree.nth(n)); } + + //! @copydoc ::boost::container::flat_set::index_of(iterator) + size_type index_of(iterator p) BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.index_of(container_detail::force_copy(p)); } + + //! @copydoc ::boost::container::flat_set::index_of(const_iterator) const + size_type index_of(const_iterator p) const BOOST_NOEXCEPT_OR_NOTHROW + { return m_flat_tree.index_of(container_detail::force_copy(p)); } + + #if !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) || defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + //! Effects: Inserts an object of type T constructed with + //! std::forward(args)... and returns the iterator pointing to the + //! newly inserted element. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + template + iterator emplace(BOOST_FWD_REF(Args)... args) + { return container_detail::force_copy(m_flat_tree.emplace_equal(boost::forward(args)...)); } + + //! Effects: Inserts an object of type T constructed with + //! std::forward(args)... in the container. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent + //! to the key of x. + //! + //! Complexity: Logarithmic search time (constant time if the value + //! is to be inserted before p) plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + template + iterator emplace_hint(const_iterator hint, BOOST_FWD_REF(Args)... args) + { + return container_detail::force_copy(m_flat_tree.emplace_hint_equal + (container_detail::force_copy(hint), boost::forward(args)...)); + } + + #else // !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) + + #define BOOST_CONTAINER_FLAT_MULTIMAP_EMPLACE_CODE(N) \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + iterator emplace(BOOST_MOVE_UREF##N)\ + { return container_detail::force_copy(m_flat_tree.emplace_equal(BOOST_MOVE_FWD##N)); }\ + \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + iterator emplace_hint(const_iterator hint BOOST_MOVE_I##N BOOST_MOVE_UREF##N)\ + {\ + return container_detail::force_copy(m_flat_tree.emplace_hint_equal\ + (container_detail::force_copy(hint) BOOST_MOVE_I##N BOOST_MOVE_FWD##N));\ + }\ + // + BOOST_MOVE_ITERATE_0TO9(BOOST_CONTAINER_FLAT_MULTIMAP_EMPLACE_CODE) + #undef BOOST_CONTAINER_FLAT_MULTIMAP_EMPLACE_CODE + + #endif // !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) + + //! Effects: Inserts x and returns the iterator pointing to the + //! newly inserted element. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const value_type& x) + { + return container_detail::force_copy( + m_flat_tree.insert_equal(container_detail::force(x))); + } + + //! Effects: Inserts a new value move-constructed from x and returns + //! the iterator pointing to the newly inserted element. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(BOOST_RV_REF(value_type) x) + { return container_detail::force_copy(m_flat_tree.insert_equal(boost::move(x))); } + + //! Effects: Inserts a new value move-constructed from x and returns + //! the iterator pointing to the newly inserted element. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(BOOST_RV_REF(impl_value_type) x) + { return container_detail::force_copy(m_flat_tree.insert_equal(boost::move(x))); } + + //! Effects: Inserts a copy of x in the container. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent + //! to the key of x. + //! + //! Complexity: Logarithmic search time (constant time if the value + //! is to be inserted before p) plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const_iterator p, const value_type& x) + { + return container_detail::force_copy + (m_flat_tree.insert_equal( container_detail::force_copy(p) + , container_detail::force(x))); + } + + //! Effects: Inserts a value move constructed from x in the container. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent + //! to the key of x. + //! + //! Complexity: Logarithmic search time (constant time if the value + //! is to be inserted before p) plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const_iterator p, BOOST_RV_REF(value_type) x) + { + return container_detail::force_copy + (m_flat_tree.insert_equal(container_detail::force_copy(p) + , boost::move(x))); + } + + //! Effects: Inserts a value move constructed from x in the container. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent + //! to the key of x. + //! + //! Complexity: Logarithmic search time (constant time if the value + //! is to be inserted before p) plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const_iterator p, BOOST_RV_REF(impl_value_type) x) + { + return container_detail::force_copy( + m_flat_tree.insert_equal(container_detail::force_copy(p), boost::move(x))); + } + + //! Requires: first, last are not iterators into *this. + //! + //! Effects: inserts each element from the range [first,last) . + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + template + void insert(InputIterator first, InputIterator last) + { m_flat_tree.insert_equal(first, last); } + + //! Requires: first, last are not iterators into *this. + //! + //! Requires: [first ,last) must be ordered according to the predicate. + //! + //! Effects: inserts each element from the range [first,last) if and only + //! if there is no element with key equivalent to the key of that element. This + //! function is more efficient than the normal range creation for ordered ranges. + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + //! + //! Note: Non-standard extension. + template + void insert(ordered_range_t, InputIterator first, InputIterator last) + { m_flat_tree.insert_equal(ordered_range, first, last); } + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! Effects: inserts each element from the range [il.begin(), il.end()) . + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + void insert(std::initializer_list il) + { m_flat_tree.insert_equal(il.begin(), il.end()); } + + //! Requires: [il.begin(), il.end()) must be ordered according to the predicate. + //! + //! Effects: inserts each element from the range [il.begin(), il.end()) if and only + //! if there is no element with key equivalent to the key of that element. This + //! function is more efficient than the normal range creation for ordered ranges. + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + //! + //! Note: Non-standard extension. + void insert(ordered_range_t, std::initializer_list il) + { m_flat_tree.insert_equal(ordered_range, il.begin(), il.end()); } +#endif + + //! Effects: Erases the element pointed to by p. + //! + //! Returns: Returns an iterator pointing to the element immediately + //! following q prior to the element being erased. If no such element exists, + //! returns end(). + //! + //! Complexity: Linear to the elements with keys bigger than p + //! + //! Note: Invalidates elements with keys + //! not less than the erased element. + iterator erase(const_iterator p) + { + return container_detail::force_copy( + m_flat_tree.erase(container_detail::force_copy(p))); + } + + //! Effects: Erases all elements in the container with key equivalent to x. + //! + //! Returns: Returns the number of erased elements. + //! + //! Complexity: Logarithmic search time plus erasure time + //! linear to the elements with bigger keys. + size_type erase(const key_type& x) + { return m_flat_tree.erase(x); } + + //! Effects: Erases all the elements in the range [first, last). + //! + //! Returns: Returns last. + //! + //! Complexity: size()*N where N is the distance from first to last. + //! + //! Complexity: Logarithmic search time plus erasure time + //! linear to the elements with bigger keys. + iterator erase(const_iterator first, const_iterator last) + { + return container_detail::force_copy + (m_flat_tree.erase( container_detail::force_copy(first) + , container_detail::force_copy(last))); + } + + //! Effects: Swaps the contents of *this and x. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + void swap(flat_multimap& x) + BOOST_NOEXCEPT_IF( allocator_traits_type::is_always_equal::value + && boost::container::container_detail::is_nothrow_swappable::value ) + { m_flat_tree.swap(x.m_flat_tree); } + + //! Effects: erase(a.begin(),a.end()). + //! + //! Postcondition: size() == 0. + //! + //! Complexity: linear in size(). + void clear() BOOST_NOEXCEPT_OR_NOTHROW + { m_flat_tree.clear(); } + + ////////////////////////////////////////////// + // + // observers + // + ////////////////////////////////////////////// + + //! Effects: Returns the comparison object out + //! of which a was constructed. + //! + //! Complexity: Constant. + key_compare key_comp() const + { return container_detail::force_copy(m_flat_tree.key_comp()); } + + //! Effects: Returns an object of value_compare constructed out + //! of the comparison object. + //! + //! Complexity: Constant. + value_compare value_comp() const + { return value_compare(container_detail::force_copy(m_flat_tree.key_comp())); } + + ////////////////////////////////////////////// + // + // map operations + // + ////////////////////////////////////////////// + + //! Returns: An iterator pointing to an element with the key + //! equivalent to x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic. + iterator find(const key_type& x) + { return container_detail::force_copy(m_flat_tree.find(x)); } + + //! Returns: An const_iterator pointing to an element with the key + //! equivalent to x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic. + const_iterator find(const key_type& x) const + { return container_detail::force_copy(m_flat_tree.find(x)); } + + //! Returns: The number of elements with key equivalent to x. + //! + //! Complexity: log(size())+count(k) + size_type count(const key_type& x) const + { return m_flat_tree.count(x); } + + //! Returns: An iterator pointing to the first element with key not less + //! than k, or a.end() if such an element is not found. + //! + //! Complexity: Logarithmic + iterator lower_bound(const key_type& x) + { return container_detail::force_copy(m_flat_tree.lower_bound(x)); } + + //! Returns: A const iterator pointing to the first element with key + //! not less than k, or a.end() if such an element is not found. + //! + //! Complexity: Logarithmic + const_iterator lower_bound(const key_type& x) const + { return container_detail::force_copy(m_flat_tree.lower_bound(x)); } + + //! Returns: An iterator pointing to the first element with key not less + //! than x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic + iterator upper_bound(const key_type& x) + {return container_detail::force_copy(m_flat_tree.upper_bound(x)); } + + //! Returns: A const iterator pointing to the first element with key + //! not less than x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic + const_iterator upper_bound(const key_type& x) const + { return container_detail::force_copy(m_flat_tree.upper_bound(x)); } + + //! Effects: Equivalent to std::make_pair(this->lower_bound(k), this->upper_bound(k)). + //! + //! Complexity: Logarithmic + std::pair equal_range(const key_type& x) + { return container_detail::force_copy >(m_flat_tree.equal_range(x)); } + + //! Effects: Equivalent to std::make_pair(this->lower_bound(k), this->upper_bound(k)). + //! + //! Complexity: Logarithmic + std::pair equal_range(const key_type& x) const + { return container_detail::force_copy >(m_flat_tree.equal_range(x)); } + + //! Effects: Returns true if x and y are equal + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator==(const flat_multimap& x, const flat_multimap& y) + { return x.size() == y.size() && ::boost::container::algo_equal(x.begin(), x.end(), y.begin()); } + + //! Effects: Returns true if x and y are unequal + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator!=(const flat_multimap& x, const flat_multimap& y) + { return !(x == y); } + + //! Effects: Returns true if x is less than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator<(const flat_multimap& x, const flat_multimap& y) + { return ::boost::container::algo_lexicographical_compare(x.begin(), x.end(), y.begin(), y.end()); } + + //! Effects: Returns true if x is greater than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator>(const flat_multimap& x, const flat_multimap& y) + { return y < x; } + + //! Effects: Returns true if x is equal or less than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator<=(const flat_multimap& x, const flat_multimap& y) + { return !(y < x); } + + //! Effects: Returns true if x is equal or greater than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator>=(const flat_multimap& x, const flat_multimap& y) + { return !(x < y); } + + //! Effects: x.swap(y) + //! + //! Complexity: Constant. + friend void swap(flat_multimap& x, flat_multimap& y) + { x.swap(y); } +}; + +}} + +#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + +namespace boost { + +//!has_trivial_destructor_after_move<> == true_type +//!specialization for optimizations +template +struct has_trivial_destructor_after_move< boost::container::flat_multimap > +{ + typedef typename ::boost::container::allocator_traits::pointer pointer; + static const bool value = ::boost::has_trivial_destructor_after_move::value && + ::boost::has_trivial_destructor_after_move::value && + ::boost::has_trivial_destructor_after_move::value; +}; + +} //namespace boost { + +#endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + +#include + +#endif // BOOST_CONTAINER_FLAT_MAP_HPP Index: libraries/win32/boost/include/boost/container/flat_set.hpp =================================================================== --- /dev/null +++ libraries/win32/boost/include/boost/container/flat_set.hpp @@ -0,0 +1,1359 @@ +////////////////////////////////////////////////////////////////////////////// +// +// (C) Copyright Ion Gaztanaga 2005-2013. Distributed under the Boost +// Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// See http://www.boost.org/libs/container for documentation. +// +////////////////////////////////////////////////////////////////////////////// +#ifndef BOOST_CONTAINER_FLAT_SET_HPP +#define BOOST_CONTAINER_FLAT_SET_HPP + +#ifndef BOOST_CONFIG_HPP +# include +#endif + +#if defined(BOOST_HAS_PRAGMA_ONCE) +# pragma once +#endif + +#include +#include + +// container +#include +#include +#include //new_allocator +// container/detail +#include +#include +// move +#include +#include +// move/detail +#if defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) +#include +#endif +#include +// intrusive/detail +#include //pair +#include //less, equal +// std +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) +#include +#endif + +namespace boost { +namespace container { + +//! flat_set is a Sorted Associative Container that stores objects of type Key. +//! It is also a Unique Associative Container, meaning that no two elements are the same. +//! +//! flat_set is similar to std::set but it's implemented like an ordered vector. +//! This means that inserting a new element into a flat_set invalidates +//! previous iterators and references +//! +//! Erasing an element of a flat_set invalidates iterators and references +//! pointing to elements that come after (their keys are bigger) the erased element. +//! +//! This container provides random-access iterators. +//! +//! \tparam Key is the type to be inserted in the set, which is also the key_type +//! \tparam Compare is the comparison functor used to order keys +//! \tparam Allocator is the allocator to be used to allocate memory for this container +#ifdef BOOST_CONTAINER_DOXYGEN_INVOKED +template , class Allocator = new_allocator > +#else +template +#endif +class flat_set + ///@cond + : public container_detail::flat_tree, Compare, Allocator> + ///@endcond +{ + #ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + private: + BOOST_COPYABLE_AND_MOVABLE(flat_set) + typedef container_detail::flat_tree, Compare, Allocator> base_t; + #endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + + public: + ////////////////////////////////////////////// + // + // types + // + ////////////////////////////////////////////// + typedef Key key_type; + typedef Key value_type; + typedef Compare key_compare; + typedef Compare value_compare; + typedef ::boost::container::allocator_traits allocator_traits_type; + typedef typename ::boost::container::allocator_traits::pointer pointer; + typedef typename ::boost::container::allocator_traits::const_pointer const_pointer; + typedef typename ::boost::container::allocator_traits::reference reference; + typedef typename ::boost::container::allocator_traits::const_reference const_reference; + typedef typename ::boost::container::allocator_traits::size_type size_type; + typedef typename ::boost::container::allocator_traits::difference_type difference_type; + typedef Allocator allocator_type; + typedef typename BOOST_CONTAINER_IMPDEF(base_t::stored_allocator_type) stored_allocator_type; + typedef typename BOOST_CONTAINER_IMPDEF(base_t::iterator) iterator; + typedef typename BOOST_CONTAINER_IMPDEF(base_t::const_iterator) const_iterator; + typedef typename BOOST_CONTAINER_IMPDEF(base_t::reverse_iterator) reverse_iterator; + typedef typename BOOST_CONTAINER_IMPDEF(base_t::const_reverse_iterator) const_reverse_iterator; + + public: + ////////////////////////////////////////////// + // + // construct/copy/destroy + // + ////////////////////////////////////////////// + + //! Effects: Default constructs an empty container. + //! + //! Complexity: Constant. + explicit flat_set() + : base_t() + {} + + //! Effects: Constructs an empty container using the specified + //! comparison object and allocator. + //! + //! Complexity: Constant. + explicit flat_set(const Compare& comp, + const allocator_type& a = allocator_type()) + : base_t(comp, a) + {} + + //! Effects: Constructs an empty container using the specified allocator. + //! + //! Complexity: Constant. + explicit flat_set(const allocator_type& a) + : base_t(a) + {} + + //! Effects: Constructs an empty container using the specified comparison object and + //! allocator, and inserts elements from the range [first ,last ). + //! + //! Complexity: Linear in N if the range [first ,last ) is already sorted using + //! comp and otherwise N logN, where N is last - first. + template + flat_set(InputIterator first, InputIterator last, + const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : base_t(true, first, last, comp, a) + {} + + //! Effects: Constructs an empty container using the specified + //! allocator, and inserts elements from the range [first ,last ). + //! + //! Complexity: Linear in N if the range [first ,last ) is already sorted using + //! comp and otherwise N logN, where N is last - first. + template + flat_set(InputIterator first, InputIterator last, const allocator_type& a) + : base_t(true, first, last, Compare(), a) + {} + + //! Effects: Constructs an empty container using the specified comparison object and + //! allocator, and inserts elements from the ordered unique range [first ,last). This function + //! is more efficient than the normal range creation for ordered ranges. + //! + //! Requires: [first ,last) must be ordered according to the predicate and must be + //! unique values. + //! + //! Complexity: Linear in N. + //! + //! Note: Non-standard extension. + template + flat_set(ordered_unique_range_t, InputIterator first, InputIterator last, + const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : base_t(ordered_range, first, last, comp, a) + {} + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! Effects: Constructs an empty container using the specified comparison object and + //! allocator, and inserts elements from the range [il.begin(), il.end()). + //! + //! Complexity: Linear in N if the range [il.begin(), il.end()) is already sorted using + //! comp and otherwise N logN, where N is il.begin() - il.end(). + flat_set(std::initializer_list il, const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : base_t(true, il.begin(), il.end(), comp, a) + {} + + //! Effects: Constructs an empty container using the specified + //! allocator, and inserts elements from the range [il.begin(), il.end()). + //! + //! Complexity: Linear in N if the range [il.begin(), il.end()) is already sorted using + //! comp and otherwise N logN, where N is il.begin() - il.end(). + flat_set(std::initializer_list il, const allocator_type& a) + : base_t(true, il.begin(), il.end(), Compare(), a) + {} + + //! Effects: Constructs an empty container using the specified comparison object and + //! allocator, and inserts elements from the ordered unique range [il.begin(), il.end()). This function + //! is more efficient than the normal range creation for ordered ranges. + //! + //! Requires: [il.begin(), il.end()) must be ordered according to the predicate and must be + //! unique values. + //! + //! Complexity: Linear in N. + //! + //! Note: Non-standard extension. + flat_set(ordered_unique_range_t, std::initializer_list il, + const Compare& comp = Compare(), const allocator_type& a = allocator_type()) + : base_t(ordered_range, il.begin(), il.end(), comp, a) + {} +#endif + + //! Effects: Copy constructs the container. + //! + //! Complexity: Linear in x.size(). + flat_set(const flat_set& x) + : base_t(static_cast(x)) + {} + + //! Effects: Move constructs thecontainer. Constructs *this using x's resources. + //! + //! Complexity: Constant. + //! + //! Postcondition: x is emptied. + flat_set(BOOST_RV_REF(flat_set) x) + : base_t(BOOST_MOVE_BASE(base_t, x)) + {} + + //! Effects: Copy constructs a container using the specified allocator. + //! + //! Complexity: Linear in x.size(). + flat_set(const flat_set& x, const allocator_type &a) + : base_t(static_cast(x), a) + {} + + //! Effects: Move constructs a container using the specified allocator. + //! Constructs *this using x's resources. + //! + //! Complexity: Constant if a == x.get_allocator(), linear otherwise + flat_set(BOOST_RV_REF(flat_set) x, const allocator_type &a) + : base_t(BOOST_MOVE_BASE(base_t, x), a) + {} + + //! Effects: Makes *this a copy of x. + //! + //! Complexity: Linear in x.size(). + flat_set& operator=(BOOST_COPY_ASSIGN_REF(flat_set) x) + { return static_cast(this->base_t::operator=(static_cast(x))); } + + //! Throws: If allocator_traits_type::propagate_on_container_move_assignment + //! is false and (allocation throws or value_type's move constructor throws) + //! + //! Complexity: Constant if allocator_traits_type:: + //! propagate_on_container_move_assignment is true or + //! this->get>allocator() == x.get_allocator(). Linear otherwise. + flat_set& operator=(BOOST_RV_REF(flat_set) x) + BOOST_NOEXCEPT_IF( allocator_traits_type::is_always_equal::value + && boost::container::container_detail::is_nothrow_move_assignable::value ) + { return static_cast(this->base_t::operator=(BOOST_MOVE_BASE(base_t, x))); } + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! Effects: Copy all elements from il to *this. + //! + //! Complexity: Linear in il.size(). + flat_set& operator=(std::initializer_list il) + { + this->clear(); + this->insert(il.begin(), il.end()); + return *this; + } +#endif + + #ifdef BOOST_CONTAINER_DOXYGEN_INVOKED + //! Effects: Returns a copy of the allocator that + //! was passed to the object's constructor. + //! + //! Complexity: Constant. + allocator_type get_allocator() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a reference to the internal allocator. + //! + //! Throws: Nothing + //! + //! Complexity: Constant. + //! + //! Note: Non-standard extension. + stored_allocator_type &get_stored_allocator() BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a reference to the internal allocator. + //! + //! Throws: Nothing + //! + //! Complexity: Constant. + //! + //! Note: Non-standard extension. + const stored_allocator_type &get_stored_allocator() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns an iterator to the first element contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + iterator begin() BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a const_iterator to the first element contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator begin() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns an iterator to the end of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + iterator end() BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a const_iterator to the end of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator end() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a reverse_iterator pointing to the beginning + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + reverse_iterator rbegin() BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a const_reverse_iterator pointing to the beginning + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator rbegin() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a reverse_iterator pointing to the end + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + reverse_iterator rend() BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a const_reverse_iterator pointing to the end + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator rend() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a const_iterator to the first element contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator cbegin() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a const_iterator to the end of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_iterator cend() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a const_reverse_iterator pointing to the beginning + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator crbegin() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns a const_reverse_iterator pointing to the end + //! of the reversed container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + const_reverse_iterator crend() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns true if the container contains no elements. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + bool empty() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns the number of the elements contained in the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + size_type size() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns the largest possible size of the container. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + size_type max_size() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Number of elements for which memory has been allocated. + //! capacity() is always greater than or equal to size(). + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + size_type capacity() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: If n is less than or equal to capacity(), this call has no + //! effect. Otherwise, it is a request for allocation of additional memory. + //! If the request is successful, then capacity() is greater than or equal to + //! n; otherwise, capacity() is unchanged. In either case, size() is unchanged. + //! + //! Throws: If memory allocation allocation throws or Key's copy constructor throws. + //! + //! Note: If capacity() is less than "cnt", iterators and references to + //! to values might be invalidated. + void reserve(size_type cnt); + + //! Effects: Tries to deallocate the excess of memory created + // with previous allocations. The size of the vector is unchanged + //! + //! Throws: If memory allocation throws, or Key's copy constructor throws. + //! + //! Complexity: Linear to size(). + void shrink_to_fit(); + + #endif // #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + ////////////////////////////////////////////// + // + // modifiers + // + ////////////////////////////////////////////// + + #if !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) || defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + //! Effects: Inserts an object x of type Key constructed with + //! std::forward(args)... if and only if there is no element in the container + //! with key equivalent to the key of x. + //! + //! Returns: The bool component of the returned pair is true if and only + //! if the insertion takes place, and the iterator component of the pair + //! points to the element with key equivalent to the key of x. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + template + std::pair emplace(BOOST_FWD_REF(Args)... args) + { return this->base_t::emplace_unique(boost::forward(args)...); } + + //! Effects: Inserts an object of type Key constructed with + //! std::forward(args)... in the container if and only if there is + //! no element in the container with key equivalent to the key of x. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent + //! to the key of x. + //! + //! Complexity: Logarithmic search time (constant if x is inserted + //! right before p) plus insertion linear to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + template + iterator emplace_hint(const_iterator p, BOOST_FWD_REF(Args)... args) + { return this->base_t::emplace_hint_unique(p, boost::forward(args)...); } + + #else // !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) + + #define BOOST_CONTAINER_FLAT_SET_EMPLACE_CODE(N) \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + std::pair emplace(BOOST_MOVE_UREF##N)\ + { return this->base_t::emplace_unique(BOOST_MOVE_FWD##N); }\ + \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + iterator emplace_hint(const_iterator hint BOOST_MOVE_I##N BOOST_MOVE_UREF##N)\ + { return this->base_t::emplace_hint_unique(hint BOOST_MOVE_I##N BOOST_MOVE_FWD##N); }\ + // + BOOST_MOVE_ITERATE_0TO9(BOOST_CONTAINER_FLAT_SET_EMPLACE_CODE) + #undef BOOST_CONTAINER_FLAT_SET_EMPLACE_CODE + + #endif // !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) + + #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + //! Effects: Inserts x if and only if there is no element in the container + //! with key equivalent to the key of x. + //! + //! Returns: The bool component of the returned pair is true if and only + //! if the insertion takes place, and the iterator component of the pair + //! points to the element with key equivalent to the key of x. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + std::pair insert(const value_type &x); + + //! Effects: Inserts a new value_type move constructed from the pair if and + //! only if there is no element in the container with key equivalent to the key of x. + //! + //! Returns: The bool component of the returned pair is true if and only + //! if the insertion takes place, and the iterator component of the pair + //! points to the element with key equivalent to the key of x. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + std::pair insert(value_type &&x); + #else + private: + typedef std::pair insert_return_pair; + public: + BOOST_MOVE_CONVERSION_AWARE_CATCH(insert, value_type, insert_return_pair, this->priv_insert) + #endif + + #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + //! Effects: Inserts a copy of x in the container if and only if there is + //! no element in the container with key equivalent to the key of x. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent + //! to the key of x. + //! + //! Complexity: Logarithmic search time (constant if x is inserted + //! right before p) plus insertion linear to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const_iterator p, const value_type &x); + + //! Effects: Inserts an element move constructed from x in the container. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent to the key of x. + //! + //! Complexity: Logarithmic search time (constant if x is inserted + //! right before p) plus insertion linear to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const_iterator p, value_type &&x); + #else + BOOST_MOVE_CONVERSION_AWARE_CATCH_1ARG(insert, value_type, iterator, this->priv_insert, const_iterator, const_iterator) + #endif + + //! Requires: first, last are not iterators into *this. + //! + //! Effects: inserts each element from the range [first,last) if and only + //! if there is no element with key equivalent to the key of that element. + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + template + void insert(InputIterator first, InputIterator last) + { this->base_t::insert_unique(first, last); } + + //! Requires: first, last are not iterators into *this and + //! must be ordered according to the predicate and must be + //! unique values. + //! + //! Effects: inserts each element from the range [first,last) .This function + //! is more efficient than the normal range creation for ordered ranges. + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: Non-standard extension. If an element is inserted it might invalidate elements. + template + void insert(ordered_unique_range_t, InputIterator first, InputIterator last) + { this->base_t::insert_unique(ordered_unique_range, first, last); } + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! Effects: inserts each element from the range [il.begin(), il.end()) if and only + //! if there is no element with key equivalent to the key of that element. + //! + //! Complexity: At most N log(size()+N) (N is the distance from il.begin() to il.end()) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + void insert(std::initializer_list il) + { this->base_t::insert_unique(il.begin(), il.end()); } + + //! Requires: Range [il.begin(), il.end()) must be ordered according to the predicate + //! and must be unique values. + //! + //! Effects: inserts each element from the range [il.begin(), il.end()) .This function + //! is more efficient than the normal range creation for ordered ranges. + //! + //! Complexity: At most N log(size()+N) (N is the distance from il.begin() to il.end()) + //! search time plus N*size() insertion time. + //! + //! Note: Non-standard extension. If an element is inserted it might invalidate elements. + void insert(ordered_unique_range_t, std::initializer_list il) + { this->base_t::insert_unique(ordered_unique_range, il.begin(), il.end()); } +#endif + + #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + //! Effects: Erases the element pointed to by p. + //! + //! Returns: Returns an iterator pointing to the element immediately + //! following q prior to the element being erased. If no such element exists, + //! returns end(). + //! + //! Complexity: Linear to the elements with keys bigger than p + //! + //! Note: Invalidates elements with keys + //! not less than the erased element. + iterator erase(const_iterator p); + + //! Effects: Erases all elements in the container with key equivalent to x. + //! + //! Returns: Returns the number of erased elements. + //! + //! Complexity: Logarithmic search time plus erasure time + //! linear to the elements with bigger keys. + size_type erase(const key_type& x); + + //! Effects: Erases all the elements in the range [first, last). + //! + //! Returns: Returns last. + //! + //! Complexity: size()*N where N is the distance from first to last. + //! + //! Complexity: Logarithmic search time plus erasure time + //! linear to the elements with bigger keys. + iterator erase(const_iterator first, const_iterator last); + + //! Effects: Swaps the contents of *this and x. + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + void swap(flat_set& x) + BOOST_NOEXCEPT_IF( allocator_traits_type::is_always_equal::value + && boost::container::container_detail::is_nothrow_swappable::value ); + + //! Effects: erase(a.begin(),a.end()). + //! + //! Postcondition: size() == 0. + //! + //! Complexity: linear in size(). + void clear() BOOST_NOEXCEPT_OR_NOTHROW; + + //! Effects: Returns the comparison object out + //! of which a was constructed. + //! + //! Complexity: Constant. + key_compare key_comp() const; + + //! Effects: Returns an object of value_compare constructed out + //! of the comparison object. + //! + //! Complexity: Constant. + value_compare value_comp() const; + + //! Returns: An iterator pointing to an element with the key + //! equivalent to x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic. + iterator find(const key_type& x); + + //! Returns: A const_iterator pointing to an element with the key + //! equivalent to x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic. + const_iterator find(const key_type& x) const; + + //! Requires: size() >= n. + //! + //! Effects: Returns an iterator to the nth element + //! from the beginning of the container. Returns end() + //! if n == size(). + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + //! + //! Note: Non-standard extension + iterator nth(size_type n) BOOST_NOEXCEPT_OR_NOTHROW; + + //! Requires: size() >= n. + //! + //! Effects: Returns a const_iterator to the nth element + //! from the beginning of the container. Returns end() + //! if n == size(). + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + //! + //! Note: Non-standard extension + const_iterator nth(size_type n) const BOOST_NOEXCEPT_OR_NOTHROW; + + //! Requires: size() >= n. + //! + //! Effects: Returns an iterator to the nth element + //! from the beginning of the container. Returns end() + //! if n == size(). + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + //! + //! Note: Non-standard extension + size_type index_of(iterator p) BOOST_NOEXCEPT_OR_NOTHROW; + + //! Requires: begin() <= p <= end(). + //! + //! Effects: Returns the index of the element pointed by p + //! and size() if p == end(). + //! + //! Throws: Nothing. + //! + //! Complexity: Constant. + //! + //! Note: Non-standard extension + size_type index_of(const_iterator p) const BOOST_NOEXCEPT_OR_NOTHROW; + + #endif // #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + //! Returns: The number of elements with key equivalent to x. + //! + //! Complexity: log(size())+count(k) + size_type count(const key_type& x) const + { return static_cast(this->base_t::find(x) != this->base_t::cend()); } + + #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + //! Returns: An iterator pointing to the first element with key not less + //! than k, or a.end() if such an element is not found. + //! + //! Complexity: Logarithmic + iterator lower_bound(const key_type& x); + + //! Returns: A const iterator pointing to the first element with key not + //! less than k, or a.end() if such an element is not found. + //! + //! Complexity: Logarithmic + const_iterator lower_bound(const key_type& x) const; + + //! Returns: An iterator pointing to the first element with key not less + //! than x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic + iterator upper_bound(const key_type& x); + + //! Returns: A const iterator pointing to the first element with key not + //! less than x, or end() if such an element is not found. + //! + //! Complexity: Logarithmic + const_iterator upper_bound(const key_type& x) const; + + #endif // #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + //! Effects: Equivalent to std::make_pair(this->lower_bound(k), this->upper_bound(k)). + //! + //! Complexity: Logarithmic + std::pair equal_range(const key_type& x) const + { return this->base_t::lower_bound_range(x); } + + //! Effects: Equivalent to std::make_pair(this->lower_bound(k), this->upper_bound(k)). + //! + //! Complexity: Logarithmic + std::pair equal_range(const key_type& x) + { return this->base_t::lower_bound_range(x); } + + #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + //! Effects: Returns true if x and y are equal + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator==(const flat_set& x, const flat_set& y); + + //! Effects: Returns true if x and y are unequal + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator!=(const flat_set& x, const flat_set& y); + + //! Effects: Returns true if x is less than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator<(const flat_set& x, const flat_set& y); + + //! Effects: Returns true if x is greater than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator>(const flat_set& x, const flat_set& y); + + //! Effects: Returns true if x is equal or less than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator<=(const flat_set& x, const flat_set& y); + + //! Effects: Returns true if x is equal or greater than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator>=(const flat_set& x, const flat_set& y); + + //! Effects: x.swap(y) + //! + //! Complexity: Constant. + friend void swap(flat_set& x, flat_set& y); + + #endif //#ifdef BOOST_CONTAINER_DOXYGEN_INVOKED + + #ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + private: + template + std::pair priv_insert(BOOST_FWD_REF(KeyType) x) + { return this->base_t::insert_unique(::boost::forward(x)); } + + template + iterator priv_insert(const_iterator p, BOOST_FWD_REF(KeyType) x) + { return this->base_t::insert_unique(p, ::boost::forward(x)); } + #endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED +}; + +#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + +} //namespace container { + +//!has_trivial_destructor_after_move<> == true_type +//!specialization for optimizations +template +struct has_trivial_destructor_after_move > +{ + typedef typename ::boost::container::allocator_traits::pointer pointer; + static const bool value = ::boost::has_trivial_destructor_after_move::value && + ::boost::has_trivial_destructor_after_move::value && + ::boost::has_trivial_destructor_after_move::value; +}; + +namespace container { + +#endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + +//! flat_multiset is a Sorted Associative Container that stores objects of type Key. +//! +//! flat_multiset can store multiple copies of the same key value. +//! +//! flat_multiset is similar to std::multiset but it's implemented like an ordered vector. +//! This means that inserting a new element into a flat_multiset invalidates +//! previous iterators and references +//! +//! Erasing an element invalidates iterators and references +//! pointing to elements that come after (their keys are bigger) the erased element. +//! +//! This container provides random-access iterators. +//! +//! \tparam Key is the type to be inserted in the multiset, which is also the key_type +//! \tparam Compare is the comparison functor used to order keys +//! \tparam Allocator is the allocator to be used to allocate memory for this container +#ifdef BOOST_CONTAINER_DOXYGEN_INVOKED +template , class Allocator = new_allocator > +#else +template +#endif +class flat_multiset + ///@cond + : public container_detail::flat_tree, Compare, Allocator> + ///@endcond +{ + #ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + private: + BOOST_COPYABLE_AND_MOVABLE(flat_multiset) + typedef container_detail::flat_tree, Compare, Allocator> base_t; + #endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + + public: + ////////////////////////////////////////////// + // + // types + // + ////////////////////////////////////////////// + typedef Key key_type; + typedef Key value_type; + typedef Compare key_compare; + typedef Compare value_compare; + typedef ::boost::container::allocator_traits allocator_traits_type; + typedef typename ::boost::container::allocator_traits::pointer pointer; + typedef typename ::boost::container::allocator_traits::const_pointer const_pointer; + typedef typename ::boost::container::allocator_traits::reference reference; + typedef typename ::boost::container::allocator_traits::const_reference const_reference; + typedef typename ::boost::container::allocator_traits::size_type size_type; + typedef typename ::boost::container::allocator_traits::difference_type difference_type; + typedef Allocator allocator_type; + typedef typename BOOST_CONTAINER_IMPDEF(base_t::stored_allocator_type) stored_allocator_type; + typedef typename BOOST_CONTAINER_IMPDEF(base_t::iterator) iterator; + typedef typename BOOST_CONTAINER_IMPDEF(base_t::const_iterator) const_iterator; + typedef typename BOOST_CONTAINER_IMPDEF(base_t::reverse_iterator) reverse_iterator; + typedef typename BOOST_CONTAINER_IMPDEF(base_t::const_reverse_iterator) const_reverse_iterator; + + //! @copydoc ::boost::container::flat_set::flat_set() + explicit flat_multiset() + : base_t() + {} + + //! @copydoc ::boost::container::flat_set::flat_set(const Compare&, const allocator_type&) + explicit flat_multiset(const Compare& comp, + const allocator_type& a = allocator_type()) + : base_t(comp, a) + {} + + //! @copydoc ::boost::container::flat_set::flat_set(const allocator_type&) + explicit flat_multiset(const allocator_type& a) + : base_t(a) + {} + + //! @copydoc ::boost::container::flat_set::flat_set(InputIterator, InputIterator, const Compare& comp, const allocator_type&) + template + flat_multiset(InputIterator first, InputIterator last, + const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : base_t(false, first, last, comp, a) + {} + + //! @copydoc ::boost::container::flat_set::flat_set(InputIterator, InputIterator, const allocator_type&) + template + flat_multiset(InputIterator first, InputIterator last, const allocator_type& a) + : base_t(false, first, last, Compare(), a) + {} + + //! Effects: Constructs an empty flat_multiset using the specified comparison object and + //! allocator, and inserts elements from the ordered range [first ,last ). This function + //! is more efficient than the normal range creation for ordered ranges. + //! + //! Requires: [first ,last) must be ordered according to the predicate. + //! + //! Complexity: Linear in N. + //! + //! Note: Non-standard extension. + template + flat_multiset(ordered_range_t, InputIterator first, InputIterator last, + const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : base_t(ordered_range, first, last, comp, a) + {} + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! @copydoc ::boost::container::flat_set::flat_set(std::initializer_list, const Compare& comp, const allocator_type&) + flat_multiset(std::initializer_list il, const Compare& comp = Compare(), + const allocator_type& a = allocator_type()) + : base_t(false, il.begin(), il.end(), comp, a) + {} + + //! @copydoc ::boost::container::flat_set::flat_set(std::initializer_list, const allocator_type&) + flat_multiset(std::initializer_list il, const allocator_type& a) + : base_t(false, il.begin(), il.end(), Compare(), a) + {} + + //! @copydoc ::boost::container::flat_set::flat_set(ordered_unique_range_t, std::initializer_list, const Compare& comp, const allocator_type&) + flat_multiset(ordered_unique_range_t, std::initializer_list il, + const Compare& comp = Compare(), const allocator_type& a = allocator_type()) + : base_t(ordered_range, il.begin(), il.end(), comp, a) + {} +#endif + + //! @copydoc ::boost::container::flat_set::flat_set(const flat_set &) + flat_multiset(const flat_multiset& x) + : base_t(static_cast(x)) + {} + + //! @copydoc ::boost::container::flat_set::flat_set(flat_set &&) + flat_multiset(BOOST_RV_REF(flat_multiset) x) + : base_t(boost::move(static_cast(x))) + {} + + //! @copydoc ::boost::container::flat_set::flat_set(const flat_set &, const allocator_type &) + flat_multiset(const flat_multiset& x, const allocator_type &a) + : base_t(static_cast(x), a) + {} + + //! @copydoc ::boost::container::flat_set::flat_set(flat_set &&, const allocator_type &) + flat_multiset(BOOST_RV_REF(flat_multiset) x, const allocator_type &a) + : base_t(BOOST_MOVE_BASE(base_t, x), a) + {} + + //! @copydoc ::boost::container::flat_set::operator=(const flat_set &) + flat_multiset& operator=(BOOST_COPY_ASSIGN_REF(flat_multiset) x) + { return static_cast(this->base_t::operator=(static_cast(x))); } + + //! @copydoc ::boost::container::flat_set::operator=(flat_set &&) + flat_multiset& operator=(BOOST_RV_REF(flat_multiset) x) + BOOST_NOEXCEPT_IF( allocator_traits_type::is_always_equal::value + && boost::container::container_detail::is_nothrow_move_assignable::value ) + { return static_cast(this->base_t::operator=(BOOST_MOVE_BASE(base_t, x))); } + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! @copydoc ::boost::container::flat_set::operator=(std::initializer_list) + flat_multiset& operator=(std::initializer_list il) + { + this->clear(); + this->insert(il.begin(), il.end()); + return *this; + } +#endif + + #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + //! @copydoc ::boost::container::flat_set::get_allocator() + allocator_type get_allocator() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::get_stored_allocator() + stored_allocator_type &get_stored_allocator() BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::get_stored_allocator() const + const stored_allocator_type &get_stored_allocator() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::begin() + iterator begin() BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::begin() const + const_iterator begin() const; + + //! @copydoc ::boost::container::flat_set::cbegin() const + const_iterator cbegin() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::end() + iterator end() BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::end() const + const_iterator end() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::cend() const + const_iterator cend() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::rbegin() + reverse_iterator rbegin() BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::rbegin() const + const_reverse_iterator rbegin() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::crbegin() const + const_reverse_iterator crbegin() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::rend() + reverse_iterator rend() BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::rend() const + const_reverse_iterator rend() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::crend() const + const_reverse_iterator crend() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::empty() const + bool empty() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::size() const + size_type size() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::max_size() const + size_type max_size() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::capacity() const + size_type capacity() const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::reserve(size_type) + void reserve(size_type cnt); + + //! @copydoc ::boost::container::flat_set::shrink_to_fit() + void shrink_to_fit(); + + #endif // #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + ////////////////////////////////////////////// + // + // modifiers + // + ////////////////////////////////////////////// + + #if !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) || defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + //! Effects: Inserts an object of type Key constructed with + //! std::forward(args)... and returns the iterator pointing to the + //! newly inserted element. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + template + iterator emplace(BOOST_FWD_REF(Args)... args) + { return this->base_t::emplace_equal(boost::forward(args)...); } + + //! Effects: Inserts an object of type Key constructed with + //! std::forward(args)... in the container. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent + //! to the key of x. + //! + //! Complexity: Logarithmic search time (constant if x is inserted + //! right before p) plus insertion linear to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + template + iterator emplace_hint(const_iterator p, BOOST_FWD_REF(Args)... args) + { return this->base_t::emplace_hint_equal(p, boost::forward(args)...); } + + #else // !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) + + #define BOOST_CONTAINER_FLAT_MULTISET_EMPLACE_CODE(N) \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + iterator emplace(BOOST_MOVE_UREF##N)\ + { return this->base_t::emplace_equal(BOOST_MOVE_FWD##N); }\ + \ + BOOST_MOVE_TMPL_LT##N BOOST_MOVE_CLASS##N BOOST_MOVE_GT##N \ + iterator emplace_hint(const_iterator hint BOOST_MOVE_I##N BOOST_MOVE_UREF##N)\ + { return this->base_t::emplace_hint_equal(hint BOOST_MOVE_I##N BOOST_MOVE_FWD##N); }\ + // + BOOST_MOVE_ITERATE_0TO9(BOOST_CONTAINER_FLAT_MULTISET_EMPLACE_CODE) + #undef BOOST_CONTAINER_FLAT_MULTISET_EMPLACE_CODE + + #endif // !defined(BOOST_NO_CXX11_VARIADIC_TEMPLATES) + + #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + //! Effects: Inserts x and returns the iterator pointing to the + //! newly inserted element. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const value_type &x); + + //! Effects: Inserts a new value_type move constructed from x + //! and returns the iterator pointing to the newly inserted element. + //! + //! Complexity: Logarithmic search time plus linear insertion + //! to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(value_type &&x); + #else + BOOST_MOVE_CONVERSION_AWARE_CATCH(insert, value_type, iterator, this->priv_insert) + #endif + + #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + //! Effects: Inserts a copy of x in the container. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent + //! to the key of x. + //! + //! Complexity: Logarithmic search time (constant if x is inserted + //! right before p) plus insertion linear to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const_iterator p, const value_type &x); + + //! Effects: Inserts a new value move constructed from x in the container. + //! p is a hint pointing to where the insert should start to search. + //! + //! Returns: An iterator pointing to the element with key equivalent + //! to the key of x. + //! + //! Complexity: Logarithmic search time (constant if x is inserted + //! right before p) plus insertion linear to the elements with bigger keys than x. + //! + //! Note: If an element is inserted it might invalidate elements. + iterator insert(const_iterator p, value_type &&x); + #else + BOOST_MOVE_CONVERSION_AWARE_CATCH_1ARG(insert, value_type, iterator, this->priv_insert, const_iterator, const_iterator) + #endif + + //! Requires: first, last are not iterators into *this. + //! + //! Effects: inserts each element from the range [first,last) . + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + template + void insert(InputIterator first, InputIterator last) + { this->base_t::insert_equal(first, last); } + + //! Requires: first, last are not iterators into *this and + //! must be ordered according to the predicate. + //! + //! Effects: inserts each element from the range [first,last) .This function + //! is more efficient than the normal range creation for ordered ranges. + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: Non-standard extension. If an element is inserted it might invalidate elements. + template + void insert(ordered_range_t, InputIterator first, InputIterator last) + { this->base_t::insert_equal(ordered_range, first, last); } + +#if !defined(BOOST_NO_CXX11_HDR_INITIALIZER_LIST) + //! Effects: inserts each element from the range [il.begin(), il.end()). + //! + //! Complexity: At most N log(size()+N) (N is the distance from first to last) + //! search time plus N*size() insertion time. + //! + //! Note: If an element is inserted it might invalidate elements. + void insert(std::initializer_list il) + { this->base_t::insert_equal(il.begin(), il.end()); } + + //! Requires: Range [il.begin(), il.end()) must be ordered according to the predicate. + //! + //! Effects: inserts each element from the range [il.begin(), il.end()). This function + //! is more efficient than the normal range creation for ordered ranges. + //! + //! Complexity: At most N log(size()+N) (N is the distance from il.begin() to il.end()) + //! search time plus N*size() insertion time. + //! + //! Note: Non-standard extension. If an element is inserted it might invalidate elements. + void insert(ordered_range_t, std::initializer_list il) + { this->base_t::insert_equal(ordered_range, il.begin(), il.end()); } +#endif + + #if defined(BOOST_CONTAINER_DOXYGEN_INVOKED) + + //! @copydoc ::boost::container::flat_set::erase(const_iterator) + iterator erase(const_iterator p); + + //! @copydoc ::boost::container::flat_set::erase(const key_type&) + size_type erase(const key_type& x); + + //! @copydoc ::boost::container::flat_set::erase(const_iterator,const_iterator) + iterator erase(const_iterator first, const_iterator last); + + //! @copydoc ::boost::container::flat_set::swap + void swap(flat_multiset& x) + BOOST_NOEXCEPT_IF( allocator_traits_type::is_always_equal::value + && boost::container::container_detail::is_nothrow_swappable::value ); + + //! @copydoc ::boost::container::flat_set::clear + void clear() BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::key_comp + key_compare key_comp() const; + + //! @copydoc ::boost::container::flat_set::value_comp + value_compare value_comp() const; + + //! @copydoc ::boost::container::flat_set::find(const key_type& ) + iterator find(const key_type& x); + + //! @copydoc ::boost::container::flat_set::find(const key_type& ) const + const_iterator find(const key_type& x) const; + + //! @copydoc ::boost::container::flat_set::nth(size_type) + iterator nth(size_type n) BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::nth(size_type) const + const_iterator nth(size_type n) const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::index_of(iterator) + size_type index_of(iterator p) BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::index_of(const_iterator) const + size_type index_of(const_iterator p) const BOOST_NOEXCEPT_OR_NOTHROW; + + //! @copydoc ::boost::container::flat_set::count(const key_type& ) const + size_type count(const key_type& x) const; + + //! @copydoc ::boost::container::flat_set::lower_bound(const key_type& ) + iterator lower_bound(const key_type& x); + + //! @copydoc ::boost::container::flat_set::lower_bound(const key_type& ) const + const_iterator lower_bound(const key_type& x) const; + + //! @copydoc ::boost::container::flat_set::upper_bound(const key_type& ) + iterator upper_bound(const key_type& x); + + //! @copydoc ::boost::container::flat_set::upper_bound(const key_type& ) const + const_iterator upper_bound(const key_type& x) const; + + //! @copydoc ::boost::container::flat_set::equal_range(const key_type& ) const + std::pair equal_range(const key_type& x) const; + + //! @copydoc ::boost::container::flat_set::equal_range(const key_type& ) + std::pair equal_range(const key_type& x); + + //! Effects: Returns true if x and y are equal + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator==(const flat_multiset& x, const flat_multiset& y); + + //! Effects: Returns true if x and y are unequal + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator!=(const flat_multiset& x, const flat_multiset& y); + + //! Effects: Returns true if x is less than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator<(const flat_multiset& x, const flat_multiset& y); + + //! Effects: Returns true if x is greater than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator>(const flat_multiset& x, const flat_multiset& y); + + //! Effects: Returns true if x is equal or less than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator<=(const flat_multiset& x, const flat_multiset& y); + + //! Effects: Returns true if x is equal or greater than y + //! + //! Complexity: Linear to the number of elements in the container. + friend bool operator>=(const flat_multiset& x, const flat_multiset& y); + + //! Effects: x.swap(y) + //! + //! Complexity: Constant. + friend void swap(flat_multiset& x, flat_multiset& y); + + #endif //#ifdef BOOST_CONTAINER_DOXYGEN_INVOKED + + #ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + private: + template + iterator priv_insert(BOOST_FWD_REF(KeyType) x) + { return this->base_t::insert_equal(::boost::forward(x)); } + + template + iterator priv_insert(const_iterator p, BOOST_FWD_REF(KeyType) x) + { return this->base_t::insert_equal(p, ::boost::forward(x)); } + #endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED +}; + +#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + +} //namespace container { + +//!has_trivial_destructor_after_move<> == true_type +//!specialization for optimizations +template +struct has_trivial_destructor_after_move > +{ + typedef typename ::boost::container::allocator_traits::pointer pointer; + static const bool value = ::boost::has_trivial_destructor_after_move::value && + ::boost::has_trivial_destructor_after_move::value && + ::boost::has_trivial_destructor_after_move::value; +}; + +namespace container { + +#endif //#ifndef BOOST_CONTAINER_DOXYGEN_INVOKED + +}} + +#include + +#endif // BOOST_CONTAINER_FLAT_SET_HPP Index: libraries/win32/boost/include/boost/intrusive/detail/minimal_less_equal_header.hpp =================================================================== --- /dev/null +++ libraries/win32/boost/include/boost/intrusive/detail/minimal_less_equal_header.hpp @@ -0,0 +1,30 @@ +///////////////////////////////////////////////////////////////////////////// +// +// (C) Copyright Ion Gaztanaga 2014-2015 +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +// See http://www.boost.org/libs/intrusive for documentation. +// +///////////////////////////////////////////////////////////////////////////// +#ifndef BOOST_INTRUSIVE_DETAIL_MINIMAL_LESS_EQUAL_HEADER_HPP +#define BOOST_INTRUSIVE_DETAIL_MINIMAL_LESS_EQUAL_HEADER_HPP +# +#ifndef BOOST_CONFIG_HPP +# include +#endif +# +#if defined(BOOST_HAS_PRAGMA_ONCE) +# pragma once +#endif +# +#//Try to avoid including , as it's quite big in C++11 +#if defined(BOOST_GNU_STDLIB) +# include +#else +# include //Fallback +#endif +# +#endif //BOOST_INTRUSIVE_DETAIL_MINIMAL_LESS_EQUAL_HEADER_HPP Index: source/ps/Replay.cpp =================================================================== --- source/ps/Replay.cpp +++ source/ps/Replay.cpp @@ -54,7 +54,7 @@ void CReplayLogger::StartGame(JS::MutableHandleValue attribs) { // Add timestamp, since the file-modification-date can change - m_ScriptInterface.SetProperty(attribs, "timestamp", std::time(nullptr)); + m_ScriptInterface.SetProperty(attribs, "timestamp", (double)std::time(nullptr)); // Add engine version and currently loaded mods for sanity checks when replaying m_ScriptInterface.SetProperty(attribs, "engine_version", CStr(engine_version)); @@ -135,6 +135,9 @@ if (ooslog) g_Game->GetSimulation2()->EnableOOSLog(); + psSetLogDir("/Users/Lancelot/Library/Application Support/0ad/logs/"); + g_Profiler2.EnableHTTP(); + // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; Index: source/simulation2/MessageTypes.h =================================================================== --- source/simulation2/MessageTypes.h +++ source/simulation2/MessageTypes.h @@ -72,24 +72,7 @@ }; /** - * Update phase for formation controller movement (must happen before individual - * units move to follow their formation). - */ -class CMessageUpdate_MotionFormation : public CMessage -{ -public: - DEFAULT_MESSAGE_IMPL(Update_MotionFormation) - - CMessageUpdate_MotionFormation(fixed turnLength) : - turnLength(turnLength) - { - } - - fixed turnLength; -}; - -/** - * Update phase for non-formation-controller unit movement. + * Update phase for unit movement. */ class CMessageUpdate_MotionUnit : public CMessage { @@ -317,21 +300,31 @@ }; /** - * Sent by CCmpUnitMotion during Update, whenever the motion status has changed - * since the previous update. + * Sent by CCmpUnitMotion as a hint whenever the unit stops moving + * Thus it will be sent if the unit's path is blocked, or if it's reached its destination. */ -class CMessageMotionChanged : public CMessage +class CMessageMovePaused : public CMessage { public: - DEFAULT_MESSAGE_IMPL(MotionChanged) + DEFAULT_MESSAGE_IMPL(MovePaused) - CMessageMotionChanged(bool starting, bool error) : - starting(starting), error(error) + CMessageMovePaused() { } +}; + +/** + * Sent by CCmpUnitMotion when a unit has determined it has no chance + * of ever reaching its assigned destination. This is a catastrophic error. + */ +class CMessageMoveFailure : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(MoveFailure) - bool starting; // whether this is a start or end of movement - bool error; // whether we failed to start moving (couldn't find any path) + CMessageMoveFailure() + { + } }; /** Index: source/simulation2/Simulation2.cpp =================================================================== --- source/simulation2/Simulation2.cpp +++ source/simulation2/Simulation2.cpp @@ -538,20 +538,13 @@ CMessageUpdate msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } - { - CMessageUpdate_MotionFormation msgUpdate(turnLengthFixed); - componentManager.BroadcastMessage(msgUpdate); - } - - // Process move commands for formations (group proxy) - if (cmpPathfinder) - cmpPathfinder->ProcessSameTurnMoves(); { PROFILE2("Sim - Motion Unit"); CMessageUpdate_MotionUnit msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } + { PROFILE2("Sim - Update Final"); CMessageUpdate_Final msgUpdate(turnLengthFixed); Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -32,7 +32,6 @@ // Message types: MESSAGE(TurnStart) MESSAGE(Update) -MESSAGE(Update_MotionFormation) MESSAGE(Update_MotionUnit) MESSAGE(Update_Final) MESSAGE(Interpolate) // non-deterministic (use with caution) @@ -45,7 +44,8 @@ MESSAGE(PositionChanged) MESSAGE(InterpolatedPositionChanged) MESSAGE(TerritoryPositionChanged) -MESSAGE(MotionChanged) +MESSAGE(MovePaused) +MESSAGE(MoveFailure) 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) const; 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) const; virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, std::vector* out) const; @@ -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 || !cmpPosition->IsInWorld()) + 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 || !cmpPosition->IsInWorld()) + return false; + + CmpPtr cmpPositionTarget(GetSimContext(), target); + if (!cmpPositionTarget || !cmpPositionTarget->IsInWorld()) + 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 || !cmpPositionTarget->IsInWorld()) + 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) const { PROFILE("TestLine"); Index: source/simulation2/components/CCmpPathfinder.cpp =================================================================== --- source/simulation2/components/CCmpPathfinder.cpp +++ source/simulation2/components/CCmpPathfinder.cpp @@ -667,6 +667,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,11 +237,35 @@ 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); } + virtual std::vector ComputePath_Script(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, const std::string& passClass) + { + WaypointPath ret; + PathGoal goal; + goal.type = PathGoal::POINT; + goal.x = x1; + goal.z = z1; + goal.maxdist = fixed::FromInt(50); // TODO: determine good value for this. + m_LongPathfinder.ComputePath(x0, z0, goal, GetPassabilityClass(passClass), ret); + std::vector output; + output.reserve(ret.m_Waypoints.size()); + for (Waypoint& wpt : ret.m_Waypoints) + output.emplace_back(CFixedVector2D(wpt.x, wpt.z)); + return output; + } + + virtual u32 ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, entity_id_t notify); virtual void ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t clearance, entity_pos_t range, const PathGoal& goal, pass_class_t passClass, WaypointPath& 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,210 +40,195 @@ #include "ps/Profile.h" #include "renderer/Scene.h" -// For debugging; units will start going straight to the target -// instead of calling the pathfinder -#define DISABLE_PATHFINDER 0 - -/** - * When advancing along the long path, and picking a new waypoint to move - * towards, we'll pick one that's up to this far from the unit's current - * position (to minimise the effects of grid-constrained movement) - */ -static const entity_pos_t WAYPOINT_ADVANCE_MAX = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*8); - /** - * Min/Max range to restrict short path queries to. (Larger ranges are slower, - * smaller ranges might miss some legitimate routes around large obstacles.) + * Minimal / usual / maximal range for a short path query. + * If the desired range for a short path goes beyond Max range, it gets hotswapped into a long range first + * as otherwise we can get large lag spikes. */ static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2); -static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*9); - -/** - * Minimum distance to goal for a long path request - */ -static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); +static const entity_pos_t SHORT_PATH_NORMAL_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*7); +static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*16); /** - * When short-pathing, and the short-range pathfinder failed to return a path, - * Assume we are at destination if we are closer than this distance to the target - * And we have no target entity. - * This is somewhat arbitrary, but setting a too big distance means units might lose sight of their end goal too much; + * Below this distance to the goal, if we're getting obstructed, we will recreate a brand new Goal for our short-pathfinder + * Instead of using the one given to us by RecomputeGoalPosition. + * This is unsafe from a shot/long pathfinder compatibility POV, so it should not be too big. */ -static const entity_pos_t SHORT_PATH_GOAL_RADIUS = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2); +static const entity_pos_t SHORT_PATH_GOAL_REDUX_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3); /** - * If we are this close to our target entity/point, then think about heading - * for it in a straight line instead of pathfinding. + * When we run in an obstruction and request a new short path, we'll drop waypoints that are + * close to our position, as defined by this constant. + * This is to avoid paths getting increasingly large and tangled and weird when units get repeatedly stuck. + * This would not be as necessary if we had pushing and could differentiate between unit/unit and unit/static collisions + * and oculd also be mitigated by checking (somehow) our path has grown larger than initially anticipated. + * This should probably be lower or at least cose to SHORT_PATH_GOAL_REDUX_DIST */ -static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); +static const entity_pos_t DISTANCE_FOR_WAYPOINT_DROP_WHEN_OBSTRUCTED = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3); -/** - * If we're following a target entity, - * we will recompute our path if the target has moved - * more than this distance from where we last pathed to. +/* + * How far ahead we'll check for collisions, to try and anticipate problems. + * This should be lowered whenever MP turns are lowered. */ -static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); +static const entity_pos_t LOOKAHEAD_DISTANCE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3); /** - * If we're following as part of a formation, - * but can't move to our assigned target point in a straight line, - * we will recompute our path if the target has moved - * more than this distance from where we last pathed to. + * Minimum distance to goal for a long path request + * Disabled, see note in RequestNewPath. */ -static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA_FORMATION = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1); +static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*0); /** - * If we're following something but it's more than this distance away along - * our path, then don't bother trying to repath regardless of how much it has - * moved, until we get this close to the end of our old path. + * If we are this close to our target entity/point, then think about heading + * for it in a straight line instead of pathfinding. + * TODO: this should probably be reintroduced */ -static const entity_pos_t CHECK_TARGET_MOVEMENT_AT_MAX_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*16); +static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); /** - * If we're following something and the angle between the (straight-line) directions to its previous target - * position and its present target position is greater than a given angle, recompute the path even far away - * (i.e. even if CHECK_TARGET_MOVEMENT_AT_MAX_DIST condition is not fulfilled). The actual check is done - * on the cosine of this angle, with a PI/6 angle. + * See unitmotion logic for details. Higher means units will retry more often before potentially failing. */ -static const fixed CHECK_TARGET_MOVEMENT_MIN_COS = fixed::FromInt(866)/1000; +static const size_t MAX_PATH_REATTEMPTS = 8; -static const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1); -static const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1); +static const CColor OVERLAY_COLOR_PATH(1, 0, 0, 1); +static const CColor OVERLAY_COLOR_ALTERNATIVE_PATH(0,1,0,1); class CCmpUnitMotion : public ICmpUnitMotion { -public: - static void ClassInit(CComponentManager& componentManager) +private: + struct SMotionGoal { - componentManager.SubscribeToMessageType(MT_Update_MotionFormation); - componentManager.SubscribeToMessageType(MT_Update_MotionUnit); - componentManager.SubscribeToMessageType(MT_PathResult); - componentManager.SubscribeToMessageType(MT_OwnershipChanged); - componentManager.SubscribeToMessageType(MT_ValueModification); - componentManager.SubscribeToMessageType(MT_Deserialized); - } - - DEFAULT_COMPONENT_ALLOCATOR(UnitMotion) + private: + bool m_Valid = false; - bool m_DebugOverlayEnabled; - std::vector m_DebugOverlayLongPathLines; - std::vector m_DebugOverlayShortPathLines; + entity_pos_t m_Range; + entity_id_t m_Entity; + CFixedVector2D m_Position; - // Template state: + public: + SMotionGoal() : m_Valid(false) {}; - bool m_FormationController; - 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,m_DebugOverlayPathLines1; - u32 m_ExpectedPathTicket; // asynchronous request ID we're waiting for, or 0 if none + // Template state, never changed after init. + fixed m_TemplateWalkSpeed, m_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. + // Should also probably be used whenever we implement step-by-step long paths using the hierarchical pathfinder. + SMotionGoal m_CurrentGoal; + + // Pathfinder-compliant goal. Should be reachable (at least when it's recomputed). + PathGoal m_Goal; + + // MOTION PLANNING + // We will abort if we are stuck after X tries, where X is the value in m_AbortIfStuck + u8 m_AbortIfStuck; + // actual unit speed, after technology and ratio fixed m_Speed; + // cached for convenience + fixed m_SpeedRatio; - // Current mean speed (over the last turn). - fixed m_CurSpeed; - - // Currently active paths (storing waypoints in reverse order). - // The last item in each path is the point we're currently heading towards. - WaypointPath m_LongPath; - WaypointPath m_ShortPath; + // Metadata about our path requests. + struct SPathRequest + { + // 0 if none, otherwise the ID of the path request to drop outdated requests should that happen. + u32 expectedPathTicket; - // Motion planning - u8 m_Tries; // how many tries we've done to get to our current Final Goal. + // If true, then the existing path will be scrapped. + bool dumpExistingPath; + // if true, some sanity checks will be performed on the returned path + bool runShortPathValidation; + // if true, this new path will not be added to the current path but instead to the alternative path, which may or may not get used. + bool isAlternativePath; + }; + SPathRequest m_PathRequest; - PathGoal m_FinalGoal; + // Currently active path (storing waypoints in reverse order). + WaypointPath m_Path; + // Alternative path. If the alternative path exists (ie not empty) and we run into an obstruction + // along the current path, we will in the same turn try the alternative path. If that fails too, we go through + // the normal "path-fixing" methods. If it succeeds, however, we swap the current path and the alternative path. + // this allows one-turn anticipation of obstacles without breaking everything and with as little downtime as possible. + // The alternative path is currently kept alive for one turn only. + WaypointPath m_AlternativePath; + + // used for the short pathfinder, incremented on each unsuccessful try. + u8 m_Tries; + // Turns to wait before a certain action. Works in concert with m_AbortIfStuck. + u8 m_WaitingTurns; + // if we actually started moving at some point. + bool m_StartedMoving; + + // Speed over the last turn + // cached so we can tell the visual actor when it changes + fixed m_ActualSpeed; static std::string GetSchema() { @@ -252,21 +238,12 @@ "7.0" "default" "" - "" - "" - "" "" "" "" "" - "" - "" - "" - "" - "" - "" - "" - "" + "" + "" "" "" "" @@ -276,19 +253,13 @@ 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 +273,17 @@ cmpObstruction->SetUnitClearance(m_Clearance); } - m_State = STATE_IDLE; - m_PathState = PATHSTATE_NONE; - - m_ExpectedPathTicket = 0; + m_PathRequest.expectedPathTicket = 0; + m_PathRequest.dumpExistingPath = false; + m_PathRequest.runShortPathValidation = false; + m_PathRequest.isAlternativePath = false; m_Tries = 0; - - m_TargetEntity = INVALID_ENTITY; - - m_FinalGoal.type = PathGoal::POINT; + m_WaitingTurns = 0; m_DebugOverlayEnabled = false; + m_AbortIfStuck = 0; + } virtual void Deinit() @@ -323,33 +293,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.NumberU32_Unbounded("ticket", m_ExpectedPathTicket); - - serialize.NumberU32_Unbounded("target entity", m_TargetEntity); - serialize.NumberFixed_Unbounded("target pos x", m_TargetPos.X); - serialize.NumberFixed_Unbounded("target pos y", m_TargetPos.Y); - serialize.NumberFixed_Unbounded("target offset x", m_TargetOffset.X); - serialize.NumberFixed_Unbounded("target offset y", m_TargetOffset.Y); - serialize.NumberFixed_Unbounded("target min range", m_TargetMinRange); - serialize.NumberFixed_Unbounded("target max range", m_TargetMaxRange); - + serialize.NumberU8("abort if stuck", m_AbortIfStuck, 0, 255); serialize.NumberFixed_Unbounded("speed", m_Speed); - serialize.NumberFixed_Unbounded("current speed", m_CurSpeed); + serialize.NumberFixed_Unbounded("speed ratio", m_SpeedRatio); - serialize.Bool("moving", m_Moving); - serialize.Bool("facePointAfterMove", m_FacePointAfterMove); + serialize.NumberU32_Unbounded("path ticket", m_PathRequest.expectedPathTicket); + serialize.Bool("dump path", m_PathRequest.dumpExistingPath); + serialize.Bool("short path validation", m_PathRequest.runShortPathValidation); + serialize.Bool("alternative path", m_PathRequest.isAlternativePath); + + 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 +333,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 +364,18 @@ break; } // fall-through - case MT_OwnershipChanged: case MT_Deserialized: + 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,76 +387,76 @@ GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender); } - virtual bool IsMoving() const + virtual bool IsActuallyMoving() { - return m_Moving; + return m_StartedMoving; } - virtual fixed GetWalkSpeed() const + virtual bool IsTryingToMove() { - return m_WalkSpeed; + return m_Destination.Valid(); } - virtual fixed GetRunSpeed() const + virtual fixed GetBaseSpeed() { - return m_RunSpeed; + return m_TechModifiedWalkSpeed; } - virtual pass_class_t GetPassabilityClass() const + virtual fixed GetSpeed() { - return m_PassClass; + return m_Speed; } - virtual std::string GetPassabilityClassName() const + virtual fixed GetSpeedRatio() { - return m_PassClassName; + return m_SpeedRatio; } - virtual void SetPassabilityClassName(const std::string& passClassName) + virtual fixed GetTopSpeedRatio() { - m_PassClassName = passClassName; - CmpPtr cmpPathfinder(GetSystemEntity()); - if (cmpPathfinder) - m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName); + return m_TechModifiedTopSpeedRatio; } - virtual fixed GetCurrentSpeed() const + virtual void SetSpeed(fixed ratio) { - return m_CurSpeed; + m_SpeedRatio = std::min(ratio, GetTopSpeedRatio()); + m_Speed = m_SpeedRatio.Multiply(GetBaseSpeed()); } - virtual void SetSpeed(fixed speed) + // convenience wrapper + void SetActualSpeed(fixed newRealSpeed) { - m_Speed = speed; + if (m_ActualSpeed == newRealSpeed) + return; + + m_ActualSpeed = newRealSpeed; + CmpPtr cmpVisualActor(GetEntityHandle()); + if (cmpVisualActor) + cmpVisualActor->SetMovingSpeed(m_ActualSpeed); } - virtual void SetFacePointAfterMove(bool facePointAfterMove) + virtual pass_class_t GetPassabilityClass() const { - m_FacePointAfterMove = facePointAfterMove; + return m_PassClass; } - virtual void SetDebugOverlay(bool enabled) + virtual std::string GetPassabilityClassName() const { - m_DebugOverlayEnabled = enabled; - UpdateMessageSubscriptions(); + return m_PassClassName; } - 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) const; - 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) const; - 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 SetPassabilityClassName(std::string passClassName) + { + m_PassClassName = passClassName; + CmpPtr cmpPathfinder(GetSystemEntity()); + if (cmpPathfinder) + m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName); + } - virtual void StopMoving() + virtual void SetDebugOverlay(bool enabled) { - m_Moving = false; - m_ExpectedPathTicket = 0; - m_State = STATE_STOPPING; - m_PathState = PATHSTATE_NONE; - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); + m_DebugOverlayEnabled = enabled; + UpdateMessageSubscriptions(); } virtual entity_pos_t GetUnitClearance() const @@ -516,82 +464,122 @@ 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; - } + virtual bool TemporaryRerouteToPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range); + virtual bool GoBackToOriginalDestination(); + + // transform a motion goal into a corresponding PathGoal + // called by RecomputeGoalPosition + PathGoal CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal); + + // take an arbitrary path goal and convert it to a 2D point goal, assign it to m_Goal. + bool RecomputeGoalPosition(PathGoal& goal); - entity_id_t GetGroup() const + virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z); + virtual void FaceTowardsEntity(entity_id_t ent); + + virtual void SetAbortIfStuck(u8 shouldAbort) { - return IsFormationMember() ? m_TargetEntity : GetEntityId(); + m_AbortIfStuck = shouldAbort; } - bool HasValidPath() const + void StartMoving() { - return m_PathState == PATHSTATE_FOLLOWING - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT; + m_StartedMoving = true; + + CmpPtr cmpObstruction(GetEntityHandle()); + if (cmpObstruction) + cmpObstruction->SetMovingFlag(true); } - void StartFailed() + void StopMoving() { - StopMoving(); - m_State = STATE_IDLE; // don't go through the STOPPING state since we never even started + m_StartedMoving = false; + + SetActualSpeed(fixed::Zero()); CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->SetMovingFlag(false); + } - CMessageMotionChanged msg(true, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + void ResetPathfinding() + { + m_Tries = 0; + m_WaitingTurns = 0; + + m_PathRequest.expectedPathTicket = 0; + m_Path.m_Waypoints.clear(); } - void MoveFailed() + virtual void DiscardMove() { StopMoving(); - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + ResetPathfinding(); - CMessageMotionChanged msg(false, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + m_CurrentGoal.Clear(); + m_Destination.Clear(); } - void StartSucceeded() + void MoveWillFail() { - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); - - m_Moving = true; + CMessageMoveFailure msg; + GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + } - CMessageMotionChanged msg(true, false); + void MoveHasPaused() + { + CMessageMovePaused msg; GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } - void MoveSucceeded() + virtual bool HasValidPath() { - m_Moving = false; + return !m_Path.m_Waypoints.empty(); + } - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + virtual CFixedVector2D GetReachableGoalPosition() + { + if (!IsTryingToMove()) + return CFixedVector2D(fixed::Zero(),fixed::Zero()); - // No longer moving, so speed is 0. - m_CurSpeed = fixed::Zero(); + return CFixedVector2D(m_Goal.x, m_Goal.z); + } - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); +private: + CFixedVector2D GetGoalPosition(const SMotionGoal& goal) const + { + ENSURE (goal.Valid()); + + 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; + } /** * Handle the result of an asynchronous path query. @@ -604,45 +592,26 @@ void Move(fixed dt); /** - * Decide whether to approximate the given range from a square target as a circle, - * rather than as a square. + * 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 ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const; + void ValidateCurrentPath(); /** - * Computes the current location of our target entity (plus offset). - * Returns false if no target entity or no valid position. + * take a 2D position and return an updated one based on estimated target velocity. */ - bool ComputeTargetPosition(CFixedVector2D& out) const; - - /** - * Attempts to replace the current path with a straight line to the goal, - * if this goal is a point, is close enough and the route is not obstructed. - */ - bool TryGoingStraightToGoalPoint(const CFixedVector2D& from); - - /** - * Attempts to replace the current path with a straight line to the target - * entity, if it's close enough and the route is not obstructed. - */ - bool TryGoingStraightToTargetEntity(const CFixedVector2D& from); - - /** - * 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 +626,14 @@ /** * Returns an appropriate obstruction filter for use with path requests. - * noTarget is true only when used inside tryGoingStraightToTargetEntity, - * in which case we do not want the target obstruction otherwise it would always fail */ - ControlGroupMovementObstructionFilter GetObstructionFilter(bool noTarget = false) const; + ControlGroupMovementObstructionFilter GetObstructionFilter(bool evenMovingUnits = true) const; /** - * Start moving to the given goal, from our current position 'from'. - * Might go in a straight line immediately, or might start an asynchronous - * path request. + * Dumps current path and request a new one asynchronously. + * Inside of UnitMotion, do not set evenUnreachable to false unless you REALLY know what you're doing. */ - void BeginPathing(const CFixedVector2D& from, const PathGoal& goal); + bool RequestNewPath(bool evenUnreachable = true); /** * Start an asynchronous long path query. @@ -677,7 +643,12 @@ /** * Start an asynchronous short path query. */ - void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool avoidMovingUnits); + void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool avoidMovingUnits, bool alternativePath = false); + + /** + * Drop waypoints that are close to us. Used by Move() before calling RequestShortPath and for anticipating obstructions. + */ + void DropCloseWaypoints(const CFixedVector2D& position, std::vector& waypoints); /** * Convert a path into a renderable list of lines @@ -691,539 +662,513 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path) { - // reset our state for sanity. - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - m_Moving = false; - // Ignore obsolete path requests - if (ticket != m_ExpectedPathTicket) + if (ticket != m_PathRequest.expectedPathTicket) return; - m_ExpectedPathTicket = 0; // we don't expect to get this result again + m_PathRequest.expectedPathTicket = 0; // we don't expect to get this result again - // Check that we are still able to do something with that path - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) + if (m_PathRequest.dumpExistingPath) { - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG || m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT) - StartFailed(); - else if (m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT) - StopMoving(); - return; + if (m_PathRequest.isAlternativePath) + m_AlternativePath.m_Waypoints.clear(); + else + m_Path.m_Waypoints.clear(); + m_PathRequest.dumpExistingPath = false; } - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG) - { - m_LongPath = path; - - // If we are following a path, leave the old m_ShortPath so we can carry on following it - // until a new short path has been computed - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG) - m_ShortPath.m_Waypoints.clear(); + if (!m_Destination.Valid()) + return; - // If there's no waypoints then we couldn't get near the target. - // Sort of hack: Just try going directly to the goal point instead - // (via the short pathfinder), so if we're stuck and the user clicks - // close enough to the unit then we can probably get unstuck - if (m_LongPath.m_Waypoints.empty()) - m_LongPath.m_Waypoints.emplace_back(Waypoint{ m_FinalGoal.x, m_FinalGoal.z }); + if (path.m_Waypoints.empty()) + { + // no waypoints, path failed. + // if we have some room, pop waypoint + // TODO: this isn't particularly bright. + if (!m_Path.m_Waypoints.empty()) + m_Path.m_Waypoints.pop_back(); - if (!HasValidPath()) - StartSucceeded(); + // we will then deal with this on the next Move() call. + return; + } - m_PathState = PATHSTATE_FOLLOWING; + // if this is a short path, verify some things + // Namely reject any path that takes us in another global region + // and any waypoint that's not passable/next to a passable cell. + // this will ensure minimal changes of long/short rance pathfinder discrepancies + if (m_PathRequest.runShortPathValidation) + { + CmpPtr cmpPathfinder(GetSystemEntity()); + ENSURE (cmpPathfinder); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); + CmpPtr cmpPosition(GetEntityHandle()); + ENSURE(cmpPosition); - m_Moving = true; - } - else if (m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT) - { - m_ShortPath = path; + u16 i0, j0; + cmpPathfinder->FindNearestPassableNavcell(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y, i0, j0, m_PassClass); - // If there's no waypoints then we couldn't get near the target - if (m_ShortPath.m_Waypoints.empty()) + m_PathRequest.runShortPathValidation = false; + for (const Waypoint& wpt : path.m_Waypoints) { - // If we're globally following a long path, try to remove the next waypoint, it might be obstructed - // If not, and we are not in a formation, retry - // unless we are close to our target and we don't have a target entity. - // This makes sure that units don't clump too much when they are not in a formation and tasked to move. - if (m_LongPath.m_Waypoints.size() > 1) - m_LongPath.m_Waypoints.pop_back(); - else if (IsFormationMember()) + u16 i1, j1; + u32 dist = cmpPathfinder->FindNearestPassableNavcell(wpt.x, wpt.z, i1, j1, m_PassClass); + if (dist > 2 || !cmpPathfinder->NavcellIsReachable(i0, j0, i1, j1, m_PassClass)) { - m_Moving = false; - CMessageMotionChanged msg(true, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + MoveWillFail(); + // we will then deal with this on the next Move() call. return; } + } + } - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - - CFixedVector2D pos = cmpPosition->GetPosition2D(); + std::vector& targetPath = m_PathRequest.isAlternativePath ? m_AlternativePath.m_Waypoints : m_Path.m_Waypoints; - if (ShouldConsiderOurselvesAtDestination(pos)) - return; + if (m_PathRequest.isAlternativePath) + m_PathRequest.isAlternativePath = false; - UpdateFinalGoal(); - RequestLongPath(pos, m_FinalGoal); - m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; + // if we're currently moving, we have a path, so check if the first waypoint can be removed + // it's not impossible that we've actually reached it already. + if (IsActuallyMoving() && path.m_Waypoints.size() >= 2) + { + CmpPtr cmpPosition(GetEntityHandle()); + CFixedVector2D nextWp = CFixedVector2D(path.m_Waypoints.back().x,path.m_Waypoints.back().z) - cmpPosition->GetPosition2D(); + if (nextWp.CompareLength(GetSpeed()/2) <= 0) + { + targetPath.insert(targetPath.end(), path.m_Waypoints.begin(), path.m_Waypoints.end()-1); return; } - - // else we could, so reset our number of tries. - m_Tries = 0; - - // Now we've got a short path that we can follow - if (!HasValidPath()) - StartSucceeded(); - - m_PathState = PATHSTATE_FOLLOWING; - - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); - - m_Moving = true; } - else - LOGWARNING("unexpected PathResult (%u %d %d)", GetEntityId(), m_State, m_PathState); + targetPath.insert(targetPath.end(), path.m_Waypoints.begin(), path.m_Waypoints.end()); } -void CCmpUnitMotion::Move(fixed dt) +void CCmpUnitMotion::ValidateCurrentPath() { - PROFILE("Move"); - - if (m_State == STATE_STOPPING) - { - m_State = STATE_IDLE; - MoveSucceeded(); - return; - } + // this should be kept in sync with RequestNewPath otherwise we'll spend our whole life repathing. - if (m_State == STATE_IDLE) + // don't validate points, they never change + if (!m_CurrentGoal.IsEntity()) return; - switch (m_PathState) - { - case PATHSTATE_NONE: - { - // If we're not pathing, do nothing + // TODO: figure out what to do when the goal dies. + // for now we'll keep on keeping on, but reset as if our goal was a position + // and send a failure message to UnitAI in case it wants to do something + // use position as a proxy for existence + CmpPtr cmpTargetPosition(GetSimContext(), m_CurrentGoal.GetEntity()); + if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) + { + // TODO: this should call a custom function for this + SMotionGoal newGoal(CFixedVector2D(m_Goal.x, m_Goal.z), m_CurrentGoal.Range()); + m_Destination = newGoal; + m_CurrentGoal = newGoal; + RequestNewPath(); + MoveWillFail(); return; } - case PATHSTATE_WAITING_REQUESTING_LONG: - case PATHSTATE_WAITING_REQUESTING_SHORT: - { - // If we're waiting for a path and don't have one yet, do nothing + // don't validate if no path. + if (!HasValidPath()) return; - } - case PATHSTATE_FOLLOWING: - case PATHSTATE_FOLLOWING_REQUESTING_SHORT: - case PATHSTATE_FOLLOWING_REQUESTING_LONG: - { - // TODO: there's some asymmetry here when units look at other - // units' positions - the result will depend on the order of execution. - // Maybe we should split the updates into multiple phases to minimise - // that problem. + // TODO: check LOS here (instead of in UnitAI like we do now). - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return; + // if our goal can move, then perhaps it has. + CmpPtr cmpTargetUnitMotion(GetSimContext(), m_CurrentGoal.GetEntity()); + if (!cmpTargetUnitMotion) + return; - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; + // Check if our current Goal's position (ie m_Goal, not m_CurrentGoal) is sensible. - CFixedVector2D initialPos = cmpPosition->GetPosition2D(); + // 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 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) + CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); + fixed certainty = (m_Clearance + cmpTargetUnitMotion->GetUnitClearance()) * 3 / 2; + UpdatePositionForTargetVelocity(m_CurrentGoal.GetEntity(), targetPos.X, targetPos.Y, certainty); - // Find the speed factor of the underlying terrain - // (We only care about the tile we start on - it doesn't matter if we're moving - // partially onto a much slower/faster tile) - // TODO: Terrain-dependent speeds are not currently supported - fixed terrainSpeed = fixed::FromInt(1); + CmpPtr cmpObstructionManager(GetSystemEntity()); + if (!cmpObstructionManager->IsPointInPointRange(m_Goal.x, m_Goal.z, targetPos.X, targetPos.Y, m_CurrentGoal.Range() - certainty, m_CurrentGoal.Range() + certainty)) + RequestNewPath(); +} - fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); +void CCmpUnitMotion::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 || !cmpTargetPosition->IsInWorld() || !cmpTargetUnitMotion || !cmpTargetUnitMotion->IsActuallyMoving()) + return; - bool wasObstructed = false; + // 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()); - // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint + // try to estimate in how much time we'll reach it. + fixed distance = (cmpTargetPosition->GetPosition2D() - cmpPosition->GetPosition2D()).Length(); - fixed timeLeft = dt; - fixed zero = fixed::Zero(); + if (GetBaseSpeed() < fixed::Epsilon()) + return; - 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; + fixed time = std::min(distance / GetBaseSpeed(), 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"); + + // early out + if (!IsTryingToMove()) + { + SetActualSpeed(fixed::Zero()); + 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. + + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) + return; + + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; + + CFixedVector2D initialPos = cmpPosition->GetPosition2D(); + + // All path updates/checks should go here, before the moving loop. + ValidateCurrentPath(); + + ////////////////////////////////////////////////////////////////////////////////////// + //// AFTER THIS POINT, NO MESSAGES SHOULD BE SENT OR HORRIBLE THINGS WILL HAPPEN. //// + //// YOU HAVE BEEN WARNED. //// + ////////////////////////////////////////////////////////////////////////////////////// + + // validate our state following the messages potentially sent above. + if (!IsTryingToMove() || !cmpPosition || !cmpPosition->IsInWorld() || !CurrentGoalHasValidPosition()) + { + // One of the messages we sent UnitAI caused us to stop moving entirely. + // Tell the visual actor we're not moving this turn to avoid gliding. + SetActualSpeed(fixed::Zero()); + return; + } + + // Keep track of the current unit's position during the update + CFixedVector2D pos = initialPos; + + // 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); + + bool wasObstructed = false; + + // save the movement loop as a lambda, since we call it twice and putting it in an external function is annoying. + auto MovementLoop = [&wasObstructed, &dt, &pos, &cmpPathfinder, this]() + { + // We want to move (at most) m_Speed*dt units from pos towards the next waypoint + fixed timeLeft = dt; + + // 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()) + return; CFixedVector2D target; - if (m_ShortPath.m_Waypoints.empty()) - target = CFixedVector2D(m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z); - else - target = CFixedVector2D(m_ShortPath.m_Waypoints.back().x, m_ShortPath.m_Waypoints.back().z); + target = CFixedVector2D(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z); CFixedVector2D offset = target - pos; - + fixed offsetLength = offset.Length(); // Work out how far we can travel in timeLeft - fixed maxdist = maxSpeed.Multiply(timeLeft); + fixed maxdist = m_Speed.Multiply(timeLeft); - // If the target is close, we can move there directly - fixed offsetLength = offset.Length(); + CFixedVector2D destination; if (offsetLength <= maxdist) + destination = target; + else { - if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) - { - pos = target; + offset.Normalize(maxdist); + destination = pos + offset; + } - // Spend the rest of the time heading towards the next waypoint - timeLeft = timeLeft - (offsetLength / maxSpeed); + // 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)) + { + pos = destination; - if (m_ShortPath.m_Waypoints.empty()) - m_LongPath.m_Waypoints.pop_back(); - else - m_ShortPath.m_Waypoints.pop_back(); + timeLeft = (timeLeft.Multiply(m_Speed) - offsetLength) / m_Speed; - continue; - } - else - { - // Error - path was obstructed - wasObstructed = true; - break; - } + if (destination == target) + m_Path.m_Waypoints.pop_back(); + continue; } 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 - - break; + // Error - path was obstructed + wasObstructed = true; + return; } } + }; - // Update the Position component after our movement (if we actually moved anywhere) - if (pos != initialPos) - { - 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; - } + // if we have an alternative path, follow it, it's likely to be better. + if (!m_AlternativePath.m_Waypoints.empty()) + { + // switch both paths. Presumably the alternative one is newer and better, so don't switch back if that one fails too. + m_Path.m_Waypoints.swap(m_AlternativePath.m_Waypoints); + // Move along the now chosen alternative path. + MovementLoop(); + // if we were obstructed, try the original, maybe it's free now? if (wasObstructed) { - // Oops, we hit something (very likely another unit). - // This is when we might easily get stuck wrongly. + wasObstructed = false; + pos = initialPos; + m_Path.m_Waypoints.swap(m_AlternativePath.m_Waypoints); - // check if we've arrived. - if (ShouldConsiderOurselvesAtDestination(pos)) - return; + // perform regular movement + MovementLoop(); + } + } + else + // perform regular movement + MovementLoop(); - // 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()) + 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; + + // clear alternative paths whatever happened. + m_AlternativePath.m_Waypoints.clear(); + + // Anticipate here future pathing needs + bool pathWillBeObstructed = !wasObstructed && HasValidPath() && !m_Path.m_Waypoints.empty(); + if (pathWillBeObstructed) + { + pathWillBeObstructed = false; + // check any waypoint closer to us than LOOKAHEAD_DISTANCE. + ssize_t index = m_Path.m_Waypoints.size() - 1; + CFixedVector2D checkPos = pos; + fixed distance = (CFixedVector2D(m_Path.m_Waypoints[index].x,m_Path.m_Waypoints[index].z) - checkPos).Length(); + while (index >= 0 && distance < LOOKAHEAD_DISTANCE) + { + if (!cmpPathfinder->CheckMovement(GetObstructionFilter(true), + checkPos.X, checkPos.Y, m_Path.m_Waypoints[index].x, m_Path.m_Waypoints[index].z, + m_Clearance, m_PassClass)) { - 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; - } + pathWillBeObstructed = true; + break; } - // Else, just entirely recompute - UpdateFinalGoal(); - BeginPathing(pos, m_FinalGoal); - - // potential TODO: We could switch the short-range pathfinder for something else entirely. - return; + CFixedVector2D wptPos = CFixedVector2D(m_Path.m_Waypoints[index].x,m_Path.m_Waypoints[index].z); + index--; + distance += (wptPos - checkPos).Length(); + checkPos = wptPos; } + } + if (pathWillBeObstructed) + { + std::vector alternativePath = m_Path.m_Waypoints; - // We successfully moved along our path, until running out of - // waypoints or time. - - if (m_PathState == PATHSTATE_FOLLOWING) + DropCloseWaypoints(pos, alternativePath); + PathGoal goal; + if (!alternativePath.empty()) + goal = { PathGoal::POINT, alternativePath.back().x, alternativePath.back().z }; + 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); + goal = CreatePathGoalFromMotionGoal(m_CurrentGoal); + m_PathRequest.dumpExistingPath = true; } + // request short path as alternative path. + RequestShortPath(pos, goal, true, true); + + // save waypoints kept so far. + m_AlternativePath.m_Waypoints = alternativePath; } - } -} -bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out) const -{ - if (m_TargetEntity == INVALID_ENTITY) - return false; + // Update the Position component after our movement (if we actually moved anywhere) + if (pos != initialPos) + { + CFixedVector2D offset = pos - initialPos; - CmpPtr cmpPosition(GetSimContext(), m_TargetEntity); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + // tell other components and visual actor we are moving. + if (!m_StartedMoving) + StartMoving(); - if (m_TargetOffset.IsZero()) - { - // No offset, just return the position directly - out = cmpPosition->GetPosition2D(); + // 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. + // 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; + ////////////////////////////////////////////////////////////////////////// + //// From this point onwards messages are "safe" to send again. //// + //// But after any messages are sent, you should validate our state. //// + ////////////////////////////////////////////////////////////////////////// - // 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; + // we've stopped moving ig we're here. + if (m_StartedMoving) + { + StopMoving(); - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; + MoveHasPaused(); + // Validate that the MoveHasPaused message hint has not caused us to be in an invalid state. + if (!IsTryingToMove() || !cmpPosition || !cmpPosition->IsInWorld() || !CurrentGoalHasValidPosition()) + return; + } - // 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; + if (ShouldConsiderOurselvesAtDestination(m_CurrentGoal)) + // If we're out of path (ie not moving) but have a valid destination (IsTryingToMove()), we'll end up here every turn. + // We should not repath if we actually are where we want to be (ie at destination). + // That we do this means that the range check performed by whoever is calling us must be more permissive than this one though + // otherwise we'll never reach our target. + return; - // 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 }); + // Oops, we have a problem. Either we were obstructed, we plan to be obstructed soon, or we ran out of path (but still have a goal). + // Handle it. + // Failure to handle it will result in stuckness and players complaining. - return true; -} + if (m_PathRequest.expectedPathTicket != 0) + // wait until we get our path to see where that leads us. + return; -bool CCmpUnitMotion::TryGoingStraightToTargetEntity(const CFixedVector2D& from) -{ - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return false; + // TODO: it would be nice to react differently to different problems + // eg running into a static obstruction can be handled by the long-range pathfinder + // but running in a unit cannot. + if (m_WaitingTurns == 0) + { + if (HasValidPath()) + m_WaitingTurns = MAX_PATH_REATTEMPTS; // You can add some "waiting" turns by bumping this up, but for now let's not do it. + else + m_WaitingTurns = 3; + } - // Fail if the target is too far away - if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) - return false; + --m_WaitingTurns; - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; + // MAX_PATH_REATTEMPTS and above: we wait. + if (m_WaitingTurns >= MAX_PATH_REATTEMPTS) + return; - // 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) + // If we're here we want to compute a short path. + if (m_WaitingTurns >= 3) + { + if (m_Path.m_Waypoints.empty()) + { + RequestNewPath(); + return; + } + /** + * Here there are two cases: + * 1) We are somewhat far away from the goal + * 2) We're really close to the goal. + * If it's (2) it's likely that we are running into units that are currently doing the same thing we want to do (gathering from the same tree…) + * Since the initial call to MakeGoalReachable gave us a specific 2D coordinate, and we can't reach it, + * We have a relatively high chance of never being able to reach that particular point. + * So we need to recreate the actual goal for this entity. This is a little dangerous in terms of short/long pathfinder compatibility + * So we'll run sanity checks on the output to try and not get stuck/go where we shouldn't. + */ + PathGoal goal; - // Find the point on the goal shape that we should head towards - CFixedVector2D goalPos = goal.NearestPointOnGoal(from); + CFixedVector2D endWptPos(m_Path.m_Waypoints.front().x, m_Path.m_Waypoints.front().z); + bool redraw = true; + if ((endWptPos - pos).CompareLength(SHORT_PATH_GOAL_REDUX_DIST) > 0) + { + DropCloseWaypoints(pos, m_Path.m_Waypoints); - // 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; + if (!m_Path.m_Waypoints.empty()) + { + redraw = false; + goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z }; + } + } + // sanity check on the right, but the code above should never allow that to happen. + if (redraw || m_Path.m_Waypoints.empty()) + { + goal = CreatePathGoalFromMotionGoal(m_CurrentGoal); + m_PathRequest.dumpExistingPath = true; + } - // 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, false); + 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; + // TODO: inform the player maybe? + // TODO: this should be sent to UnitAI with a stronger "definitely failed" parameter. + MoveWillFail(); + // fall through, we don't want to discard the move on our own. } - // 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_REATTEMPTS; -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 +1218,333 @@ } } -ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter(bool noTarget) const -{ - entity_id_t group = noTarget ? m_TargetEntity : GetGroup(); - return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), group); -} - - - -void CCmpUnitMotion::BeginPathing(const CFixedVector2D& from, const PathGoal& goal) +void CCmpUnitMotion::FaceTowardsEntity(entity_id_t ent) { - // reset our state for sanity. - m_ExpectedPathTicket = 0; - - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - m_Moving = false; - - m_PathState = PATHSTATE_NONE; - -#if DISABLE_PATHFINDER - { - CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY); - CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from); - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); - m_PathState = PATHSTATE_FOLLOWING; - 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(bool evenMovingUnits) 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()); + return ControlGroupMovementObstructionFilter(evenMovingUnits, GetEntityId()); } -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_PathRequest.isAlternativePath || m_PathRequest.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_PathRequest.dumpExistingPath = true; + m_PathRequest.isAlternativePath = false; - 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; - } + // Do not submit a path request if we've been told it's not going to be used anyhow. + DiscardMove(); + 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; + ENSURE(m_Goal.x >= fixed::Zero()); + + /** + * A (long) note on short vs long range pathfinder, their conflict, and stuck units. + * A long-standing issue with 0 A.D.'s pathfinding has been that the short-range pathfinder is "better" than the long-range + * Indeed it can find paths that the long-range one cannot, since the grid is coarser than the real vector representation. + * This leads to units going where they shouldn't go, notably impassable and "unreachable" areas. + * This has been a -real- plague. Made worse by the facts that groups of trees tended to trigger it, leading to stuck gatherers… + * Thus, in general, we'd want the short-range and the long-range pathfinder to coincide. But making the short-range pathfinder + * register all impassable navcells as edges would just be way too slow, so we can't do that, so we -cannot- fix the issue + * by just changing the pathfinders' behavior. + * + * All hope is not lost, however. + * + * A big part of the problem is that before the unitMotion rewrite, UnitMotion requested a path to the goal, and then the pathfinder + * made that goal "reachable" by calling MakeGoalReachable, which uses the same grid as the long-range pathfinder. Thus, over short ranges, + * the pathfinder entirely short-circuited this. Since UnitMotion now calls MakeGoalReachable on its own, it only ever requests + * paths to points that are indeed supposed to be reachable. This does fix a number of cases. + * + * But then, why set LONG_PATH_MIN_DIST to 0 and disable the use of short paths here? Well it turns out you still had a few edge cases. + * + * Imagine two houses next to each other, with a space between them just wide enough that there are no passable navcells, + * but enough space for the short-range pathfinder to return a path through them (make them in a test map if you have to). + * If you ask a unit to cross there, the goal won't change: it's reachable by the long-range pathfinder by going around the house. + * However, the distance is < LONG_PATH_MIN_DIST, so the short-range pathfinder is called, so it goes through the house. Edge case. + * There's a variety of similar cases that can be imagined around the idea that there exists a shorter path visible only by the short-range pathfinder. + * If we never use the short-pathfinder in RequestNewPath, we can safely avoid those edge cases. + * + * However, we still call the short-pathfinder when running into an obstruction to avoid units. Can't that get us stuck too? + * Well, it probably can. But there's a few things to consider: + * -It's harder to trigger it if you actually have to run into a unit + * -In those cases, UnitMotion requests a path to the next existing waypoint (if there are none, it calls requestnewPath to get those) + * and the next existing waypoint has -necessarily- been given to us by the long-range pathfinder since we're using it here + * -We are running sanity checks on the output (see PathResult). + * Thus it's far less likely that the short-range pathfinder will return us an impassable path. + * It -is- not entirely impossible. A freak construction with many units strategically positionned could probably reveal the bug. + * But it's in my opinion rare enough that this discrepancy can be considered fixed. + */ - BeginPathing(pos, goal); + if (m_Goal.DistanceToPoint(position) < LONG_PATH_MIN_DIST) + RequestShortPath(position, m_Goal, true); + else + RequestLongPath(position, m_Goal); - return true; + return reachable; } -bool CCmpUnitMotion::IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) const +PathGoal CCmpUnitMotion::CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal) { + PathGoal goal = PathGoal(); + goal.x = fixed::FromInt(-1); // to figure out whether it's false-unreachable or false-buggy + CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + ENSURE(cmpPosition); 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); + // 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. - // See if we're too close to the target square - if (distance < minRange) - return false; + // defaut to point at position + goal.type = PathGoal::POINT; + goal.x = GetGoalPosition(motionGoal).X; + goal.z = GetGoalPosition(motionGoal).Y; - // See if we're close enough to the target square - if (maxRange < entity_pos_t::Zero() || distance <= maxRange) - return true; + // few cases to consider. + if (motionGoal.IsEntity()) + { + 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); - return false; + goal.type = PathGoal::CIRCLE; + goal.x = obstruction.x; + goal.z = obstruction.z; + goal.hw = obstruction.hw + motionGoal.Range() + m_Clearance; + + // if not a unit, treat as a square + if (cmpObstruction->GetUnitRadius() == fixed::Zero()) + { + goal.type = PathGoal::SQUARE; + goal.hh = obstruction.hh + motionGoal.Range() + m_Clearance; + goal.u = obstruction.u; + goal.v = obstruction.v; + + fixed distance = Geometry::DistanceToSquare(pos - CFixedVector2D(goal.x,goal.z), goal.u, goal.v, CFixedVector2D(goal.hw, goal.hh), true); + if (distance == fixed::Zero()) + goal.type = PathGoal::INVERTED_SQUARE; + } + else if ((pos - CFixedVector2D(goal.x,goal.z)).CompareLength(goal.hw) <= 0) + goal.type = PathGoal::INVERTED_CIRCLE; + } + } + // if no obstruction, keep treating as a point } - else + if (goal.type == PathGoal::POINT && motionGoal.Range() > fixed::Zero()) { - entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length(); - - if (distance < minRange) - return false; - else if (maxRange >= entity_pos_t::Zero() && distance > maxRange) - return false; - else - return true; + goal.type = PathGoal::CIRCLE; + goal.hw = motionGoal.Range(); + if ((pos - CFixedVector2D(goal.x,goal.z)).CompareLength(goal.hw) <= 0) + goal.type = PathGoal::INVERTED_CIRCLE; } -} -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 goal; } -bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) +bool CCmpUnitMotion::RecomputeGoalPosition(PathGoal& goal) { - PROFILE("MoveToTargetRange"); + if (!CurrentGoalHasValidPosition()) + return false; // we're not going anywhere - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + goal = CreatePathGoalFromMotionGoal(m_CurrentGoal); - CFixedVector2D pos = cmpPosition->GetPosition2D(); + // We now have a correct goal. + // Make it reachable - CmpPtr cmpObstructionManager(GetSystemEntity()); - if (!cmpObstructionManager) - return false; + CmpPtr cmpPathfinder(GetSystemEntity()); + ENSURE(cmpPathfinder); - bool hasObstruction = false; - ICmpObstructionManager::ObstructionSquare obstruction; - CmpPtr cmpObstruction(GetSimContext(), target); - if (cmpObstruction) - hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); + CmpPtr cmpPosition(GetEntityHandle()); + ENSURE(cmpPosition); - if (!hasObstruction) - { - // The target didn't have an obstruction or obstruction shape, so treat it as a point instead + CFixedVector2D pos = cmpPosition->GetPosition2D(); - CmpPtr cmpTargetPosition(GetSimContext(), target); - if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) - return false; + bool reachable = cmpPathfinder->MakeGoalReachable(pos.X, pos.Y, goal, m_PassClass); - CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); + // TODO: ought to verify that the returned navcell is in range if it's reachable as a sanity check - return MoveToPointRange(targetPos.X, targetPos.Y, minRange, maxRange); - } + m_Goal = goal; - /* - * 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 + return reachable; +} - // Circumscribe the square - entity_pos_t circleRadius = halfSize.Length(); +void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal) +{ + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) + return; - entity_pos_t goalDistance = minRange + Pathfinding::GOAL_DELTA; + m_PathRequest.runShortPathValidation = false; + m_PathRequest.isAlternativePath = false; - if (ShouldTreatTargetAsCircle(minRange, circleRadius)) - { - // The target is small relative to our range, so pretend it's a circle - goal.type = PathGoal::INVERTED_CIRCLE; - goal.hw = circleRadius + goalDistance; - } - else - { - goal.type = PathGoal::INVERTED_SQUARE; - goal.u = obstruction.u; - goal.v = obstruction.v; - goal.hw = obstruction.hw + goalDistance; - goal.hh = obstruction.hh + goalDistance; - } - } - else if (maxRange < entity_pos_t::Zero() || distance < maxRange || previousDistance < maxRange) - { - // We're already in range - no need to move anywhere - FaceTowardsPointFromPos(pos, goal.x, goal.z); - return false; - } - else - { - // We might need to move closer: + // 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); - // Circumscribe the square - 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 + m_PathRequest.expectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId()); +} - // Note that the distance to the circle will always be less than - // the distance to the square, so the previous "distance < maxRange" - // check is still valid (though not sufficient) - entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius; - entity_pos_t previousCircleDistance = (pos - CFixedVector2D(previousObstruction.x, previousObstruction.z)).Length() - circleRadius; +void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits, bool alternativePath) +{ + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) + return; - 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; - } + // restrict the search space as much as possible, bumping it up on increasing m_tries if it wasn't enough to find a path. + // TODO: this might not be very useful following my unitMotion rewrite, and could probably be removed somewhat safely. + fixed distanceToGoal = goal.DistanceToPoint(from) + std::max(fixed::FromInt(TERRAIN_TILE_SIZE), goal.hw); - entity_pos_t goalDistance = maxRange - Pathfinding::GOAL_DELTA; + // wrapping around on m_Tries isn't really a problem so don't check for overflow. + fixed searchRange = std::max(SHORT_PATH_MIN_SEARCH_RANGE * (++m_Tries + 1), distanceToGoal); + fixed upperBound = std::max(SHORT_PATH_NORMAL_SEARCH_RANGE, distanceToGoal); - 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 + if (searchRange > upperBound) + searchRange = upperBound; - entity_pos_t goalDistance = (maxRange - Pathfinding::GOAL_DELTA)*2 / 3; // multiply by slightly less than 1/sqrt(2) + if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE && alternativePath) + { + // this is too far away, trash our path + if (m_PathRequest.expectedPathTicket != 0) + return; - goal.type = PathGoal::SQUARE; - goal.u = obstruction.u; - goal.v = obstruction.v; - entity_pos_t delta = std::max(goalDistance, m_Clearance + entity_pos_t::FromInt(TERRAIN_TILE_SIZE)/16); // ensure it's far enough to not intersect the building itself - goal.hw = obstruction.hw + delta; - goal.hh = obstruction.hh + delta; - } + m_PathRequest.isAlternativePath = false; + RequestNewPath(); + return; } - m_State = STATE_INDIVIDUAL_PATH; - m_TargetEntity = target; - m_TargetOffset = CFixedVector2D(); - m_TargetMinRange = minRange; - m_TargetMaxRange = maxRange; - m_FinalGoal = goal; - m_Tries = 0; + m_PathRequest.runShortPathValidation = true; + m_PathRequest.isAlternativePath = alternativePath; - BeginPathing(pos, goal); - - return true; + m_PathRequest.expectedPathTicket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetEntityId(), GetEntityId()); } -bool CCmpUnitMotion::IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) const +void CCmpUnitMotion::DropCloseWaypoints(const CFixedVector2D& position, std::vector& waypoints) { - // This function closely mirrors MoveToTargetRange - it needs to return true - // after that Move has completed + CmpPtr cmpPathfinder(GetSystemEntity()); - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + // we're far away, just drop waypoints up to + // Check wether our next waypoint is obstructed in which case skip it. + // TODO: would be good to have a faster function here. + if (!cmpPathfinder->CheckMovement(GetObstructionFilter(false), waypoints.back().x, waypoints.back().z, waypoints.back().x, waypoints.back().z, m_Clearance, m_PassClass)) + waypoints.pop_back(); - CFixedVector2D pos = cmpPosition->GetPosition2D(); + // let's drop waypoints until we have dropped enough or are out of waypoints. + fixed droppedDistance = fixed::Zero(); + CFixedVector2D currentPos = position; + while (!waypoints.empty() && droppedDistance < DISTANCE_FOR_WAYPOINT_DROP_WHEN_OBSTRUCTED) + { + CFixedVector2D nextWpt(waypoints.back().x, waypoints.back().z); + droppedDistance += (nextWpt - currentPos).Length(); + currentPos = nextWpt; + if (droppedDistance < DISTANCE_FOR_WAYPOINT_DROP_WHEN_OBSTRUCTED) + waypoints.pop_back(); + } +} - CmpPtr cmpObstructionManager(GetSystemEntity()); - if (!cmpObstructionManager) - return false; +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(); - 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_Destination = SMotionGoal(CFixedVector2D(x, z), range); + m_CurrentGoal = m_Destination; - // See if we're close enough to the target square - if (maxRange < entity_pos_t::Zero() || distance <= maxRange || previousDistance <= maxRange) - return true; + bool reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition - entity_pos_t circleRadius = halfSize.Length(); + return reachable; +} - 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; +bool CCmpUnitMotion::SetNewDestinationAsEntity(entity_id_t ent, entity_pos_t range, bool evenUnreachable) +{ + // This sets up a new destination, scrap whatever came before. + DiscardMove(); - return circleDistance <= maxRange || previousCircleDistance <= maxRange; - } + // validate entity's existence. + CmpPtr cmpPosition(GetSimContext(), ent); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return false; - // 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_Destination = SMotionGoal(ent, range); + m_CurrentGoal = m_Destination; - CFixedVector2D targetPos = cmpTargetPosition->GetPreviousPosition2D(); - entity_pos_t distance = (pos - targetPos).Length(); + bool reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition - return minRange <= distance && (maxRange < entity_pos_t::Zero() || distance <= maxRange); - } + return reachable; } -void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) +bool CCmpUnitMotion::TemporaryRerouteToPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range) { - CmpPtr cmpPosition(GetSimContext(), target); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - - CFixedVector2D pos = cmpPosition->GetPosition2D(); - - PathGoal goal; - goal.type = PathGoal::POINT; - goal.x = pos.X; - goal.z = pos.Y; + // Does not reset destination, this is a temporary rerouting. + StopMoving(); + ResetPathfinding(); + m_CurrentGoal = SMotionGoal(CFixedVector2D(x, z), range); - 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; + // we must set evenUnreachable to true otherwise if the path is unreachable we'll DiscardMove() and we don't want to do that. + bool reachable = RequestNewPath(true); // calls RecomputeGoalPosition - BeginPathing(pos, goal); + // if this is false, whoever called this function should probably un-reroute us and let us go on our way. + return reachable; } +bool CCmpUnitMotion::GoBackToOriginalDestination() +{ + SMotionGoal original = m_Destination; - - + // assume evenUnreachable, since if it was false originally we'd have stopped by now. + if (original.IsEntity()) + return SetNewDestinationAsEntity(original.GetEntity(), original.Range(), true); + else + return SetNewDestinationAsPosition(original.GetPosition().X,original.GetPosition().Y, original.Range(), true); +} void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color) { @@ -1785,8 +1565,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 +1596,11 @@ if (!m_DebugOverlayEnabled) return; - RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH); - RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH); - - for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i) - collector.Submit(&m_DebugOverlayLongPathLines[i]); + RenderPath(m_Path, m_DebugOverlayPathLines, OVERLAY_COLOR_PATH); + RenderPath(m_AlternativePath, m_DebugOverlayPathLines1, OVERLAY_COLOR_ALTERNATIVE_PATH); - for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i) - collector.Submit(&m_DebugOverlayShortPathLines[i]); + for (size_t i = 0; i < m_DebugOverlayPathLines1.size(); ++i) + collector.Submit(&m_DebugOverlayPathLines1[i]); + for (size_t i = 0; i < m_DebugOverlayPathLines.size(); ++i) + collector.Submit(&m_DebugOverlayPathLines[i]); } Index: source/simulation2/components/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,18 +70,18 @@ fixed m_R, m_G, m_B; // shading color - std::map m_AnimOverride; - // Current animation state - fixed m_AnimRunThreshold; // if non-zero this is the special walk/run mode std::string m_AnimName; bool m_AnimOnce; fixed m_AnimSpeed; std::wstring m_SoundGroup; fixed m_AnimDesync; - fixed m_AnimSyncRepeatTime; // 0.0 if not synced + fixed m_AnimSyncRepeatTime; fixed m_AnimSyncOffsetTime; + std::string m_MovingPrefix; + fixed m_MovingSpeed; + std::map m_VariantSelections; u32 m_Seed; // seed used for random variations @@ -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); @@ -236,6 +233,9 @@ serialize.NumberFixed_Unbounded("anim sync repeat time", m_AnimSyncRepeatTime); serialize.NumberFixed_Unbounded("anim sync offset time", m_AnimSyncOffsetTime); + serialize.NumberFixed_Unbounded("anim moving speed", m_MovingSpeed); + serialize.StringASCII("anim moving prefix", m_MovingPrefix, 0, 256); + SerializeMap()(serialize, "variation", m_VariantSelections); serialize.NumberU32_Unbounded("seed", m_Seed); @@ -282,12 +282,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) @@ -428,7 +422,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; @@ -437,28 +430,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) @@ -539,8 +540,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) @@ -746,45 +745,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 @@ -114,8 +114,7 @@ * @param x,z coordinates of center, in world space * @param clearance pathfinding clearance of the unit (works as a radius) * @param flags a set of EFlags values - * @param group control group (typically the owner entity, or a formation controller entity - * - units ignore collisions with others in the same group) + * @param group control group (typically the owner entity) * @return a valid tag for manipulating the shape * @see UnitShape */ @@ -159,6 +158,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. @@ -293,7 +312,7 @@ * * @param tag tag of shape being tested * @param flags set of EFlags for the shape - * @param group the control group of the shape (typically the shape's unit, or the unit's formation controller, or 0) + * @param group the control group of the shape (typically the shape's unit, or 0) * @param group2 an optional secondary control group of the shape, or INVALID_ENTITY if none specified. Currently * exists only for static shapes. */ 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. @@ -96,6 +114,11 @@ virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, WaypointPath& ret) = 0; /** + * Version for JS components + */ + virtual std::vector ComputePath_Script(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, const std::string& passClass) = 0; + + /** * Asynchronous version of ComputePath. * The result will be sent as CMessagePathResult to 'notify'. * Returns a unique non-zero number, which will match the 'ticket' in the result, Index: source/simulation2/components/ICmpPathfinder.cpp =================================================================== --- source/simulation2/components/ICmpPathfinder.cpp +++ source/simulation2/components/ICmpPathfinder.cpp @@ -24,4 +24,5 @@ BEGIN_INTERFACE_WRAPPER(Pathfinder) DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpPathfinder, SetDebugOverlay, bool) DEFINE_INTERFACE_METHOD_1("SetHierDebugOverlay", void, ICmpPathfinder, SetHierDebugOverlay, bool) +DEFINE_INTERFACE_METHOD_5("ComputePath", std::vector, ICmpPathfinder, ComputePath_Script, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, std::string) END_INTERFACE_WRAPPER(Pathfinder) Index: source/simulation2/components/ICmpUnitMotion.h =================================================================== --- source/simulation2/components/ICmpUnitMotion.h +++ source/simulation2/components/ICmpUnitMotion.h @@ -27,94 +27,118 @@ * Motion interface for entities with complex movement capabilities. * (Simpler motion is handled by ICmpMotion instead.) * - * It should eventually support different movement speeds, moving to areas - * instead of points, moving as part of a group, moving as part of a formation, - * etc. + * This component is designed to handle clever individual movement, + * not movement as part of an integrated group (formation, bataillon…) + * and another component should probably be designed for that. */ class ICmpUnitMotion : public IComponent { public: /** - * Attempt to walk into range of a to a given point, or as close as possible. - * The range is measured from the center of the unit. - * If the unit is already in range, or cannot move anywhere at all, or if there is - * some other error, then returns false. - * Otherwise, returns true and sends a MotionChanged message after starting to move, - * and sends another MotionChanged after finishing moving. - * If maxRange is negative, then the maximum range is treated as infinity. + * Resets motion and assigns a new 2D position as destination. + * Returns false if the position is unreachable (or if the move could not be completed for any other reason). + * Otherwise, returns true. + * If evenUnreachable is false, and the point is unreachable, then the unit will not start moving. + * Otherwise, the unit will try to go to another position as close as possible to the destination. */ - virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0; + virtual bool SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool evenUnreachable) = 0; /** - * Determine wether the givven point is within the given range, using the same measurement - * as MoveToPointRange. + * 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 IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) const = 0; + virtual bool SetNewDestinationAsEntity(entity_id_t target, entity_pos_t range, bool evenUnreachable) = 0; /** - * Determine whether the target is within the given range, using the same measurement - * as MoveToTargetRange. + * Set m_CurrentGoal to the given position. This does not affect m_Destination. + * This does not reset the current move or send any specific message. */ - virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) const = 0; + virtual bool TemporaryRerouteToPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range) = 0; /** - * Attempt to walk into range of a given target entity, or as close as possible. - * The range is measured between approximately the edges of the unit and the target, so that - * maxRange=0 is not unreachably close to the target. - * If the unit is already in range, or cannot move anywhere at all, or if there is - * some other error, then returns false. - * Otherwise, returns true and sends a MotionChanged message after starting to move, - * and sends another MotionChanged after finishing moving. - * If maxRange is negative, then the maximum range is treated as infinity. + * Resets our pathfinding towards the original m_Destination. + * If it wasn't modified by a TemporaryRerouteToPosition, this does nothing (except potentially stop the unit for one turn). + * returns the same as the original SetNewDestinationAsXYZ call, with evenUnreachable set to true. */ - virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; + virtual bool GoBackToOriginalDestination() = 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. + * Turn to look towards the given point. */ - virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0; + virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; /** - * Turn to look towards the given point. + * Turn to look towards the given entity. */ - virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; + virtual void FaceTowardsEntity(entity_id_t ent) = 0; + + /** + * Determine whether to abort or retry X times if pathing fails. + * Generally safer to let it abort and inform us. + */ + virtual void SetAbortIfStuck(u8 shouldAbort) = 0; + + /** + * Stop moving, clear any destination, path, and ticket pending. + * Basically resets the unit's motion. + * Won't send any message. + */ + virtual void DiscardMove() = 0; + + /** + * Returns the reachable goal of the unit, ie the goal it's actually moving towards. + * If the caller knows the original goal, it can more intelligently decide to stop or continue walking order. + */ + virtual CFixedVector2D GetReachableGoalPosition() = 0; + + /** + * Asks wether the unit has a path to follow + */ + virtual bool HasValidPath() = 0; /** - * Stop moving immediately. + * Get how much faster/slower we are at than normal. */ - virtual void StopMoving() = 0; + virtual fixed GetSpeedRatio() = 0; /** - * Get the current movement speed. + * Get how much faster than our regular speed we can go. */ - virtual fixed GetCurrentSpeed() const = 0; + 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 IsMoving() const = 0; + virtual bool IsActuallyMoving() = 0; /** - * Get the default speed that this unit will have when walking, in metres per second. + * Get whether a unit is trying to go somewhere + * NB: this does not mean its position is actually changing right now. */ - virtual fixed GetWalkSpeed() const = 0; + virtual bool IsTryingToMove() = 0; /** - * Get the default speed that this unit will have when running, 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 GetRunSpeed() const = 0; + virtual fixed GetSpeed() = 0; /** - * Set whether the unit will turn to face the target point after finishing moving. + * Get the unit base/walk speed in metres per second. + * This is NOT affected by SetSpeed. */ - virtual void SetFacePointAfterMove(bool facePointAfterMove) = 0; + virtual fixed GetBaseSpeed() = 0; /** * Get the unit's passability class. Index: source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- source/simulation2/components/ICmpUnitMotion.cpp +++ source/simulation2/components/ICmpUnitMotion.cpp @@ -23,67 +23,74 @@ #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_CONST_4("IsInPointRange", bool, ICmpUnitMotion, IsInPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t) -DEFINE_INTERFACE_METHOD_CONST_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_0("StopMoving", void, ICmpUnitMotion, StopMoving) -DEFINE_INTERFACE_METHOD_CONST_0("GetCurrentSpeed", fixed, ICmpUnitMotion, GetCurrentSpeed) +DEFINE_INTERFACE_METHOD_1("FaceTowardsEntity", void, ICmpUnitMotion, FaceTowardsEntity, entity_id_t) +DEFINE_INTERFACE_METHOD_1("SetAbortIfStuck", void, ICmpUnitMotion, SetAbortIfStuck, u8) +DEFINE_INTERFACE_METHOD_0("DiscardMove", void, ICmpUnitMotion, DiscardMove) +DEFINE_INTERFACE_METHOD_0("GetReachableGoalPosition", CFixedVector2D, ICmpUnitMotion, GetReachableGoalPosition) +DEFINE_INTERFACE_METHOD_0("HasValidPath", bool, ICmpUnitMotion, HasValidPath) +DEFINE_INTERFACE_METHOD_0("GetTopSpeedRatio", fixed, ICmpUnitMotion, GetTopSpeedRatio) DEFINE_INTERFACE_METHOD_1("SetSpeed", void, ICmpUnitMotion, SetSpeed, fixed) -DEFINE_INTERFACE_METHOD_CONST_0("IsMoving", bool, ICmpUnitMotion, IsMoving) -DEFINE_INTERFACE_METHOD_CONST_0("GetWalkSpeed", fixed, ICmpUnitMotion, GetWalkSpeed) -DEFINE_INTERFACE_METHOD_CONST_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_CONST_0("GetPassabilityClassName", std::string, ICmpUnitMotion, GetPassabilityClassName) DEFINE_INTERFACE_METHOD_CONST_0("GetUnitClearance", entity_pos_t, ICmpUnitMotion, GetUnitClearance) -DEFINE_INTERFACE_METHOD_1("SetFacePointAfterMove", void, ICmpUnitMotion, SetFacePointAfterMove, bool) DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpUnitMotion, SetDebugOverlay, bool) END_INTERFACE_WRAPPER(UnitMotion) + class CCmpUnitMotionScripted : public ICmpUnitMotion { public: DEFAULT_SCRIPT_WRAPPER(UnitMotionScripted) - virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) + virtual bool SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool UNUSED(evenUnreachable)) { - return m_Script.Call("MoveToPointRange", x, z, minRange, maxRange); + return m_Script.Call("SetNewDestinationAsPosition", x, z, range, true); } - virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) const + 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) const + virtual bool TemporaryRerouteToPosition(entity_pos_t, entity_pos_t, entity_pos_t) + { + return false; + } + + virtual bool GoBackToOriginalDestination() { - return m_Script.Call("IsInTargetRange", target, minRange, maxRange); + return true; } - virtual bool MoveToTargetRange(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("MoveToTargetRange", target, minRange, maxRange); + m_Script.CallVoid("FaceTowardsPoint", x, z); } - virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) + virtual void FaceTowardsEntity(entity_id_t ent) { - m_Script.CallVoid("MoveToFormationOffset", target, x, z); + m_Script.CallVoid("FaceTowardsEntity", ent); } - virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) + virtual void DiscardMove() { - m_Script.CallVoid("FaceTowardsPoint", x, z); + m_Script.CallVoid("DiscardMove"); } - virtual void StopMoving() + virtual void SetAbortIfStuck(u8 shouldAbort) { - m_Script.CallVoid("StopMoving"); + m_Script.CallVoid("SetAbortIfStuck", shouldAbort); } - virtual fixed GetCurrentSpeed() const + virtual fixed GetActualSpeed() { - return m_Script.Call("GetCurrentSpeed"); + return m_Script.Call("GetActualSpeed"); } virtual void SetSpeed(fixed speed) @@ -91,24 +98,39 @@ m_Script.CallVoid("SetSpeed", speed); } - virtual bool IsMoving() const + virtual fixed GetTopSpeedRatio() + { + return m_Script.Call("GetTopSpeedRatio"); + } + + virtual CFixedVector2D GetReachableGoalPosition() { - return m_Script.Call("IsMoving"); + return m_Script.Call("GetReachableGoalPosition"); } - virtual fixed GetWalkSpeed() const + virtual bool HasValidPath() { - return m_Script.Call("GetWalkSpeed"); + return m_Script.Call("HasValidPath"); } - virtual fixed GetRunSpeed() const + virtual bool IsActuallyMoving() { - return m_Script.Call("GetRunSpeed"); + return m_Script.Call("IsActuallyMoving"); } - virtual void SetFacePointAfterMove(bool facePointAfterMove) + virtual bool IsTryingToMove() { - m_Script.CallVoid("SetFacePointAfterMove", facePointAfterMove); + return m_Script.Call("IsTryingToMove"); + } + + virtual fixed GetSpeed() + { + return m_Script.Call("GetSpeed"); + } + + virtual fixed GetBaseSpeed() + { + return m_Script.Call("GetBaseSpeed"); } virtual pass_class_t GetPassabilityClass() const @@ -116,6 +138,11 @@ return m_Script.Call("GetPassabilityClass"); } + virtual fixed GetSpeedRatio() + { + return fixed::FromInt(1); + } + virtual std::string GetPassabilityClassName() const { return m_Script.Call("GetPassabilityClassName"); Index: source/simulation2/components/ICmpVisual.h =================================================================== --- source/simulation2/components/ICmpVisual.h +++ source/simulation2/components/ICmpVisual.h @@ -104,25 +104,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 @@ -25,13 +25,11 @@ DEFINE_INTERFACE_METHOD_2("SetVariant", void, ICmpVisual, SetVariant, CStr, CStr) DEFINE_INTERFACE_METHOD_CONST_0("GetAnimationName", std::string, ICmpVisual, GetAnimationName) 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_CONST_0("GetActorSeed", u32, ICmpVisual, GetActorSeed) DEFINE_INTERFACE_METHOD_1("SetActorSeed", void, ICmpVisual, SetActorSeed, u32) DEFINE_INTERFACE_METHOD_CONST_0("HasConstructionPreview", bool, ICmpVisual, HasConstructionPreview) Index: source/simulation2/components/tests/test_Pathfinder.h =================================================================== --- source/simulation2/components/tests/test_Pathfinder.h +++ source/simulation2/components/tests/test_Pathfinder.h @@ -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,22 +105,28 @@ 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) - * to the nearest passable navcell. + * Updates @p i, @p j to the nearest passable navcell. */ void FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass); @@ -125,12 +147,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 @@ -141,18 +163,43 @@ void RegionCenter(u16 r, int& i, int& j) const; + void NearestNavcell(int iGoal, int jGoal, int& iBest, int& jBest, u32& dist2Best) const; + 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 @@ -174,6 +221,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 @@ -22,6 +22,8 @@ #include "graphics/Overlay.h" #include "ps/Profile.h" +#include "Geometry.h" + // Find the root ID of a region, used by InitRegions inline u16 RootID(u16 x, const std::vector& v) { @@ -111,13 +113,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 @@ -173,6 +174,39 @@ /** * Returns the global navcell coords, and the squared distance to the goal + * navcell, of whichever navcell inside the given chunk is closest to + * that goal. + */ +void HierarchicalPathfinder::Chunk::NearestNavcell(int iGoal, int jGoal, int& iBest, int& jBest, u32& dist2Best) const +{ + iBest = 0; + jBest = 0; + dist2Best = std::numeric_limits::max(); + + for (int j = 0; j < CHUNK_SIZE; ++j) + { + for (int i = 0; i < CHUNK_SIZE; ++i) + { + if (m_Regions[j][i] == 0) + continue; + + u32 dist2 = (i + m_ChunkI*CHUNK_SIZE - iGoal)*(i + m_ChunkI*CHUNK_SIZE - iGoal) + + (j + m_ChunkJ*CHUNK_SIZE - jGoal)*(j + m_ChunkJ*CHUNK_SIZE - jGoal); + if (dist2 < dist2Best) + { + iBest = i + m_ChunkI*CHUNK_SIZE; + jBest = j + m_ChunkJ*CHUNK_SIZE; + dist2Best = dist2; + } + else + // we can safely break, since the first time we'll necessary improve on max, and then if we don't then this one was the best. + break; + } + } +} + +/** + * Returns the global navcell coords, and the squared distance to the goal * navcell, of whichever navcell inside the given region is closest to * that goal. */ @@ -191,7 +225,6 @@ u32 dist2 = (i + m_ChunkI*CHUNK_SIZE - iGoal)*(i + m_ChunkI*CHUNK_SIZE - iGoal) + (j + m_ChunkJ*CHUNK_SIZE - jGoal)*(j + m_ChunkJ*CHUNK_SIZE - jGoal); - if (dist2 < dist2Best) { iBest = i + m_ChunkI*CHUNK_SIZE; @@ -224,6 +257,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 +401,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 +420,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 +463,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 +474,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 +575,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 +605,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 +647,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 +772,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,80 +786,368 @@ 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); + + // 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); + } - // Check whether any reachable region contains the goal - // And get the navcell that's the closest to the point +#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; + } + } + } - u16 bestI = 0; - u16 bestJ = 0; - u32 dist2Best = std::numeric_limits::max(); + 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; + } - 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); + 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); + if(!goal.NavcellContainsGoal(bestI, bestJ)) + { + // try to get as close as possible to the goal inside this navcell. + CFixedVector2D relativePos(goal.x - newGoal.x, goal.z - newGoal.z); + CFixedVector2D u(fixed::FromInt(1), fixed::FromInt(0)); + CFixedVector2D v(fixed::FromInt(0), fixed::FromInt(1)); + // use a halfsize slightly below 1, otherwise we can end up on the edge and that confuses the pathfinder. + CFixedVector2D halfsize(fixed::FromInt(2)/5, fixed::FromInt(2)/5); + + relativePos = Geometry::NearestPointOnSquare(relativePos, u, v, halfsize); + newGoal.x += relativePos.X; + newGoal.z += relativePos.Y; + } goal = newGoal; + + return reachable; } void HierarchicalPathfinder::FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass) { - std::set regions; - FindPassableRegions(regions, passClass); - FindNearestNavcellInRegions(regions, i, j, passClass); + // If our current region is not 0, then this is passable, return immediately. + if (Get(i, j, passClass).r != 0) + return; + + u32 dist2Best = std::numeric_limits::max(); + + u16 oi = i, oj= j; + // in most cases we're < 5 cells away so go for this first + for (u16 tj = std::max(0, oj-5); tj <= std::min(m_H-1, oj+5); ++tj) + for (u16 ti = std::max(0, oi-5); ti <= std::min(m_W-1, oi+5); ++ti) + if (Get(ti, tj, passClass).r != 0) + { + u32 dist = abs(ti-oi) + abs(tj-oj); + if (dist < dist2Best) + { + i = ti; + j = tj; + dist2Best = dist; + } + } + if (dist2Best < std::numeric_limits::max()) + return; + + // We are on an impassable area, so we cannot rely on our accessibility. + // This function does not necessarily need to return the absolute closest navcell, + // since standing on impassable terrain is already a little wonky + // We'll expand in a cross-wise way, returning the best passable cell after each expansion (if any) + // this will ensure that we return an acceptable close navcell in general. + // since the first time we could be close to an edge, we'll expand at least once. + + u16 iBest = i; + u16 jBest = j; + dist2Best = std::numeric_limits::max(); + + u16 si = i/CHUNK_SIZE, sj = j/CHUNK_SIZE; + std::set> visited = { { si, sj } }; + std::vector> openList = { { si, sj } }, openListTemp; + static const int expander[4][2] = { { 1, 0 },{ -1, 0 },{ 0, 1 },{ 0, -1 } }; + for (size_t step = 0;;++step) + { + openListTemp.clear(); + for (std::pair chunk : openList) + { + // go through all regions of a chunk + int tempi, tempj; + u32 dist2; + GetChunk(chunk.first, chunk.second, passClass).NearestNavcell(i, j, tempi, tempj, dist2); + if (dist2 < dist2Best) + { + iBest = tempi; + jBest = tempj; + dist2Best = dist2; + } + // expand cross + for (const int* dir : expander) + { + u8 x = (u8)std::min(std::max((int)si + dir[0], 0), (int)(m_ChunksW-1)); + u8 z = (u8)std::min(std::max((int)sj + dir[1], 0), (int)(m_ChunksH-1)); + if (visited.find({ x, z }) == visited.end()) + { + visited.insert({ x, z }); + openListTemp.push_back({ x, z }); + } + } + } + if (openListTemp.empty() || (step > 0 && dist2Best < std::numeric_limits::max())) + break; + openList.swap(openListTemp); + } + i = iBest; + j = jBest; } void HierarchicalPathfinder::FindNearestNavcellInRegions(const std::set& regions, u16& iGoal, u16& jGoal, pass_class_t passClass) @@ -710,15 +1213,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) @@ -726,15 +1230,38 @@ } } -void HierarchicalPathfinder::FindPassableRegions(std::set& regions, pass_class_t passClass) +void HierarchicalPathfinder::FindGoalRegions(u16 gi, u16 gj, const PathGoal& goal, std::set& regions, pass_class_t passClass) { - // Construct a set of all regions of all chunks for this pass class - for (const Chunk& chunk : m_Chunks[passClass]) + if (goal.type == PathGoal::POINT) { - // region 0 is impassable tiles - for (int r = 1; r <= chunk.m_NumRegions; ++r) - regions.insert(RegionID(chunk.m_ChunkI, chunk.m_ChunkJ, r)); + 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 = std::max(0,(gj - size) / CHUNK_SIZE); sz <= std::min(m_ChunksH-1, (gj + size + 1) / CHUNK_SIZE); ++sz) + for (u8 sx = std::max(0,(gi - size) / CHUNK_SIZE); sx <= std::min(m_ChunksW-1, (gi + size + 1) / 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) 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/helpers/Rasterize.cpp =================================================================== --- source/simulation2/helpers/Rasterize.cpp +++ source/simulation2/helpers/Rasterize.cpp @@ -40,6 +40,10 @@ // A side effect is that the basic clearance has been set to 0.8, so removing this constant should be done // in parallel with setting clearance back to 1 for the default passability class (though this isn't strictly necessary). // Also: the code detecting foundation obstruction in CcmpObstructionManager had to be changed similarly. + // + // NB: because of this, if a entity wants to move very close to the actual obstruction of another entity + // it may never be able to get there, as there could be 1 extra navcell in between the actual square shape and the raster. + // So while this stands, max ranges should not be too small, or units might get stuck. entity_pos_t rasterClearance = clearance + Pathfinding::CLEARANCE_EXTENSION_RADIUS; // Get the bounds of cells that might possibly be within the shape Index: source/simulation2/scripting/MessageTypeConversions.cpp =================================================================== --- source/simulation2/scripting/MessageTypeConversions.cpp +++ source/simulation2/scripting/MessageTypeConversions.cpp @@ -91,7 +91,6 @@ } MESSAGE_1(Update, fixed, turnLength) -MESSAGE_1(Update_MotionFormation, fixed, turnLength) MESSAGE_1(Update_MotionUnit, fixed, turnLength) MESSAGE_1(Update_Final, fixed, turnLength) @@ -267,20 +266,30 @@ //////////////////////////////// -JS::Value CMessageMotionChanged::ToJSVal(ScriptInterface& scriptInterface) const +JS::Value CMessageMovePaused::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* CMessageMovePaused::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 CMessageMovePaused(); +} + +//////////////////////////////// + +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 @@ -396,7 +396,7 @@ { CmpPtr cmpUnitMotion(m.Simulation2, m.Entity); if (cmpUnitMotion) - speed = cmpUnitMotion->GetWalkSpeed().ToFloat(); + speed = cmpUnitMotion->GetBaseSpeed().ToFloat(); else speed = 7.f; // typical unit speed @@ -406,7 +406,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