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 @@ -350,6 +350,14 @@ "autocontinue": true, "queued": queued }); + + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "x": placementSupport.position.x, + "z": placementSupport.position.z, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); if (!queued) Index: binaries/data/mods/public/gui/session/session.js =================================================================== --- binaries/data/mods/public/gui/session/session.js +++ binaries/data/mods/public/gui/session/session.js @@ -757,7 +757,10 @@ // Display rally points for selected buildings if (Engine.GetPlayerID() != -1) + { Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); + Engine.GuiInterfaceCall("DisplayWayPoint", { "entities": g_Selection.toList() }); + } } else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength) recalculateStatusBarDisplay(); 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 @@ -45,6 +45,13 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "x": target.x, + "z": target.z, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] @@ -85,6 +92,13 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "x": target.x, + "z": target.z, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] @@ -123,6 +137,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] @@ -169,6 +189,14 @@ "allowCapture": false }); + Engine.GuiInterfaceCall("DisplayWayPoint", + { + "entities": selection, + "x": target.x, + "z": target.z, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] @@ -229,6 +257,14 @@ "queued": queued, "allowCapture": false }); + + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "x": target.x, + "z": target.z, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_patrol", "entity": selection[0] }); return true; }, @@ -242,8 +278,8 @@ "hotkeyActionCheck": function(target, selection) { if (!someCanPatrol(selection) || - !Engine.HotkeyIsPressed("session.patrol") || - !getActionInfo("patrol", target).possible) + !Engine.HotkeyIsPressed("session.patrol") || + !getActionInfo("patrol", target).possible) return false; return { "type": "patrol", @@ -275,6 +311,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] @@ -326,6 +368,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] @@ -367,6 +415,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] @@ -438,6 +492,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] @@ -486,6 +546,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] @@ -547,6 +613,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": selection[0] @@ -638,6 +710,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] 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 @@ -32,6 +32,7 @@ this.timeNotificationID = 1; this.timeNotifications = []; this.entsRallyPointsDisplayed = []; + this.entsWayPointsDisplayed = []; this.entsWithAuraAndStatusBars = new Set(); this.enabledVisualRangeOverlayTypes = {}; }; @@ -964,43 +965,29 @@ GuiInterface.prototype.GetNonGaiaEntities = function() { - return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities(); + return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities(); }; -/** - * Displays the rally points of a given list of entities (carried in cmd.entities). - * - * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should - * be rendered, in order to support instantaneously rendering a rally point marker at a specified location - * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). - * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the - * RallyPoint component. - */ -GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) -{ - let cmpPlayer = QueryPlayerIDInterface(player); - // If there are some rally points already displayed, first hide them - for (let ent of this.entsRallyPointsDisplayed) +GuiInterface.prototype.DisplayItineraryPoint = function (player, cmd, itineraryPointsDisplayed, renderer_IID, ent_IID) { + let cmpPlayer = QueryPlayerIDInterface(player); + for (let ent of itineraryPointsDisplayed) { - let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); - if (cmpRallyPointRenderer) - cmpRallyPointRenderer.SetDisplayed(false); + let cmpItineraryPointRenderer = Engine.QueryInterface(ent, renderer_IID); + if (cmpItineraryPointRenderer) + cmpItineraryPointRenderer.SetDisplayed(false); } - this.entsRallyPointsDisplayed = []; + itineraryPointsDisplayed = []; - // Show the rally points for the passed entities - for (let ent of cmd.entities) + for (let ent of cmd.entities) { - let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); - if (!cmpRallyPointRenderer) + let cmpItineraryPointRenderer = Engine.QueryInterface(ent, renderer_IID); + if (!cmpItineraryPointRenderer) continue; - // entity must have a rally point component to display a rally point marker - // (regardless of whether cmd specifies a custom location) - let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); - if (!cmpRallyPoint) + let cmpItineraryPoint = Engine.QueryInterface(ent, ent_IID); + if (!cmpItineraryPoint) continue; // Verify the owner @@ -1014,29 +1001,59 @@ let pos; if (cmd.x && cmd.z) pos = cmd; + else if (cmd.target) + pos = Engine.QueryInterface(cmd.target, IID_Position).GetPosition(); else - pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set + pos = cmpItineraryPoint.GetPositions()[0]; // may return undefined if no rally point is set + + if (!pos) + continue; - if (pos) - { - // Only update the position if we changed it (cmd.queued is set) - if ("queued" in cmd) - if (cmd.queued == true) - cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z - else - cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z - - // rebuild the renderer when not set (when reading saved game or in case of building update) - else if (!cmpRallyPointRenderer.IsSet()) - for (let posi of cmpRallyPoint.GetPositions()) - cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z }); - - cmpRallyPointRenderer.SetDisplayed(true); - - // remember which entities have their rally points displayed so we can hide them again - this.entsRallyPointsDisplayed.push(ent); - } + // Only update the position if we changed it (cmd.queued is set) + if ("queued" in cmd) + if (cmd.queued == true) + cmpItineraryPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z + else + cmpItineraryPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z + + // Rebuild the renderer when not set (when reading saved game or in case of building update) + else if (!cmpItineraryPointRenderer.IsSet()) + for (let posi of cmpItineraryPoint.GetPositions()) + cmpItineraryPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z }); + + cmpItineraryPointRenderer.SetDisplayed(true); + itineraryPointsDisplayed.push(ent); } + + return itineraryPointsDisplayed; +} + +/** + * Displays the rally points of a given list of entities (carried in cmd.entities). + * + * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should + * be rendered, in order to support instantaneously rendering a rally point marker at a specified location + * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). + * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the + * RallyPoint component. + */ +GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) +{ + this.entsRallyPointsDisplayed = this.DisplayItineraryPoint(player, cmd, this.entsRallyPointsDisplayed, IID_RallyPointRenderer, IID_RallyPoint) +}; + +/** + * Displays the way points of a given list of entities (carried in cmd.entities). + * + * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the way point should + * be rendered or a target to retrieve that location, in order to support instantaneously rendering a way point + * marker at a specified location instead of incurring a delay before PostNetworkCommand is processed. + * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the + * WayPoint component. + */ +GuiInterface.prototype.DisplayWayPoint = function(player, cmd) +{ + this.entsWayPointsDisplayed = this.DisplayItineraryPoint(player, cmd, this.entsWayPointsDisplayed, IID_WayPointRenderer, IID_WayPoint) }; /** @@ -1990,7 +2007,7 @@ "SetStatusBars": 1, "GetPlayerEntities": 1, "GetNonGaiaEntities": 1, - "DisplayRallyPoint": 1, + "DisplayWayPoint": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, @@ -2001,6 +2018,8 @@ "GetTradingDetails": 1, "CanAttack": 1, "GetBatchTime": 1, + "DisplayRallyPoint": 1, + "DisplayItineraryPoint": 1, "IsMapRevealed": 1, "SetPathfinderDebugOverlay": 1, 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 @@ -529,6 +529,10 @@ this.PushOrderFront("Pack", { "force": true }); return; } + this.StackWayPoint({ "x": this.order.data.x, "z": this.order.data.z }); + let cmpWayPoint = Engine.QueryInterface(this.entity, IID_WayPoint); + if (cmpWayPoint) + cmpWayPoint.Shift(); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("INDIVIDUAL.PATROL"); @@ -3698,6 +3702,16 @@ this.orderQueue.shift(); this.order = this.orderQueue[0]; + // Remove current waypoint + let cmpWayPoint = Engine.QueryInterface(this.entity, IID_WayPoint); + if (cmpWayPoint) + { + cmpWayPoint.Shift(); + let cmpWayPointRenderer = Engine.QueryInterface(this.entity, IID_WayPointRenderer); + if (cmpWayPointRenderer) + cmpWayPointRenderer.Shift(); + } + // TODO: Waypoints for formations if (this.orderQueue.length) { @@ -3749,6 +3763,9 @@ var order = { "type": type, "data": data }; this.orderQueue.push(order); + if (!data.force) + this.StackWayPoint(data); + // If we didn't already have an order, then process this new one if (this.orderQueue.length == 1) { @@ -3779,6 +3796,7 @@ { var cheeringOrder = this.orderQueue.shift(); this.orderQueue.unshift(cheeringOrder, order); + // TODO: AddWayPoint } else if (this.order && this.IsPacking()) { @@ -3789,6 +3807,9 @@ { this.orderQueue.unshift(order); this.order = order; + + this.StackWayPoint(data); + let ret = this.UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data} ); @@ -3802,6 +3823,8 @@ this.orderQueue.shift(); this.order = this.orderQueue[0]; } + + // TODO: WayPoints } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); @@ -3862,11 +3885,14 @@ var order = { "type": type, "data": data }; var packingOrder = this.orderQueue.shift(); this.orderQueue = [packingOrder, order]; + + // TODO Waypoints } else { this.orderQueue = []; this.PushOrder(type, data); + this.ReplaceWayPoint(data); } if (garrisonHolder) @@ -3875,6 +3901,51 @@ Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; +UnitAI.prototype.StackWayPoint = function(data) +{ + let cmpWayPoint = Engine.QueryInterface(this.entity, IID_WayPoint); + if(!cmpWayPoint) + return; + + let pos; + let cmpPosition; + if (data.target && (cmpPosition = Engine.QueryInterface(data.target, IID_Position))) + pos = cmpPosition.GetPosition(); + else + pos = {'x': data.x, 'z': data.z }; + + cmpWayPoint.Stack(pos.x, pos.z); + let cmpWayPointRenderer = Engine.QueryInterface(this.entity, IID_WayPointRenderer); + if (cmpWayPointRenderer) + cmpWayPointRenderer.Stack({'x': pos.x, 'y': pos.z}); +}; + +UnitAI.prototype.ReplaceWayPoint = function(data) +{ + let cmpWayPoint = Engine.QueryInterface(this.entity, IID_WayPoint); + + if (!cmpWayPoint) + return; + + cmpWayPoint.Unset(); + + let pos; + let cmpPosition; + + if (data.target && (cmpPosition = Engine.QueryInterface(data.target, IID_Position))) + pos = cmpPosition.GetPosition(); + else if (data.x && data.z) + pos = {'x': data.x, 'z': data.z}; + else + return; + + cmpWayPoint.Stack(pos.x, pos.z); + // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z + let cmpWayPointRenderer = Engine.QueryInterface(this.entity, IID_WayPointRenderer); + if (cmpWayPointRenderer) + cmpWayPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); +}; + UnitAI.prototype.GetOrders = function() { return this.orderQueue.slice(); @@ -5316,6 +5387,10 @@ var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (cmpTrader.HasBothMarkets()) { + + this.StackWayPoint({"target": cmpTrader.GetFirstMarket()}); + this.StackWayPoint({"target": cmpTrader.GetSecondMarket()}); + let data = { "target": cmpTrader.GetFirstMarket(), "route": route, @@ -5370,6 +5445,14 @@ UnitAI.prototype.MoveToMarket = function(targetMarket) { + let cmpWayPoint = Engine.QueryInterface(this.entity, IID_WayPoint); + if (cmpWayPoint) + { + cmpWayPoint.Shift(); + let cmpWayPointRenderer = Engine.QueryInterface(this.entity, IID_WayPointRenderer); + if (cmpWayPointRenderer) + cmpWayPointRenderer.Shift(); + } if (this.waypoints && this.waypoints.length > 1) { let point = this.waypoints.pop(); @@ -5385,11 +5468,13 @@ if (!this.CanTrade(currentMarket)) { this.StopTrading(); + // TODO clear waypoints return; } if (!this.CheckTargetRange(currentMarket, IID_Trader)) { + this.StackWayPoint({"target": currentMarket}); if (!this.MoveToMarket(currentMarket)) // If the current market is not reached try again this.StopTrading(); return; Index: binaries/data/mods/public/simulation/components/WayPoint.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/WayPoint.js @@ -0,0 +1,33 @@ +function WayPoint() {} + +WayPoint.prototype.Schema = ""; + +WayPoint.prototype.Init = function() +{ + this.positions = []; +}; + +WayPoint.prototype.Stack = function(x, z) +{ + this.positions.push({ + 'x': x, + 'z': z + }); +}; + +WayPoint.prototype.GetPositions = function() +{ + return this.positions; +}; + +WayPoint.prototype.Unset = function() +{ + this.positions = []; +}; + +WayPoint.prototype.Shift = function() +{ + this.positions.shift(); +}; + +Engine.RegisterComponentType(IID_WayPoint, "WayPoint", WayPoint); Index: binaries/data/mods/public/simulation/components/interfaces/WayPoint.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/interfaces/WayPoint.js @@ -0,0 +1 @@ +Engine.RegisterInterface("WayPoint"); 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 @@ -13,6 +13,7 @@ Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); +Engine.LoadComponentScript("interfaces/WayPoint.js"); Engine.LoadComponentScript("Formation.js"); Engine.LoadComponentScript("UnitAI.js"); Index: binaries/data/mods/public/simulation/components/tests/test_WayPoint.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/tests/test_WayPoint.js @@ -0,0 +1,15 @@ +Engine.LoadComponentScript("interfaces/WayPoint.js"); +Engine.LoadComponentScript("WayPoint.js"); + +let entId = 1; +let cmpWayPoint = ConstructComponent(entId, "WayPoint", {}); + +TS_ASSERT_UNEVAL_EQUALS(cmpWayPoint.GetPositions(), []); +cmpWayPoint.Stack(10, 5); +TS_ASSERT_UNEVAL_EQUALS(cmpWayPoint.GetPositions(), [{ 'x': 10, 'z': 5 }]); +cmpWayPoint.Stack(6, 8); +TS_ASSERT_UNEVAL_EQUALS(cmpWayPoint.GetPositions(), [{ 'x': 10, 'z': 5 }, { 'x': 6, 'z': 8 }]); +cmpWayPoint.Shift(); +TS_ASSERT_UNEVAL_EQUALS(cmpWayPoint.GetPositions(), [{ 'x': 6, 'z': 8 }]); +cmpWayPoint.Unset(); +TS_ASSERT_UNEVAL_EQUALS(cmpWayPoint.GetPositions(), []); 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 @@ -150,6 +150,8 @@ GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued); }); + + StackWayPoint(player, cmd, data, { "x": cmd.x, "z": cmd.z }); }, "walk-to-range": function(player, cmd, data) @@ -168,6 +170,8 @@ GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued); }); + + StackWayPoint(player, cmd, data, { "x": cmd.x, "z": cmd.z }); }, "attack": function(player, cmd, data) @@ -181,6 +185,7 @@ GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.Attack(cmd.target, cmd.queued, allowCapture); }); + StackWayPoint(player, cmd, data); }, "patrol": function(player, cmd, data) @@ -188,6 +193,14 @@ GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.queued) ); + for(let ent of data.entities) + { + let cmpWayPoint = Engine.QueryInterface(ent, IID_WayPoint); + if (cmpWayPoint) + cmpWayPoint.Shift(); + } + + StackWayPoint(player, cmd, data, { "x": cmd.x, "z": cmd.z }); }, "heal": function(player, cmd, data) @@ -198,6 +211,8 @@ GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.Heal(cmd.target, cmd.queued); }); + + StackWayPoint(player, cmd, data); }, "repair": function(player, cmd, data) @@ -209,6 +224,7 @@ GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued); }); + }, "gather": function(player, cmd, data) @@ -226,6 +242,7 @@ GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued); }); + StackWayPoint(player, cmd, data, { "x": cmd.x, "z": cmd.z }); }, "returnresource": function(player, cmd, data) @@ -236,6 +253,7 @@ GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.ReturnResource(cmd.target, cmd.queued); }); + StackWayPoint(player, cmd, data); }, "back-to-work": function(player, cmd, data) @@ -457,6 +475,8 @@ GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.Garrison(cmd.target, cmd.queued); }); + + StackWayPoint(player, cmd, data); }, "guard": function(player, cmd, data) @@ -472,6 +492,8 @@ GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.Guard(cmd.target, cmd.queued); }); + + StackWayPoint(player, cmd, data); }, "stop": function(player, cmd, data) @@ -1291,6 +1313,11 @@ constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup(); } + // Waypoints + let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + if (cmpGuiInterface) + cmpGuiInterface.DisplayWayPoint(player, constructPieceCmd); + var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd); if (pieceEntityId) { @@ -1685,6 +1712,29 @@ return entities.filter(ent => CanControlUnitOrIsAlly(ent, player, controlAll)); } +/** + * Sets the js component garrison points to keep them in sync with the renderer + * @param {number} player - The player id + * @param {object} cmd - The command that was sent + * @param {object} data - additionnal data. + */ +function StackWayPoint(player, cmd, data, pos) +{ + if(!pos) + return; + + for (let ent of data.entities) + { + var cmpWayPoint = Engine.QueryInterface(ent, IID_WayPoint); + if (cmpWayPoint) + { + if (!cmd.queued) + cmpWayPoint.Unset(); + cmpWayPoint.Stack(pos.x, pos.z); + } + } +} + Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements); Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation); Engine.RegisterGlobal("GetDockAngle", GetDockAngle); 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 @@ -135,4 +135,17 @@ false false + + + special/rallypoint + art/textures/misc/rallypoint_line.png + art/textures/misc/rallypoint_line_mask.png + 0.2 + + + square + round + default + default + Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -193,3 +193,6 @@ INTERFACE(WaterManager) COMPONENT(WaterManager) + +INTERFACE(WayPointRenderer) +COMPONENT(WayPointRenderer) Index: source/simulation2/components/CCmpItineraryPointRenderer.h =================================================================== --- /dev/null +++ source/simulation2/components/CCmpItineraryPointRenderer.h @@ -0,0 +1,269 @@ +/* Copyright (C) 2017 Wildfire Games. +* This file is part of 0 A.D. +* +* 0 A.D. is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 of the License, or +* (at your option) any later version. +* +* 0 A.D. is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with 0 A.D. If not, see . +*/ +#ifndef INCLUDED_CCMPITINERARYPOINTRENDERER +#define INCLUDED_CCMPITINERARYPOINTRENDERER + +#include "simulation2/MessageTypes.h" +#include "simulation2/components/ICmpFootprint.h" +#include "simulation2/components/ICmpObstructionManager.h" +#include "simulation2/components/ICmpOwnership.h" +#include "simulation2/components/ICmpPathfinder.h" +#include "simulation2/components/ICmpPlayer.h" +#include "simulation2/components/ICmpPlayerManager.h" +#include "simulation2/components/ICmpPosition.h" +#include "simulation2/components/ICmpRangeManager.h" +#include "simulation2/components/ICmpTerrain.h" +#include "simulation2/components/ICmpVisual.h" +#include "simulation2/components/ICmpWaterManager.h" +#include "simulation2/helpers/Render.h" +#include "simulation2/helpers/Geometry.h" +#include "simulation2/system/Component.h" + +#include "graphics/Overlay.h" +#include "graphics/TextureManager.h" +#include "ps/CLogger.h" +#include "ps/Profile.h" +#include "renderer/Renderer.h" + +struct SVisibilitySegment +{ + bool m_Visible; + + size_t m_StartIndex; + + size_t m_EndIndex; // inclusive + + SVisibilitySegment(bool visible, size_t startIndex, size_t endIndex) + : m_Visible(visible), m_StartIndex(startIndex), m_EndIndex(endIndex) + {} + + bool operator==(const SVisibilitySegment& other) const + { + return (m_Visible == other.m_Visible && m_StartIndex == other.m_StartIndex && m_EndIndex == other.m_EndIndex); + } + + bool operator!=(const SVisibilitySegment& other) const + { + return !(*this == other); + } + + bool IsSinglePoint() const + { + return (m_StartIndex == m_EndIndex); + } +}; + +class CCmpItineraryPointRenderer +{ +public: + static void ClassInit(CComponentManager& componentManager); + + static void MergeVisibilitySegments(std::vector& segments); + + virtual void Init(const CParamNode& paramNode); + + virtual void Deinit(); + + virtual void Serialize(ISerializer& UNUSED(serialize)); + + virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)); + + virtual void AddPosition_wrapper(const CFixedVector2D&) = 0; + + virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) = 0; + /** + * Returns true if at least one display rally point is set; i.e., if we have a point to render our marker/line at. + */ + virtual bool IsSet() const = 0; + + virtual void SetDisplayed(bool displayed) = 0; + + virtual void SetPosition(const CFixedVector2D& pos) = 0; + + virtual ~CCmpItineraryPointRenderer() = 0; + +protected: + + /// Import some types for less verbosity + typedef WaypointPath Path; + typedef PathGoal Goal; + typedef ICmpRangeManager::CLosQuerier CLosQuerier; + typedef SOverlayTexturedLine::LineCapType LineCapType; + + ///< Should we render the itinerary points and the path lines? (set from JS when e.g. the unit is selected/deselected) + bool m_Displayed; + + ///< Smooth the path before rendering? + bool m_SmoothPath; + + /// Draw little overlay circles to indicate where the exact path points are? + bool m_EnableDebugNodeOverlay; + + size_t m_LastMarkerCount; + + ///< Last seen owner of this entity (used to keep track of ownership changes). + player_id_t m_LastOwner; + + CColor m_LineColor; + + CColor m_LineDashColor; + + CTexturePtr m_Texture; + + CTexturePtr m_TextureMask; + + /// Marker connector line settings (loaded from XML) + float m_LineThickness; + + LineCapType m_LineStartCapType; + + LineCapType m_LineEndCapType; + + /// Display position of the points. Note that this are merely the display positions; they not necessarily the same as the + /// actual positions used in the simulation at any given time. In particular, we need this separate copy to support + /// instantaneously rendering the way point markers/lines when the user sets one in-game (instead of waiting until the + /// network-synchronization code sets it on the WayPoint component, which might take up to half a second). + std::vector m_ItineraryPoints; + + /// Full path to the itinerary points as returned by the pathfinder, with some post-processing applied to reduce zig/zagging. + std::vector > m_Path; + + /// Visibility segments of the rally point paths; splits the path into SoD/non-SoD segments. + std::vector > m_VisibilitySegments; + + /// Entity IDs of the way point markers. + std::vector m_MarkerEntityIds; + + std::vector > m_DebugNodeOverlays; + + /// Textured overlay lines to be used for rendering the marker line. There can be multiple because we may need to render + /// dashes for segments that are inside the SoD. + std::vector > m_TexturedOverlayLines; + + /// Pathfinder passability class to use for computing the (long-range) marker line path. + std::string m_LinePassabilityClass; + + /// Template name of the rally point markers. + std::wstring m_MarkerTemplate; + + std::wstring m_LineTexturePath; + + std::wstring m_LineTextureMaskPath; + + ///< Pathfinder cost class to use for computing the (long-range) marker line path. + std::string m_LineCostClass; + + /** + * Helper function for AddPosition_wrapper and SetPosition. + */ + virtual void AddPosition(CFixedVector2D pos, bool recompute) = 0; + + /** + + * Sets up all overlay lines for rendering according to the current full path and visibility segments. Splits the line into solid + * and dashed pieces (for the SoD). Should be called whenever the SoD has changed. If no full path is currently set, this method + * does nothing. + */ + virtual void ConstructAllOverlayLines() = 0; + + /** + * Sets up the overlay lines for rendering according to the full path and visibility segments at @p index. Splits the line into + * solid and dashed pieces (for the SoD). Should be called whenever the SoD of the path at @p index has changed. + */ + virtual void ConstructOverlayLines(size_t index) = 0; + + /** + * Removes points from @p coords that are obstructed by the originating building's footprint, and links up the last point + * nicely to the edge of the building's footprint. Only needed if the pathfinder can possibly return obstructed tile waypoints, + * i.e. when pathfinding is started from an obstructed tile. + */ + virtual void FixFootprintWaypoints(std::vector& coords, CmpPtr cmpPosition, CmpPtr cmpFootprint) const = 0; + + /** + * Get the point on the footprint edge that's as close from "start" as possible. + */ + virtual void GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr cmpPosition, CmpPtr cmpFootprint) const = 0; + + /** + * Returns a list of indices of waypoints in the current path (m_Path[index]) where the LOS visibility changes, ordered from + * building/previous rally point to rally point. Used to construct the overlay line segments and track changes to the SoD. + */ + virtual void GetVisibilitySegments(std::vector& out, size_t index) const = 0; + + /** + * Helper function to GetVisibilitySegments, factored out for testing. Merges single-point segments with its neighbouring + * segments. You should not have to call this method directly. + */ + virtual void RecomputeAllItineraryPointPaths() = 0; + + /** + * Recomputes the full path from this entity/the previous rally point to the next rally point, and does all the necessary + * post-processing to make it prettier. This doesn't check if we have a valid position or if a rally point is set. + * + * You shouldn't need to call this method directly. + */ + virtual void RecomputeItineraryPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& cmpFootprint, CmpPtr cmpPathfinder) = 0; + + /** + * Recomputes the full path for m_Path[ @p index], and does all the necessary post-processing to make it prettier. + * + * Should be called whenever either the starting position or the rally point's position changes. + */ + virtual void RecomputeItineraryPointPath_wrapper(size_t index) = 0; + + /** + * Repositions the way point markers; moves them outside of the world (ie. hides them), or positions them at the currently + * set way points. Also updates the actor's variation according to the entity's current owning player's civilization. + * + * Should be called whenever either the position of a way point changes (including whether it is set or not), or the display + * flag changes, or the ownership of the entity changes. + */ + virtual void UpdateMarkers() = 0; + + /* + * Must be called whenever m_Displayed or the size of m_RallyPoints change, + * to determine whether we need to respond to render messages. + */ + virtual void UpdateMessageSubscriptions() = 0; + + /** + * Checks for changes to the SoD to the previously saved state, and reconstructs the visibility segments and overlay lines to + * match if necessary. Does nothing if the rally point lines are not currently set to be displayed, or if no rally point is set. + */ + virtual void UpdateOverlayLines() = 0; + + /** + * Simplifies the path by removing waypoints that lie between two points that are visible from one another. This is primarily + * intended to reduce some unnecessary curviness of the path; the pathfinder returns a mathematically (near-)optimal path, which + * will happily curve and bend to reduce costs. Visually, it doesn't make sense for a rally point path to curve and bend when it + * could just as well have gone in a straight line; that's why we have this, to make it look more natural. + * + * @p coords array of path coordinates to simplify + * @p maxSegmentLinks if non-zero, indicates the maximum amount of consecutive node-to-node links that can be joined into a + * single link. If this value is set to e.g. 1, then no reductions will be performed. A value of 3 means that + * at most 3 consecutive node links will be joined into a single link. + * @p floating whether to consider nodes who are under the water level as floating on top of the water + */ + virtual void ReduceSegmentsByVisibility(std::vector& coords, unsigned maxSegmentLinks = 0, bool floating = true) const = 0; + + /** + * Displays the rally point, and if m_EnableDebugNodeOverlay is set the debug overlay + */ + virtual void RenderSubmit(SceneCollector& collector) = 0; +}; + +#endif // INCLUDED_CCMPITINERARYPOINTRENDERER \ No newline at end of file Index: source/simulation2/components/CCmpItineraryPointRenderer.cpp =================================================================== --- /dev/null +++ source/simulation2/components/CCmpItineraryPointRenderer.cpp @@ -0,0 +1,294 @@ +/* Copyright (C) 2017 Wildfire Games. +* This file is part of 0 A.D. +* +* 0 A.D. is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 of the License, or +* (at your option) any later version. +* +* 0 A.D. is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with 0 A.D. If not, see . +*/ + +#include "precompiled.h" +#include "CCmpItineraryPointRenderer.h" + +void CCmpItineraryPointRenderer::ClassInit(CComponentManager& componentManager) +{ + componentManager.SubscribeToMessageType(MT_OwnershipChanged); + componentManager.SubscribeToMessageType(MT_TurnStart); + componentManager.SubscribeToMessageType(MT_Destroy); + componentManager.SubscribeToMessageType(MT_PositionChanged); +} + +void CCmpItineraryPointRenderer::MergeVisibilitySegments(std::vector& segments) +{ + // Scan for single-point segments; if they are inbetween two other segments, delete them and merge the surrounding segments. + // If they're at either end of the path, include them in their bordering segment (but only if those bordering segments aren't + // themselves single-point segments, because then we would want those to get absorbed by its surrounding ones first). + + // first scan for absorptions of single-point surrounded segments (i.e. excluding edge segments) + size_t numSegments = segments.size(); + + // WARNING: FOR LOOP TRICKERY AHEAD! + for (size_t i = 1; i < numSegments - 1;) + { + SVisibilitySegment& segment = segments[i]; + if (segment.IsSinglePoint()) + { + // since the segments' visibility alternates, the surrounding ones should have the same visibility + ENSURE(segments[i - 1].m_Visible == segments[i + 1].m_Visible); + + segments[i - 1].m_EndIndex = segments[i + 1].m_EndIndex; // make previous segment span all the way across to the next + segments.erase(segments.begin() + i); // erase this segment ... + segments.erase(segments.begin() + i); // and the next (we removed [i], so [i+1] is now at position [i]) + numSegments -= 2; // we removed 2 segments, so update the loop condition + // in the next iteration, i should still point to the segment right after the one that got expanded, which is now + // at position i; so don't increment i here + } + else + { + ++i; + } + } + + ENSURE(numSegments == segments.size()); + + // check to see if the first segment needs to be merged with its neighbour + if (segments.size() >= 2 && segments[0].IsSinglePoint()) + { + int firstSegmentStartIndex = segments.front().m_StartIndex; + ENSURE(firstSegmentStartIndex == 0); + ENSURE(!segments[1].IsSinglePoint()); // at this point, the second segment should never be a single-point segment + + segments.erase(segments.begin()); + segments.front().m_StartIndex = firstSegmentStartIndex; + } + + // check to see if the last segment needs to be merged with its neighbour + if (segments.size() >= 2 && segments[segments.size() - 1].IsSinglePoint()) + { + int lastSegmentEndIndex = segments.back().m_EndIndex; + ENSURE(!segments[segments.size() - 2].IsSinglePoint()); // at this point, the second-to-last segment should never be a single-point segment + + segments.erase(segments.end()); + segments.back().m_EndIndex = lastSegmentEndIndex; + } + + // -------------------------------------------------------------------------------------------------------- + // at this point, every segment should have at least 2 points + for (size_t i = 0; i < segments.size(); ++i) + { + ENSURE(!segments[i].IsSinglePoint()); + ENSURE(segments[i].m_EndIndex > segments[i].m_StartIndex); + } +} + +void CCmpItineraryPointRenderer::Init(const CParamNode& paramNode) +{ + m_Displayed = false; + m_SmoothPath = true; + m_LastOwner = INVALID_PLAYER; + m_LastMarkerCount = 0; + m_EnableDebugNodeOverlay = false; + // --------------------------------------------------------------------------------------------- + // load some XML configuration data (schema guarantees that all these nodes are valid) + + m_MarkerTemplate = paramNode.GetChild("MarkerTemplate").ToString(); + + const CParamNode& lineColor = paramNode.GetChild("LineColor"); + m_LineColor = CColor( + lineColor.GetChild("@r").ToInt() / 255.f, + lineColor.GetChild("@g").ToInt() / 255.f, + lineColor.GetChild("@b").ToInt() / 255.f, + 1.f + ); + + const CParamNode& lineDashColor = paramNode.GetChild("LineDashColor"); + m_LineDashColor = CColor( + lineDashColor.GetChild("@r").ToInt() / 255.f, + lineDashColor.GetChild("@g").ToInt() / 255.f, + lineDashColor.GetChild("@b").ToInt() / 255.f, + 1.f + ); + + m_LineThickness = paramNode.GetChild("LineThickness").ToFixed().ToFloat(); + m_LineTexturePath = paramNode.GetChild("LineTexture").ToString(); + m_LineTextureMaskPath = paramNode.GetChild("LineTextureMask").ToString(); + m_LineStartCapType = SOverlayTexturedLine::StrToLineCapType(paramNode.GetChild("LineStartCap").ToString()); + m_LineEndCapType = SOverlayTexturedLine::StrToLineCapType(paramNode.GetChild("LineEndCap").ToString()); + m_LineCostClass = paramNode.GetChild("LineCostClass").ToUTF8(); + m_LinePassabilityClass = paramNode.GetChild("LinePassabilityClass").ToUTF8(); + + // --------------------------------------------------------------------------------------------- + // load some textures + if (CRenderer::IsInitialised()) + { + CTextureProperties texturePropsBase(m_LineTexturePath); + texturePropsBase.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); + texturePropsBase.SetMaxAnisotropy(4.f); + m_Texture = g_Renderer.GetTextureManager().CreateTexture(texturePropsBase); + + CTextureProperties texturePropsMask(m_LineTextureMaskPath); + texturePropsMask.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); + texturePropsMask.SetMaxAnisotropy(4.f); + m_TextureMask = g_Renderer.GetTextureManager().CreateTexture(texturePropsMask); + } +} + +void CCmpItineraryPointRenderer::Deinit() {} + +void CCmpItineraryPointRenderer::Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) +{ + Init(paramNode); +} + +void CCmpItineraryPointRenderer::Serialize(ISerializer& UNUSED(serialize)) +{ + // Do not serialize anything; this is a rendering-only component, it does not and should not affect simulation state +} + +void CCmpItineraryPointRenderer::AddPosition(CFixedVector2D pos, bool recompute) +{ + m_ItineraryPoints.push_back(pos); + UpdateMarkers(); + + if (recompute) + RecomputeAllItineraryPointPaths(); + else + RecomputeItineraryPointPath_wrapper(m_ItineraryPoints.size() - 1); + + UpdateMessageSubscriptions(); +} + +void CCmpItineraryPointRenderer::AddPosition_wrapper(const CFixedVector2D& pos) +{ + AddPosition(pos, false); +} + +void CCmpItineraryPointRenderer::ConstructAllOverlayLines() +{ + m_TexturedOverlayLines.clear(); + + for (size_t i = 0; i < m_Path.size(); ++i) + ConstructOverlayLines(i); +} + +bool CCmpItineraryPointRenderer::IsSet() const +{ + return !m_ItineraryPoints.empty(); +} + +void CCmpItineraryPointRenderer::RenderSubmit(SceneCollector& collector) +{ + // we only get here if the rally point is set and should be displayed + for (std::vector >::iterator it = m_TexturedOverlayLines.begin(); + it != m_TexturedOverlayLines.end(); ++it) + { + for (std::vector::iterator iter = (*it).begin(); iter != (*it).end(); ++iter) + { + if (!(*iter).m_Coords.empty()) + collector.Submit(&(*iter)); + } + } + + if (m_EnableDebugNodeOverlay && !m_DebugNodeOverlays.empty()) + { + for (size_t i = 0; i < m_DebugNodeOverlays.size(); ++i) + for (size_t j = 0; j < m_DebugNodeOverlays[i].size(); ++j) + collector.Submit(&m_DebugNodeOverlays[i][j]); + } +} + +void CCmpItineraryPointRenderer::SetDisplayed(bool displayed) +{ + if (m_Displayed != displayed) + { + m_Displayed = displayed; + + // move the markers out of oblivion and back into the real world, or vice-versa + UpdateMarkers(); + + // Check for changes to the SoD and update the overlay lines accordingly. We need to do this here because this method + // only takes effect when the display flag is active; we need to pick up changes to the SoD that might have occurred + // while this rally point was not being displayed. + UpdateOverlayLines(); + + UpdateMessageSubscriptions(); + } +} + +void CCmpItineraryPointRenderer::SetPosition(const CFixedVector2D& pos) +{ + if (!(m_ItineraryPoints.size() == 1 && m_ItineraryPoints.front() == pos)) + { + m_ItineraryPoints.clear(); + AddPosition(pos, true); + // Don't need to UpdateMessageSubscriptions here since AddPosition already calls it + } +} + +void CCmpItineraryPointRenderer::UpdateOverlayLines() +{ + // We should only do this if the rally point is currently being displayed and set inside the world, otherwise it's a massive + // waste of time to calculate all this stuff (this method is called every turn) + if (!m_Displayed || !CCmpItineraryPointRenderer::IsSet()) + return; + + // see if there have been any changes to the SoD by grabbing the visibility edge points and comparing them to the previous ones + std::vector > newVisibilitySegments; + for (size_t i = 0; i < m_Path.size(); ++i) + { + std::vector tmp; + newVisibilitySegments.push_back(tmp); + GetVisibilitySegments(newVisibilitySegments[i], i); + } + + // Check if the full path changed, then reconstruct all overlay lines, otherwise check if a segment changed and update that. + if (m_VisibilitySegments.size() != newVisibilitySegments.size()) + { + m_VisibilitySegments = newVisibilitySegments; // save the new visibility segments to compare against next time + ConstructAllOverlayLines(); + } + else + { + for (size_t i = 0; i < m_VisibilitySegments.size(); ++i) + { + if (m_VisibilitySegments[i] != newVisibilitySegments[i]) + { + // The visibility segments have changed, reconstruct the overlay lines to match. NOTE: The path itself doesn't + // change, only the overlay lines we construct from it. + m_VisibilitySegments[i] = newVisibilitySegments[i]; // save the new visibility segments to compare against next time + ConstructOverlayLines(i); + } + } + } +} + +CCmpItineraryPointRenderer::~CCmpItineraryPointRenderer() +{ + +} + + + + + + + + + + + + + + + + + + Index: source/simulation2/components/CCmpRallyPointRenderer.h =================================================================== --- /dev/null +++ source/simulation2/components/CCmpRallyPointRenderer.h @@ -0,0 +1,87 @@ +/* Copyright (C) 2017 Wildfire Games. +* This file is part of 0 A.D. +* +* 0 A.D. is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 of the License, or +* (at your option) any later version. +* +* 0 A.D. is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with 0 A.D. If not, see . +*/ + +#ifndef INCLUDED_CCMPRALLYPOINTRENDERER +#define INCLUDED_CCMPRALLYPOINTRENDERER + +#include "CCmpItineraryPointRenderer.h" +#include "ICmpRallyPointRenderer.h" + +#include "simulation2/MessageTypes.h" +#include "simulation2/components/ICmpFootprint.h" +#include "simulation2/components/ICmpObstructionManager.h" +#include "simulation2/components/ICmpOwnership.h" +#include "simulation2/components/ICmpPathfinder.h" +#include "simulation2/components/ICmpPlayer.h" +#include "simulation2/components/ICmpPlayerManager.h" +#include "simulation2/components/ICmpPosition.h" +#include "simulation2/components/ICmpRangeManager.h" +#include "simulation2/components/ICmpTerrain.h" +#include "simulation2/components/ICmpVisual.h" +#include "simulation2/components/ICmpWaterManager.h" +#include "simulation2/helpers/Render.h" +#include "simulation2/helpers/Geometry.h" +#include "simulation2/system/Component.h" + +#include "graphics/Overlay.h" +#include "graphics/TextureManager.h" +#include "ps/CLogger.h" +#include "ps/Profile.h" +#include "renderer/Renderer.h" + +class CCmpRallyPointRenderer : public CCmpItineraryPointRenderer, public ICmpRallyPointRenderer +{ + +public: + DEFAULT_COMPONENT_ALLOCATOR(RallyPointRenderer) + + static std::string GetSchema(); + + virtual void Init(const CParamNode& paramNode); + virtual void Deinit(); + virtual void Serialize(ISerializer& serialize); + virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize); + + virtual void AddPosition_wrapper(const CFixedVector2D& pos); + virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)); + virtual bool IsSet() const; + virtual void Reset(); + virtual void SetDisplayed(bool displayed); + virtual void SetPosition(const CFixedVector2D& pos); + virtual void UpdateMessageSubscriptions(); + virtual void UpdatePosition(u32 rallyPointId, const CFixedVector2D& pos); + +protected: + + virtual void AddPosition(CFixedVector2D pos, bool recompute); + virtual void ConstructAllOverlayLines(); + virtual void ConstructOverlayLines(size_t index); + virtual void FixFootprintWaypoints(std::vector& coords, CmpPtr cmpPosition, CmpPtr cmpFootprint) const; + virtual void GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr cmpPosition, CmpPtr cmpFootprint) const; + virtual void GetVisibilitySegments(std::vector& out, size_t index) const; + virtual void RecomputeAllItineraryPointPaths(); + virtual void RecomputeItineraryPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& cmpFootprint, CmpPtr cmpPathfinder); + virtual void RecomputeItineraryPointPath_wrapper(size_t index); + virtual void ReduceSegmentsByVisibility(std::vector& coords, unsigned maxSegmentLinks = 0, bool floating = true) const; + virtual void RenderSubmit(SceneCollector& collector); + virtual void UpdateMarkers(); + virtual void UpdateOverlayLines(); +}; + +REGISTER_COMPONENT_TYPE(RallyPointRenderer) + +#endif // INCLUDED_CCMPRALLYPOINTRENDERER Index: source/simulation2/components/CCmpRallyPointRenderer.cpp =================================================================== --- source/simulation2/components/CCmpRallyPointRenderer.cpp +++ source/simulation2/components/CCmpRallyPointRenderer.cpp @@ -16,725 +16,105 @@ */ #include "precompiled.h" -#include "ICmpRallyPointRenderer.h" - -#include "simulation2/MessageTypes.h" -#include "simulation2/components/ICmpFootprint.h" -#include "simulation2/components/ICmpObstructionManager.h" -#include "simulation2/components/ICmpOwnership.h" -#include "simulation2/components/ICmpPathfinder.h" -#include "simulation2/components/ICmpPlayer.h" -#include "simulation2/components/ICmpPlayerManager.h" -#include "simulation2/components/ICmpPosition.h" -#include "simulation2/components/ICmpRangeManager.h" -#include "simulation2/components/ICmpTerrain.h" -#include "simulation2/components/ICmpVisual.h" -#include "simulation2/components/ICmpWaterManager.h" -#include "simulation2/helpers/Render.h" -#include "simulation2/helpers/Geometry.h" -#include "simulation2/system/Component.h" - -#include "graphics/Overlay.h" -#include "graphics/TextureManager.h" -#include "ps/CLogger.h" -#include "ps/Profile.h" -#include "renderer/Renderer.h" - -struct SVisibilitySegment -{ - bool m_Visible; - size_t m_StartIndex; - size_t m_EndIndex; // inclusive - - SVisibilitySegment(bool visible, size_t startIndex, size_t endIndex) - : m_Visible(visible), m_StartIndex(startIndex), m_EndIndex(endIndex) - {} - - bool operator==(const SVisibilitySegment& other) const - { - return (m_Visible == other.m_Visible && m_StartIndex == other.m_StartIndex && m_EndIndex == other.m_EndIndex); - } - - bool operator!=(const SVisibilitySegment& other) const - { - return !(*this == other); - } +#include "CCmpRallyPointRenderer.h" - bool IsSinglePoint() const - { - return (m_StartIndex == m_EndIndex); - } -}; - -class CCmpRallyPointRenderer : public ICmpRallyPointRenderer +std::string CCmpRallyPointRenderer::GetSchema() { - // import some types for less verbosity - typedef WaypointPath Path; - typedef PathGoal Goal; - typedef ICmpRangeManager::CLosQuerier CLosQuerier; - typedef SOverlayTexturedLine::LineCapType LineCapType; - -public: - static void ClassInit(CComponentManager& componentManager) - { - componentManager.SubscribeToMessageType(MT_OwnershipChanged); - componentManager.SubscribeToMessageType(MT_TurnStart); - componentManager.SubscribeToMessageType(MT_Destroy); - componentManager.SubscribeToMessageType(MT_PositionChanged); - } - - DEFAULT_COMPONENT_ALLOCATOR(RallyPointRenderer) - -protected: - - /// Display position of the rally points. Note that this are merely the display positions; they not necessarily the same as the - /// actual positions used in the simulation at any given time. In particular, we need this separate copy to support - /// instantaneously rendering the rally point markers/lines when the user sets one in-game (instead of waiting until the - /// network-synchronization code sets it on the RallyPoint component, which might take up to half a second). - std::vector m_RallyPoints; - /// Full path to the rally points as returned by the pathfinder, with some post-processing applied to reduce zig/zagging. - std::vector > m_Path; - /// Visibility segments of the rally point paths; splits the path into SoD/non-SoD segments. - std::deque > m_VisibilitySegments; - - bool m_Displayed; ///< Should we render the rally points and the path lines? (set from JS when e.g. the unit is selected/deselected) - bool m_SmoothPath; ///< Smooth the path before rendering? - - std::vector m_MarkerEntityIds; ///< Entity IDs of the rally point markers. - size_t m_LastMarkerCount; - player_id_t m_LastOwner; ///< Last seen owner of this entity (used to keep track of ownership changes). - std::wstring m_MarkerTemplate; ///< Template name of the rally point markers. - - /// Marker connector line settings (loaded from XML) - float m_LineThickness; - CColor m_LineColor; - CColor m_LineDashColor; - LineCapType m_LineStartCapType; - LineCapType m_LineEndCapType; - std::wstring m_LineTexturePath; - std::wstring m_LineTextureMaskPath; - std::string m_LinePassabilityClass; ///< Pathfinder passability class to use for computing the (long-range) marker line path. - - CTexturePtr m_Texture; - CTexturePtr m_TextureMask; - - /// Textured overlay lines to be used for rendering the marker line. There can be multiple because we may need to render - /// dashes for segments that are inside the SoD. - std::vector > m_TexturedOverlayLines; - - /// Draw little overlay circles to indicate where the exact path points are? - bool m_EnableDebugNodeOverlay; - std::vector > m_DebugNodeOverlays; - -public: - - static std::string GetSchema() - { - return - "Displays a rally point marker where created units will gather when spawned" - "" - "special/rallypoint" - "0.75" - "round" - "square" - "" - "" - "default" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "0255" - "" - "" - "0255" - "" - "" - "0255" - "" - "" - "" - "" - "0255" - "" - "" - "0255" - "" - "" - "0255" - "" - "" - "" - "" - "flat" - "round" - "sharp" - "square" - "" - "" - "" - "" - "flat" - "round" - "sharp" - "square" - "" - "" - "" - "" - ""; - } - - virtual void Init(const CParamNode& paramNode); - - virtual void Deinit() - { - } - - virtual void Serialize(ISerializer& UNUSED(serialize)) - { - // do NOT serialize anything; this is a rendering-only component, it does not and should not affect simulation state - } - - virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) - { - Init(paramNode); - } - - virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) - { - switch (msg.GetType()) - { - case MT_RenderSubmit: - { - PROFILE("RallyPoint::RenderSubmit"); - if (m_Displayed && IsSet()) - { - const CMessageRenderSubmit& msgData = static_cast (msg); - RenderSubmit(msgData.collector); - } - } - break; - case MT_OwnershipChanged: - { - UpdateMarkers(); // update marker variation to new player's civilization - } - break; - case MT_TurnStart: - { - UpdateOverlayLines(); // check for changes to the SoD and update the overlay lines accordingly - } - break; - case MT_Destroy: - { - for (std::vector::iterator it = m_MarkerEntityIds.begin(); it < m_MarkerEntityIds.end(); ++it) - { - if (*it != INVALID_ENTITY) - { - GetSimContext().GetComponentManager().DestroyComponentsSoon(*it); - *it = INVALID_ENTITY; - } - } - } - break; - case MT_PositionChanged: - { - // Unlikely to happen in-game, but can occur in atlas - // Just recompute the path from the entity to the first rally point - RecomputeRallyPointPath_wrapper(0); - } - break; - } - } - - /* - * Must be called whenever m_Displayed or the size of m_RallyPoints change, - * to determine whether we need to respond to render messages. - */ - void UpdateMessageSubscriptions() - { - bool needRender = m_Displayed && IsSet(); - GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender); - } - - virtual void AddPosition_wrapper(const CFixedVector2D& pos) - { - AddPosition(pos, false); - } - - virtual void SetPosition(const CFixedVector2D& pos) - { - if (!(m_RallyPoints.size() == 1 && m_RallyPoints.front() == pos)) - { - m_RallyPoints.clear(); - AddPosition(pos, true); - // Don't need to UpdateMessageSubscriptions here since AddPosition already calls it - } - } - - virtual void UpdatePosition(u32 rallyPointId, const CFixedVector2D& pos) - { - if (rallyPointId >= m_RallyPoints.size()) - return; - - m_RallyPoints[rallyPointId] = pos; - - UpdateMarkers(); - - // Compute a new path for the current, and if existing the next rally point - RecomputeRallyPointPath_wrapper(rallyPointId); - if (rallyPointId+1 < m_RallyPoints.size()) - RecomputeRallyPointPath_wrapper(rallyPointId+1); - } - - virtual void SetDisplayed(bool displayed) - { - if (m_Displayed != displayed) - { - m_Displayed = displayed; - - // move the markers out of oblivion and back into the real world, or vice-versa - UpdateMarkers(); - - // Check for changes to the SoD and update the overlay lines accordingly. We need to do this here because this method - // only takes effect when the display flag is active; we need to pick up changes to the SoD that might have occurred - // while this rally point was not being displayed. - UpdateOverlayLines(); - - UpdateMessageSubscriptions(); - } - } - - virtual void Reset() - { - m_RallyPoints.clear(); - RecomputeAllRallyPointPaths(); - UpdateMessageSubscriptions(); - } - - /** - * Returns true if at least one display rally point is set; i.e., if we have a point to render our marker/line at. - */ - bool IsSet() const - { - return !m_RallyPoints.empty(); - } - -private: - - /** - * Helper function for AddPosition_wrapper and SetPosition. - */ - void AddPosition(CFixedVector2D pos, bool recompute) - { - m_RallyPoints.push_back(pos); - UpdateMarkers(); - - if (recompute) - RecomputeAllRallyPointPaths(); - else - RecomputeRallyPointPath_wrapper(m_RallyPoints.size()-1); - - UpdateMessageSubscriptions(); - } - - /** - * Repositions the rally point markers; moves them outside of the world (ie. hides them), or positions them at the currently - * set rally points. Also updates the actor's variation according to the entity's current owning player's civilization. - * - * Should be called whenever either the position of a rally point changes (including whether it is set or not), or the display - * flag changes, or the ownership of the entity changes. - */ - void UpdateMarkers(); - - /** - * Recomputes all the full paths from this entity to the rally point and from the rally point to the next, and does all the necessary - * post-processing to make them prettier. - * - * Should be called whenever all rally points' position changes. - */ - void RecomputeAllRallyPointPaths(); - - /** - * Recomputes the full path for m_Path[ @p index], and does all the necessary post-processing to make it prettier. - * - * Should be called whenever either the starting position or the rally point's position changes. - */ - void RecomputeRallyPointPath_wrapper(size_t index); - - /** - * Recomputes the full path from this entity/the previous rally point to the next rally point, and does all the necessary - * post-processing to make it prettier. This doesn't check if we have a valid position or if a rally point is set. - * - * You shouldn't need to call this method directly. - */ - void RecomputeRallyPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& cmpFootprint, CmpPtr cmpPathfinder); - - /** - * Checks for changes to the SoD to the previously saved state, and reconstructs the visibility segments and overlay lines to - * match if necessary. Does nothing if the rally point lines are not currently set to be displayed, or if no rally point is set. - */ - void UpdateOverlayLines(); - - /** - * Sets up all overlay lines for rendering according to the current full path and visibility segments. Splits the line into solid - * and dashed pieces (for the SoD). Should be called whenever the SoD has changed. If no full path is currently set, this method - * does nothing. - */ - void ConstructAllOverlayLines(); - - /** - * Sets up the overlay lines for rendering according to the full path and visibility segments at @p index. Splits the line into - * solid and dashed pieces (for the SoD). Should be called whenever the SoD of the path at @p index has changed. - */ - void ConstructOverlayLines(size_t index); - - /** - * Removes points from @p coords that are obstructed by the originating building's footprint, and links up the last point - * nicely to the edge of the building's footprint. Only needed if the pathfinder can possibly return obstructed tile waypoints, - * i.e. when pathfinding is started from an obstructed tile. - */ - void FixFootprintWaypoints(std::vector& coords, CmpPtr cmpPosition, CmpPtr cmpFootprint) const; - - /** - * Get the point on the footprint edge that's as close from "start" as possible. - */ - void GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr cmpPosition, CmpPtr cmpFootprint) const; - - /** - * Returns a list of indices of waypoints in the current path (m_Path[index]) where the LOS visibility changes, ordered from - * building/previous rally point to rally point. Used to construct the overlay line segments and track changes to the SoD. - */ - void GetVisibilitySegments(std::deque& out, size_t index) const; - - /** - * Simplifies the path by removing waypoints that lie between two points that are visible from one another. This is primarily - * intended to reduce some unnecessary curviness of the path; the pathfinder returns a mathematically (near-)optimal path, which - * will happily curve and bend to reduce costs. Visually, it doesn't make sense for a rally point path to curve and bend when it - * could just as well have gone in a straight line; that's why we have this, to make it look more natural. - * - * @p coords array of path coordinates to simplify - * @p maxSegmentLinks if non-zero, indicates the maximum amount of consecutive node-to-node links that can be joined into a - * single link. If this value is set to e.g. 1, then no reductions will be performed. A value of 3 means that - * at most 3 consecutive node links will be joined into a single link. - * @p floating whether to consider nodes who are under the water level as floating on top of the water - */ - void ReduceSegmentsByVisibility(std::vector& coords, unsigned maxSegmentLinks = 0, bool floating = true) const; - - /** - * Helper function to GetVisibilitySegments, factored out for testing. Merges single-point segments with its neighbouring - * segments. You should not have to call this method directly. - */ - static void MergeVisibilitySegments(std::deque& segments); - - void RenderSubmit(SceneCollector& collector); -}; - -REGISTER_COMPONENT_TYPE(RallyPointRenderer) + return + "Displays a rally point marker where created units will gather when spawned" + "" + "special/rallypoint" + "0.75" + "round" + "square" + "" + "" + "default" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "0255" + "" + "" + "0255" + "" + "" + "0255" + "" + "" + "" + "" + "0255" + "" + "" + "0255" + "" + "" + "0255" + "" + "" + "" + "" + "flat" + "round" + "sharp" + "square" + "" + "" + "" + "" + "flat" + "round" + "sharp" + "square" + "" + "" + "" + "" + ""; +} void CCmpRallyPointRenderer::Init(const CParamNode& paramNode) { - m_Displayed = false; - m_SmoothPath = true; - m_LastOwner = INVALID_PLAYER; - m_LastMarkerCount = 0; - m_EnableDebugNodeOverlay = false; - - // --------------------------------------------------------------------------------------------- - // load some XML configuration data (schema guarantees that all these nodes are valid) - - m_MarkerTemplate = paramNode.GetChild("MarkerTemplate").ToString(); - - const CParamNode& lineColor = paramNode.GetChild("LineColor"); - m_LineColor = CColor( - lineColor.GetChild("@r").ToInt()/255.f, - lineColor.GetChild("@g").ToInt()/255.f, - lineColor.GetChild("@b").ToInt()/255.f, - 1.f - ); - - const CParamNode& lineDashColor = paramNode.GetChild("LineDashColor"); - m_LineDashColor = CColor( - lineDashColor.GetChild("@r").ToInt()/255.f, - lineDashColor.GetChild("@g").ToInt()/255.f, - lineDashColor.GetChild("@b").ToInt()/255.f, - 1.f - ); - - m_LineThickness = paramNode.GetChild("LineThickness").ToFixed().ToFloat(); - m_LineTexturePath = paramNode.GetChild("LineTexture").ToString(); - m_LineTextureMaskPath = paramNode.GetChild("LineTextureMask").ToString(); - m_LineStartCapType = SOverlayTexturedLine::StrToLineCapType(paramNode.GetChild("LineStartCap").ToString()); - m_LineEndCapType = SOverlayTexturedLine::StrToLineCapType(paramNode.GetChild("LineEndCap").ToString()); - m_LinePassabilityClass = paramNode.GetChild("LinePassabilityClass").ToUTF8(); - - // --------------------------------------------------------------------------------------------- - // load some textures - - if (CRenderer::IsInitialised()) - { - CTextureProperties texturePropsBase(m_LineTexturePath); - texturePropsBase.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); - texturePropsBase.SetMaxAnisotropy(4.f); - m_Texture = g_Renderer.GetTextureManager().CreateTexture(texturePropsBase); - - CTextureProperties texturePropsMask(m_LineTextureMaskPath); - texturePropsMask.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); - texturePropsMask.SetMaxAnisotropy(4.f); - m_TextureMask = g_Renderer.GetTextureManager().CreateTexture(texturePropsMask); - } + CCmpItineraryPointRenderer::Init(paramNode); } - -void CCmpRallyPointRenderer::UpdateMarkers() +void CCmpRallyPointRenderer::Deinit() { - player_id_t previousOwner = m_LastOwner; - for (size_t i = 0; i < m_RallyPoints.size(); ++i) - { - if (i >= m_MarkerEntityIds.size()) - m_MarkerEntityIds.push_back(INVALID_ENTITY); - - if (m_MarkerEntityIds[i] == INVALID_ENTITY) - { - // no marker exists yet, create one first - CComponentManager& componentMgr = GetSimContext().GetComponentManager(); - - // allocate a new entity for the marker - if (!m_MarkerTemplate.empty()) - { - m_MarkerEntityIds[i] = componentMgr.AllocateNewLocalEntity(); - if (m_MarkerEntityIds[i] != INVALID_ENTITY) - m_MarkerEntityIds[i] = componentMgr.AddEntity(m_MarkerTemplate, m_MarkerEntityIds[i]); - } - } - - // the marker entity should be valid at this point, otherwise something went wrong trying to allocate it - if (m_MarkerEntityIds[i] == INVALID_ENTITY) - LOGERROR("Failed to create rally point marker entity"); - - CmpPtr markerCmpPosition(GetSimContext(), m_MarkerEntityIds[i]); - if (markerCmpPosition) - { - if (m_Displayed && IsSet()) - { - markerCmpPosition->MoveTo(m_RallyPoints[i].X, m_RallyPoints[i].Y); - } - else - { - markerCmpPosition->MoveOutOfWorld(); // hide it - } - } - - // set rally point flag selection based on player civilization - CmpPtr cmpOwnership(GetEntityHandle()); - if (!cmpOwnership) - continue; - - player_id_t ownerId = cmpOwnership->GetOwner(); - if (ownerId == INVALID_PLAYER || (ownerId == previousOwner && m_LastMarkerCount >= i)) - continue; - - m_LastOwner = ownerId; - CmpPtr cmpPlayerManager(GetSystemEntity()); - // cmpPlayerManager should not be null as long as this method is called on-demand instead of at Init() time - // (we can't rely on component initialization order in Init()) - if (!cmpPlayerManager) - continue; - - CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(ownerId)); - if (!cmpPlayer) - continue; - - CmpPtr cmpVisualActor(GetSimContext(), m_MarkerEntityIds[i]); - if (cmpVisualActor) - cmpVisualActor->SetVariant("civ", CStrW(cmpPlayer->GetCiv()).ToUTF8()); - } - m_LastMarkerCount = m_RallyPoints.size() - 1; + CCmpItineraryPointRenderer::Deinit(); } - -void CCmpRallyPointRenderer::RecomputeAllRallyPointPaths() +void CCmpRallyPointRenderer::Serialize(ISerializer& serialize) { - m_Path.clear(); - m_VisibilitySegments.clear(); - m_TexturedOverlayLines.clear(); - - //// /////////////////////////////////////////////// - if (m_EnableDebugNodeOverlay) - m_DebugNodeOverlays.clear(); - //// ////////////////////////////////////////////// - - if (!IsSet()) - return; // no use computing a path if the rally point isn't set - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; // no point going on if this entity doesn't have a position or is outside of the world - - CmpPtr cmpFootprint(GetEntityHandle()); - CmpPtr cmpPathfinder(GetSystemEntity()); - - for (size_t i = 0; i < m_RallyPoints.size(); ++i) - { - RecomputeRallyPointPath(i, cmpPosition, cmpFootprint, cmpPathfinder); - } + CCmpItineraryPointRenderer::Serialize(serialize); } - -void CCmpRallyPointRenderer::RecomputeRallyPointPath_wrapper(size_t index) +void CCmpRallyPointRenderer::Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { - if (!IsSet()) - return; // no use computing a path if the rally point isn't set - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; // no point going on if this entity doesn't have a position or is outside of the world - - CmpPtr cmpFootprint(GetEntityHandle()); - CmpPtr cmpPathfinder(GetSystemEntity()); - - RecomputeRallyPointPath(index, cmpPosition, cmpFootprint, cmpPathfinder); + CCmpItineraryPointRenderer::Deserialize(paramNode, deserialize); } -void CCmpRallyPointRenderer::RecomputeRallyPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& cmpFootprint, CmpPtr cmpPathfinder) +void CCmpRallyPointRenderer::AddPosition(CFixedVector2D pos, bool recompute) { - while (index >= m_Path.size()) - { - std::vector tmp; - m_Path.push_back(tmp); - } - m_Path[index].clear(); - - while (index >= m_VisibilitySegments.size()) - { - std::deque tmp; - m_VisibilitySegments.push_back(tmp); - } - m_VisibilitySegments[index].clear(); - - // Find a long path to the goal point -- this uses the tile-based pathfinder, which will return a - // list of waypoints (i.e. a Path) from the goal to the foundation/previous rally point, where each - // waypoint is centered at a tile. We'll have to do some post-processing on the path to get it smooth. - Path path; - std::vector& waypoints = path.m_Waypoints; - - CFixedVector2D start(cmpPosition->GetPosition2D()); - Goal goal = { Goal::POINT, m_RallyPoints[index].X, m_RallyPoints[index].Y }; - - if (index == 0) - GetClosestsEdgePointFrom(start,m_RallyPoints[index], cmpPosition, cmpFootprint); - else - { - start.X = m_RallyPoints[index-1].X; - start.Y = m_RallyPoints[index-1].Y; - } - cmpPathfinder->ComputePath(start.X, start.Y, goal, cmpPathfinder->GetPassabilityClass(m_LinePassabilityClass), path); - - // Check if we got a path back; if not we probably have two markers less than one tile apart. - if (path.m_Waypoints.size() < 2) - { - m_Path[index].emplace_back(start.X.ToFloat(), start.Y.ToFloat()); - m_Path[index].emplace_back(m_RallyPoints[index].X.ToFloat(), m_RallyPoints[index].Y.ToFloat()); - return; - } - else if (index == 0) - { - // sometimes this ends up not being optimal if you asked for a long path, so improve. - CFixedVector2D newend(waypoints[waypoints.size()-2].x,waypoints[waypoints.size()-2].z); - GetClosestsEdgePointFrom(newend,newend, cmpPosition, cmpFootprint); - waypoints.back().x = newend.X; - waypoints.back().z = newend.Y; - } - else - { - // make sure we actually start at the rallypoint because the pathfinder moves us to a usable tile. - waypoints.back().x = m_RallyPoints[index-1].X; - waypoints.back().z = m_RallyPoints[index-1].Y; - } - - // pathfinder makes us go to the nearest passable cell which isn't always what we want - waypoints[0].x = m_RallyPoints[index].X; - waypoints[0].z = m_RallyPoints[index].Y; - - // From here on, we choose to represent the waypoints as CVector2D floats to avoid to have to convert back and forth - // between fixed-point Waypoint/CFixedVector2D and various other float-based formats used by interpolation and whatnot. - // Since we'll only be further using these points for rendering purposes, using floats should be fine. - - for (Waypoint& waypoint : waypoints) - m_Path[index].emplace_back(waypoint.x.ToFloat(), waypoint.z.ToFloat()); - - // add the start position - // m_Path[index].emplace_back(m_RallyPoints[index].X.ToFloat(), m_RallyPoints[index].Y.ToFloat()); - - // Post-processing - - // Linearize the path; - // Pass through the waypoints, averaging each waypoint with its next one except the last one. Because the path - // goes from the marker to this entity/the previous flag and we want to keep the point at the marker's exact position, - // loop backwards through the waypoints so that the marker waypoint is maintained. - // TODO: see if we can do this at the same time as the waypoint -> coord conversion above - for(size_t i = m_Path[index].size() - 2; i > 0; --i) - m_Path[index][i] = (m_Path[index][i] + m_Path[index][i-1]) / 2.0f; - - // if there's a footprint and this path starts from this entity, remove any points returned by the pathfinder that may be on obstructed footprint tiles - //if (index == 0 && cmpFootprint) - // FixFootprintWaypoints(m_Path[index], cmpPosition, cmpFootprint); - - // Eliminate some consecutive waypoints that are visible from eachother. Reduce across a maximum distance of approx. 6 tiles - // (prevents segments that are too long to properly stick to the terrain) - ReduceSegmentsByVisibility(m_Path[index], 6); - - // Debug overlays - if (m_EnableDebugNodeOverlay) - { - while (index >= m_DebugNodeOverlays.size()) - m_DebugNodeOverlays.emplace_back(); - - m_DebugNodeOverlays[index].clear(); - } - if (m_EnableDebugNodeOverlay && m_SmoothPath) - { - // Create separate control point overlays so we can differentiate when using smoothing (offset them a little higher from the - // terrain so we can still see them after the interpolated points are added) - for (CVector2D& point : m_Path[index]) - { - SOverlayLine overlayLine; - overlayLine.m_Color = CColor(1.0f, 0.0f, 0.0f, 1.0f); - overlayLine.m_Thickness = 2; - SimRender::ConstructSquareOnGround(GetSimContext(), point.X, point.Y, 0.2f, 0.2f, 1.0f, overlayLine, true); - m_DebugNodeOverlays[index].push_back(overlayLine); - } - } - - if (m_SmoothPath) - // The number of points to interpolate goes hand in hand with the maximum amount of node links allowed to be joined together - // by the visibility reduction. The more node links that can be joined together, the more interpolated points you need to - // generate to be able to deal with local terrain height changes. - SimRender::InterpolatePointsRNS(m_Path[index], false, 0, 4); // no offset, keep line at its exact path - - // find which point is the last visible point before going into the SoD, so we have a point to compare to on the next turn - GetVisibilitySegments(m_VisibilitySegments[index], index); - - // build overlay lines for the new path - ConstructOverlayLines(index); + CCmpItineraryPointRenderer::AddPosition(pos, recompute); +} +void CCmpRallyPointRenderer::AddPosition_wrapper(const CFixedVector2D& pos) +{ + CCmpItineraryPointRenderer::AddPosition_wrapper(pos); } - void CCmpRallyPointRenderer::ConstructAllOverlayLines() { - m_TexturedOverlayLines.clear(); - - for (size_t i = 0; i < m_Path.size(); ++i) - ConstructOverlayLines(i); + CCmpItineraryPointRenderer::ConstructAllOverlayLines(); } - void CCmpRallyPointRenderer::ConstructOverlayLines(size_t index) { // We need to create a new SOverlayTexturedLine every time we want to change the coordinates after having passed it to the @@ -745,14 +125,19 @@ std::vector tmp; m_TexturedOverlayLines.push_back(tmp); } - m_TexturedOverlayLines[index].clear(); + + std::vector >::iterator iter = m_TexturedOverlayLines.begin(); + size_t count = index; + while (count--) + iter++; + (*iter).clear(); if (m_Path[index].size() < 2) return; LineCapType dashesLineCapType = SOverlayTexturedLine::LINECAP_ROUND; // line caps to use for the dashed segments (and any other segment's edges that border it) - for (std::deque::const_iterator it = m_VisibilitySegments[index].begin(); it != m_VisibilitySegments[index].end(); ++it) + for (std::vector::const_iterator it = m_VisibilitySegments[index].begin(); it != m_VisibilitySegments[index].end(); ++it) { const SVisibilitySegment& segment = (*it); @@ -785,7 +170,7 @@ overlayLine.m_Coords.push_back(m_Path[index][j].Y); } - m_TexturedOverlayLines[index].push_back(overlayLine); + (*iter).push_back(overlayLine); } else { @@ -809,7 +194,7 @@ // See how many pairs (dash + clear) of unmodified size can fit into the distance. Then check the remaining distance; if it's not exactly // a dash size's worth (which it probably won't be), then adjust the dash/clear sizes slightly so that it is. - int numFitUnmodified = floor(distance/(dashSize + clearSize)); + int numFitUnmodified = floor(distance / (dashSize + clearSize)); float remainderDistance = distance - (numFitUnmodified * (dashSize + clearSize)); // Now we want to make remainderDistance equal exactly one dash size (i.e. maxDashSize) by scaling dashSize and clearSize slightly. @@ -818,21 +203,20 @@ // (which will be positive or negative accordingly). This number can then be distributed further proportionally among the dash's // length and the clear's length. - // we always want to have at least one dash/clear pair (i.e., "|===| |===|"); also, we need to avoid division by zero below. + // We always want to have at least one dash/clear pair (i.e., "|===| |===|"); also, we need to avoid division by zero below. numFitUnmodified = std::max(1, numFitUnmodified); - float pairwiseLengthDifference = (remainderDistance - maxDashSize)/numFitUnmodified; // can be either positive or negative + // Can be either positive or negative + float pairwiseLengthDifference = (remainderDistance - maxDashSize) / numFitUnmodified; dashSize += pairDashRatio * pairwiseLengthDifference; clearSize += (1 - pairDashRatio) * pairwiseLengthDifference; - // ------------------------------------------------------------------------------------------------ - SDashedLine dashedLine; SimRender::ConstructDashedLine(straightLine, dashedLine, dashSize, clearSize); - // build overlay lines for dashes + // Build overlay lines for dashes size_t numDashes = dashedLine.m_StartIndices.size(); - for (size_t i=0; i < numDashes; i++) + for (size_t i = 0; i < numDashes; i++) { SOverlayTexturedLine dashOverlay; @@ -845,8 +229,8 @@ dashOverlay.m_StartCapType = dashesLineCapType; dashOverlay.m_EndCapType = dashesLineCapType; dashOverlay.m_AlwaysVisible = true; - // TODO: maybe adjust the elevation of the dashes to be a little lower, so that it slides underneath the actual path + // TODO: maybe adjust the elevation of the dashes to be a little lower, so that it slides underneath the actual path size_t dashStartIndex = dashedLine.m_StartIndices[i]; size_t dashEndIndex = dashedLine.GetEndIndex(i); ENSURE(dashEndIndex > dashStartIndex); @@ -857,13 +241,12 @@ dashOverlay.m_Coords.push_back(dashedLine.m_Points[n].Y); } - m_TexturedOverlayLines[index].push_back(dashOverlay); + (*iter).push_back(dashOverlay); } } } - //// ////////////////////////////////////////////// if (m_EnableDebugNodeOverlay) { while (index >= m_DebugNodeOverlays.size()) @@ -880,178 +263,385 @@ m_DebugNodeOverlays[index].push_back(overlayLine); } } - //// ////////////////////////////////////////////// } +void CCmpRallyPointRenderer::FixFootprintWaypoints(std::vector& coords, CmpPtr cmpPosition, CmpPtr cmpFootprint) const +{ + ENSURE(cmpPosition); + ENSURE(cmpFootprint); + + // ----------------------------------------------------------------------------------------------------- + // TODO: nasty fixed/float conversions everywhere + + // grab the shape and dimensions of the footprint + entity_pos_t footprintSize0, footprintSize1, footprintHeight; + ICmpFootprint::EShape footprintShape; + cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight); + + // grab the center of the footprint + CFixedVector2D center = cmpPosition->GetPosition2D(); + + // ----------------------------------------------------------------------------------------------------- + + switch (footprintShape) + { + case ICmpFootprint::SQUARE: + { + // in this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively. + + // the building's footprint could be rotated any which way, so let's get the rotation around the Y axis + // and the rotated unit vectors in the X/Z plane of the shape's footprint + // (the Footprint itself holds only the outline, the Position holds the orientation) + + fixed s, c; // sine and cosine of the Y axis rotation angle (aka the yaw) + fixed a = cmpPosition->GetRotation().Y; + sincos_approx(a, s, c); + CFixedVector2D u(c, -s); // unit vector along the rotated X axis + CFixedVector2D v(s, c); // unit vector along the rotated Z axis + CFixedVector2D halfSize(footprintSize0 / 2, footprintSize1 / 2); + + // starting from the start position, check if any points are within the footprint of the building + // (this is possible if the pathfinder was started from a point located within the footprint) + for (int i = (int)(coords.size() - 1); i >= 0; i--) + { + const CVector2D& wp = coords[i]; + if (Geometry::PointIsInSquare(CFixedVector2D(fixed::FromFloat(wp.X), fixed::FromFloat(wp.Y)) - center, u, v, halfSize)) + { + coords.erase(coords.begin() + i); + } + else + { + break; // point no longer inside footprint, from this point on neither will any of the following be + } + } + + // add a point right on the edge of the footprint (nearest to the last waypoint) so that it links up nicely with the rest of the path + CFixedVector2D lastWaypoint(fixed::FromFloat(coords.back().X), fixed::FromFloat(coords.back().Y)); + CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(lastWaypoint - center, u, v, halfSize); // relative to the shape origin (center) + CVector2D footprintEdge((center.X + footprintEdgePoint.X).ToFloat(), (center.Y + footprintEdgePoint.Y).ToFloat()); + coords.push_back(footprintEdge); + + } + break; + case ICmpFootprint::CIRCLE: + { + // in this case, both footprintSize0 and 1 indicate the circle's radius + + for (int i = (int)(coords.size() - 1); i >= 0; i--) + { + const CVector2D& wp = coords[i]; + fixed pointDistance = (CFixedVector2D(fixed::FromFloat(wp.X), fixed::FromFloat(wp.Y)) - center).Length(); + if (pointDistance <= footprintSize0) + { + coords.erase(coords.begin() + i); + } + else + { + break; // point no longer inside footprint, from this point on neither will any of the following be + } + } + + // add a point right on the edge of the footprint so that it links up nicely with the rest of the path + CVector2D centerVec2D(center.X.ToFloat(), center.Y.ToFloat()); + CVector2D centerToLast(coords.back() - centerVec2D); + coords.push_back(centerVec2D + (centerToLast.Normalized() * footprintSize0.ToFloat())); + } + break; + } +} +void CCmpRallyPointRenderer::GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr cmpPosition, CmpPtr cmpFootprint) const +{ + ENSURE(cmpPosition); + ENSURE(cmpFootprint); + + // grab the shape and dimensions of the footprint + entity_pos_t footprintSize0, footprintSize1, footprintHeight; + ICmpFootprint::EShape footprintShape; + cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight); + + // grab the center of the footprint + CFixedVector2D center = cmpPosition->GetPosition2D(); + + switch (footprintShape) + { + case ICmpFootprint::SQUARE: + { + // in this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively. + + // the building's footprint could be rotated any which way, so let's get the rotation around the Y axis + // and the rotated unit vectors in the X/Z plane of the shape's footprint + // (the Footprint itself holds only the outline, the Position holds the orientation) + + fixed s, c; // sine and cosine of the Y axis rotation angle (aka the yaw) + fixed a = cmpPosition->GetRotation().Y; + sincos_approx(a, s, c); + CFixedVector2D u(c, -s); // unit vector along the rotated X axis + CFixedVector2D v(s, c); // unit vector along the rotated Z axis + CFixedVector2D halfSize(footprintSize0 / 2, footprintSize1 / 2); + + CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(start - center, u, v, halfSize); + result = center + footprintEdgePoint; + break; + } + case ICmpFootprint::CIRCLE: + { + // in this case, both footprintSize0 and 1 indicate the circle's radius + // Transform target to the point nearest on the edge. + CFixedVector2D centerVec2D(center.X, center.Y); + CFixedVector2D centerToLast(start - centerVec2D); + centerToLast.Normalize(); + result = centerVec2D + (centerToLast.Multiply(footprintSize0)); + break; + } + } +} +void CCmpRallyPointRenderer::GetVisibilitySegments(std::vector& out, size_t index) const +{ + out.clear(); + + if (m_Path[index].size() < 2) + return; + + CmpPtr cmpRangeMgr(GetSystemEntity()); + + player_id_t currentPlayer = GetSimContext().GetCurrentDisplayedPlayer(); + CLosQuerier losQuerier(cmpRangeMgr->GetLosQuerier(currentPlayer)); + + // go through the path node list, comparing each node's visibility with the previous one. If it changes, end the current segment and start + // a new one at the next point. + + bool lastVisible = losQuerier.IsExplored( + (fixed::FromFloat(m_Path[index][0].X) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(), + (fixed::FromFloat(m_Path[index][0].Y) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest() + ); + size_t curSegmentStartIndex = 0; // starting node index of the current segment + + for (size_t k = 1; k < m_Path[index].size(); ++k) + { + // grab tile indices for this coord + int i = (fixed::FromFloat(m_Path[index][k].X) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); + int j = (fixed::FromFloat(m_Path[index][k].Y) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); + + bool nodeVisible = losQuerier.IsExplored(i, j); + if (nodeVisible != lastVisible) + { + // visibility changed; write out the segment that was just completed and get ready for the new one + out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, k - 1)); + + //curSegmentStartIndex = k; // new segment starts here + curSegmentStartIndex = k - 1; + lastVisible = nodeVisible; + } + + } + + // terminate the last segment + out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, m_Path[index].size() - 1)); + MergeVisibilitySegments(out); +} +void CCmpRallyPointRenderer::HandleMessage(const CMessage& msg, bool UNUSED(global)) +{ + switch (msg.GetType()) + { + case MT_RenderSubmit: + { + PROFILE("RallyPoint::RenderSubmit"); + if (m_Displayed && CCmpItineraryPointRenderer::IsSet()) + { + const CMessageRenderSubmit& msgData = static_cast (msg); + RenderSubmit(msgData.collector); + } + } + break; + case MT_OwnershipChanged: + { + UpdateMarkers(); // update marker variation to new player's civilization + } + break; + case MT_TurnStart: + { + UpdateOverlayLines(); // check for changes to the SoD and update the overlay lines accordingly + } + break; + case MT_Destroy: + { + for (std::vector::iterator it = m_MarkerEntityIds.begin(); it != m_MarkerEntityIds.end(); ++it) + { + if (*it != INVALID_ENTITY) + { + GetSimContext().GetComponentManager().DestroyComponentsSoon(*it); + *it = INVALID_ENTITY; + } + } + } + break; + case MT_PositionChanged: + { + // Unlikely to happen in-game, but can occur in atlas + // Just recompute the path from the entity to the first rally point + RecomputeItineraryPointPath_wrapper(0); + } + break; + } +} +bool CCmpRallyPointRenderer::IsSet() const +{ + return CCmpItineraryPointRenderer::IsSet(); +} +void CCmpRallyPointRenderer::RecomputeAllItineraryPointPaths() +{ + m_Path.clear(); + m_VisibilitySegments.clear(); + m_TexturedOverlayLines.clear(); + + if (!IsSet()) + return; + + CmpPtr cmpPosition(GetSimContext(), GetEntityId()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; + + CmpPtr cmpFootprint(GetSimContext(), GetEntityId()); + CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); + + for (size_t i = 0; i < m_ItineraryPoints.size(); ++i) + RecomputeItineraryPointPath(i, cmpPosition, cmpFootprint, cmpPathfinder); +} +void CCmpRallyPointRenderer::RecomputeItineraryPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& cmpFootprint, CmpPtr cmpPathfinder) +{ + while (index >= m_Path.size()) + { + std::vector tmp; + m_Path.push_back(tmp); + } + m_Path[index].clear(); + + while (index >= m_VisibilitySegments.size()) + { + std::vector tmp; + m_VisibilitySegments.push_back(tmp); + } + m_VisibilitySegments[index].clear(); + + // Find a long path to the goal point -- this uses the tile-based pathfinder, which will return a + // list of waypoints (i.e. a Path) from the goal to the foundation/previous rally point, where each + // waypoint is centered at a tile. We'll have to do some post-processing on the path to get it smooth. + Path path; + std::vector& waypoints = path.m_Waypoints; -void CCmpRallyPointRenderer::UpdateOverlayLines() -{ - // We should only do this if the rally point is currently being displayed and set inside the world, otherwise it's a massive - // waste of time to calculate all this stuff (this method is called every turn) - if (!m_Displayed || !IsSet()) - return; + CFixedVector2D start(cmpPosition->GetPosition2D()); + Goal goal = { Goal::POINT, m_ItineraryPoints[index].X, m_ItineraryPoints[index].Y }; - // see if there have been any changes to the SoD by grabbing the visibility edge points and comparing them to the previous ones - std::deque > newVisibilitySegments; - for (size_t i = 0; i < m_Path.size(); ++i) + if (index == 0) + GetClosestsEdgePointFrom(start, m_ItineraryPoints[index], cmpPosition, cmpFootprint); + else { - std::deque tmp; - newVisibilitySegments.push_back(tmp); - GetVisibilitySegments(newVisibilitySegments[i], i); + start.X = m_ItineraryPoints[index - 1].X; + start.Y = m_ItineraryPoints[index - 1].Y; } + cmpPathfinder->ComputePath(start.X, start.Y, goal, cmpPathfinder->GetPassabilityClass(m_LinePassabilityClass), path); - // Check if the full path changed, then reconstruct all overlay lines, otherwise check if a segment changed and update that. - if (m_VisibilitySegments.size() != newVisibilitySegments.size()) + // Check if we got a path back; if not we probably have two markers less than one tile apart. + if (path.m_Waypoints.size() < 2) + { + m_Path[index].emplace_back(start.X.ToFloat(), start.Y.ToFloat()); + m_Path[index].emplace_back(m_ItineraryPoints[index].X.ToFloat(), m_ItineraryPoints[index].Y.ToFloat()); + return; + } + else if (index == 0) { - m_VisibilitySegments = newVisibilitySegments; // save the new visibility segments to compare against next time - ConstructAllOverlayLines(); + // sometimes this ends up not being optimal if you asked for a long path, so improve. + CFixedVector2D newend(waypoints[waypoints.size() - 2].x, waypoints[waypoints.size() - 2].z); + GetClosestsEdgePointFrom(newend, newend, cmpPosition, cmpFootprint); + waypoints.back().x = newend.X; + waypoints.back().z = newend.Y; } else { - for (size_t i = 0; i < m_VisibilitySegments.size(); ++i) - { - if (m_VisibilitySegments[i] != newVisibilitySegments[i]) - { - // The visibility segments have changed, reconstruct the overlay lines to match. NOTE: The path itself doesn't - // change, only the overlay lines we construct from it. - m_VisibilitySegments[i] = newVisibilitySegments[i]; // save the new visibility segments to compare against next time - ConstructOverlayLines(i); - } - } + // make sure we actually start at the rallypoint because the pathfinder moves us to a usable tile. + waypoints.back().x = m_ItineraryPoints[index - 1].X; + waypoints.back().z = m_ItineraryPoints[index - 1].Y; } -} -void CCmpRallyPointRenderer::GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr cmpPosition, CmpPtr cmpFootprint) const -{ - ENSURE(cmpPosition); - ENSURE(cmpFootprint); + // pathfinder makes us go to the nearest passable cell which isn't always what we want + waypoints[0].x = m_ItineraryPoints[index].X; + waypoints[0].z = m_ItineraryPoints[index].Y; - // grab the shape and dimensions of the footprint - entity_pos_t footprintSize0, footprintSize1, footprintHeight; - ICmpFootprint::EShape footprintShape; - cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight); + // From here on, we choose to represent the waypoints as CVector2D floats to avoid to have to convert back and forth + // between fixed-point Waypoint/CFixedVector2D and various other float-based formats used by interpolation and whatnot. + // Since we'll only be further using these points for rendering purposes, using floats should be fine. - // grab the center of the footprint - CFixedVector2D center = cmpPosition->GetPosition2D(); + for (Waypoint& waypoint : waypoints) + m_Path[index].emplace_back(waypoint.x.ToFloat(), waypoint.z.ToFloat()); - switch (footprintShape) - { - case ICmpFootprint::SQUARE: - { - // in this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively. - - // the building's footprint could be rotated any which way, so let's get the rotation around the Y axis - // and the rotated unit vectors in the X/Z plane of the shape's footprint - // (the Footprint itself holds only the outline, the Position holds the orientation) - - fixed s, c; // sine and cosine of the Y axis rotation angle (aka the yaw) - fixed a = cmpPosition->GetRotation().Y; - sincos_approx(a, s, c); - CFixedVector2D u(c, -s); // unit vector along the rotated X axis - CFixedVector2D v(s, c); // unit vector along the rotated Z axis - CFixedVector2D halfSize(footprintSize0/2, footprintSize1/2); - - CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(start - center, u, v, halfSize); - result = center + footprintEdgePoint; - break; - } - case ICmpFootprint::CIRCLE: - { - // in this case, both footprintSize0 and 1 indicate the circle's radius - // Transform target to the point nearest on the edge. - CFixedVector2D centerVec2D(center.X, center.Y); - CFixedVector2D centerToLast(start - centerVec2D); - centerToLast.Normalize(); - result = centerVec2D + (centerToLast.Multiply(footprintSize0)); - break; - } - } -} + // add the start position + // m_Path[index].emplace_back(m_ItineraryPoints[index].X.ToFloat(), m_ItineraryPoints[index].Y.ToFloat()); -void CCmpRallyPointRenderer::FixFootprintWaypoints(std::vector& coords, CmpPtr cmpPosition, CmpPtr cmpFootprint) const -{ - ENSURE(cmpPosition); - ENSURE(cmpFootprint); + // Post-processing - // ----------------------------------------------------------------------------------------------------- - // TODO: nasty fixed/float conversions everywhere + // Linearize the path; + // Pass through the waypoints, averaging each waypoint with its next one except the last one. Because the path + // goes from the marker to this entity/the previous flag and we want to keep the point at the marker's exact position, + // loop backwards through the waypoints so that the marker waypoint is maintained. + // TODO: see if we can do this at the same time as the waypoint -> coord conversion above + for (size_t i = m_Path[index].size() - 2; i > 0; --i) + m_Path[index][i] = (m_Path[index][i] + m_Path[index][i - 1]) / 2.0f; - // grab the shape and dimensions of the footprint - entity_pos_t footprintSize0, footprintSize1, footprintHeight; - ICmpFootprint::EShape footprintShape; - cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight); + // if there's a footprint and this path starts from this entity, remove any points returned by the pathfinder that may be on obstructed footprint tiles + //if (index == 0 && cmpFootprint) + // FixFootprintWaypoints(m_Path[index], cmpPosition, cmpFootprint); - // grab the center of the footprint - CFixedVector2D center = cmpPosition->GetPosition2D(); + // Eliminate some consecutive waypoints that are visible from eachother. Reduce across a maximum distance of approx. 6 tiles + // (prevents segments that are too long to properly stick to the terrain) + ReduceSegmentsByVisibility(m_Path[index], 6); - // ----------------------------------------------------------------------------------------------------- + // Debug overlays + if (m_EnableDebugNodeOverlay) + { + while (index >= m_DebugNodeOverlays.size()) + m_DebugNodeOverlays.emplace_back(); - switch (footprintShape) + m_DebugNodeOverlays[index].clear(); + } + if (m_EnableDebugNodeOverlay && m_SmoothPath) { - case ICmpFootprint::SQUARE: + // Create separate control point overlays so we can differentiate when using smoothing (offset them a little higher from the + // terrain so we can still see them after the interpolated points are added) + for (CVector2D& point : m_Path[index]) { - // in this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively. - - // the building's footprint could be rotated any which way, so let's get the rotation around the Y axis - // and the rotated unit vectors in the X/Z plane of the shape's footprint - // (the Footprint itself holds only the outline, the Position holds the orientation) - - fixed s, c; // sine and cosine of the Y axis rotation angle (aka the yaw) - fixed a = cmpPosition->GetRotation().Y; - sincos_approx(a, s, c); - CFixedVector2D u(c, -s); // unit vector along the rotated X axis - CFixedVector2D v(s, c); // unit vector along the rotated Z axis - CFixedVector2D halfSize(footprintSize0/2, footprintSize1/2); - - // starting from the start position, check if any points are within the footprint of the building - // (this is possible if the pathfinder was started from a point located within the footprint) - for(int i = (int)(coords.size() - 1); i >= 0; i--) - { - const CVector2D& wp = coords[i]; - if (Geometry::PointIsInSquare(CFixedVector2D(fixed::FromFloat(wp.X), fixed::FromFloat(wp.Y)) - center, u, v, halfSize)) - { - coords.erase(coords.begin() + i); - } - else - { - break; // point no longer inside footprint, from this point on neither will any of the following be - } - } - - // add a point right on the edge of the footprint (nearest to the last waypoint) so that it links up nicely with the rest of the path - CFixedVector2D lastWaypoint(fixed::FromFloat(coords.back().X), fixed::FromFloat(coords.back().Y)); - CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(lastWaypoint - center, u, v, halfSize); // relative to the shape origin (center) - CVector2D footprintEdge((center.X + footprintEdgePoint.X).ToFloat(), (center.Y + footprintEdgePoint.Y).ToFloat()); - coords.push_back(footprintEdge); - + SOverlayLine overlayLine; + overlayLine.m_Color = CColor(1.0f, 0.0f, 0.0f, 1.0f); + overlayLine.m_Thickness = 2; + SimRender::ConstructSquareOnGround(GetSimContext(), point.X, point.Y, 0.2f, 0.2f, 1.0f, overlayLine, true); + m_DebugNodeOverlays[index].push_back(overlayLine); } - break; - case ICmpFootprint::CIRCLE: - { - // in this case, both footprintSize0 and 1 indicate the circle's radius + } - for(int i = (int)(coords.size() - 1); i >= 0; i--) - { - const CVector2D& wp = coords[i]; - fixed pointDistance = (CFixedVector2D(fixed::FromFloat(wp.X), fixed::FromFloat(wp.Y)) - center).Length(); - if (pointDistance <= footprintSize0) - { - coords.erase(coords.begin() + i); - } - else - { - break; // point no longer inside footprint, from this point on neither will any of the following be - } - } + if (m_SmoothPath) + // The number of points to interpolate goes hand in hand with the maximum amount of node links allowed to be joined together + // by the visibility reduction. The more node links that can be joined together, the more interpolated points you need to + // generate to be able to deal with local terrain height changes. + SimRender::InterpolatePointsRNS(m_Path[index], false, 0, 4); // no offset, keep line at its exact path - // add a point right on the edge of the footprint so that it links up nicely with the rest of the path - CVector2D centerVec2D(center.X.ToFloat(), center.Y.ToFloat()); - CVector2D centerToLast(coords.back() - centerVec2D); - coords.push_back(centerVec2D + (centerToLast.Normalized() * footprintSize0.ToFloat())); - } - break; - } + // find which point is the last visible point before going into the SoD, so we have a point to compare to on the next turn + GetVisibilitySegments(m_VisibilitySegments[index], index); + + // build overlay lines for the new path + ConstructOverlayLines(index); } +void CCmpRallyPointRenderer::RecomputeItineraryPointPath_wrapper(size_t index) +{ + if (!IsSet()) + return; + + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; + + CmpPtr cmpFootprint(GetEntityHandle()); + CmpPtr cmpPathfinder(GetSystemEntity()); + RecomputeItineraryPointPath(index, cmpPosition, cmpFootprint, cmpPathfinder); +} void CCmpRallyPointRenderer::ReduceSegmentsByVisibility(std::vector& coords, unsigned maxSegmentLinks, bool floating) const { CmpPtr cmpPathFinder(GetSystemEntity()); @@ -1127,7 +717,7 @@ newCoords.push_back(coords[baseNodeIdx]); // add new base node to output list - // update base node coordinates + // update base node coordinates baseNodeX = fixed::FromFloat(coords[baseNodeIdx].X); baseNodeZ = fixed::FromFloat(coords[baseNodeIdx].Y); baseNodeY = cmpTerrain->GetExactGroundLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y); @@ -1145,132 +735,108 @@ coords.swap(newCoords); } - -void CCmpRallyPointRenderer::GetVisibilitySegments(std::deque& out, size_t index) const +void CCmpRallyPointRenderer::RenderSubmit(SceneCollector& collector) { - out.clear(); - - if (m_Path[index].size() < 2) - return; - - CmpPtr cmpRangeMgr(GetSystemEntity()); - - player_id_t currentPlayer = GetSimContext().GetCurrentDisplayedPlayer(); - CLosQuerier losQuerier(cmpRangeMgr->GetLosQuerier(currentPlayer)); - - // go through the path node list, comparing each node's visibility with the previous one. If it changes, end the current segment and start - // a new one at the next point. - - bool lastVisible = losQuerier.IsExplored( - (fixed::FromFloat(m_Path[index][0].X) / (int) TERRAIN_TILE_SIZE).ToInt_RoundToNearest(), - (fixed::FromFloat(m_Path[index][0].Y) / (int) TERRAIN_TILE_SIZE).ToInt_RoundToNearest() - ); - size_t curSegmentStartIndex = 0; // starting node index of the current segment - - for (size_t k = 1; k < m_Path[index].size(); ++k) + CCmpItineraryPointRenderer::RenderSubmit(collector); +} +void CCmpRallyPointRenderer::Reset() +{ + m_ItineraryPoints.clear(); + RecomputeAllItineraryPointPaths(); + UpdateMessageSubscriptions(); +} +void CCmpRallyPointRenderer::SetDisplayed(bool displayed) +{ + CCmpItineraryPointRenderer::SetDisplayed(displayed); +} +void CCmpRallyPointRenderer::SetPosition(const CFixedVector2D& pos) +{ + CCmpItineraryPointRenderer::SetPosition(pos); +} +void CCmpRallyPointRenderer::UpdateMarkers() +{ + player_id_t previousOwner = m_LastOwner; + for (size_t i = 0; i < m_ItineraryPoints.size(); ++i) { - // grab tile indices for this coord - int i = (fixed::FromFloat(m_Path[index][k].X) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); - int j = (fixed::FromFloat(m_Path[index][k].Y) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); + if (i >= m_MarkerEntityIds.size()) + m_MarkerEntityIds.push_back(INVALID_ENTITY); - bool nodeVisible = losQuerier.IsExplored(i, j); - if (nodeVisible != lastVisible) + if (m_MarkerEntityIds[i] == INVALID_ENTITY) { - // visibility changed; write out the segment that was just completed and get ready for the new one - out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, k - 1)); + // no marker exists yet, create one first + CComponentManager& componentMgr = GetSimContext().GetComponentManager(); - //curSegmentStartIndex = k; // new segment starts here - curSegmentStartIndex = k - 1; - lastVisible = nodeVisible; + // allocate a new entity for the marker + if (!m_MarkerTemplate.empty()) + { + m_MarkerEntityIds[i] = componentMgr.AllocateNewLocalEntity(); + if (m_MarkerEntityIds[i] != INVALID_ENTITY) + m_MarkerEntityIds[i] = componentMgr.AddEntity(m_MarkerTemplate, m_MarkerEntityIds[i]); + } } - } - - // terminate the last segment - out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, m_Path[index].size() - 1)); - - MergeVisibilitySegments(out); -} - -void CCmpRallyPointRenderer::MergeVisibilitySegments(std::deque& segments) -{ - // Scan for single-point segments; if they are inbetween two other segments, delete them and merge the surrounding segments. - // If they're at either end of the path, include them in their bordering segment (but only if those bordering segments aren't - // themselves single-point segments, because then we would want those to get absorbed by its surrounding ones first). - - // first scan for absorptions of single-point surrounded segments (i.e. excluding edge segments) - size_t numSegments = segments.size(); + // the marker entity should be valid at this point, otherwise something went wrong trying to allocate it + if (m_MarkerEntityIds[i] == INVALID_ENTITY) + LOGERROR("Failed to create rally point marker entity"); - // WARNING: FOR LOOP TRICKERY AHEAD! - for (size_t i = 1; i < numSegments - 1;) - { - SVisibilitySegment& segment = segments[i]; - if (segment.IsSinglePoint()) - { - // since the segments' visibility alternates, the surrounding ones should have the same visibility - ENSURE(segments[i-1].m_Visible == segments[i+1].m_Visible); - - segments[i-1].m_EndIndex = segments[i+1].m_EndIndex; // make previous segment span all the way across to the next - segments.erase(segments.begin() + i); // erase this segment ... - segments.erase(segments.begin() + i); // and the next (we removed [i], so [i+1] is now at position [i]) - numSegments -= 2; // we removed 2 segments, so update the loop condition - // in the next iteration, i should still point to the segment right after the one that got expanded, which is now - // at position i; so don't increment i here - } - else + CmpPtr markerCmpPosition(GetSimContext(), m_MarkerEntityIds[i]); + if (markerCmpPosition) { - ++i; + if (m_Displayed && CCmpItineraryPointRenderer::IsSet()) + { + markerCmpPosition->MoveTo(m_ItineraryPoints[i].X, m_ItineraryPoints[i].Y); + } + else + { + markerCmpPosition->MoveOutOfWorld(); // hide it + } } - } - - ENSURE(numSegments == segments.size()); - // check to see if the first segment needs to be merged with its neighbour - if (segments.size() >= 2 && segments[0].IsSinglePoint()) - { - int firstSegmentStartIndex = segments.front().m_StartIndex; - ENSURE(firstSegmentStartIndex == 0); - ENSURE(!segments[1].IsSinglePoint()); // at this point, the second segment should never be a single-point segment + // set rally point flag selection based on player civilization + CmpPtr cmpOwnership(GetEntityHandle()); + if (!cmpOwnership) + continue; - segments.erase(segments.begin()); - segments.front().m_StartIndex = firstSegmentStartIndex; - } + player_id_t ownerId = cmpOwnership->GetOwner(); + if (ownerId == INVALID_PLAYER || (ownerId == previousOwner && m_LastMarkerCount >= i)) + continue; - // check to see if the last segment needs to be merged with its neighbour - if (segments.size() >= 2 && segments[segments.size()-1].IsSinglePoint()) - { - int lastSegmentEndIndex = segments.back().m_EndIndex; - ENSURE(!segments[segments.size()-2].IsSinglePoint()); // at this point, the second-to-last segment should never be a single-point segment + m_LastOwner = ownerId; + CmpPtr cmpPlayerManager(GetSystemEntity()); + // cmpPlayerManager should not be null as long as this method is called on-demand instead of at Init() time + // (we can't rely on component initialization order in Init()) + if (!cmpPlayerManager) + continue; - segments.erase(segments.end()); - segments.back().m_EndIndex = lastSegmentEndIndex; - } + CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(ownerId)); + if (!cmpPlayer) + continue; - // -------------------------------------------------------------------------------------------------------- - // at this point, every segment should have at least 2 points - for (size_t i = 0; i < segments.size(); ++i) - { - ENSURE(!segments[i].IsSinglePoint()); - ENSURE(segments[i].m_EndIndex > segments[i].m_StartIndex); + CmpPtr cmpVisualActor(GetSimContext(), m_MarkerEntityIds[i]); + if (cmpVisualActor) + cmpVisualActor->SetVariant("civ", CStrW(cmpPlayer->GetCiv()).ToUTF8()); } + m_LastMarkerCount = m_ItineraryPoints.size() - 1; } - -void CCmpRallyPointRenderer::RenderSubmit(SceneCollector& collector) +void CCmpRallyPointRenderer::UpdateMessageSubscriptions() { - // we only get here if the rally point is set and should be displayed - for (size_t i = 0; i < m_TexturedOverlayLines.size(); ++i) - { - for (size_t j = 0; j < m_TexturedOverlayLines[i].size(); ++j) - { - if (!m_TexturedOverlayLines[i][j].m_Coords.empty()) - collector.Submit(&m_TexturedOverlayLines[i][j]); - } - } + GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, m_Displayed && CCmpItineraryPointRenderer::IsSet()); +} +void CCmpRallyPointRenderer::UpdatePosition(u32 rallyPointId, const CFixedVector2D& pos) +{ + if (rallyPointId >= m_ItineraryPoints.size()) + return; - if (m_EnableDebugNodeOverlay && !m_DebugNodeOverlays.empty()) - { - for (size_t i = 0; i < m_DebugNodeOverlays.size(); ++i) - for (size_t j = 0; j < m_DebugNodeOverlays[i].size(); ++j) - collector.Submit(&m_DebugNodeOverlays[i][j]); - } + m_ItineraryPoints[rallyPointId] = pos; + + UpdateMarkers(); + + // Compute a new path for the current, and if existing the next rally point + RecomputeItineraryPointPath_wrapper(rallyPointId); + if (rallyPointId + 1 < m_ItineraryPoints.size()) + RecomputeItineraryPointPath_wrapper(rallyPointId + 1); +} +void CCmpRallyPointRenderer::UpdateOverlayLines() +{ + CCmpItineraryPointRenderer::UpdateOverlayLines(); } Index: source/simulation2/components/CCmpWayPointRenderer.h =================================================================== --- /dev/null +++ source/simulation2/components/CCmpWayPointRenderer.h @@ -0,0 +1,82 @@ +/* Copyright (C) 2017 Wildfire Games. +* This file is part of 0 A.D. +* +* 0 A.D. is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 of the License, or +* (at your option) any later version. +* +* 0 A.D. is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with 0 A.D. If not, see . +*/ + +#ifndef INCLUDED_CCMPWAYPOINTRENDERER +#define INCLUDED_CCMPWAYPOINTRENDERER + +#include "CCmpItineraryPointRenderer.h" +#include "ICmpWayPointRenderer.h" + +#include "simulation2/MessageTypes.h" +#include "simulation2/components/ICmpFootprint.h" +#include "simulation2/components/ICmpObstructionManager.h" +#include "simulation2/components/ICmpOwnership.h" +#include "simulation2/components/ICmpPathfinder.h" +#include "simulation2/components/ICmpPlayer.h" +#include "simulation2/components/ICmpPlayerManager.h" +#include "simulation2/components/ICmpPosition.h" +#include "simulation2/components/ICmpRangeManager.h" +#include "simulation2/components/ICmpTerrain.h" +#include "simulation2/components/ICmpVisual.h" +#include "simulation2/components/ICmpWaterManager.h" +#include "simulation2/helpers/Render.h" +#include "simulation2/helpers/Geometry.h" +#include "simulation2/system/Component.h" + +#include "graphics/Overlay.h" +#include "graphics/TextureManager.h" +#include "ps/CLogger.h" +#include "ps/Profile.h" +#include "renderer/Renderer.h" + +class CCmpWayPointRenderer : public CCmpItineraryPointRenderer, public ICmpWayPointRenderer +{ +public: + DEFAULT_COMPONENT_ALLOCATOR(WayPointRenderer) + static std::string GetSchema(); + + virtual void Init(const CParamNode& paramNode); + virtual void Deinit(); + virtual void Serialize(ISerializer& UNUSED(serialize)); + virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize); + + virtual void AddPosition_wrapper(const CFixedVector2D& pos); + virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)); + virtual void UpdateMessageSubscriptions(); + virtual void Stack(const CFixedVector2D& pos); + virtual void SetDisplayed(bool displayed); + virtual void SetPosition(const CFixedVector2D& pos); + virtual void Shift(); + +private: + virtual void AddPosition(CFixedVector2D pos, bool recompute); + virtual void ConstructAllOverlayLines(); + virtual void ConstructOverlayLines(size_t index); + virtual void GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr cmpPosition, CmpPtr cmpFootprint) const; + virtual void GetVisibilitySegments(std::vector& out, size_t index) const; + virtual void FixFootprintWaypoints(std::vector& coords, CmpPtr cmpPosition, CmpPtr cmpFootprint) const; + virtual bool IsSet() const; + virtual void RenderSubmit(SceneCollector& collector); + virtual void RecomputeAllItineraryPointPaths(); + virtual void RecomputeItineraryPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& cmpFootprint, CmpPtr cmpPathfinder); + virtual void RecomputeItineraryPointPath_wrapper(size_t index); + virtual void ReduceSegmentsByVisibility(std::vector& coords, unsigned maxSegmentLinks = 0, bool floating = true) const; + virtual void UpdateOverlayLines(); + virtual void UpdateMarkers(); +}; +REGISTER_COMPONENT_TYPE(WayPointRenderer) +#endif //INCLUDED_CCMPWAYPOINTRENDERER Index: source/simulation2/components/CCmpWayPointRenderer.cpp =================================================================== --- /dev/null +++ source/simulation2/components/CCmpWayPointRenderer.cpp @@ -0,0 +1,857 @@ +/* Copyright (C) 2017 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" +#include "CCmpWayPointRenderer.h" + +std::string CCmpWayPointRenderer::GetSchema() +{ + return + "Displays a way point marker where units will go to when tasked to move" + "" + "special/WayPoint" + "0.75" + "round" + "square" + "" + "" + "default" + "default" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "0255" + "" + "" + "0255" + "" + "" + "0255" + "" + "" + "" + "" + "0255" + "" + "" + "0255" + "" + "" + "0255" + "" + "" + "" + "" + "flat" + "round" + "sharp" + "square" + "" + "" + "" + "" + "flat" + "round" + "sharp" + "square" + "" + "" + "" + "" + "" + "" + "" + ""; +} + +void CCmpWayPointRenderer::Init(const CParamNode& paramNode) +{ + CCmpItineraryPointRenderer::Init(paramNode); +} +void CCmpWayPointRenderer::Deinit() +{ + CCmpItineraryPointRenderer::Deinit(); +} +void CCmpWayPointRenderer::Serialize(ISerializer& serialize) +{ + CCmpItineraryPointRenderer::Serialize(serialize); +} +void CCmpWayPointRenderer::Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) +{ + CCmpItineraryPointRenderer::Deserialize(paramNode, deserialize); +} + +void CCmpWayPointRenderer::AddPosition(CFixedVector2D pos, bool recompute) +{ + CCmpItineraryPointRenderer::AddPosition(pos, recompute); +} +void CCmpWayPointRenderer::AddPosition_wrapper(const CFixedVector2D& pos) +{ + CCmpItineraryPointRenderer::AddPosition_wrapper(pos); +} +void CCmpWayPointRenderer::ConstructAllOverlayLines() +{ + CCmpItineraryPointRenderer::ConstructAllOverlayLines(); +} +void CCmpWayPointRenderer::ConstructOverlayLines(size_t index) +{ + // We need to create a new SOverlayTexturedLine every time we want to change the coordinates after having passed it to the + // renderer, because it does some fancy vertex buffering thing and caches them internally instead of recomputing them on every + // pass (which is only sensible). + while (index >= m_TexturedOverlayLines.size()) + { + std::vector tmp; + m_TexturedOverlayLines.push_back(tmp); + } + + std::vector >::iterator iter = m_TexturedOverlayLines.begin(); + size_t count = index; + while (count--) + iter++; + (*iter).clear(); + + if (m_Path[index].size() < 2) + return; + + LineCapType dashesLineCapType = SOverlayTexturedLine::LINECAP_ROUND; // line caps to use for the dashed segments (and any other segment's edges that border it) + + for (std::vector::const_iterator it = m_VisibilitySegments[index].begin(); it != m_VisibilitySegments[index].end(); ++it) + { + const SVisibilitySegment& segment = (*it); + + if (segment.m_Visible) + { + // does this segment border on the building or rally point flag on either side? + bool bordersBuilding = (segment.m_EndIndex == m_Path[index].size() - 1); + bool bordersFlag = (segment.m_StartIndex == 0); + + // construct solid textured overlay line along a subset of the full path points from startPointIdx to endPointIdx + SOverlayTexturedLine overlayLine; + overlayLine.m_Thickness = m_LineThickness; + overlayLine.m_SimContext = &GetSimContext(); + overlayLine.m_TextureBase = m_Texture; + overlayLine.m_TextureMask = m_TextureMask; + overlayLine.m_Color = m_LineColor; + overlayLine.m_Closed = false; + // we should take care to only use m_LineXCap for the actual end points at the building and the rally point; any intermediate + // end points (i.e., that border a dashed segment) should have the dashed cap + // the path line is actually in reverse order as well, so let's swap out the start and end caps + overlayLine.m_StartCapType = (bordersFlag ? m_LineEndCapType : dashesLineCapType); + overlayLine.m_EndCapType = (bordersBuilding ? m_LineStartCapType : dashesLineCapType); + overlayLine.m_AlwaysVisible = true; + + // push overlay line coordinates + ENSURE(segment.m_EndIndex > segment.m_StartIndex); + for (size_t j = segment.m_StartIndex; j <= segment.m_EndIndex; ++j) // end index is inclusive here + { + overlayLine.m_Coords.push_back(m_Path[index][j].X); + overlayLine.m_Coords.push_back(m_Path[index][j].Y); + } + + (*iter).push_back(overlayLine); + } + else + { + // construct dashed line from startPointIdx to endPointIdx; add textured overlay lines for it to the render list + std::vector straightLine; + straightLine.push_back(m_Path[index][segment.m_StartIndex]); + straightLine.push_back(m_Path[index][segment.m_EndIndex]); + + // We always want to the dashed line to end at either point with a full dash (i.e. not a cleared space), so that the dashed + // area is visually obvious. This requires some calculations to see what size we should make the dashes and clears for them + // to fit exactly. + + float maxDashSize = 3.f; + float maxClearSize = 3.f; + + float dashSize = maxDashSize; + float clearSize = maxClearSize; + float pairDashRatio = (dashSize / (dashSize + clearSize)); // ratio of the dash's length to a (dash + clear) pair's length + + float distance = (m_Path[index][segment.m_StartIndex] - m_Path[index][segment.m_EndIndex]).Length(); // straight-line distance between the points + + // See how many pairs (dash + clear) of unmodified size can fit into the distance. Then check the remaining distance; if it's not exactly + // a dash size's worth (which it probably won't be), then adjust the dash/clear sizes slightly so that it is. + int numFitUnmodified = floor(distance / (dashSize + clearSize)); + float remainderDistance = distance - (numFitUnmodified * (dashSize + clearSize)); + + // Now we want to make remainderDistance equal exactly one dash size (i.e. maxDashSize) by scaling dashSize and clearSize slightly. + // We have (remainderDistance - maxDashSize) of space to distribute over numFitUnmodified instances of (dashSize + clearSize) to make + // it fit, so each (dashSize + clearSize) pair needs to adjust its length by (remainderDistance - maxDashSize)/numFitUnmodified + // (which will be positive or negative accordingly). This number can then be distributed further proportionally among the dash's + // length and the clear's length. + + // we always want to have at least one dash/clear pair (i.e., "|===| |===|"); also, we need to avoid division by zero below. + numFitUnmodified = std::max(1, numFitUnmodified); + + float pairwiseLengthDifference = (remainderDistance - maxDashSize) / numFitUnmodified; // can be either positive or negative + dashSize += pairDashRatio * pairwiseLengthDifference; + clearSize += (1 - pairDashRatio) * pairwiseLengthDifference; + + // ------------------------------------------------------------------------------------------------ + + SDashedLine dashedLine; + SimRender::ConstructDashedLine(straightLine, dashedLine, dashSize, clearSize); + + // build overlay lines for dashes + size_t numDashes = dashedLine.m_StartIndices.size(); + for (size_t i = 0; i < numDashes; i++) + { + SOverlayTexturedLine dashOverlay; + + dashOverlay.m_Thickness = m_LineThickness; + dashOverlay.m_SimContext = &GetSimContext(); + dashOverlay.m_TextureBase = m_Texture; + dashOverlay.m_TextureMask = m_TextureMask; + dashOverlay.m_Color = m_LineDashColor; + dashOverlay.m_Closed = false; + dashOverlay.m_StartCapType = dashesLineCapType; + dashOverlay.m_EndCapType = dashesLineCapType; + dashOverlay.m_AlwaysVisible = true; + // TODO: maybe adjust the elevation of the dashes to be a little lower, so that it slides underneath the actual path + + size_t dashStartIndex = dashedLine.m_StartIndices[i]; + size_t dashEndIndex = dashedLine.GetEndIndex(i); + ENSURE(dashEndIndex > dashStartIndex); + + for (size_t n = dashStartIndex; n < dashEndIndex; n++) + { + dashOverlay.m_Coords.push_back(dashedLine.m_Points[n].X); + dashOverlay.m_Coords.push_back(dashedLine.m_Points[n].Y); + } + + (*iter).push_back(dashOverlay); + } + + } + } + //// ////////////////////////////////////////////// + if (m_EnableDebugNodeOverlay) + { + while (index >= m_DebugNodeOverlays.size()) + { + std::vector tmp; + m_DebugNodeOverlays.push_back(tmp); + } + for (size_t j = 0; j < m_Path[index].size(); ++j) + { + SOverlayLine overlayLine; + overlayLine.m_Color = CColor(1.0f, 1.0f, 1.0f, 1.0f); + overlayLine.m_Thickness = 1; + SimRender::ConstructCircleOnGround(GetSimContext(), m_Path[index][j].X, m_Path[index][j].Y, 0.075f, overlayLine, true); + m_DebugNodeOverlays[index].push_back(overlayLine); + } + } + //// ////////////////////////////////////////////// +} +void CCmpWayPointRenderer::FixFootprintWaypoints(std::vector& coords, CmpPtr cmpPosition, CmpPtr cmpFootprint) const +{ + ENSURE(cmpPosition); + ENSURE(cmpFootprint); + + // ----------------------------------------------------------------------------------------------------- + // TODO: nasty fixed/float conversions everywhere + + // grab the shape and dimensions of the footprint + entity_pos_t footprintSize0, footprintSize1, footprintHeight; + ICmpFootprint::EShape footprintShape; + cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight); + + // grab the center of the footprint + CFixedVector2D center = cmpPosition->GetPosition2D(); + + // ----------------------------------------------------------------------------------------------------- + + switch (footprintShape) + { + case ICmpFootprint::SQUARE: + { + // in this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively. + + // the building's footprint could be rotated any which way, so let's get the rotation around the Y axis + // and the rotated unit vectors in the X/Z plane of the shape's footprint + // (the Footprint itself holds only the outline, the Position holds the orientation) + + fixed s, c; // sine and cosine of the Y axis rotation angle (aka the yaw) + fixed a = cmpPosition->GetRotation().Y; + sincos_approx(a, s, c); + CFixedVector2D u(c, -s); // unit vector along the rotated X axis + CFixedVector2D v(s, c); // unit vector along the rotated Z axis + CFixedVector2D halfSize(footprintSize0 / 2, footprintSize1 / 2); + + // starting from the start position, check if any points are within the footprint of the building + // (this is possible if the pathfinder was started from a point located within the footprint) + for (int i = (int)(coords.size() - 1); i >= 0; i--) + { + const CVector2D& wp = coords[i]; + if (Geometry::PointIsInSquare(CFixedVector2D(fixed::FromFloat(wp.X), fixed::FromFloat(wp.Y)) - center, u, v, halfSize)) + { + coords.erase(coords.begin() + i); + } + else + { + break; // point no longer inside footprint, from this point on neither will any of the following be + } + } + + // add a point right on the edge of the footprint (nearest to the last waypoint) so that it links up nicely with the rest of the path + CFixedVector2D lastWaypoint(fixed::FromFloat(coords.back().X), fixed::FromFloat(coords.back().Y)); + CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(lastWaypoint - center, u, v, halfSize); // relative to the shape origin (center) + CVector2D footprintEdge((center.X + footprintEdgePoint.X).ToFloat(), (center.Y + footprintEdgePoint.Y).ToFloat()); + coords.push_back(footprintEdge); + + } + break; + case ICmpFootprint::CIRCLE: + { + // in this case, both footprintSize0 and 1 indicate the circle's radius + + for (int i = (int)(coords.size() - 1); i >= 0; i--) + { + const CVector2D& wp = coords[i]; + fixed pointDistance = (CFixedVector2D(fixed::FromFloat(wp.X), fixed::FromFloat(wp.Y)) - center).Length(); + if (pointDistance <= footprintSize0) + { + coords.erase(coords.begin() + i); + } + else + { + break; // point no longer inside footprint, from this point on neither will any of the following be + } + } + + // add a point right on the edge of the footprint so that it links up nicely with the rest of the path + CVector2D centerVec2D(center.X.ToFloat(), center.Y.ToFloat()); + CVector2D centerToLast(coords.back() - centerVec2D); + coords.push_back(centerVec2D + (centerToLast.Normalized() * footprintSize0.ToFloat())); + } + break; + } +} +void CCmpWayPointRenderer::GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr cmpPosition, CmpPtr cmpFootprint) const +{ + ENSURE(cmpPosition); + ENSURE(cmpFootprint); + + // grab the shape and dimensions of the footprint + entity_pos_t footprintSize0, footprintSize1, footprintHeight; + ICmpFootprint::EShape footprintShape; + cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight); + + // grab the center of the footprint + CFixedVector2D center = cmpPosition->GetPosition2D(); + + switch (footprintShape) + { + case ICmpFootprint::SQUARE: + { + // in this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively. + + // the building's footprint could be rotated any which way, so let's get the rotation around the Y axis + // and the rotated unit vectors in the X/Z plane of the shape's footprint + // (the Footprint itself holds only the outline, the Position holds the orientation) + + fixed s, c; // sine and cosine of the Y axis rotation angle (aka the yaw) + fixed a = cmpPosition->GetRotation().Y; + sincos_approx(a, s, c); + CFixedVector2D u(c, -s); // unit vector along the rotated X axis + CFixedVector2D v(s, c); // unit vector along the rotated Z axis + CFixedVector2D halfSize(footprintSize0 / 2, footprintSize1 / 2); + + CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(start - center, u, v, halfSize); + result = center + footprintEdgePoint; + break; + } + case ICmpFootprint::CIRCLE: + { + // in this case, both footprintSize0 and 1 indicate the circle's radius + // Transform target to the point nearest on the edge. + CFixedVector2D centerVec2D(center.X, center.Y); + CFixedVector2D centerToLast(start - centerVec2D); + centerToLast.Normalize(); + result = centerVec2D + (centerToLast.Multiply(footprintSize0)); + break; + } + } +} +void CCmpWayPointRenderer::GetVisibilitySegments(std::vector& out, size_t index) const +{ + out.clear(); + + if (m_Path[index].size() < 2) + return; + + CmpPtr cmpRangeMgr(GetSystemEntity()); + + player_id_t currentPlayer = GetSimContext().GetCurrentDisplayedPlayer(); + CLosQuerier losQuerier(cmpRangeMgr->GetLosQuerier(currentPlayer)); + + // go through the path node list, comparing each node's visibility with the previous one. If it changes, end the current segment and start + // a new one at the next point. + + bool lastVisible = losQuerier.IsExplored( + (fixed::FromFloat(m_Path[index][0].X) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(), + (fixed::FromFloat(m_Path[index][0].Y) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest() + ); + size_t curSegmentStartIndex = 0; // starting node index of the current segment + + for (size_t k = 1; k < m_Path[index].size(); ++k) + { + // grab tile indices for this coord + int i = (fixed::FromFloat(m_Path[index][k].X) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); + int j = (fixed::FromFloat(m_Path[index][k].Y) / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); + + bool nodeVisible = losQuerier.IsExplored(i, j); + if (nodeVisible != lastVisible) + { + // visibility changed; write out the segment that was just completed and get ready for the new one + out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, k - 1)); + + //curSegmentStartIndex = k; // new segment starts here + curSegmentStartIndex = k - 1; + lastVisible = nodeVisible; + } + + } + + // terminate the last segment + out.push_back(SVisibilitySegment(lastVisible, curSegmentStartIndex, m_Path[index].size() - 1)); + MergeVisibilitySegments(out); +} +void CCmpWayPointRenderer::HandleMessage(const CMessage& msg, bool UNUSED(global)) +{ + switch (msg.GetType()) + { + case MT_RenderSubmit: + { + PROFILE("RallyPoint::RenderSubmit"); + if (m_Displayed && IsSet()) + { + const CMessageRenderSubmit& msgData = static_cast (msg); + RenderSubmit(msgData.collector); + } + } + break; + case MT_OwnershipChanged: + { + UpdateMarkers(); // update marker variation to new player's civilization + } + break; + case MT_TurnStart: + { + UpdateOverlayLines(); // check for changes to the SoD and update the overlay lines accordingly + } + break; + case MT_Destroy: + { + for (std::vector::iterator it = m_MarkerEntityIds.begin(); it != m_MarkerEntityIds.end(); ++it) + { + if (*it != INVALID_ENTITY) + { + GetSimContext().GetComponentManager().DestroyComponentsSoon(*it); + *it = INVALID_ENTITY; + } + } + } + break; + case MT_PositionChanged: + { + // Unlikely to happen in-game, but can occur in atlas + // Just recompute the path from the entity to the first rally point + RecomputeItineraryPointPath_wrapper(0); + } + break; + } +} +bool CCmpWayPointRenderer::IsSet() const +{ + return CCmpItineraryPointRenderer::IsSet(); +} +void CCmpWayPointRenderer::RecomputeAllItineraryPointPaths() +{ + m_Path.clear(); + m_VisibilitySegments.clear(); + m_TexturedOverlayLines.clear(); + + if (!IsSet()) + return; + + CmpPtr cmpPosition(GetSimContext(), GetEntityId()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; + + CmpPtr cmpFootprint(GetSimContext(), GetEntityId()); + CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); + + for (size_t i = 0; i < m_ItineraryPoints.size(); ++i) + RecomputeItineraryPointPath(i, cmpPosition, cmpFootprint, cmpPathfinder); +} +void CCmpWayPointRenderer::RecomputeItineraryPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& cmpFootprint, CmpPtr cmpPathfinder) +{ + while (index >= m_Path.size()) + { + std::vector tmp; + m_Path.push_back(tmp); + } + m_Path[index].clear(); + + while (index >= m_VisibilitySegments.size()) + { + std::vector tmp; + m_VisibilitySegments.push_back(tmp); + } + m_VisibilitySegments[index].clear(); + + // Find a long path to the goal point -- this uses the tile-based pathfinder, which will return a + // list of waypoints (i.e. a Path) from the goal to the foundation/previous rally point, where each + // waypoint is centered at a tile. We'll have to do some post-processing on the path to get it smooth. + Path path; + std::vector& waypoints = path.m_Waypoints; + + CFixedVector2D start(cmpPosition->GetPosition2D()); + Goal goal = { Goal::POINT, m_ItineraryPoints[index].X, m_ItineraryPoints[index].Y }; + + if (index == 0) + GetClosestsEdgePointFrom(start, m_ItineraryPoints[index], cmpPosition, cmpFootprint); + else + { + start.X = m_ItineraryPoints[index - 1].X; + start.Y = m_ItineraryPoints[index - 1].Y; + } + cmpPathfinder->ComputePath(start.X, start.Y, goal, cmpPathfinder->GetPassabilityClass(m_LinePassabilityClass), path); + + // Check if we got a path back; if not we probably have two markers less than one tile apart. + if (path.m_Waypoints.size() < 2) + { + m_Path[index].emplace_back(start.X.ToFloat(), start.Y.ToFloat()); + m_Path[index].emplace_back(m_ItineraryPoints[index].X.ToFloat(), m_ItineraryPoints[index].Y.ToFloat()); + return; + } + else if (index == 0) + { + // sometimes this ends up not being optimal if you asked for a long path, so improve. + CFixedVector2D newend(waypoints[waypoints.size() - 2].x, waypoints[waypoints.size() - 2].z); + GetClosestsEdgePointFrom(newend, newend, cmpPosition, cmpFootprint); + waypoints.back().x = newend.X; + waypoints.back().z = newend.Y; + } + else + { + // make sure we actually start at the rallypoint because the pathfinder moves us to a usable tile. + waypoints.back().x = m_ItineraryPoints[index - 1].X; + waypoints.back().z = m_ItineraryPoints[index - 1].Y; + } + + // pathfinder makes us go to the nearest passable cell which isn't always what we want + waypoints[0].x = m_ItineraryPoints[index].X; + waypoints[0].z = m_ItineraryPoints[index].Y; + + // From here on, we choose to represent the waypoints as CVector2D floats to avoid to have to convert back and forth + // between fixed-point Waypoint/CFixedVector2D and various other float-based formats used by interpolation and whatnot. + // Since we'll only be further using these points for rendering purposes, using floats should be fine. + + for (Waypoint& waypoint : waypoints) + m_Path[index].emplace_back(waypoint.x.ToFloat(), waypoint.z.ToFloat()); + + // add the start position + // m_Path[index].emplace_back(m_ItineraryPoints[index].X.ToFloat(), m_ItineraryPoints[index].Y.ToFloat()); + + // Post-processing + + // Linearize the path; + // Pass through the waypoints, averaging each waypoint with its next one except the last one. Because the path + // goes from the marker to this entity/the previous flag and we want to keep the point at the marker's exact position, + // loop backwards through the waypoints so that the marker waypoint is maintained. + // TODO: see if we can do this at the same time as the waypoint -> coord conversion above + for (size_t i = m_Path[index].size() - 2; i > 0; --i) + m_Path[index][i] = (m_Path[index][i] + m_Path[index][i - 1]) / 2.0f; + + // if there's a footprint and this path starts from this entity, remove any points returned by the pathfinder that may be on obstructed footprint tiles + //if (index == 0 && cmpFootprint) + // FixFootprintWaypoints(m_Path[index], cmpPosition, cmpFootprint); + + // Eliminate some consecutive waypoints that are visible from eachother. Reduce across a maximum distance of approx. 6 tiles + // (prevents segments that are too long to properly stick to the terrain) + ReduceSegmentsByVisibility(m_Path[index], 6); + + // Debug overlays + if (m_EnableDebugNodeOverlay) + { + while (index >= m_DebugNodeOverlays.size()) + m_DebugNodeOverlays.emplace_back(); + + m_DebugNodeOverlays[index].clear(); + } + if (m_EnableDebugNodeOverlay && m_SmoothPath) + { + // Create separate control point overlays so we can differentiate when using smoothing (offset them a little higher from the + // terrain so we can still see them after the interpolated points are added) + for (CVector2D& point : m_Path[index]) + { + SOverlayLine overlayLine; + overlayLine.m_Color = CColor(1.0f, 0.0f, 0.0f, 1.0f); + overlayLine.m_Thickness = 2; + SimRender::ConstructSquareOnGround(GetSimContext(), point.X, point.Y, 0.2f, 0.2f, 1.0f, overlayLine, true); + m_DebugNodeOverlays[index].push_back(overlayLine); + } + } + + if (m_SmoothPath) + // The number of points to interpolate goes hand in hand with the maximum amount of node links allowed to be joined together + // by the visibility reduction. The more node links that can be joined together, the more interpolated points you need to + // generate to be able to deal with local terrain height changes. + SimRender::InterpolatePointsRNS(m_Path[index], false, 0, 4); // no offset, keep line at its exact path + + // find which point is the last visible point before going into the SoD, so we have a point to compare to on the next turn + GetVisibilitySegments(m_VisibilitySegments[index], index); + + // build overlay lines for the new path + ConstructOverlayLines(index); +} +void CCmpWayPointRenderer::RecomputeItineraryPointPath_wrapper(size_t index) +{ + if (!IsSet()) + return; + + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; + + CmpPtr cmpFootprint(GetEntityHandle()); + CmpPtr cmpPathfinder(GetSystemEntity()); + + RecomputeItineraryPointPath(index, cmpPosition, cmpFootprint, cmpPathfinder); +} +void CCmpWayPointRenderer::ReduceSegmentsByVisibility(std::vector& coords, unsigned maxSegmentLinks, bool floating) const +{ + CmpPtr cmpPathFinder(GetSystemEntity()); + CmpPtr cmpTerrain(GetSystemEntity()); + CmpPtr cmpWaterManager(GetSystemEntity()); + ENSURE(cmpPathFinder && cmpTerrain && cmpWaterManager); + + if (coords.size() < 3) + return; + + // The basic idea is this: starting from a base node, keep checking each individual point along the path to see if there's a visible + // line between it and the base point. If so, keep going, otherwise, make the last visible point the new base node and start the same + // process from there on until the entire line is checked. The output is the array of base nodes. + + std::vector newCoords; + StationaryOnlyObstructionFilter obstructionFilter; + entity_pos_t lineRadius = fixed::FromFloat(m_LineThickness); + pass_class_t passabilityClass = cmpPathFinder->GetPassabilityClass(m_LinePassabilityClass); + + newCoords.push_back(coords[0]); // save the first base node + + size_t baseNodeIdx = 0; + size_t curNodeIdx = 1; + + float baseNodeY; + entity_pos_t baseNodeX; + entity_pos_t baseNodeZ; + + // set initial base node coords + baseNodeX = fixed::FromFloat(coords[baseNodeIdx].X); + baseNodeZ = fixed::FromFloat(coords[baseNodeIdx].Y); + baseNodeY = cmpTerrain->GetExactGroundLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y); + if (floating) + baseNodeY = std::max(baseNodeY, cmpWaterManager->GetExactWaterLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y)); + + while (curNodeIdx < coords.size()) + { + ENSURE(curNodeIdx > baseNodeIdx); // this needs to be true at all times, otherwise we're checking visibility between a point and itself + + entity_pos_t curNodeX = fixed::FromFloat(coords[curNodeIdx].X); + entity_pos_t curNodeZ = fixed::FromFloat(coords[curNodeIdx].Y); + float curNodeY = cmpTerrain->GetExactGroundLevel(coords[curNodeIdx].X, coords[curNodeIdx].Y); + if (floating) + curNodeY = std::max(curNodeY, cmpWaterManager->GetExactWaterLevel(coords[curNodeIdx].X, coords[curNodeIdx].Y)); + + // find out whether curNode is visible from baseNode (careful; this is in 2D only; terrain height differences are ignored!) + bool curNodeVisible = cmpPathFinder->CheckMovement(obstructionFilter, baseNodeX, baseNodeZ, curNodeX, curNodeZ, lineRadius, passabilityClass); + + // since height differences are ignored by CheckMovement, let's call two points visible from one another only if they're at + // roughly the same terrain elevation + curNodeVisible = curNodeVisible && (fabsf(curNodeY - baseNodeY) < 3.f); // TODO: this could probably use some tuning + if (maxSegmentLinks > 0) + // max. amount of node-to-node links to be eliminated (unsigned subtraction is valid because curNodeIdx is always > baseNodeIdx) + curNodeVisible = curNodeVisible && ((curNodeIdx - baseNodeIdx) <= maxSegmentLinks); + + if (!curNodeVisible) + { + // current node is not visible from the base node, so the previous one was the last visible point from baseNode and should + // hence become the new base node for further iterations. + + // if curNodeIdx is adjacent to the current baseNode (which is possible due to steep height differences, e.g. hills), then + // we should take care not to stay stuck at the current base node + if (curNodeIdx > baseNodeIdx + 1) + { + baseNodeIdx = curNodeIdx - 1; + } + else + { + // curNodeIdx == baseNodeIdx + 1 + baseNodeIdx = curNodeIdx; + curNodeIdx++; // move the next candidate node one forward so that we don't test a point against itself in the next iteration + } + + newCoords.push_back(coords[baseNodeIdx]); // add new base node to output list + + // update base node coordinates + baseNodeX = fixed::FromFloat(coords[baseNodeIdx].X); + baseNodeZ = fixed::FromFloat(coords[baseNodeIdx].Y); + baseNodeY = cmpTerrain->GetExactGroundLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y); + if (floating) + baseNodeY = std::max(baseNodeY, cmpWaterManager->GetExactWaterLevel(coords[baseNodeIdx].X, coords[baseNodeIdx].Y)); + } + + curNodeIdx++; + } + + // we always need to add the last point back to the array; if e.g. all the points up to the last one are all visible from the current + // base node, then the loop above just ends and no endpoint is ever added to the list. + ENSURE(curNodeIdx == coords.size()); + newCoords.push_back(coords[coords.size() - 1]); + + coords.swap(newCoords); +} +void CCmpWayPointRenderer::RenderSubmit(SceneCollector& collector) +{ + CCmpItineraryPointRenderer::RenderSubmit(collector); +} +void CCmpWayPointRenderer::SetDisplayed(bool displayed) +{ + CCmpItineraryPointRenderer::SetDisplayed(displayed); +} +void CCmpWayPointRenderer::SetPosition(const CFixedVector2D& pos) +{ + CCmpItineraryPointRenderer::SetPosition(pos); +} +void CCmpWayPointRenderer::Shift() +{ + if (!m_ItineraryPoints.empty()) + m_ItineraryPoints.erase(m_ItineraryPoints.begin()); + + if (!m_MarkerEntityIds.empty()) + { + CmpPtr markerCmpPosition(GetSimContext(), m_MarkerEntityIds[0]); + if (markerCmpPosition) + markerCmpPosition->MoveOutOfWorld(); + + m_MarkerEntityIds.erase(m_MarkerEntityIds.begin()); + } + + if (!m_VisibilitySegments.empty()) + m_VisibilitySegments.erase(m_VisibilitySegments.begin()); + + if (!m_TexturedOverlayLines.empty()) + m_TexturedOverlayLines.erase(m_TexturedOverlayLines.begin()); + + if (!m_Path.empty()) + m_Path.erase(m_Path.begin()); + + UpdateMarkers(); +} +void CCmpWayPointRenderer::Stack(const CFixedVector2D& pos) +{ + m_ItineraryPoints.insert(m_ItineraryPoints.begin(), pos); + UpdateMarkers(); + // TODO don't recompute everything (maybe just shift everything one back? and recompute first only + RecomputeAllItineraryPointPaths(); +} +void CCmpWayPointRenderer::UpdateMarkers() +{ + player_id_t previousOwner = m_LastOwner; + for (size_t i = 0; i < m_ItineraryPoints.size(); ++i) + { + if (i >= m_MarkerEntityIds.size()) + m_MarkerEntityIds.push_back(INVALID_ENTITY); + + if (m_MarkerEntityIds[i] == INVALID_ENTITY) + { + // no marker exists yet, create one first + CComponentManager& componentMgr = GetSimContext().GetComponentManager(); + + // allocate a new entity for the marker + if (!m_MarkerTemplate.empty()) + { + m_MarkerEntityIds[i] = componentMgr.AllocateNewLocalEntity(); + if (m_MarkerEntityIds[i] != INVALID_ENTITY) + m_MarkerEntityIds[i] = componentMgr.AddEntity(m_MarkerTemplate, m_MarkerEntityIds[i]); + } + } + + // the marker entity should be valid at this point, otherwise something went wrong trying to allocate it + if (m_MarkerEntityIds[i] == INVALID_ENTITY) + LOGERROR("Failed to create way point marker entity"); + + CmpPtr markerCmpPosition(GetSimContext(), m_MarkerEntityIds[i]); + if (markerCmpPosition) + { + if (m_Displayed && IsSet()) + markerCmpPosition->MoveTo(m_ItineraryPoints[i].X, m_ItineraryPoints[i].Y); + else + markerCmpPosition->MoveOutOfWorld(); + } + + // Set rally point flag selection based on player civilization + CmpPtr cmpOwnership(GetEntityHandle()); + if (!cmpOwnership) + continue; + + player_id_t ownerId = cmpOwnership->GetOwner(); + if (ownerId == INVALID_PLAYER || (ownerId == previousOwner && m_LastMarkerCount > i)) + continue; + + m_LastOwner = ownerId; + CmpPtr cmpPlayerManager(GetSystemEntity()); + // cmpPlayerManager should not be null as long as this method is called on-demand instead of at Init() time + // (we can't rely on component initialization order in Init()) + if (!cmpPlayerManager) + continue; + + CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(ownerId)); + if (!cmpPlayer) + continue; + + CmpPtr cmpVisualActor(GetSimContext(), m_MarkerEntityIds[i]); + if (cmpVisualActor) + cmpVisualActor->SetVariant("civ", CStrW(cmpPlayer->GetCiv()).ToUTF8()); + } + m_LastMarkerCount = m_ItineraryPoints.size(); +} + +void CCmpWayPointRenderer::UpdateMessageSubscriptions() +{ + GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, m_Displayed && CCmpItineraryPointRenderer::IsSet()); +} +void CCmpWayPointRenderer::UpdateOverlayLines() +{ + CCmpItineraryPointRenderer::UpdateOverlayLines(); +} Index: source/simulation2/components/ICmpWayPointRenderer.h =================================================================== --- /dev/null +++ source/simulation2/components/ICmpWayPointRenderer.h @@ -0,0 +1,50 @@ +/* Copyright (C) 2017 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_ICMPWAYPOINT +#define INCLUDED_ICMPWAYPOINT + +#include "maths/FixedVector2D.h" +#include "simulation2/helpers/Position.h" +#include "simulation2/system/Interface.h" + +/** + * Way Point. + * Holds the position(s) of a unit's way points, and renders them to screen. + */ +class ICmpWayPointRenderer : public IComponent +{ +public: + virtual void SetDisplayed(bool displayed) = 0; + + virtual void SetPosition(const CFixedVector2D& pos) = 0; + + virtual void AddPosition_wrapper(const CFixedVector2D& pos) = 0; + + /// At a position at which a marker should be displayed, connected to + /// the previous first. + virtual void Stack(const CFixedVector2D& pos) = 0; + + /// Remove the first way point (as we have reached it) + virtual void Shift() = 0; + + /// Returns true if at least one display rally point is set + virtual bool IsSet() const = 0; + + DECLARE_INTERFACE_TYPE(WayPointRenderer) +}; +#endif // INCLUDED_ICMPWAYPOINT Index: source/simulation2/components/ICmpWayPointRenderer.cpp =================================================================== --- /dev/null +++ source/simulation2/components/ICmpWayPointRenderer.cpp @@ -0,0 +1,32 @@ +/* Copyright (C) 2017 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "ICmpWayPointRenderer.h" +#include "simulation2/system/InterfaceScripted.h" + +class CFixedVector2D; + +BEGIN_INTERFACE_WRAPPER(WayPointRenderer) +DEFINE_INTERFACE_METHOD_1("SetDisplayed", void, ICmpWayPointRenderer, SetDisplayed, bool) +DEFINE_INTERFACE_METHOD_1("SetPosition", void, ICmpWayPointRenderer, SetPosition, CFixedVector2D) +DEFINE_INTERFACE_METHOD_1("AddPosition", void, ICmpWayPointRenderer, AddPosition_wrapper, CFixedVector2D) +DEFINE_INTERFACE_METHOD_1("Stack", void, ICmpWayPointRenderer, Stack, CFixedVector2D) +DEFINE_INTERFACE_METHOD_0("Shift", void, ICmpWayPointRenderer, Shift) +DEFINE_INTERFACE_METHOD_CONST_0("IsSet", bool, ICmpWayPointRenderer, IsSet) +END_INTERFACE_WRAPPER(WayPointRenderer)