Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js +++ ps/trunk/binaries/data/mods/public/gui/session/input.js @@ -346,6 +346,7 @@ "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: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js +++ ps/trunk/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,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] @@ -123,6 +136,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] @@ -169,6 +188,13 @@ "allowCapture": false }); + Engine.GuiInterfaceCall("DisplayWayPoint", + { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] @@ -229,6 +255,13 @@ "queued": queued, "allowCapture": false }); + + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_patrol", "entity": selection[0] }); return true; }, @@ -275,6 +308,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] @@ -326,6 +365,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] @@ -367,6 +412,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] @@ -438,6 +489,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] @@ -486,6 +543,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] @@ -547,6 +610,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": selection[0] @@ -638,6 +707,12 @@ "queued": queued }); + Engine.GuiInterfaceCall("DisplayWayPoint", { + "entities": selection, + "target": action.target, + "queued": queued + }); + Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js +++ ps/trunk/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 = {}; }; @@ -1035,6 +1036,67 @@ }; /** + * 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) +{ + let cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + let cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player); + // If there are some way points already displayed, first hide them + for (let ent of this.entsWayPointsDisplayed) + { + let cmpWayPointRenderer = Engine.QueryInterface(ent, IID_WayPointRenderer); + if (cmpWayPointRenderer) + cmpWayPointRenderer.SetDisplayed(false); + } + this.entsWayPointsDisplayed = []; + // Show the way points for the passed entities + for (let ent of cmd.entities) + { + let cmpWayPointRenderer = Engine.QueryInterface(ent, IID_WayPointRenderer); + if (!cmpWayPointRenderer) + continue; + // entity must have a way point component to display a way point marker + // (regardless of whether cmd specifies a custom location) + let cmpWayPoint = Engine.QueryInterface(ent, IID_WayPoint); + if (!cmpWayPoint) + continue; + // Verify the owner + let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); + if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) + if (!cmpOwnership || cmpOwnership.GetOwner() != player) + continue; + // If the command was passed an explicit position or a target, use that and + // override the real way point position; otherwise use the real position + let pos; + if (cmd.x && cmd.z) + pos = cmd; + else if (cmd.target) + pos = Engine.QueryInterface(cmd.target, IID_Position).GetPosition(); + else + pos = cmpWayPoint.GetPositions()[0]; // may return undefined if no way point is set + if (pos) + { + // Only update the position if we changed it (cmd.queued is set) + if (cmd.queued == true) + cmpWayPointRenderer.AddPosition({'x': pos.x, 'y': pos.z}); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z + else if (cmd.queued == false) + cmpWayPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z + cmpWayPointRenderer.SetDisplayed(true); + + // remember which entities have their way points displayed so we can hide them again + this.entsWayPointsDisplayed.push(ent); + } + } +}; + +/** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. @@ -1986,6 +2048,7 @@ "GetPlayerEntities": 1, "GetNonGaiaEntities": 1, "DisplayRallyPoint": 1, + "DisplayWayPoint": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js @@ -3687,6 +3687,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) { @@ -3738,6 +3748,9 @@ var order = { "type": type, "data": data }; this.orderQueue.push(order); + if (!data.force) + this.AddWayPoint(data); + // If we didn't already have an order, then process this new one if (this.orderQueue.length == 1) { @@ -3768,6 +3781,7 @@ { var cheeringOrder = this.orderQueue.shift(); this.orderQueue.unshift(cheeringOrder, order); + // TODO: AddWayPoint } else if (this.order && this.IsPacking()) { @@ -3778,6 +3792,9 @@ { this.orderQueue.unshift(order); this.order = order; + + this.AddWayPointFront(data); + let ret = this.UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data} ); @@ -3791,6 +3808,8 @@ this.orderQueue.shift(); this.order = this.orderQueue[0]; } + + // TODO: WayPoints } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); @@ -3849,15 +3868,88 @@ 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); } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; +UnitAI.prototype.AddWayPointFront = function(data) +{ + let cmpWayPoint = Engine.QueryInterface(this.entity, IID_WayPoint); + + if(!cmpWayPoint) + return; + + let pos; + let cmpPosition = Engine.QueryInterface(data.target, IID_Position); + + if (data.target && cmpPosition) + pos = cmpPosition.GetPosition(); + else + pos = {'x': data.x, 'z': data.z }; + + cmpWayPoint.AddPositionFront(pos.x, pos.z); + + let cmpWayPointRenderer = Engine.QueryInterface(this.entity, IID_WayPointRenderer); + + // AddPositionFront takes a CFixedVector2D which has X/Y components, not X/Z + if (cmpWayPointRenderer) + cmpWayPointRenderer.AddPositionFront({'x': pos.x, 'y': pos.z}); +}; + +UnitAI.prototype.AddWayPoint = function(data) +{ + var cmpWayPoint = Engine.QueryInterface(this.entity, IID_WayPoint); + if (cmpWayPoint) + { + var pos; + var 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.AddPosition(pos.x, pos.z); + var cmpWayPointRenderer = Engine.QueryInterface(this.entity, IID_WayPointRenderer); + if (cmpWayPointRenderer) + cmpWayPointRenderer.AddPosition({'x': pos.x, 'y': pos.z}); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z + } +}; + +UnitAI.prototype.ReplaceWayPoint = function(data) +{ + let cmpWayPoint = Engine.QueryInterface(this.entity, IID_WayPoint); + if (!cmpWayPoint) + return; + + let pos; + let cmpPosition; + + cmpWayPoint.Unset(); + + 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.AddPosition(pos.x, pos.z); + let cmpWayPointRenderer = Engine.QueryInterface(this.entity, IID_WayPointRenderer); + + // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z + if (cmpWayPointRenderer) + cmpWayPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); } + UnitAI.prototype.GetOrders = function() { return this.orderQueue.slice(); @@ -5292,6 +5384,10 @@ var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (cmpTrader.HasBothMarkets()) { + + this.AddWayPoint({"target": cmpTrader.GetFirstMarket()}); + this.AddWayPoint({"target": cmpTrader.GetSecondMarket()}); + let data = { "target": cmpTrader.GetFirstMarket(), "route": route, @@ -5346,6 +5442,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(); @@ -5361,11 +5465,13 @@ if (!this.CanTrade(currentMarket)) { this.StopTrading(); + // TODO clear waypoints return; } if (!this.CheckTargetRange(currentMarket, IID_Trader)) { + this.AddWayPoint({"target": currentMarket}); if (!this.MoveToMarket(currentMarket)) // If the current market is not reached try again this.StopTrading(); return; Index: ps/trunk/binaries/data/mods/public/simulation/components/WayPoint.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/WayPoint.js +++ ps/trunk/binaries/data/mods/public/simulation/components/WayPoint.js @@ -0,0 +1,41 @@ +function WayPoint() {} + +WayPoint.prototype.Schema = ""; + +WayPoint.prototype.Init = function() +{ + this.pos = []; +}; + +WayPoint.prototype.AddPosition = function(x, z) +{ + this.pos.push({ + 'x': x, + 'z': z + }); +}; + +WayPoint.prototype.AddPositionFront = function(x, z) +{ + this.pos.push({ + 'x': x, + 'z': z + }); +}; + +WayPoint.prototype.GetPositions = function() +{ + return this.pos; +}; + +WayPoint.prototype.Unset = function() +{ + this.pos = []; +}; + +WayPoint.prototype.Shift = function() +{ + this.pos.shift(); +}; + +Engine.RegisterComponentType(IID_WayPoint, "WayPoint", WayPoint); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/WayPoint.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/WayPoint.js +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/WayPoint.js @@ -0,0 +1 @@ +Engine.RegisterInterface("WayPoint"); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ ps/trunk/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: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_WayPoint.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_WayPoint.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_WayPoint.js @@ -0,0 +1,14 @@ +Engine.LoadComponentScript("interfaces/WayPoint.js"); +Engine.LoadComponentScript("WayPoint.js"); + +let cmpWayPoint = ConstructComponent(ent, "Pack", {}); + +TS_ASSERT_UNEVAL_EQUALS(cmpWayPoint.GetPositions, []); +cmpWayPoint.AddPosition(10, 5); +TS_ASSERT_UNEVAL_EQUALS(cmpWayPoint.GetPositions, [{ 'x': 10, 'z': 5 }]); +cmpWayPoint.AddPosition(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: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js @@ -2,8 +2,7 @@ // are likely to fail, which may be useful for debugging AIs var g_DebugCommands = false; -function ProcessCommand(player, cmd) -{ +function ProcessCommand(player, cmd) { let data = { "cmpPlayerManager": Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager) }; @@ -40,30 +39,26 @@ // moves the entities closer to the target before giving up.) // Now handle various commands - if (g_Commands[cmd.type]) - { + if (g_Commands[cmd.type]) { var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("PlayerCommand", { "player": player, "cmd": cmd }); g_Commands[cmd.type](player, cmd, data); } else - error("Invalid command: unknown command type: "+uneval(cmd)); + error("Invalid command: unknown command type: " + uneval(cmd)); } var g_Commands = { - "debug-print": function(player, cmd, data) - { + "debug-print": function (player, cmd, data) { print(cmd.message); }, - "chat": function(player, cmd, data) - { + "chat": function (player, cmd, data) { var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": cmd.type, "players": [player], "message": cmd.message }); }, - "aichat": function(player, cmd, data) - { + "aichat": function (player, cmd, data) { var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); var notification = { "players": [player] }; for (var key in cmd) @@ -71,31 +66,28 @@ cmpGuiInterface.PushNotification(notification); }, - "cheat": function(player, cmd, data) - { + "cheat": function (player, cmd, data) { Cheat(cmd); }, - "diplomacy": function(player, cmd, data) - { + "diplomacy": function (player, cmd, data) { let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (data.cmpPlayer.GetLockTeams() || - cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive()) + cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive()) return; - switch(cmd.to) - { - case "ally": - data.cmpPlayer.SetAlly(cmd.player); - break; - case "neutral": - data.cmpPlayer.SetNeutral(cmd.player); - break; - case "enemy": - data.cmpPlayer.SetEnemy(cmd.player); - break; - default: - warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to); + switch (cmd.to) { + case "ally": + data.cmpPlayer.SetAlly(cmd.player); + break; + case "neutral": + data.cmpPlayer.SetNeutral(cmd.player); + break; + case "enemy": + data.cmpPlayer.SetEnemy(cmd.player); + break; + default: + warn("Invalid command: Could not set " + player + " diplomacy status of player " + cmd.player + " to " + cmd.to); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); @@ -107,13 +99,11 @@ }); }, - "tribute": function(player, cmd, data) - { + "tribute": function (player, cmd, data) { data.cmpPlayer.TributeResource(cmd.player, cmd.amounts); }, - "control-all": function(player, cmd, data) - { + "control-all": function (player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; @@ -127,8 +117,7 @@ data.cmpPlayer.SetControlAllUnits(cmd.flag); }, - "reveal-map": function(player, cmd, data) - { + "reveal-map": function (player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; @@ -145,35 +134,30 @@ cmpRangeManager.SetLosRevealAll(-1, cmd.enable); }, - "walk": function(player, cmd, data) - { + "walk": function (player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued); }); }, - "walk-to-range": function(player, cmd, data) - { + "walk-to-range": function (player, cmd, data) { // Only used by the AI - for (let ent of data.entities) - { + for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued); } }, - "attack-walk": function(player, cmd, data) - { + "attack-walk": function (player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued); }); }, - "attack": function(player, cmd, data) - { + "attack": function (player, cmd, data) { if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target))) - warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); + warn("Invalid command: attack target is not owned by enemy of player " + player + ": " + uneval(cmd)); let allowCapture = cmd.allowCapture || cmd.allowCapture == null; @@ -182,83 +166,72 @@ }); }, - "patrol": function(player, cmd, data) - { + "patrol": function (player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.queued) ); }, - "heal": function(player, cmd, data) - { + "heal": function (player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target))) - warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd)); + warn("Invalid command: heal target is not owned by player " + player + " or their ally: " + uneval(cmd)); GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.Heal(cmd.target, cmd.queued); }); }, - "repair": function(player, cmd, data) - { + "repair": function (player, cmd, data) { // This covers both repairing damaged buildings, and constructing unfinished foundations if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) - warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd)); + warn("Invalid command: repair target is not owned by ally of player " + player + ": " + uneval(cmd)); GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued); }); }, - "gather": function(player, cmd, data) - { + "gather": function (player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target))) - warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd)); + warn("Invalid command: resource is not owned by gaia or player " + player + ": " + uneval(cmd)); GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.Gather(cmd.target, cmd.queued); }); }, - "gather-near-position": function(player, cmd, data) - { + "gather-near-position": function (player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued); }); }, - "returnresource": function(player, cmd, data) - { + "returnresource": function (player, cmd, data) { if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target)) - warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd)); + warn("Invalid command: dropsite is not owned by player " + player + ": " + uneval(cmd)); GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.ReturnResource(cmd.target, cmd.queued); }); }, - "back-to-work": function(player, cmd, data) - { - for (let ent of data.entities) - { + "back-to-work": function (player, cmd, data) { + for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); - if(!cmpUnitAI || !cmpUnitAI.BackToWork()) + if (!cmpUnitAI || !cmpUnitAI.BackToWork()) notifyBackToWorkFailure(player); } }, - "remove-guard": function(player, cmd, data) - { - for (let ent of data.entities) - { + "remove-guard": function (player, cmd, data) { + for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.RemoveGuard(); } }, - "train": function(player, cmd, data) - { + "train": function (player, cmd, data) { // Check entity limits var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template); var unitCategory = null; @@ -266,20 +239,16 @@ unitCategory = template.TrainingRestrictions.Category; // Verify that the building(s) can be controlled by the player - if (data.entities.length <= 0) - { + if (data.entities.length <= 0) { if (g_DebugCommands) - warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd)); + warn("Invalid command: training building(s) cannot be controlled by player " + player + ": " + uneval(cmd)); return; } - for (let ent of data.entities) - { - if (unitCategory) - { + for (let ent of data.entities) { + if (unitCategory) { var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits); - if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count)) - { + if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count)) { if (g_DebugCommands) warn(unitCategory + " train limit is reached: " + uneval(cmd)); continue; @@ -287,8 +256,7 @@ } var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager); - if (!cmpTechnologyManager.CanProduce(cmd.template)) - { + if (!cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) warn("Invalid command: training requires unresearched technology: " + uneval(cmd)); continue; @@ -298,13 +266,10 @@ // Check if the building can train the unit // TODO: the AI API does not take promotion technologies into account for the list // of trainable units (taken directly from the unit template). Here is a temporary fix. - if (queue && data.cmpPlayer.IsAI()) - { + if (queue && data.cmpPlayer.IsAI()) { var list = queue.GetEntitiesList(); - if (list.indexOf(cmd.template) === -1 && cmd.promoted) - { - for (var promoted of cmd.promoted) - { + if (list.indexOf(cmd.template) === -1 && cmd.promoted) { + for (var promoted of cmd.promoted) { if (list.indexOf(promoted) === -1) continue; cmd.template = promoted; @@ -320,18 +285,15 @@ } }, - "research": function(player, cmd, data) - { - if (!CanControlUnit(cmd.entity, player, data.controlAllUnits)) - { + "research": function (player, cmd, data) { + if (!CanControlUnit(cmd.entity, player, data.controlAllUnits)) { if (g_DebugCommands) - warn("Invalid command: research building cannot be controlled by player "+player+": "+uneval(cmd)); + warn("Invalid command: research building cannot be controlled by player " + player + ": " + uneval(cmd)); return; } var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager); - if (!cmpTechnologyManager.CanResearch(cmd.template)) - { + if (!cmpTechnologyManager.CanResearch(cmd.template)) { if (g_DebugCommands) warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd)); return; @@ -342,12 +304,10 @@ queue.AddBatch(cmd.template, "technology"); }, - "stop-production": function(player, cmd, data) - { - if (!CanControlUnit(cmd.entity, player, data.controlAllUnits)) - { + "stop-production": function (player, cmd, data) { + if (!CanControlUnit(cmd.entity, player, data.controlAllUnits)) { if (g_DebugCommands) - warn("Invalid command: production building cannot be controlled by player "+player+": "+uneval(cmd)); + warn("Invalid command: production building cannot be controlled by player " + player + ": " + uneval(cmd)); return; } @@ -356,29 +316,24 @@ queue.RemoveBatch(cmd.id); }, - "construct": function(player, cmd, data) - { + "construct": function (player, cmd, data) { TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd); }, - "construct-wall": function(player, cmd, data) - { + "construct-wall": function (player, cmd, data) { TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd); }, - "delete-entities": function(player, cmd, data) - { - for (let ent of data.entities) - { + "delete-entities": function (player, cmd, data) { + for (let ent of data.entities) { let cmpHealth = QueryMiragedInterface(ent, IID_Health); - if (!data.controlAllUnits) - { + if (!data.controlAllUnits) { if (cmpHealth && cmpHealth.IsUndeletable()) continue; let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable && - cmpCapturable.GetCapturePoints()[player] < cmpCapturable.GetMaxCapturePoints() / 2) + cmpCapturable.GetCapturePoints()[player] < cmpCapturable.GetMaxCapturePoints() / 2) continue; let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); @@ -387,8 +342,7 @@ } let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); - if (cmpMirage) - { + if (cmpMirage) { let cmpMiragedHealth = Engine.QueryInterface(cmpMirage.parent, IID_Health); if (cmpMiragedHealth) cmpMiragedHealth.Kill(); @@ -404,13 +358,10 @@ } }, - "set-rallypoint": function(player, cmd, data) - { - for (let ent of data.entities) - { + "set-rallypoint": function (player, cmd, data) { + for (let ent of data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); - if (cmpRallyPoint) - { + if (cmpRallyPoint) { if (!cmd.queued) cmpRallyPoint.Unset(); @@ -420,30 +371,25 @@ } }, - "unset-rallypoint": function(player, cmd, data) - { - for (let ent of data.entities) - { + "unset-rallypoint": function (player, cmd, data) { + for (let ent of data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) cmpRallyPoint.Reset(); } }, - "defeat-player": function(player, cmd, data) - { + "defeat-player": function (player, cmd, data) { let cmpPlayer = QueryPlayerIDInterface(player); if (cmpPlayer) cmpPlayer.SetState("defeated", !!cmd.resign); }, - "garrison": function(player, cmd, data) - { + "garrison": function (player, cmd, data) { // Verify that the building can be controlled by the player or is mutualAlly - if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) - { + if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) - warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); + warn("Invalid command: garrison target cannot be controlled by player " + player + " (or ally): " + uneval(cmd)); return; } @@ -452,13 +398,11 @@ }); }, - "guard": function(player, cmd, data) - { + "guard": function (player, cmd, data) { // Verify that the target can be controlled by the player or is mutualAlly - if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) - { + if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) - warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd)); + warn("Invalid command: guard/escort target cannot be controlled by player " + player + ": " + uneval(cmd)); return; } @@ -467,20 +411,17 @@ }); }, - "stop": function(player, cmd, data) - { + "stop": function (player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.Stop(cmd.queued); }); }, - "unload": function(player, cmd, data) - { + "unload": function (player, cmd, data) { // Verify that the building can be controlled by the player or is mutualAlly - if (!CanControlUnitOrIsAlly(cmd.garrisonHolder, player, data.controlAllUnits)) - { + if (!CanControlUnitOrIsAlly(cmd.garrisonHolder, player, data.controlAllUnits)) { if (g_DebugCommands) - warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); + warn("Invalid command: unload target cannot be controlled by player " + player + " (or ally): " + uneval(cmd)); return; } @@ -499,18 +440,15 @@ notifyUnloadFailure(player, cmd.garrisonHolder); }, - "unload-template": function(player, cmd, data) - { + "unload-template": function (player, cmd, data) { var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); - for (let garrisonHolder of entities) - { + for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); - if (cmpGarrisonHolder) - { + if (cmpGarrisonHolder) { // Only the owner of the garrisonHolder may unload entities from any owners if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits - && player != +cmd.owner) - continue; + && player != +cmd.owner) + continue; if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.owner, cmd.all)) notifyUnloadFailure(player, garrisonHolder); @@ -518,57 +456,47 @@ } }, - "unload-all-by-owner": function(player, cmd, data) - { + "unload-all-by-owner": function (player, cmd, data) { var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); - for (let garrisonHolder of entities) - { + for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllByOwner(player)) notifyUnloadFailure(player, garrisonHolder); } }, - "unload-all": function(player, cmd, data) - { + "unload-all": function (player, cmd, data) { var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits); - for (let garrisonHolder of entities) - { + for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll()) notifyUnloadFailure(player, garrisonHolder); } }, - "increase-alert-level": function(player, cmd, data) - { - for (let ent of data.entities) - { + "increase-alert-level": function (player, cmd, data) { + for (let ent of data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (!cmpAlertRaiser || !cmpAlertRaiser.IncreaseAlertLevel()) notifyAlertFailure(player); } }, - "alert-end": function(player, cmd, data) - { - for (let ent of data.entities) - { + "alert-end": function (player, cmd, data) { + for (let ent of data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.EndOfAlert(); } }, - "formation": function(player, cmd, data) - { + "formation": function (player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd.name).forEach(cmpUnitAI => { cmpUnitAI.MoveIntoFormation(cmd); }); }, - "promote": function(player, cmd, data) - { + "promote": function (player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; @@ -580,28 +508,23 @@ "translateMessage": true }); - for (let ent of cmd.entities) - { + for (let ent of cmd.entities) { var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp()); } }, - "stance": function(player, cmd, data) - { - for (let ent of data.entities) - { + "stance": function (player, cmd, data) { + for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && !cmpUnitAI.IsTurret()) cmpUnitAI.SwitchToStance(cmd.name); } }, - "lock-gate": function(player, cmd, data) - { - for (let ent of data.entities) - { + "lock-gate": function (player, cmd, data) { + for (let ent of data.entities) { var cmpGate = Engine.QueryInterface(ent, IID_Gate); if (!cmpGate) continue; @@ -613,43 +536,59 @@ } }, - "setup-trade-route": function(player, cmd, data) - { + "setup-trade-route": function (player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued); }); + + for (var ent in cmd.entities) { + var cmpWayPoint = Engine.QueryInterface(ent, IID_WayPoint); + if (cmpWayPoint) { + if (!cmd.queued) + cmpWayPoint.Unset(); + if (cmd.x && cmd.z) + cmpWayPoint.AddPosition(cmd.x, cmd.z); + else if (cmd.target) { + var cmpPosition = Engine.QueryInterface(cmd.target, IID_Position); + if (cmpPosition) { + cmpWayPoint.AddPosition(cmpPosition.GetPosition().x, cmpPosition.GetPosition().z); + // For setup-trade-route call from RallyPointCommands + if (cmd.source) { + warn("cmd.source " + uneval(cmd)); + var cmpSourcePosition = Engine.QueryInterface(cmd.source, IID_Position); + if (cmpSourcePosition) + cmpWayPoint.AddPosition(cmpSourcePosition.GetPosition().x, cmpSourcePosition.GetPosition().z); + } + } + } + } + } }, - "set-trading-goods": function(player, cmd, data) - { + "set-trading-goods": function (player, cmd, data) { data.cmpPlayer.SetTradingGoods(cmd.tradingGoods); }, - "barter": function(player, cmd, data) - { + "barter": function (player, cmd, data) { var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); cmpBarter.ExchangeResources(data.playerEnt, cmd.sell, cmd.buy, cmd.amount); }, - "set-shading-color": function(player, cmd, data) - { + "set-shading-color": function (player, cmd, data) { // Prevent multiplayer abuse if (!data.cmpPlayer.IsAI()) return; // Debug command to make an entity brightly colored - for (let ent of cmd.entities) - { + for (let ent of cmd.entities) { var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0); // alpha isn't used so just send 0 } }, - "pack": function(player, cmd, data) - { - for (let ent of data.entities) - { + "pack": function (player, cmd, data) { + for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; @@ -661,10 +600,8 @@ } }, - "cancel-pack": function(player, cmd, data) - { - for (let ent of data.entities) - { + "cancel-pack": function (player, cmd, data) { + for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; @@ -676,17 +613,14 @@ } }, - "upgrade": function(player, cmd, data) - { - for (let ent of data.entities) - { + "upgrade": function (player, cmd, data) { + for (let ent of data.entities) { var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template)) continue; - if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template)) - { + if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template)) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [data.cmpPlayer.GetPlayerID()], @@ -695,8 +629,7 @@ continue; } - if (!CanGarrisonedChangeTemplate(ent, cmd.template)) - { + if (!CanGarrisonedChangeTemplate(ent, cmd.template)) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [data.cmpPlayer.GetPlayerID()], @@ -710,16 +643,14 @@ var template = cmpTemplateManager.GetTemplate(cmd.template); var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); if (template.TrainingRestrictions && !cmpEntityLimits.AllowedToTrain(template.TrainingRestrictions.Category, 1) || - template.BuildRestrictions && !cmpEntityLimits.AllowedToBuild(template.BuildRestrictions.Category)) - { + template.BuildRestrictions && !cmpEntityLimits.AllowedToBuild(template.BuildRestrictions.Category)) { if (g_DebugCommands) warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd)); continue; } var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager); - if (cmpUpgrade.GetRequiredTechnology(cmd.template) && !cmpTechnologyManager.IsTechnologyResearched(cmpUpgrade.GetRequiredTechnology(cmd.template))) - { + if (cmpUpgrade.GetRequiredTechnology(cmd.template) && !cmpTechnologyManager.IsTechnologyResearched(cmpUpgrade.GetRequiredTechnology(cmd.template))) { if (g_DebugCommands) warn("Invalid command: upgrading requires unresearched technology: " + uneval(cmd)); continue; @@ -729,18 +660,15 @@ } }, - "cancel-upgrade": function(player, cmd, data) - { - for (let ent of data.entities) - { + "cancel-upgrade": function (player, cmd, data) { + for (let ent of data.entities) { let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) cmpUpgrade.CancelUpgrade(data.cmpPlayer.playerID); } }, - "attack-request": function(player, cmd, data) - { + "attack-request": function (player, cmd, data) { // Send a chat message to human players var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ @@ -757,8 +685,7 @@ cmpAIInterface.PushEvent("AttackRequest", cmd); }, - "spy-request": function(player, cmd, data) - { + "spy-request": function (player, cmd, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let ent = pickRandom(cmpRangeManager.GetEntitiesByPlayer(cmd.player).filter(ent => { let cmpVisionSharing = Engine.QueryInterface(ent, IID_VisionSharing); @@ -766,8 +693,7 @@ })); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - if (ent) - { + if (ent) { Engine.QueryInterface(ent, IID_VisionSharing).AddSpy(cmd.source); cmpGUIInterface.PushNotification({ "type": "spy-response", @@ -804,10 +730,8 @@ // message to any component you like. }, - "set-dropsite-sharing": function(player, cmd, data) - { - for (let ent of data.entities) - { + "set-dropsite-sharing": function (player, cmd, data) { + for (let ent of data.entities) { let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite && cmpResourceDropsite.IsSharable()) cmpResourceDropsite.SetSharing(cmd.shared); @@ -818,8 +742,7 @@ /** * Sends a GUI notification about unit(s) that failed to ungarrison. */ -function notifyUnloadFailure(player, garrisonHolder) -{ +function notifyUnloadFailure(player, garrisonHolder) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", @@ -832,8 +755,7 @@ /** * Sends a GUI notification about worker(s) that failed to go back to work. */ -function notifyBackToWorkFailure(player) -{ +function notifyBackToWorkFailure(player) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", @@ -846,8 +768,7 @@ /** * Sends a GUI notification about Alerts that failed to be raised */ -function notifyAlertFailure(player) -{ +function notifyAlertFailure(player) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", @@ -861,16 +782,13 @@ * Get some information about the formations used by entities. * The entities must have a UnitAI component. */ -function ExtractFormations(ents) -{ +function ExtractFormations(ents) { var entities = []; // subset of ents that have UnitAI var members = {}; // { formationentity: [ent, ent, ...], ... } - for (let ent of ents) - { + for (let ent of ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var fid = cmpUnitAI.GetFormationController(); - if (fid != INVALID_ENTITY) - { + if (fid != INVALID_ENTITY) { if (!members[fid]) members[fid] = []; members[fid].push(ent); @@ -878,7 +796,7 @@ entities.push(ent); } - var ids = [ id for (id in members) ]; + var ids = [id for (id in members) ]; return { "entities": entities, "members": members, "ids": ids }; } @@ -887,8 +805,7 @@ * Tries to find the best angle to put a dock at a given position * Taken from GuiInterface.js */ -function GetDockAngle(template, x, z) -{ +function GetDockAngle(template, x, z) { var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); if (!cmpTerrain || !cmpWaterManager) @@ -897,7 +814,7 @@ // Get footprint size var halfSize = 0; if (template.Footprint.Square) - halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2; + halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"]) / 2; else if (template.Footprint.Circle) halfSize = template.Footprint.Circle["@radius"]; @@ -911,15 +828,13 @@ * 6. Calculate angle using average of sequence */ const numPoints = 16; - for (var dist = 0; dist < 4; ++dist) - { + for (var dist = 0; dist < 4; ++dist) { var waterPoints = []; - for (var i = 0; i < numPoints; ++i) - { - var angle = (i/numPoints)*2*Math.PI; - var d = halfSize*(dist+1); - var nx = x - d*Math.sin(angle); - var nz = z + d*Math.cos(angle); + for (var i = 0; i < numPoints; ++i) { + var angle = (i / numPoints) * 2 * Math.PI; + var d = halfSize * (dist + 1); + var nx = x - d * Math.sin(angle); + var nz = z + d * Math.cos(angle); if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz)) waterPoints.push(i); @@ -928,11 +843,9 @@ var length = waterPoints.length; if (!length) continue; - for (var i = 0; i < length; ++i) - { + for (var i = 0; i < length; ++i) { var count = 0; - for (let j = 0; j < length - 1; ++j) - { + for (let j = 0; j < length - 1; ++j) { if ((waterPoints[(i + j) % length] + 1) % numPoints == waterPoints[(i + j + 1) % length]) ++count; else @@ -942,18 +855,16 @@ } var start = 0; var count = 0; - for (var c in consec) - { - if (consec[c] > count) - { + for (var c in consec) { + if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching - if (count != numPoints-1) - return -((waterPoints[start] + consec[start]/2) % numPoints) / numPoints * 2 * Math.PI; + if (count != numPoints - 1) + return -((waterPoints[start] + consec[start] / 2) % numPoints) / numPoints * 2 * Math.PI; } return undefined; } @@ -962,8 +873,7 @@ * Attempts to construct a building using the specified parameters. * Returns true on success, false on failure. */ -function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd) -{ +function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd) { // Message structure: // { // "type": "construct", @@ -1001,8 +911,7 @@ // Tentatively create the foundation (we might find later that it's a invalid build command) var ent = Engine.AddEntity(foundationTemplate); - if (ent == INVALID_ENTITY) - { + if (ent == INVALID_ENTITY) { // Error (e.g. invalid template names) error("Error creating foundation entity for '" + cmd.template + "'"); return false; @@ -1011,8 +920,7 @@ // If it's a dock, get the right angle. var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template); var angle = cmd.angle; - if (template.BuildRestrictions.Category === "Dock") - { + if (template.BuildRestrictions.Category === "Dock") { let angleDock = GetDockAngle(template, cmd.x, cmd.z); if (angleDock !== undefined) angle = angleDock; @@ -1024,13 +932,11 @@ cmpPosition.SetYRotation(angle); // Set the obstruction control group if needed - if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2) - { + if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2) { var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); // primary control group must always be valid - if (cmd.obstructionControlGroup) - { + if (cmd.obstructionControlGroup) { if (cmd.obstructionControlGroup <= 0) warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0"); @@ -1047,13 +953,11 @@ // Check whether building placement is valid var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); - if (cmpBuildRestrictions) - { + if (cmpBuildRestrictions) { var ret = cmpBuildRestrictions.CheckPlacement(); - if (!ret.success) - { + if (!ret.success) { if (g_DebugCommands) - warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd)); + warn("Invalid command: build restrictions check failed with '" + ret.message + "' for player " + player + ": " + uneval(cmd)); var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); ret.players = [player]; @@ -1071,10 +975,9 @@ // Check entity limits var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); - if (cmpEntityLimits && !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) - { + if (cmpEntityLimits && !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) { if (g_DebugCommands) - warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd)); + warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd)); // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); @@ -1083,10 +986,9 @@ } var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); - if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template)) - { + if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) - warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd)); + warn("Invalid command: required technology check failed for player " + player + ": " + uneval(cmd)); var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ @@ -1106,10 +1008,9 @@ let cmpCost = Engine.QueryInterface(ent, IID_Cost); let costs = cmpCost.GetResourceCosts(player); - if (!cmpPlayer.TrySubtractResources(costs)) - { + if (!cmpPlayer.TrySubtractResources(costs)) { if (g_DebugCommands) - warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd)); + warn("Invalid command: building cost check failed for player " + player + ": " + uneval(cmd)); Engine.DestroyEntity(ent); cmpPosition.MoveOutOfWorld(); @@ -1126,11 +1027,10 @@ // send Metadata info if any if (cmd.metadata) - Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } ); + Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata": cmd.metadata, "owner": player }); // Tell the units to start building this new entity - if (cmd.autorepair) - { + if (cmd.autorepair) { ProcessCommand(player, { "type": "repair", "entities": entities, @@ -1143,8 +1043,7 @@ return ent; } -function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd) -{ +function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd) { // 'cmd' message structure: // { // "type": "construct-wall", @@ -1177,14 +1076,12 @@ if (cmd.pieces.length <= 0) return; - if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower) - { + if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side"); return; } - if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower) - { + if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side"); return; } @@ -1224,11 +1121,9 @@ // If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that // the first wall piece can be built while overlapping it. - if (cmd.startSnappedEntity) - { + if (cmd.startSnappedEntity) { var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction); - if (!cmpSnappedStartObstruction) - { + if (!cmpSnappedStartObstruction) { error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component"); return; } @@ -1240,8 +1135,7 @@ var i = 0; var queued = cmd.queued; var pieces = clone(cmd.pieces); - for (; i < pieces.length; ++i) - { + for (; i < pieces.length; ++i) { var piece = pieces[i]; // All wall pieces after the first must be queued. @@ -1250,12 +1144,10 @@ // 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do // start position snapping (implying that the first entity we build must be a tower) - if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY) - { - if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity)) - { - error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")"); - break; + if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY) { + if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity)) { + error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")"); + break; } } @@ -1277,29 +1169,30 @@ // If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's // control group directly at construction time (instead of setting it in the second pass) to allow it to be built // while overlapping the snapped entity. - if (i == pieces.length - 1 && cmd.endSnappedEntity) - { + if (i == pieces.length - 1 && cmd.endSnappedEntity) { var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); if (cmpEndSnappedObstruction) 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) - { + if (pieceEntityId) { // wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later piece.ent = pieceEntityId; // if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex - if (piece.template == cmd.wallSet.templates.tower) - { + if (piece.template == cmd.wallSet.templates.tower) { var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction); var newTowerControlGroup = pieceEntityId; - if (i > 0) - { + if (i > 0) { //warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup); - var cmpPreviousObstruction = Engine.QueryInterface(pieces[i-1].ent, IID_Obstruction); + var cmpPreviousObstruction = Engine.QueryInterface(pieces[i - 1].ent, IID_Obstruction); // TODO: ensure that cmpPreviousObstruction exists // TODO: ensure that the previous obstruction does not yet have a secondary control group set cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup); @@ -1326,11 +1219,9 @@ lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces // only start off with the ending side's snapped tower's control group if we were able to build the entire wall - if (cmd.endSnappedEntity && wallComplete) - { + if (cmd.endSnappedEntity && wallComplete) { var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); - if (!cmpSnappedEndObstruction) - { + if (!cmpSnappedEndObstruction) { error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component"); return; } @@ -1338,44 +1229,36 @@ lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup(); } - for (var j = lastBuiltPieceIndex; j >= 0; --j) - { + for (var j = lastBuiltPieceIndex; j >= 0; --j) { var piece = pieces[j]; - if (!piece.ent) - { + if (!piece.ent) { error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'"); continue; } var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction); - if (!cmpPieceObstruction) - { + if (!cmpPieceObstruction) { error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component"); continue; } - if (piece.template == cmd.wallSet.templates.tower) - { + if (piece.template == cmd.wallSet.templates.tower) { // encountered a tower entity, update the last tower control group lastTowerControlGroup = cmpPieceObstruction.GetControlGroup(); } - else - { + else { // Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'. // Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group // dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'. var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2(); - if (existingSecondaryControlGroup == INVALID_ENTITY) - { - if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY) - { + if (existingSecondaryControlGroup == INVALID_ENTITY) { + if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY) { cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup); } } - else if (existingSecondaryControlGroup != lastTowerControlGroup) - { + else if (existingSecondaryControlGroup != lastTowerControlGroup) { error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")"); break; } @@ -1386,11 +1269,9 @@ /** * Remove the given list of entities from their current formations. */ -function RemoveFromFormation(ents) -{ +function RemoveFromFormation(ents) { var formation = ExtractFormations(ents); - for (var fid in formation.members) - { + for (var fid in formation.members) { var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); @@ -1401,12 +1282,10 @@ * Returns a list of UnitAI components, each belonging either to a * selected unit or to a formation entity for groups of the selected units. */ -function GetFormationUnitAIs(ents, player, formationTemplate) -{ +function GetFormationUnitAIs(ents, player, formationTemplate) { // If an individual was selected, remove it from any formation // and command it individually - if (ents.length == 1) - { + if (ents.length == 1) { // Skip unit if it has no UnitAI var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI); if (!cmpUnitAI) @@ -1414,14 +1293,13 @@ RemoveFromFormation(ents); - return [ cmpUnitAI ]; + return [cmpUnitAI]; } // Separate out the units that don't support the chosen formation var formedEnts = []; var nonformedUnitAIs = []; - for (let ent of ents) - { + for (let ent of ents) { // Skip units with no UnitAI or no position var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var cmpPosition = Engine.QueryInterface(ent, IID_Position); @@ -1435,16 +1313,14 @@ var nullFormation = (formationTemplate || cmpUnitAI.GetLastFormationTemplate()) == "formations/null"; if (!nullFormation && cmpIdentity && cmpIdentity.CanUseFormation(formationTemplate || "formations/null")) formedEnts.push(ent); - else - { + else { if (nullFormation) cmpUnitAI.SetLastFormationTemplate("formations/null"); nonformedUnitAIs.push(cmpUnitAI); } } - if (formedEnts.length == 0) - { + if (formedEnts.length == 0) { // No units support the foundation - return all the others return nonformedUnitAIs; } @@ -1453,15 +1329,13 @@ var formation = ExtractFormations(formedEnts); var formationUnitAIs = []; - if (formation.ids.length == 1) - { + if (formation.ids.length == 1) { // Selected units either belong to this formation or have no formation // Check that all its members are selected var fid = formation.ids[0]; var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length - && cmpFormation.GetMemberCount() == formation.entities.length) - { + && cmpFormation.GetMemberCount() == formation.entities.length) { cmpFormation.DeleteTwinFormations(); // The whole formation was selected, so reuse its controller for this command formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)]; @@ -1470,13 +1344,11 @@ } } - if (!formationUnitAIs.length) - { + if (!formationUnitAIs.length) { // We need to give the selected units a new formation controller // Remove selected units from their current formation - for (var fid in formation.members) - { + for (var fid in formation.members) { var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); @@ -1486,24 +1358,18 @@ var formationSeparation = 60; var clusters = ClusterEntities(formation.entities, formationSeparation); var formationEnts = []; - for (let cluster of clusters) - { - if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate)) - { + for (let cluster of clusters) { + if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate)) { // get the most recently used formation, or default to line closed var lastFormationTemplate = undefined; - for (let ent of cluster) - { + for (let ent of cluster) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); - if (cmpUnitAI) - { + if (cmpUnitAI) { var template = cmpUnitAI.GetLastFormationTemplate(); - if (lastFormationTemplate === undefined) - { + if (lastFormationTemplate === undefined) { lastFormationTemplate = template; } - else if (lastFormationTemplate != template) - { + else if (lastFormationTemplate != template) { lastFormationTemplate = undefined; break; } @@ -1537,8 +1403,7 @@ /** * Group a list of entities in clusters via single-links */ -function ClusterEntities(ents, separationDistance) -{ +function ClusterEntities(ents, separationDistance) { var clusters = []; if (!ents.length) return clusters; @@ -1548,8 +1413,7 @@ // triangular matrix with the (squared) distances between the different clusters // the other half is not initialised var matrix = []; - for (let i = 0; i < ents.length; ++i) - { + for (let i = 0; i < ents.length; ++i) { matrix[i] = []; clusters.push([ents[i]]); var cmpPosition = Engine.QueryInterface(ents[i], IID_Position); @@ -1557,15 +1421,14 @@ for (let j = 0; j < i; ++j) matrix[i][j] = positions[i].distanceToSquared(positions[j]); } - while (clusters.length > 1) - { + while (clusters.length > 1) { // search two clusters that are closer than the required distance var closeClusters = undefined; for (var i = matrix.length - 1; i >= 0 && !closeClusters; --i) for (var j = i - 1; j >= 0 && !closeClusters; --j) if (matrix[i][j] < distSq) - closeClusters = [i,j]; + closeClusters = [i, j]; // if no more close clusters found, just return all found clusters so far if (!closeClusters) @@ -1577,8 +1440,7 @@ // calculate the minimum distance between the new cluster and all other remaining // clusters by taking the minimum of the two distances. var distances = []; - for (let i = 0; i < clusters.length; ++i) - { + for (let i = 0; i < clusters.length; ++i) { if (i == closeClusters[1] || i == closeClusters[0]) continue; var dist1 = matrix[closeClusters[1]][i] || matrix[i][closeClusters[1]]; @@ -1587,16 +1449,15 @@ } // remove the rows and columns in the matrix for the merged clusters, // and the clusters themselves from the cluster list - clusters.splice(closeClusters[0],1); - clusters.splice(closeClusters[1],1); - matrix.splice(closeClusters[0],1); - matrix.splice(closeClusters[1],1); - for (let i = 0; i < matrix.length; ++i) - { + clusters.splice(closeClusters[0], 1); + clusters.splice(closeClusters[1], 1); + matrix.splice(closeClusters[0], 1); + matrix.splice(closeClusters[1], 1); + for (let i = 0; i < matrix.length; ++i) { if (matrix[i].length > closeClusters[0]) - matrix[i].splice(closeClusters[0],1); + matrix[i].splice(closeClusters[0], 1); if (matrix[i].length > closeClusters[1]) - matrix[i].splice(closeClusters[1],1); + matrix[i].splice(closeClusters[1], 1); } // add a new row of distances to the matrix and the new cluster clusters.push(newCluster); @@ -1605,8 +1466,7 @@ return clusters; } -function GetFormationRequirements(formationTemplate) -{ +function GetFormationRequirements(formationTemplate) { var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate); if (!template.Formation) return false; @@ -1615,8 +1475,7 @@ } -function CanMoveEntsIntoFormation(ents, formationTemplate) -{ +function CanMoveEntsIntoFormation(ents, formationTemplate) { // TODO: should check the player's civ is allowed to use this formation // See simulation/components/Player.js GetFormations() for a list of all allowed formations @@ -1625,8 +1484,7 @@ return false; var count = 0; - for (let ent of ents) - { + for (let ent of ents) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate)) continue; @@ -1642,8 +1500,7 @@ * returns: true if the entity is valid and owned by the player * or control all units is activated, else false */ -function CanControlUnit(entity, player, controlAll) -{ +function CanControlUnit(entity, player, controlAll) { return IsOwnedByPlayer(player, entity) || controlAll; } @@ -1653,24 +1510,21 @@ * or the entity is owned by an mutualAlly * or control all units is activated, else false */ -function CanControlUnitOrIsAlly(entity, player, controlAll) -{ +function CanControlUnitOrIsAlly(entity, player, controlAll) { return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity) || controlAll; } /** * Filter entities which the player can control */ -function FilterEntityList(entities, player, controlAll) -{ +function FilterEntityList(entities, player, controlAll) { return entities.filter(ent => CanControlUnit(ent, player, controlAll)); } /** * Filter entities which the player can control or are mutualAlly */ -function FilterEntityListWithAllies(entities, player, controlAll) -{ +function FilterEntityListWithAllies(entities, player, controlAll) { return entities.filter(ent => CanControlUnitOrIsAlly(ent, player, controlAll)); } Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml +++ ps/trunk/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: ps/trunk/source/simulation2/TypeList.h =================================================================== --- ps/trunk/source/simulation2/TypeList.h +++ ps/trunk/source/simulation2/TypeList.h @@ -193,3 +193,6 @@ INTERFACE(WaterManager) COMPONENT(WaterManager) + +INTERFACE(WayPointRenderer) +COMPONENT(WayPointRenderer) Index: ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.cpp +++ ps/trunk/source/simulation2/components/CCmpRallyPointRenderer.cpp @@ -91,16 +91,16 @@ /// 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; + std::deque 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; + std::deque > 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. + std::deque 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. @@ -120,11 +120,11 @@ /// 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; + std::list > m_TexturedOverlayLines; /// Draw little overlay circles to indicate where the exact path points are? bool m_EnableDebugNodeOverlay; - std::vector > m_DebugNodeOverlays; + std::deque > m_DebugNodeOverlays; public: @@ -238,7 +238,7 @@ break; case MT_Destroy: { - for (std::vector::iterator it = m_MarkerEntityIds.begin(); it < m_MarkerEntityIds.end(); ++it) + for (std::deque::iterator it = m_MarkerEntityIds.begin(); it < m_MarkerEntityIds.end(); ++it) { if (*it != INVALID_ENTITY) { @@ -742,11 +742,16 @@ // pass (which is only sensible). while (index >= m_TexturedOverlayLines.size()) { - std::vector tmp; + std::list tmp; m_TexturedOverlayLines.push_back(tmp); } - m_TexturedOverlayLines[index].clear(); + std::list >::iterator iter = m_TexturedOverlayLines.begin(); + size_t count = index; + while(count--) + iter++; + (*iter).clear(); + if (m_Path[index].size() < 2) return; @@ -785,7 +790,7 @@ overlayLine.m_Coords.push_back(m_Path[index][j].Y); } - m_TexturedOverlayLines[index].push_back(overlayLine); + (*iter).push_back(overlayLine); } else { @@ -857,7 +862,7 @@ dashOverlay.m_Coords.push_back(dashedLine.m_Points[n].Y); } - m_TexturedOverlayLines[index].push_back(dashOverlay); + (*iter).push_back(dashOverlay); } } @@ -868,7 +873,7 @@ { while (index >= m_DebugNodeOverlays.size()) { - std::vector tmp; + std::deque tmp; m_DebugNodeOverlays.push_back(tmp); } for (size_t j = 0; j < m_Path[index].size(); ++j) @@ -1258,12 +1263,13 @@ void CCmpRallyPointRenderer::RenderSubmit(SceneCollector& collector) { // 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 (std::list >::iterator it = m_TexturedOverlayLines.begin(); + it != m_TexturedOverlayLines.end(); ++it) { - for (size_t j = 0; j < m_TexturedOverlayLines[i].size(); ++j) + for (std::list::iterator iter = (*it).begin(); iter != (*it).end(); ++iter) { - if (!m_TexturedOverlayLines[i][j].m_Coords.empty()) - collector.Submit(&m_TexturedOverlayLines[i][j]); + if (!(*iter).m_Coords.empty()) + collector.Submit(&(*iter)); } } Index: ps/trunk/source/simulation2/components/CCmpWayPointRenderer.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpWayPointRenderer.cpp +++ ps/trunk/source/simulation2/components/CCmpWayPointRenderer.cpp @@ -0,0 +1,903 @@ +/* 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/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 "ps/CLogger.h" +#include "graphics/Overlay.h" +#include "graphics/TextureManager.h" +#include "renderer/Renderer.h" +// TODO refactor +// Maybe create a common ancestor for this and RallyPointRenderer FlagPointRenderer +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() + { + return (m_StartIndex == m_EndIndex); + } +}; +class CCmpWayPointRenderer : public ICmpWayPointRenderer +{ + // import some types for less verbosity + typedef ICmpRangeManager::CLosQuerier CLosQuerier; + typedef SOverlayTexturedLine::LineCapType LineCapType; +public: + static void ClassInit(CComponentManager& componentManager) + { + componentManager.SubscribeToMessageType(MT_RenderSubmit); + componentManager.SubscribeToMessageType(MT_OwnershipChanged); + componentManager.SubscribeToMessageType(MT_TurnStart); + componentManager.SubscribeToMessageType(MT_Destroy); + componentManager.SubscribeToMessageType(MT_PositionChanged); + } + + DEFAULT_COMPONENT_ALLOCATOR(WayPointRenderer) + +protected: + /// Display position of the way 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::deque m_WayPoints; + // TODO are we using the pathfinder? / should we be using it that much? TODO wait for the pathfinder rewrite and use hopefully exported data + // from that to make a curved path that uses the same data as the moving unit. + /// Full path to the way points as returned by the pathfinder, with some post-processing applied to reduce zig/zagging. + std::deque > m_Path; + /// Visibility segments of the way point paths; splits the path into SoD/non-SoD segments. + std::deque > m_VisibilitySegments; + bool m_Displayed; ///< Should we render the way points and the path lines? (set from JS when e.g. the unit is selected/deselected) + std::deque m_MarkerEntityIds; ///< Entity IDs of the way 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 way 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. + std::string m_LineCostClass; ///< Pathfinder cost 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::list > m_TexturedOverlayLines; +public: + static std::string 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" + "" + "" + "" + "" + "" + "" + "" + ""; + } + + 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: + { + 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::deque::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: + { + RecomputeWayPointPath_wrapper(0); + } + break; + } + } + + virtual void AddPosition_wrapper(const CFixedVector2D& pos) + { + AddPosition(pos, false); + } + // Rename to Unshift??? + virtual void AddPositionFront(const CFixedVector2D& pos) + { + m_WayPoints.push_front(pos); + UpdateMarkers(); + // TODO don't recompute everything (maybe just shift everything one back? and recompute first only + RecomputeAllWayPointPaths(); + } + virtual void SetPosition(const CFixedVector2D& pos) + { + if (!(m_WayPoints.size() == 1 && m_WayPoints.front() == pos)) + { + m_WayPoints.clear(); + AddPosition(pos, true); + } + } + 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 way point was not being displayed. + UpdateOverlayLines(); + } + } + virtual void Shift() + { + LOGERROR("m_WayPoints %d \tm_Path %d \tm_VisibilitySegments %d \tm_TexturedOverlayLines %d\n",m_WayPoints.size(),m_Path.size(),m_VisibilitySegments.size(),m_TexturedOverlayLines.size()); + // Still not sure why size is sometimes <= 0 + if (m_WayPoints.size() > 0) + { + m_WayPoints.pop_front(); + m_Path.pop_front(); + m_VisibilitySegments.pop_front(); + m_TexturedOverlayLines.pop_front(); + // TODO should we skip this as this is already done on MT_PositionChanged? + RecomputeWayPointPath_wrapper(0); + UpdateMarkers(); + } + } +private: + /** + * Helper function for AddPosition_wrapper and SetPosition. + */ + void AddPosition(CFixedVector2D pos, bool recompute) + { + m_WayPoints.push_back(pos); + UpdateMarkers(); + if (recompute) + RecomputeAllWayPointPaths(); + else + RecomputeWayPointPath_wrapper(m_WayPoints.size()-1); + } + /** + * Returns true iff at least one display way point is set; i.e., if we have a point to render our marker/line at. + */ + bool IsSet() + { + return !m_WayPoints.empty(); + } + /** + * 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. + */ + void UpdateMarkers(); + /** + * Recomputes all the full paths from this entity to the way point and from the way point to the next, and does all the necessary + * post-processing to make them prettier. + * + * Should be called whenever all way points' position changes. + */ + void RecomputeAllWayPointPaths(); + /** + * 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 way point's position changes. + */ + void RecomputeWayPointPath_wrapper(size_t index); + /** + * Recomputes the full path from this entity/the previous way point to the next way 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 way point is set. + * + * You shouldn't need to call this method directly. + */ + void RecomputeWayPointPath(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 way point lines are not currently set to be displayed, or if no way 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); + /** + * Returns a list of indices of waypoints in the current path (m_Path[index]) where the LOS visibility changes, ordered from + * building/previous way point to way point. Used to construct the overlay line segments and track changes to the SoD. + */ + void GetVisibilitySegments(std::deque& out, size_t index); + /** + * 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 way 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::deque& coords, unsigned maxSegmentLinks = 0, bool floating = true); + /** + * 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(WayPointRenderer) + +void CCmpWayPointRenderer::Init(const CParamNode& paramNode) +{ + m_Displayed = false; + m_LastOwner = INVALID_PLAYER; + m_LastMarkerCount = 0; + // --------------------------------------------------------------------------------------------- + // load some XML configuration data (schema guarantees that all these nodes are valid) + m_MarkerTemplate = paramNode.GetChild("MarkerTemplate").ToString(); + const CParamNode& lineColor = paramNode.GetChild("LineColour"); + 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("LineDashColour"); + 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 CCmpWayPointRenderer::UpdateMarkers() +{ + player_id_t previousOwner = m_LastOwner; + for (size_t i = 0; i < m_WayPoints.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 cmpPosition(GetSimContext(), m_MarkerEntityIds[i]); + if (cmpPosition) + { + if (m_Displayed && IsSet()) + { + cmpPosition->JumpTo(m_WayPoints[i].X, m_WayPoints[i].Y); + } + else + { + cmpPosition->MoveOutOfWorld(); // hide it + } + } + // set way point flag selection based on player civilization + CmpPtr cmpOwnership(GetSimContext(), GetEntityId()); + if (cmpOwnership) + { + player_id_t ownerId = cmpOwnership->GetOwner(); + if (ownerId != INVALID_PLAYER && (ownerId != previousOwner || m_LastMarkerCount <= i)) + { + m_LastOwner = ownerId; + CmpPtr cmpPlayerManager(GetSimContext(), SYSTEM_ENTITY); + // 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) + { + CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(ownerId)); + if (cmpPlayer) + { + CmpPtr cmpVisualActor(GetSimContext(), m_MarkerEntityIds[i]); + if (cmpVisualActor) + { + cmpVisualActor->SetVariant("civ", CStrW(cmpPlayer->GetCiv()).ToUTF8()); + } + } + } + } + } + } + // Hide currently unused markers + for (size_t i = m_WayPoints.size(); i < m_LastMarkerCount; ++i) + { + CmpPtr cmpPosition(GetSimContext(), m_MarkerEntityIds[i]); + if (cmpPosition) + cmpPosition->MoveOutOfWorld(); + } + m_LastMarkerCount = m_WayPoints.size(); +} + +void CCmpWayPointRenderer::RecomputeAllWayPointPaths() +{ + m_Path.clear(); + m_VisibilitySegments.clear(); + m_TexturedOverlayLines.clear(); + if (!IsSet()) + return; // no use computing a path if the way point isn't set + CmpPtr cmpPosition(GetSimContext(), GetEntityId()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; // no point going on if this entity doesn't have a position or is outside of the world + // Not used + CmpPtr cmpFootprint(GetSimContext(), GetEntityId()); + CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); + for (size_t i = 0; i < m_WayPoints.size(); ++i) + { + RecomputeWayPointPath(i, cmpPosition, cmpFootprint, cmpPathfinder); + } +} + +void CCmpWayPointRenderer::RecomputeWayPointPath_wrapper(size_t index) +{ + if (!IsSet()) + return; // no use computing a path if the wayw point isn't set + CmpPtr cmpPosition(GetSimContext(), GetEntityId()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; // no point going on if this entity doesn't have a position or is outside of the world + // Not used + CmpPtr cmpFootprint(GetSimContext(), GetEntityId()); + CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); + RecomputeWayPointPath(index, cmpPosition, cmpFootprint, cmpPathfinder); +} + +void CCmpWayPointRenderer::RecomputeWayPointPath(size_t index, CmpPtr& cmpPosition, CmpPtr& UNUSED(cmpFootprint), CmpPtr UNUSED(cmpPathfinder)) +{ + while (index >= m_Path.size()) + { + std::deque 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(); + entity_pos_t pathStartX; + entity_pos_t pathStartY; + if (index == 0) + { + pathStartX = cmpPosition->GetPosition2D().X; + pathStartY = cmpPosition->GetPosition2D().Y; + } + else + { + pathStartX = m_WayPoints[index-1].X; + pathStartY = m_WayPoints[index-1].Y; + } + // 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 building/previous way point to the goal, where each + // waypoint is centered at a tile. We'll have to do some post-processing on the path to get it smooth. + // TODO update comment + m_Path[index].push_back(CVector2D(m_WayPoints[index].X.ToFloat(), m_WayPoints[index].Y.ToFloat())); + m_Path[index].push_back(CVector2D(pathStartX.ToFloat(), pathStartY.ToFloat())); + // 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::ConstructAllOverlayLines() +{ + m_TexturedOverlayLines.clear(); + for (size_t i = 0; i < m_Path.size(); ++i) + ConstructOverlayLines(i); +} + +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::list tmp; + m_TexturedOverlayLines.push_back(tmp); + } + std::list >::iterator iter = m_TexturedOverlayLines.begin(); + size_t count = index; + while(count--) + iter++; + (*iter).clear(); + if (m_Path[index].size() < 2) + return; + CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY); + 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) + { + const SVisibilitySegment& segment = (*it); + if (segment.m_Visible) + { + // does this segment border on the building or way 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 way 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); + } + + } + } +} + +void CCmpWayPointRenderer::UpdateOverlayLines() +{ + // We should only do this if the way 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; + // 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) + { + std::deque 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); + } + } + } +} +// TODO remove pathfinder?; or remove this as a whole? +void CCmpWayPointRenderer::ReduceSegmentsByVisibility(std::deque& coords, unsigned maxSegmentLinks, bool floating) +{ + CmpPtr cmpPathFinder(GetSimContext(), SYSTEM_ENTITY); + CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY); + CmpPtr cmpWaterManager(GetSimContext(), SYSTEM_ENTITY); + 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::deque 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::GetVisibilitySegments(std::deque& out, size_t index) +{ + out.clear(); + if (m_Path[index].size() < 2) + return; + CmpPtr cmpRangeMgr(GetSimContext(), SYSTEM_ENTITY); + 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::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(); + // 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 CCmpWayPointRenderer::RenderSubmit(SceneCollector& collector) +{ + // we only get here if the way point is set and should be displayed + for (std::list >::iterator it = m_TexturedOverlayLines.begin(); + it != m_TexturedOverlayLines.end(); ++it) + { + for (std::list::iterator iter = (*it).begin(); iter != (*it).end(); ++iter) + { + if (!(*iter).m_Coords.empty()) + collector.Submit(&(*iter)); + } + } +} Index: ps/trunk/source/simulation2/components/ICmpRallyPointRenderer.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpRallyPointRenderer.h +++ ps/trunk/source/simulation2/components/ICmpRallyPointRenderer.h @@ -24,7 +24,7 @@ /** * Rally Point. - * Holds the position of a unit's rally points, and renders them to screen. + * Holds the position(s) of a unit's rally points, and renders them to screen. */ class ICmpRallyPointRenderer : public IComponent { Index: ps/trunk/source/simulation2/components/ICmpWayPointRenderer.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpWayPointRenderer.h +++ ps/trunk/source/simulation2/components/ICmpWayPointRenderer.h @@ -0,0 +1,48 @@ +/* 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: + /// Sets whether the way point marker and line should be displayed. + virtual void SetDisplayed(bool displayed) = 0; + /// Sets the position at which the way point marker should be displayed. + /// Discards all previous positions + virtual void SetPosition(const CFixedVector2D& position) = 0; + /// Add another position at which a marker should be displayed, connected + /// to the previous one. + virtual void AddPosition_wrapper(const CFixedVector2D& position) = 0; + /// At a position at which a marker should be displayed, connected to + /// the previous first. + virtual void AddPositionFront(const CFixedVector2D& position) = 0; + /// Remove the first way point (as we have reached it) + virtual void Shift() = 0; + + DECLARE_INTERFACE_TYPE(WayPointRenderer) +}; +#endif // INCLUDED_ICMPWAYPOINT Index: ps/trunk/source/simulation2/components/ICmpWayPointRenderer.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpWayPointRenderer.cpp +++ ps/trunk/source/simulation2/components/ICmpWayPointRenderer.cpp @@ -0,0 +1,31 @@ +/* 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("AddPositionFront", void, ICmpWayPointRenderer, AddPositionFront, CFixedVector2D) +DEFINE_INTERFACE_METHOD_0("Shift", void, ICmpWayPointRenderer, Shift) +END_INTERFACE_WRAPPER(WayPointRenderer)