LCOV - code coverage report
Current view: top level - simulation/helpers - Commands.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 17 730 2.3 %
Date: 2023-04-02 12:52:40 Functions: 1 111 0.9 %

          Line data    Source code
       1             : // Setting this to true will display some warnings when commands
       2             : //      are likely to fail, which may be useful for debugging AIs
       3           1 : var g_DebugCommands = false;
       4             : 
       5             : function ProcessCommand(player, cmd)
       6             : {
       7           0 :         let cmpPlayer = QueryPlayerIDInterface(player);
       8           0 :         if (!cmpPlayer)
       9           0 :                 return;
      10             : 
      11           0 :         let data = {
      12             :                 "cmpPlayer": cmpPlayer,
      13             :                 "controlAllUnits": cmpPlayer.CanControlAllUnits()
      14             :         };
      15             : 
      16           0 :         if (cmd.entities)
      17           0 :                 data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits);
      18             : 
      19             :         // TODO: queuing order and forcing formations doesn't really work.
      20             :         // To play nice, we'll still no-formation queued order if units are in formation
      21             :         // but the opposite perhaps ought to be implemented.
      22           0 :         if (!cmd.queued || cmd.formation == NULL_FORMATION)
      23           0 :                 data.formation = cmd.formation || undefined;
      24             : 
      25             :         // Allow focusing the camera on recent commands
      26           0 :         let commandData = {
      27             :                 "type": "playercommand",
      28             :                 "players": [player],
      29             :                 "cmd": cmd
      30             :         };
      31             : 
      32             :         // Save the position, since the GUI event is received after the unit died
      33           0 :         if (cmd.type == "delete-entities")
      34             :         {
      35           0 :                 let cmpPosition = cmd.entities[0] && Engine.QueryInterface(cmd.entities[0], IID_Position);
      36           0 :                 commandData.position = cmpPosition && cmpPosition.IsInWorld() && cmpPosition.GetPosition2D();
      37             :         }
      38             : 
      39           0 :         let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
      40           0 :         cmpGuiInterface.PushNotification(commandData);
      41             : 
      42             :         // Note: checks of UnitAI targets are not robust enough here, as ownership
      43             :         //      can change after the order is issued, they should be checked by UnitAI
      44             :         //      when the specific behavior (e.g. attack, garrison) is performed.
      45             :         // (Also it's not ideal if a command silently fails, it's nicer if UnitAI
      46             :         //      moves the entities closer to the target before giving up.)
      47             : 
      48             :         // Now handle various commands
      49           0 :         if (g_Commands[cmd.type])
      50             :         {
      51           0 :                 var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
      52           0 :                 cmpTrigger.CallEvent("OnPlayerCommand", { "player": player, "cmd": cmd });
      53           0 :                 g_Commands[cmd.type](player, cmd, data);
      54             :         }
      55             :         else
      56           0 :                 error("Invalid command: unknown command type: "+uneval(cmd));
      57             : }
      58             : 
      59           1 : var g_Commands = {
      60             : 
      61             :         "aichat": function(player, cmd, data)
      62             :         {
      63           0 :                 var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
      64           0 :                 var notification = { "players": [player] };
      65           0 :                 for (var key in cmd)
      66           0 :                         notification[key] = cmd[key];
      67           0 :                 cmpGuiInterface.PushNotification(notification);
      68             :         },
      69             : 
      70             :         "cheat": function(player, cmd, data)
      71             :         {
      72           0 :                 Cheat(cmd);
      73             :         },
      74             : 
      75             :         "collect-treasure": function(player, cmd, data)
      76             :         {
      77           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
      78           0 :                         cmpUnitAI.CollectTreasure(cmd.target, cmd.queued);
      79             :                 });
      80             :         },
      81             : 
      82             :         "collect-treasure-near-position": function(player, cmd, data)
      83             :         {
      84           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
      85           0 :                         cmpUnitAI.CollectTreasureNearPosition(cmd.x, cmd.z, cmd.queued);
      86             :                 });
      87             :         },
      88             : 
      89             :         "diplomacy": function(player, cmd, data)
      90             :         {
      91           0 :                 let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
      92           0 :                 if (data.cmpPlayer.GetLockTeams() ||
      93             :                         cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive())
      94           0 :                         return;
      95             : 
      96           0 :                 switch(cmd.to)
      97             :                 {
      98             :                 case "ally":
      99           0 :                         data.cmpPlayer.SetAlly(cmd.player);
     100           0 :                         break;
     101             :                 case "neutral":
     102           0 :                         data.cmpPlayer.SetNeutral(cmd.player);
     103           0 :                         break;
     104             :                 case "enemy":
     105           0 :                         data.cmpPlayer.SetEnemy(cmd.player);
     106           0 :                         break;
     107             :                 default:
     108           0 :                         warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to);
     109             :                 }
     110             : 
     111           0 :                 var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     112           0 :                 cmpGuiInterface.PushNotification({
     113             :                         "type": "diplomacy",
     114             :                         "players": [player],
     115             :                         "targetPlayer": cmd.player,
     116             :                         "status": cmd.to
     117             :                 });
     118             :         },
     119             : 
     120             :         "tribute": function(player, cmd, data)
     121             :         {
     122           0 :                 data.cmpPlayer.TributeResource(cmd.player, cmd.amounts);
     123             :         },
     124             : 
     125             :         "control-all": function(player, cmd, data)
     126             :         {
     127           0 :                 if (!data.cmpPlayer.GetCheatsEnabled())
     128           0 :                         return;
     129             : 
     130           0 :                 var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     131           0 :                 cmpGuiInterface.PushNotification({
     132             :                         "type": "aichat",
     133             :                         "players": [player],
     134             :                         "message": markForTranslation("(Cheat - control all units)")
     135             :                 });
     136             : 
     137           0 :                 data.cmpPlayer.SetControlAllUnits(cmd.flag);
     138             :         },
     139             : 
     140             :         "reveal-map": function(player, cmd, data)
     141             :         {
     142           0 :                 if (!data.cmpPlayer.GetCheatsEnabled())
     143           0 :                         return;
     144             : 
     145           0 :                 var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     146           0 :                 cmpGuiInterface.PushNotification({
     147             :                         "type": "aichat",
     148             :                         "players": [player],
     149             :                         "message": markForTranslation("(Cheat - reveal map)")
     150             :                 });
     151             : 
     152             :                 // Reveal the map for all players, not just the current player,
     153             :                 // primarily to make it obvious to everyone that the player is cheating
     154           0 :                 var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     155           0 :                 cmpRangeManager.SetLosRevealAll(-1, cmd.enable);
     156             :         },
     157             : 
     158             :         "walk": function(player, cmd, data)
     159             :         {
     160           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     161           0 :                         cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued, cmd.pushFront);
     162             :                 });
     163             :         },
     164             : 
     165             :         "walk-custom": function(player, cmd, data)
     166             :         {
     167           0 :                 for (let ent in data.entities)
     168           0 :                         GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
     169           0 :                                 cmpUnitAI.Walk(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.queued, cmd.pushFront);
     170             :                         });
     171             :         },
     172             : 
     173             :         "walk-to-range": function(player, cmd, data)
     174             :         {
     175             :                 // Only used by the AI
     176           0 :                 for (let ent of data.entities)
     177             :                 {
     178           0 :                         var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     179           0 :                         if (cmpUnitAI)
     180           0 :                                 cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued, cmd.pushFront);
     181             :                 }
     182             :         },
     183             : 
     184             :         "attack-walk": function(player, cmd, data)
     185             :         {
     186           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     187           0 :                         cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.allowCapture, cmd.queued, cmd.pushFront);
     188             :                 });
     189             :         },
     190             : 
     191             :         "attack-walk-custom": function(player, cmd, data)
     192             :         {
     193           0 :                 for (let ent in data.entities)
     194           0 :                         GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
     195           0 :                                 cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, cmd.allowCapture, cmd.queued, cmd.pushFront);
     196             :                         });
     197             :         },
     198             : 
     199             :         "attack": function(player, cmd, data)
     200             :         {
     201           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     202           0 :                         cmpUnitAI.Attack(cmd.target, cmd.allowCapture, cmd.queued, cmd.pushFront);
     203             :                 });
     204             :         },
     205             : 
     206             :         "patrol": function(player, cmd, data)
     207             :         {
     208           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI =>
     209           0 :                         cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.allowCapture, cmd.queued)
     210             :                 );
     211             :         },
     212             : 
     213             :         "heal": function(player, cmd, data)
     214             :         {
     215           0 :                 if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
     216           0 :                         warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
     217             : 
     218           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     219           0 :                         cmpUnitAI.Heal(cmd.target, cmd.queued, cmd.pushFront);
     220             :                 });
     221             :         },
     222             : 
     223             :         "repair": function(player, cmd, data)
     224             :         {
     225             :                 // This covers both repairing damaged buildings, and constructing unfinished foundations
     226           0 :                 if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
     227           0 :                         warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
     228             : 
     229           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     230           0 :                         cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued, cmd.pushFront);
     231             :                 });
     232             :         },
     233             : 
     234             :         "gather": function(player, cmd, data)
     235             :         {
     236           0 :                 if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
     237           0 :                         warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
     238             : 
     239           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     240           0 :                         cmpUnitAI.Gather(cmd.target, cmd.queued, cmd.pushFront);
     241             :                 });
     242             :         },
     243             : 
     244             :         "gather-near-position": function(player, cmd, data)
     245             :         {
     246           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     247           0 :                         cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued, cmd.pushFront);
     248             :                 });
     249             :         },
     250             : 
     251             :         "returnresource": function(player, cmd, data)
     252             :         {
     253           0 :                 if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target))
     254           0 :                         warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
     255             : 
     256           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     257           0 :                         cmpUnitAI.ReturnResource(cmd.target, cmd.queued, cmd.pushFront);
     258             :                 });
     259             :         },
     260             : 
     261             :         "back-to-work": function(player, cmd, data)
     262             :         {
     263           0 :                 for (let ent of data.entities)
     264             :                 {
     265           0 :                         var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     266           0 :                         if(!cmpUnitAI || !cmpUnitAI.BackToWork())
     267           0 :                                 notifyBackToWorkFailure(player);
     268             :                 }
     269             :         },
     270             : 
     271             :         "call-to-arms": function(player, cmd, data)
     272             :         {
     273           0 :                 const unitsToMove = data.entities.filter(ent =>
     274           0 :                         MatchesClassList(Engine.QueryInterface(ent, IID_Identity).GetClassesList(),
     275             :                                 ["Soldier", "Warship", "Siege", "Healer"])
     276             :                 );
     277           0 :                 GetFormationUnitAIs(unitsToMove, player, cmd, data.formation).forEach(cmpUnitAI => {
     278           0 :                         if (cmd.pushFront)
     279             :                         {
     280           0 :                                 cmpUnitAI.WalkAndFight(cmd.position.x, cmd.position.z, cmd.targetClasses, cmd.allowCapture, false, cmd.pushFront);
     281           0 :                                 cmpUnitAI.DropAtNearestDropSite(false, cmd.pushFront);
     282             :                         }
     283             :                         else
     284             :                         {
     285           0 :                                 cmpUnitAI.DropAtNearestDropSite(cmd.queued, false)
     286           0 :                                 cmpUnitAI.WalkAndFight(cmd.position.x, cmd.position.z, cmd.targetClasses, cmd.allowCapture, true, false);
     287             :                         }
     288             :                 });
     289             :         },
     290             : 
     291             :         "remove-guard": function(player, cmd, data)
     292             :         {
     293           0 :                 for (let ent of data.entities)
     294             :                 {
     295           0 :                         var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     296           0 :                         if (cmpUnitAI)
     297           0 :                                 cmpUnitAI.RemoveGuard();
     298             :                 }
     299             :         },
     300             : 
     301             :         "train": function(player, cmd, data)
     302             :         {
     303           0 :                 if (!Number.isInteger(cmd.count) || cmd.count <= 0)
     304             :                 {
     305           0 :                         warn("Invalid command: can't train " + uneval(cmd.count) + " units");
     306           0 :                         return;
     307             :                 }
     308             : 
     309             :                 // Check entity limits
     310           0 :                 var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template);
     311           0 :                 var unitCategory = null;
     312           0 :                 if (template.TrainingRestrictions)
     313           0 :                         unitCategory = template.TrainingRestrictions.Category;
     314             : 
     315             :                 // Verify that the building(s) can be controlled by the player
     316           0 :                 if (data.entities.length <= 0)
     317             :                 {
     318           0 :                         if (g_DebugCommands)
     319           0 :                                 warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd));
     320           0 :                         return;
     321             :                 }
     322             : 
     323           0 :                 for (let ent of data.entities)
     324             :                 {
     325           0 :                         if (unitCategory)
     326             :                         {
     327           0 :                                 var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits);
     328           0 :                                 if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count, cmd.template, template.TrainingRestrictions.MatchLimit))
     329             :                                 {
     330           0 :                                         if (g_DebugCommands)
     331           0 :                                                 warn(unitCategory + " train limit is reached: " + uneval(cmd));
     332           0 :                                         continue;
     333             :                                 }
     334             :                         }
     335             : 
     336           0 :                         var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
     337           0 :                         if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template))
     338             :                         {
     339           0 :                                 if (g_DebugCommands)
     340           0 :                                         warn("Invalid command: training requires unresearched technology: " + uneval(cmd));
     341           0 :                                 continue;
     342             :                         }
     343             : 
     344           0 :                         const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer);
     345           0 :                         if (!cmpTrainer)
     346           0 :                                 continue;
     347             : 
     348           0 :                         let templateName = cmd.template;
     349             :                         // Check if the building can train the unit
     350             :                         // TODO: the AI API does not take promotion technologies into account for the list
     351             :                         // of trainable units (taken directly from the unit template). Here is a temporary fix.
     352           0 :                         if (data.cmpPlayer.IsAI())
     353           0 :                                 templateName = cmpTrainer.GetUpgradedTemplate(cmd.template);
     354             : 
     355           0 :                         if (cmpTrainer.CanTrain(templateName))
     356           0 :                                 Engine.QueryInterface(ent, IID_ProductionQueue)?.AddItem(templateName, "unit", +cmd.count, cmd.metadata, cmd.pushFront);
     357             :                 }
     358             :         },
     359             : 
     360             :         "research": function(player, cmd, data)
     361             :         {
     362           0 :                 var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager);
     363           0 :                 if (cmpTechnologyManager && !cmpTechnologyManager.CanResearch(cmd.template))
     364             :                 {
     365           0 :                         if (g_DebugCommands)
     366           0 :                                 warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd));
     367           0 :                         return;
     368             :                 }
     369             : 
     370           0 :                 var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
     371           0 :                 if (queue)
     372           0 :                         queue.AddItem(cmd.template, "technology", undefined, cmd.metadata, cmd.pushFront);
     373             :         },
     374             : 
     375             :         "stop-production": function(player, cmd, data)
     376             :         {
     377           0 :                 let cmpProductionQueue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
     378           0 :                 if (cmpProductionQueue)
     379           0 :                         cmpProductionQueue.RemoveItem(cmd.id);
     380             :         },
     381             : 
     382             :         "construct": function(player, cmd, data)
     383             :         {
     384           0 :                 TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd);
     385             :         },
     386             : 
     387             :         "construct-wall": function(player, cmd, data)
     388             :         {
     389           0 :                 TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd);
     390             :         },
     391             : 
     392             :         "delete-entities": function(player, cmd, data)
     393             :         {
     394           0 :                 for (let ent of data.entities)
     395             :                 {
     396           0 :                         if (!data.controlAllUnits)
     397             :                         {
     398           0 :                                 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
     399           0 :                                 if (cmpIdentity && cmpIdentity.IsUndeletable())
     400           0 :                                         continue;
     401             : 
     402           0 :                                 let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
     403           0 :                                 if (cmpCapturable &&
     404             :                                     cmpCapturable.GetCapturePoints()[player] < cmpCapturable.GetMaxCapturePoints() / 2)
     405           0 :                                         continue;
     406             : 
     407           0 :                                 let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
     408           0 :                                 if (cmpResourceSupply && cmpResourceSupply.GetKillBeforeGather())
     409           0 :                                         continue;
     410             :                         }
     411             : 
     412           0 :                         let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
     413           0 :                         if (cmpMirage)
     414             :                         {
     415           0 :                                 let cmpMiragedHealth = Engine.QueryInterface(cmpMirage.parent, IID_Health);
     416           0 :                                 if (cmpMiragedHealth)
     417           0 :                                         cmpMiragedHealth.Kill();
     418             :                                 else
     419           0 :                                         Engine.DestroyEntity(cmpMirage.parent);
     420             : 
     421           0 :                                 Engine.DestroyEntity(ent);
     422           0 :                                 continue;
     423             :                         }
     424             : 
     425           0 :                         let cmpHealth = Engine.QueryInterface(ent, IID_Health);
     426           0 :                         if (cmpHealth)
     427           0 :                                 cmpHealth.Kill();
     428             :                         else
     429           0 :                                 Engine.DestroyEntity(ent);
     430             :                 }
     431             :         },
     432             : 
     433             :         "set-rallypoint": function(player, cmd, data)
     434             :         {
     435           0 :                 for (let ent of data.entities)
     436             :                 {
     437           0 :                         var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
     438           0 :                         if (cmpRallyPoint)
     439             :                         {
     440           0 :                                 if (!cmd.queued)
     441           0 :                                         cmpRallyPoint.Unset();
     442             : 
     443           0 :                                 cmpRallyPoint.AddPosition(cmd.x, cmd.z);
     444           0 :                                 cmpRallyPoint.AddData(clone(cmd.data));
     445             :                         }
     446             :                 }
     447             :         },
     448             : 
     449             :         "unset-rallypoint": function(player, cmd, data)
     450             :         {
     451           0 :                 for (let ent of data.entities)
     452             :                 {
     453           0 :                         var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
     454           0 :                         if (cmpRallyPoint)
     455           0 :                                 cmpRallyPoint.Reset();
     456             :                 }
     457             :         },
     458             : 
     459             :         "resign": function(player, cmd, data)
     460             :         {
     461           0 :                 data.cmpPlayer.Defeat(markForTranslation("%(player)s has resigned."));
     462             :         },
     463             : 
     464             :         "occupy-turret": function(player, cmd, data)
     465             :         {
     466           0 :                 GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
     467           0 :                         cmpUnitAI.OccupyTurret(cmd.target, cmd.queued);
     468             :                 });
     469             :         },
     470             : 
     471             :         "garrison": function(player, cmd, data)
     472             :         {
     473           0 :                 if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits))
     474             :                 {
     475           0 :                         if (g_DebugCommands)
     476           0 :                                 warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
     477           0 :                         return;
     478             :                 }
     479             : 
     480           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     481           0 :                         cmpUnitAI.Garrison(cmd.target, cmd.queued, cmd.pushFront);
     482             :                 });
     483             :         },
     484             : 
     485             :         "guard": function(player, cmd, data)
     486             :         {
     487           0 :                 if (!IsOwnedByPlayerOrMutualAlly(cmd.target, player, data.controlAllUnits))
     488             :                 {
     489           0 :                         if (g_DebugCommands)
     490           0 :                                 warn("Invalid command: Guard/escort target is not owned by player " + player + " or ally thereof: " + uneval(cmd));
     491           0 :                         return;
     492             :                 }
     493             : 
     494           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     495           0 :                         cmpUnitAI.Guard(cmd.target, cmd.queued, cmd.pushFront);
     496             :                 });
     497             :         },
     498             : 
     499             :         "stop": function(player, cmd, data)
     500             :         {
     501           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     502           0 :                         cmpUnitAI.Stop(cmd.queued);
     503             :                 });
     504             :         },
     505             : 
     506             :         "leave-turret": function(player, cmd, data)
     507             :         {
     508           0 :                 let notUnloaded = 0;
     509           0 :                 for (let ent of data.entities)
     510             :                 {
     511           0 :                         let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
     512           0 :                         if (!cmpTurretable || !cmpTurretable.LeaveTurret())
     513           0 :                                 ++notUnloaded;
     514             :                 }
     515             : 
     516           0 :                 if (notUnloaded)
     517           0 :                         notifyUnloadFailure(player);
     518             :         },
     519             : 
     520             :         "unload-turrets": function(player, cmd, data)
     521             :         {
     522           0 :                 let notUnloaded = 0;
     523           0 :                 for (let ent of data.entities)
     524             :                 {
     525           0 :                         let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
     526           0 :                         for (let turret of cmpTurretHolder.GetEntities())
     527             :                         {
     528           0 :                                 let cmpTurretable = Engine.QueryInterface(turret, IID_Turretable);
     529           0 :                                 if (!cmpTurretable || !cmpTurretable.LeaveTurret())
     530           0 :                                         ++notUnloaded;
     531             :                         }
     532             :                 }
     533             : 
     534           0 :                 if (notUnloaded)
     535           0 :                         notifyUnloadFailure(player);
     536             :         },
     537             : 
     538             :         "unload": function(player, cmd, data)
     539             :         {
     540           0 :                 if (!CanPlayerOrAllyControlUnit(cmd.garrisonHolder, player, data.controlAllUnits))
     541             :                 {
     542           0 :                         if (g_DebugCommands)
     543           0 :                                 warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
     544           0 :                         return;
     545             :                 }
     546             : 
     547           0 :                 var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
     548           0 :                 var notUngarrisoned = 0;
     549             : 
     550             :                 // The owner can ungarrison every garrisoned unit
     551           0 :                 if (IsOwnedByPlayer(player, cmd.garrisonHolder))
     552           0 :                         data.entities = cmd.entities;
     553             : 
     554           0 :                 for (let ent of data.entities)
     555           0 :                         if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent))
     556           0 :                                 ++notUngarrisoned;
     557             : 
     558           0 :                 if (notUngarrisoned != 0)
     559           0 :                         notifyUnloadFailure(player, cmd.garrisonHolder);
     560             :         },
     561             : 
     562             :         "unload-template": function(player, cmd, data)
     563             :         {
     564           0 :                 var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
     565           0 :                 for (let garrisonHolder of entities)
     566             :                 {
     567           0 :                         var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
     568           0 :                         if (cmpGarrisonHolder)
     569             :                         {
     570             :                                 // Only the owner of the garrisonHolder may unload entities from any owners
     571           0 :                                 if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits
     572             :                                     && player != +cmd.owner)
     573           0 :                                                 continue;
     574             : 
     575           0 :                                 if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.owner, cmd.all))
     576           0 :                                         notifyUnloadFailure(player, garrisonHolder);
     577             :                         }
     578             :                 }
     579             :         },
     580             : 
     581             :         "unload-all-by-owner": function(player, cmd, data)
     582             :         {
     583           0 :                 var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
     584           0 :                 for (let garrisonHolder of entities)
     585             :                 {
     586           0 :                         var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
     587           0 :                         if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllByOwner(player))
     588           0 :                                 notifyUnloadFailure(player, garrisonHolder);
     589             :                 }
     590             :         },
     591             : 
     592             :         "unload-all": function(player, cmd, data)
     593             :         {
     594           0 :                 var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits);
     595           0 :                 for (let garrisonHolder of entities)
     596             :                 {
     597           0 :                         var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
     598           0 :                         if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll())
     599           0 :                                 notifyUnloadFailure(player, garrisonHolder);
     600             :                 }
     601             :         },
     602             : 
     603             :         "alert-raise": function(player, cmd, data)
     604             :         {
     605           0 :                 for (let ent of data.entities)
     606             :                 {
     607           0 :                         var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
     608           0 :                         if (cmpAlertRaiser)
     609           0 :                                 cmpAlertRaiser.RaiseAlert();
     610             :                 }
     611             :         },
     612             : 
     613             :         "alert-end": function(player, cmd, data)
     614             :         {
     615           0 :                 for (let ent of data.entities)
     616             :                 {
     617           0 :                         var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
     618           0 :                         if (cmpAlertRaiser)
     619           0 :                                 cmpAlertRaiser.EndOfAlert();
     620             :                 }
     621             :         },
     622             : 
     623             :         "formation": function(player, cmd, data)
     624             :         {
     625           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation, true).forEach(cmpUnitAI => {
     626           0 :                         cmpUnitAI.MoveIntoFormation(cmd);
     627             :                 });
     628             :         },
     629             : 
     630             :         "promote": function(player, cmd, data)
     631             :         {
     632           0 :                 if (!data.cmpPlayer.GetCheatsEnabled())
     633           0 :                         return;
     634             : 
     635           0 :                 var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     636           0 :                 cmpGuiInterface.PushNotification({
     637             :                         "type": "aichat",
     638             :                         "players": [player],
     639             :                         "message": markForTranslation("(Cheat - promoted units)"),
     640             :                         "translateMessage": true
     641             :                 });
     642             : 
     643           0 :                 for (let ent of cmd.entities)
     644             :                 {
     645           0 :                         var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
     646           0 :                         if (cmpPromotion)
     647           0 :                                 cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp());
     648             :                 }
     649             :         },
     650             : 
     651             :         "stance": function(player, cmd, data)
     652             :         {
     653           0 :                 for (let ent of data.entities)
     654             :                 {
     655           0 :                         var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     656           0 :                         if (cmpUnitAI && !cmpUnitAI.IsTurret())
     657           0 :                                 cmpUnitAI.SwitchToStance(cmd.name);
     658             :                 }
     659             :         },
     660             : 
     661             :         "lock-gate": function(player, cmd, data)
     662             :         {
     663           0 :                 for (let ent of data.entities)
     664             :                 {
     665           0 :                         var cmpGate = Engine.QueryInterface(ent, IID_Gate);
     666           0 :                         if (!cmpGate)
     667           0 :                                 continue;
     668             : 
     669           0 :                         if (cmd.lock)
     670           0 :                                 cmpGate.LockGate();
     671             :                         else
     672           0 :                                 cmpGate.UnlockGate();
     673             :                 }
     674             :         },
     675             : 
     676             :         "setup-trade-route": function(player, cmd, data)
     677             :         {
     678           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     679           0 :                         cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
     680             :                 });
     681             :         },
     682             : 
     683             :         "cancel-setup-trade-route": function(player, cmd, data)
     684             :         {
     685           0 :                 GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
     686           0 :                         cmpUnitAI.CancelSetupTradeRoute(cmd.target);
     687             :                 });
     688             :         },
     689             : 
     690             :         "set-trading-goods": function(player, cmd, data)
     691             :         {
     692           0 :                 data.cmpPlayer.SetTradingGoods(cmd.tradingGoods);
     693             :         },
     694             : 
     695             :         "barter": function(player, cmd, data)
     696             :         {
     697           0 :                 var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
     698           0 :                 cmpBarter.ExchangeResources(player, cmd.sell, cmd.buy, cmd.amount);
     699             :         },
     700             : 
     701             :         "set-shading-color": function(player, cmd, data)
     702             :         {
     703             :                 // Prevent multiplayer abuse
     704           0 :                 if (!data.cmpPlayer.IsAI())
     705           0 :                         return;
     706             : 
     707             :                 // Debug command to make an entity brightly colored
     708           0 :                 for (let ent of cmd.entities)
     709             :                 {
     710           0 :                         var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
     711           0 :                         if (cmpVisual)
     712           0 :                                 cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0); // alpha isn't used so just send 0
     713             :                 }
     714             :         },
     715             : 
     716             :         "pack": function(player, cmd, data)
     717             :         {
     718           0 :                 for (let ent of data.entities)
     719             :                 {
     720           0 :                         var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     721           0 :                         if (!cmpUnitAI)
     722           0 :                                 continue;
     723             : 
     724           0 :                         if (cmd.pack)
     725           0 :                                 cmpUnitAI.Pack(cmd.queued, cmd.pushFront);
     726             :                         else
     727           0 :                                 cmpUnitAI.Unpack(cmd.queued, cmd.pushFront);
     728             :                 }
     729             :         },
     730             : 
     731             :         "cancel-pack": function(player, cmd, data)
     732             :         {
     733           0 :                 for (let ent of data.entities)
     734             :                 {
     735           0 :                         var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     736           0 :                         if (!cmpUnitAI)
     737           0 :                                 continue;
     738             : 
     739           0 :                         if (cmd.pack)
     740           0 :                                 cmpUnitAI.CancelPack(cmd.queued, cmd.pushFront);
     741             :                         else
     742           0 :                                 cmpUnitAI.CancelUnpack(cmd.queued, cmd.pushFront);
     743             :                 }
     744             :         },
     745             : 
     746             :         "upgrade": function(player, cmd, data)
     747             :         {
     748           0 :                 for (let ent of data.entities)
     749             :                 {
     750           0 :                         var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
     751             : 
     752           0 :                         if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template))
     753           0 :                                 continue;
     754             : 
     755           0 :                         if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template))
     756             :                         {
     757           0 :                                 var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     758           0 :                                 cmpGUIInterface.PushNotification({
     759             :                                         "players": [player],
     760             :                                         "message": markForTranslation("Cannot upgrade as distance requirements are not verified or terrain is obstructed.")
     761             :                                 });
     762           0 :                                 continue;
     763             :                         }
     764             : 
     765             :                         // Check entity limits
     766           0 :                         var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
     767           0 :                         if (cmpEntityLimits && !cmpEntityLimits.AllowedToReplace(ent, cmd.template))
     768             :                         {
     769           0 :                                 if (g_DebugCommands)
     770           0 :                                         warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd));
     771           0 :                                 continue;
     772             :                         }
     773             : 
     774           0 :                         if (!RequirementsHelper.AreRequirementsMet(cmpUpgrade.GetRequirements(cmd.template), player))
     775             :                         {
     776           0 :                                 if (g_DebugCommands)
     777           0 :                                         warn("Invalid command: upgrading is not possible for this player or requires unresearched technology: " + uneval(cmd));
     778           0 :                                 continue;
     779             :                         }
     780             : 
     781           0 :                         cmpUpgrade.Upgrade(cmd.template, data.cmpPlayer);
     782             :                 }
     783             :         },
     784             : 
     785             :         "cancel-upgrade": function(player, cmd, data)
     786             :         {
     787           0 :                 for (let ent of data.entities)
     788             :                 {
     789           0 :                         let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
     790           0 :                         if (cmpUpgrade)
     791           0 :                                 cmpUpgrade.CancelUpgrade(player);
     792             :                 }
     793             :         },
     794             : 
     795             :         "attack-request": function(player, cmd, data)
     796             :         {
     797             :                 // Send a chat message to human players
     798           0 :                 var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     799           0 :                 cmpGuiInterface.PushNotification({
     800             :                         "type": "aichat",
     801             :                         "players": [player],
     802             :                         "message": "/allies " + markForTranslation("Attack against %(_player_)s requested."),
     803             :                         "translateParameters": ["_player_"],
     804             :                         "parameters": { "_player_": cmd.player }
     805             :                 });
     806             : 
     807             :                 // And send an attackRequest event to the AIs
     808           0 :                 let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
     809           0 :                 if (cmpAIInterface)
     810           0 :                         cmpAIInterface.PushEvent("AttackRequest", cmd);
     811             :         },
     812             : 
     813             :         "spy-request": function(player, cmd, data)
     814             :         {
     815           0 :                 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     816           0 :                 let ent = pickRandom(cmpRangeManager.GetEntitiesByPlayer(cmd.player).filter(ent => {
     817           0 :                         let cmpVisionSharing = Engine.QueryInterface(ent, IID_VisionSharing);
     818           0 :                         return cmpVisionSharing && cmpVisionSharing.IsBribable() && !cmpVisionSharing.ShareVisionWith(player);
     819             :                 }));
     820             : 
     821           0 :                 let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     822           0 :                 cmpGUIInterface.PushNotification({
     823             :                         "type": "spy-response",
     824             :                         "players": [player],
     825             :                         "target": cmd.player,
     826             :                         "entity": ent
     827             :                 });
     828           0 :                 if (ent)
     829           0 :                         Engine.QueryInterface(ent, IID_VisionSharing).AddSpy(cmd.source);
     830             :                 else
     831             :                 {
     832           0 :                         let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy");
     833           0 :                         IncurBribeCost(template, player, cmd.player, true);
     834             :                         // update statistics for failed bribes
     835           0 :                         let cmpBribesStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker);
     836           0 :                         if (cmpBribesStatisticsTracker)
     837           0 :                                 cmpBribesStatisticsTracker.IncreaseFailedBribesCounter();
     838           0 :                         cmpGUIInterface.PushNotification({
     839             :                                 "type": "text",
     840             :                                 "players": [player],
     841             :                                 "message": markForTranslation("There are no bribable units"),
     842             :                                 "translateMessage": true
     843             :                         });
     844             :                 }
     845             :         },
     846             : 
     847             :         "diplomacy-request": function(player, cmd, data)
     848             :         {
     849           0 :                 let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
     850           0 :                 if (cmpAIInterface)
     851           0 :                         cmpAIInterface.PushEvent("DiplomacyRequest", cmd);
     852             :         },
     853             : 
     854             :         "tribute-request": function(player, cmd, data)
     855             :         {
     856           0 :                 let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
     857           0 :                 if (cmpAIInterface)
     858           0 :                         cmpAIInterface.PushEvent("TributeRequest", cmd);
     859             :         },
     860             : 
     861             :         "dialog-answer": function(player, cmd, data)
     862             :         {
     863             :                 // Currently nothing. Triggers can read it anyway, and send this
     864             :                 // message to any component you like.
     865             :         },
     866             : 
     867             :         "set-dropsite-sharing": function(player, cmd, data)
     868             :         {
     869           0 :                 for (let ent of data.entities)
     870             :                 {
     871           0 :                         let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
     872           0 :                         if (cmpResourceDropsite && cmpResourceDropsite.IsSharable())
     873           0 :                                 cmpResourceDropsite.SetSharing(cmd.shared);
     874             :                 }
     875             :         },
     876             : 
     877             :         "map-flare": function(player, cmd, data)
     878             :         {
     879           0 :                 let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     880           0 :                 cmpGuiInterface.PushNotification({
     881             :                         "type": "map-flare",
     882             :                         "players": [player],
     883             :                         "position": cmd.position
     884             :                 });
     885             :         },
     886             : 
     887             :         "autoqueue-on": function(player, cmd, data)
     888             :         {
     889           0 :                 for (let ent of data.entities)
     890             :                 {
     891           0 :                         let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
     892           0 :                         if (cmpProductionQueue)
     893           0 :                                 cmpProductionQueue.EnableAutoQueue();
     894             :                 }
     895             :         },
     896             : 
     897             :         "autoqueue-off": function(player, cmd, data)
     898             :         {
     899           0 :                 for (let ent of data.entities)
     900             :                 {
     901           0 :                         let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
     902           0 :                         if (cmpProductionQueue)
     903           0 :                                 cmpProductionQueue.DisableAutoQueue();
     904             :                 }
     905             :         },
     906             : 
     907             : };
     908             : 
     909             : /**
     910             :  * Sends a GUI notification about unit(s) that failed to ungarrison.
     911             :  */
     912             : function notifyUnloadFailure(player)
     913             : {
     914           0 :         let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     915           0 :         cmpGUIInterface.PushNotification({
     916             :                 "type": "text",
     917             :                 "players": [player],
     918             :                 "message": markForTranslation("Unable to unload unit(s)."),
     919             :                 "translateMessage": true
     920             :         });
     921             : }
     922             : 
     923             : /**
     924             :  * Sends a GUI notification about worker(s) that failed to go back to work.
     925             :  */
     926             : function notifyBackToWorkFailure(player)
     927             : {
     928           0 :         var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     929           0 :         cmpGUIInterface.PushNotification({
     930             :                 "type": "text",
     931             :                 "players": [player],
     932             :                 "message": markForTranslation("Some unit(s) can't go back to work"),
     933             :                 "translateMessage": true
     934             :         });
     935             : }
     936             : 
     937             : /**
     938             :  * Sends a GUI notification about entities that can't be controlled.
     939             :  * @param {number} player - The player-ID of the player that needs to receive this message.
     940             :  */
     941             : function notifyOrderFailure(entity, player)
     942             : {
     943           0 :         let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
     944           0 :         if (!cmpIdentity)
     945           0 :                 return;
     946             : 
     947           0 :         let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     948           0 :         cmpGUIInterface.PushNotification({
     949             :                 "type": "text",
     950             :                 "players": [player],
     951             :                 "message": markForTranslation("%(unit)s can't be controlled."),
     952             :                 "parameters": { "unit": cmpIdentity.GetGenericName() },
     953             :                 "translateParameters": ["unit"],
     954             :                 "translateMessage": true
     955             :         });
     956             : }
     957             : 
     958             : /**
     959             :  * Get some information about the formations used by entities.
     960             :  */
     961             : function ExtractFormations(ents)
     962             : {
     963           0 :         let entities = []; // Entities with UnitAI.
     964           0 :         let members = {}; // { formationentity: [ent, ent, ...], ... }
     965           0 :         let templates = {};  // { formationentity: template }
     966           0 :         for (let ent of ents)
     967             :         {
     968           0 :                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     969           0 :                 if (!cmpUnitAI)
     970           0 :                         continue;
     971             : 
     972           0 :                 entities.push(ent);
     973             : 
     974           0 :                 let fid = cmpUnitAI.GetFormationController();
     975           0 :                 if (fid == INVALID_ENTITY)
     976           0 :                         continue;
     977             : 
     978           0 :                 if (!members[fid])
     979             :                 {
     980           0 :                         members[fid] = [];
     981           0 :                         templates[fid] = cmpUnitAI.GetFormationTemplate();
     982             :                 }
     983           0 :                 members[fid].push(ent);
     984             :         }
     985             : 
     986           0 :         return {
     987             :                 "entities": entities,
     988             :                 "members": members,
     989             :                 "templates": templates
     990             :         };
     991             : }
     992             : 
     993             : /**
     994             :  * Tries to find the best angle to put a dock at a given position
     995             :  * Taken from GuiInterface.js
     996             :  */
     997             : function GetDockAngle(template, x, z)
     998             : {
     999           0 :         var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
    1000           0 :         var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
    1001           0 :         if (!cmpTerrain || !cmpWaterManager)
    1002           0 :                 return undefined;
    1003             : 
    1004             :         // Get footprint size
    1005           0 :         var halfSize = 0;
    1006           0 :         if (template.Footprint.Square)
    1007           0 :                 halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
    1008           0 :         else if (template.Footprint.Circle)
    1009           0 :                 halfSize = template.Footprint.Circle["@radius"];
    1010             : 
    1011             :         /* Find direction of most open water, algorithm:
    1012             :          *      1. Pick points in a circle around dock
    1013             :          *      2. If point is in water, add to array
    1014             :          *      3. Scan array looking for consecutive points
    1015             :          *      4. Find longest sequence of consecutive points
    1016             :          *      5. If sequence equals all points, no direction can be determined,
    1017             :          *              expand search outward and try (1) again
    1018             :          *      6. Calculate angle using average of sequence
    1019             :          */
    1020           0 :         const numPoints = 16;
    1021           0 :         for (var dist = 0; dist < 4; ++dist)
    1022             :         {
    1023           0 :                 var waterPoints = [];
    1024           0 :                 for (var i = 0; i < numPoints; ++i)
    1025             :                 {
    1026           0 :                         var angle = (i/numPoints)*2*Math.PI;
    1027           0 :                         var d = halfSize*(dist+1);
    1028           0 :                         var nx = x - d*Math.sin(angle);
    1029           0 :                         var nz = z + d*Math.cos(angle);
    1030             : 
    1031           0 :                         if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
    1032           0 :                                 waterPoints.push(i);
    1033             :                 }
    1034           0 :                 var consec = [];
    1035           0 :                 var length = waterPoints.length;
    1036           0 :                 if (!length)
    1037           0 :                         continue;
    1038           0 :                 for (var i = 0; i < length; ++i)
    1039             :                 {
    1040           0 :                         var count = 0;
    1041           0 :                         for (let j = 0; j < length - 1; ++j)
    1042             :                         {
    1043           0 :                                 if ((waterPoints[(i + j) % length] + 1) % numPoints == waterPoints[(i + j + 1) % length])
    1044           0 :                                         ++count;
    1045             :                                 else
    1046           0 :                                         break;
    1047             :                         }
    1048           0 :                         consec[i] = count;
    1049             :                 }
    1050           0 :                 var start = 0;
    1051           0 :                 var count = 0;
    1052           0 :                 for (var c in consec)
    1053             :                 {
    1054           0 :                         if (consec[c] > count)
    1055             :                         {
    1056           0 :                                 start = c;
    1057           0 :                                 count = consec[c];
    1058             :                         }
    1059             :                 }
    1060             : 
    1061             :                 // If we've found a shoreline, stop searching
    1062           0 :                 if (count != numPoints-1)
    1063           0 :                         return -((waterPoints[start] + consec[start]/2) % numPoints) / numPoints * 2 * Math.PI;
    1064             :         }
    1065           0 :         return undefined;
    1066             : }
    1067             : 
    1068             : /**
    1069             :  * Attempts to construct a building using the specified parameters.
    1070             :  * Returns true on success, false on failure.
    1071             :  */
    1072             : function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
    1073             : {
    1074             :         // Message structure:
    1075             :         // {
    1076             :         //   "type": "construct",
    1077             :         //   "entities": [...],                 // entities that will be ordered to construct the building (if applicable)
    1078             :         //   "template": "...",                 // template name of the entity being constructed
    1079             :         //   "x": ...,
    1080             :         //   "z": ...,
    1081             :         //   "angle": ...,
    1082             :         //   "metadata": "...",                 // AI metadata of the building
    1083             :         //   "actorSeed": ...,
    1084             :         //   "autorepair": true,                // whether to automatically start constructing/repairing the new foundation
    1085             :         //   "autocontinue": true,              // whether to automatically gather/build/etc after finishing this
    1086             :         //   "queued": true,                    // whether to add the construction/repairing of this foundation to entities' queue (if applicable)
    1087             :         //   "obstructionControlGroup": ...,    // Optional; the obstruction control group ID that should be set for this building prior to obstruction
    1088             :         //                                      // testing to determine placement validity. If specified, must be a valid control group ID (> 0).
    1089             :         //   "obstructionControlGroup2": ...,   // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction
    1090             :         //                                      // testing to determine placement validity. May be INVALID_ENTITY.
    1091             :         // }
    1092             : 
    1093             :         /*
    1094             :          * Construction process:
    1095             :          *  . Take resources away immediately.
    1096             :          *  . Create a foundation entity with 1hp, 0% build progress.
    1097             :          *  . Increase hp and build progress up to 100% when people work on it.
    1098             :          *  . If it's destroyed, an appropriate fraction of the resource cost is refunded.
    1099             :          *  . If it's completed, it gets replaced with the real building.
    1100             :          */
    1101             : 
    1102             :         // Check whether we can control these units
    1103           0 :         var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
    1104           0 :         if (!entities.length)
    1105           0 :                 return false;
    1106             : 
    1107           0 :         var foundationTemplate = "foundation|" + cmd.template;
    1108             : 
    1109             :         // Tentatively create the foundation (we might find later that it's a invalid build command)
    1110           0 :         var ent = Engine.AddEntity(foundationTemplate);
    1111           0 :         if (ent == INVALID_ENTITY)
    1112             :         {
    1113             :                 // Error (e.g. invalid template names)
    1114           0 :                 error("Error creating foundation entity for '" + cmd.template + "'");
    1115           0 :                 return false;
    1116             :         }
    1117             : 
    1118             :         // If it's a dock, get the right angle.
    1119           0 :         var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template);
    1120           0 :         var angle = cmd.angle;
    1121           0 :         if (template.BuildRestrictions.PlacementType === "shore")
    1122             :         {
    1123           0 :                 let angleDock = GetDockAngle(template, cmd.x, cmd.z);
    1124           0 :                 if (angleDock !== undefined)
    1125           0 :                         angle = angleDock;
    1126             :         }
    1127             : 
    1128             :         // Move the foundation to the right place
    1129           0 :         var cmpPosition = Engine.QueryInterface(ent, IID_Position);
    1130           0 :         cmpPosition.JumpTo(cmd.x, cmd.z);
    1131           0 :         cmpPosition.SetYRotation(angle);
    1132             : 
    1133             :         // Set the obstruction control group if needed
    1134           0 :         if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2)
    1135             :         {
    1136           0 :                 var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
    1137             : 
    1138             :                 // primary control group must always be valid
    1139           0 :                 if (cmd.obstructionControlGroup)
    1140             :                 {
    1141           0 :                         if (cmd.obstructionControlGroup <= 0)
    1142           0 :                                 warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0");
    1143             : 
    1144           0 :                         cmpObstruction.SetControlGroup(cmd.obstructionControlGroup);
    1145             :                 }
    1146             : 
    1147           0 :                 if (cmd.obstructionControlGroup2)
    1148           0 :                         cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2);
    1149             :         }
    1150             : 
    1151             :         // Make it owned by the current player
    1152           0 :         var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
    1153           0 :         cmpOwnership.SetOwner(player);
    1154             : 
    1155             :         // Check whether building placement is valid
    1156           0 :         var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
    1157           0 :         if (cmpBuildRestrictions)
    1158             :         {
    1159           0 :                 var ret = cmpBuildRestrictions.CheckPlacement();
    1160           0 :                 if (!ret.success)
    1161             :                 {
    1162           0 :                         if (g_DebugCommands)
    1163           0 :                                 warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd));
    1164             : 
    1165           0 :                         var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
    1166           0 :                         ret.players = [player];
    1167           0 :                         cmpGuiInterface.PushNotification(ret);
    1168             : 
    1169             :                         // Remove the foundation because the construction was aborted
    1170             :                         // move it out of world because it's not destroyed immediately.
    1171           0 :                         cmpPosition.MoveOutOfWorld();
    1172           0 :                         Engine.DestroyEntity(ent);
    1173           0 :                         return false;
    1174             :                 }
    1175             :         }
    1176             :         else
    1177           0 :                 error("cmpBuildRestrictions not defined");
    1178             : 
    1179             :         // Check entity limits
    1180           0 :         var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
    1181           0 :         if (cmpEntityLimits && !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
    1182             :         {
    1183           0 :                 if (g_DebugCommands)
    1184           0 :                         warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
    1185             : 
    1186             :                 // Remove the foundation because the construction was aborted
    1187           0 :                 cmpPosition.MoveOutOfWorld();
    1188           0 :                 Engine.DestroyEntity(ent);
    1189           0 :                 return false;
    1190             :         }
    1191             : 
    1192           0 :         var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
    1193           0 :         if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template))
    1194             :         {
    1195           0 :                 if (g_DebugCommands)
    1196           0 :                         warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd));
    1197             : 
    1198           0 :                 var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
    1199           0 :                 cmpGuiInterface.PushNotification({
    1200             :                         "type": "text",
    1201             :                         "players": [player],
    1202             :                         "message": markForTranslation("The building's technology requirements are not met."),
    1203             :                         "translateMessage": true
    1204             :                 });
    1205             : 
    1206             :                 // Remove the foundation because the construction was aborted
    1207           0 :                 cmpPosition.MoveOutOfWorld();
    1208           0 :                 Engine.DestroyEntity(ent);
    1209             :         }
    1210             : 
    1211             :         // We need the cost after tech and aura modifications.
    1212           0 :         let cmpCost = Engine.QueryInterface(ent, IID_Cost);
    1213           0 :         let costs = cmpCost.GetResourceCosts();
    1214             : 
    1215           0 :         if (!cmpPlayer.TrySubtractResources(costs))
    1216             :         {
    1217           0 :                 if (g_DebugCommands)
    1218           0 :                         warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
    1219             : 
    1220           0 :                 Engine.DestroyEntity(ent);
    1221           0 :                 cmpPosition.MoveOutOfWorld();
    1222           0 :                 return false;
    1223             :         }
    1224             : 
    1225           0 :         var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
    1226           0 :         if (cmpVisual && cmd.actorSeed !== undefined)
    1227           0 :                 cmpVisual.SetActorSeed(cmd.actorSeed);
    1228             : 
    1229             :         // Initialise the foundation
    1230           0 :         var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
    1231           0 :         cmpFoundation.InitialiseConstruction(cmd.template);
    1232             : 
    1233             :         // send Metadata info if any
    1234           0 :         if (cmd.metadata)
    1235           0 :                 Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } );
    1236             : 
    1237             :         // Tell the units to start building this new entity
    1238           0 :         if (cmd.autorepair)
    1239             :         {
    1240           0 :                 ProcessCommand(player, {
    1241             :                         "type": "repair",
    1242             :                         "entities": entities,
    1243             :                         "target": ent,
    1244             :                         "autocontinue": cmd.autocontinue,
    1245             :                         "queued": cmd.queued,
    1246             :                         "pushFront": cmd.pushFront,
    1247             :                         "formation": cmd.formation || undefined
    1248             :                 });
    1249             :         }
    1250             : 
    1251           0 :         return ent;
    1252             : }
    1253             : 
    1254             : function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd)
    1255             : {
    1256             :         // 'cmd' message structure:
    1257             :         // {
    1258             :         //   "type": "construct-wall",
    1259             :         //   "entities": [...],           // entities that will be ordered to construct the wall (if applicable)
    1260             :         //   "pieces": [                  // ordered list of information about the pieces making up the wall (towers, wall segments, ...)
    1261             :         //      {
    1262             :         //         "template": "...",     // one of the templates from the wallset
    1263             :         //         "x": ...,
    1264             :         //         "z": ...,
    1265             :         //         "angle": ...,
    1266             :         //      },
    1267             :         //      ...
    1268             :         //   ],
    1269             :         //   "wallSet": {
    1270             :         //      "templates": {
    1271             :         //        "tower":                // tower template name
    1272             :         //        "long":                 // long wall segment template name
    1273             :         //        ...                     // etc.
    1274             :         //      },
    1275             :         //      "maxTowerOverlap": ...,
    1276             :         //      "minTowerOverlap": ...,
    1277             :         //   },
    1278             :         //   "startSnappedEntity":        // optional; entity ID of tower being snapped to at the starting side of the wall
    1279             :         //   "endSnappedEntity":          // optional; entity ID of tower being snapped to at the ending side of the wall
    1280             :         //   "autorepair": true,          // whether to automatically start constructing/repairing the new foundation
    1281             :         //   "autocontinue": true,        // whether to automatically gather/build/etc after finishing this
    1282             :         //   "queued": true,              // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable)
    1283             :         // }
    1284             : 
    1285           0 :         if (cmd.pieces.length <= 0)
    1286           0 :                 return;
    1287             : 
    1288           0 :         if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower)
    1289             :         {
    1290           0 :                 error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side");
    1291           0 :                 return;
    1292             :         }
    1293             : 
    1294           0 :         if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower)
    1295             :         {
    1296           0 :                 error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side");
    1297           0 :                 return;
    1298             :         }
    1299             : 
    1300             :         // Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement
    1301             :         // and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control
    1302             :         // groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing
    1303             :         // towers in the case of snapping). The towers themselves all keep their default unique control groups.
    1304             : 
    1305             :         // To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour
    1306             :         // it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the
    1307             :         // first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of
    1308             :         // the first tower encountered towards the ending side of the wall (if any).
    1309             : 
    1310             :         // We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the
    1311             :         // wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes:
    1312             :         //
    1313             :         //   FIRST PASS:
    1314             :         //    - Go from start to end and construct wall piece foundations as far as we can without running into a piece that
    1315             :         //        cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID
    1316             :         //        as the primary control group, thus allowing it to be built overlapping the previous piece.
    1317             :         //    - If we encounter a new tower along the way (which will gain its own control group), do the following:
    1318             :         //        o First build it using temporarily the same control group of the previous (non-tower) piece
    1319             :         //        o Set the previous piece's secondary control group to the tower's entity ID
    1320             :         //        o Restore the primary control group of the constructed tower back its original (unique) value.
    1321             :         //      The temporary control group is necessary to allow the newer tower with its unique control group ID to be able
    1322             :         //        to be placed while overlapping the previous piece.
    1323             :         //
    1324             :         //   SECOND PASS:
    1325             :         //    - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this
    1326             :         //      time registering the right neighbouring tower in each non-tower piece.
    1327             : 
    1328             :         // first pass; L -> R
    1329             : 
    1330           0 :         var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces
    1331           0 :         var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
    1332             : 
    1333             :         // If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that
    1334             :         // the first wall piece can be built while overlapping it.
    1335           0 :         if (cmd.startSnappedEntity)
    1336             :         {
    1337           0 :                 var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction);
    1338           0 :                 if (!cmpSnappedStartObstruction)
    1339             :                 {
    1340           0 :                         error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component");
    1341           0 :                         return;
    1342             :                 }
    1343             : 
    1344           0 :                 lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup();
    1345             :                 //warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup);
    1346             :         }
    1347             : 
    1348           0 :         var i = 0;
    1349           0 :         var queued = cmd.queued;
    1350           0 :         var pieces = clone(cmd.pieces);
    1351           0 :         for (; i < pieces.length; ++i)
    1352             :         {
    1353           0 :                 var piece = pieces[i];
    1354             : 
    1355             :                 // All wall pieces after the first must be queued.
    1356           0 :                 if (i > 0 && !queued)
    1357           0 :                         queued = true;
    1358             : 
    1359             :                 // 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do
    1360             :                 // start position snapping (implying that the first entity we build must be a tower)
    1361           0 :                 if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
    1362             :                 {
    1363           0 :                         if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity))
    1364             :                         {
    1365           0 :                         error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")");
    1366           0 :                         break;
    1367             :                         }
    1368             :                 }
    1369             : 
    1370           0 :                 var constructPieceCmd = {
    1371             :                         "type": "construct",
    1372             :                         "entities": cmd.entities,
    1373             :                         "template": piece.template,
    1374             :                         "x": piece.x,
    1375             :                         "z": piece.z,
    1376             :                         "angle": piece.angle,
    1377             :                         "autorepair": cmd.autorepair,
    1378             :                         "autocontinue": cmd.autocontinue,
    1379             :                         "queued": queued,
    1380             :                         // Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed
    1381             :                         // using the control group of the last tower (see comments above).
    1382             :                         "obstructionControlGroup": lastTowerControlGroup,
    1383             :                 };
    1384             : 
    1385             :                 // If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's
    1386             :                 // control group directly at construction time (instead of setting it in the second pass) to allow it to be built
    1387             :                 // while overlapping the snapped entity.
    1388           0 :                 if (i == pieces.length - 1 && cmd.endSnappedEntity)
    1389             :                 {
    1390           0 :                         var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
    1391           0 :                         if (cmpEndSnappedObstruction)
    1392           0 :                                 constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup();
    1393             :                 }
    1394             : 
    1395           0 :                 var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd);
    1396           0 :                 if (pieceEntityId)
    1397             :                 {
    1398             :                         // wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later
    1399           0 :                         piece.ent = pieceEntityId;
    1400             : 
    1401             :                         // if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex
    1402           0 :                         if (piece.template == cmd.wallSet.templates.tower)
    1403             :                         {
    1404           0 :                                 var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction);
    1405           0 :                                 var newTowerControlGroup = pieceEntityId;
    1406             : 
    1407           0 :                                 if (i > 0)
    1408             :                                 {
    1409             :                                         //warn("   updating previous wall piece's secondary control group to " + newTowerControlGroup);
    1410           0 :                                         var cmpPreviousObstruction = Engine.QueryInterface(pieces[i-1].ent, IID_Obstruction);
    1411             :                                         // TODO: ensure that cmpPreviousObstruction exists
    1412             :                                         // TODO: ensure that the previous obstruction does not yet have a secondary control group set
    1413           0 :                                         cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup);
    1414             :                                 }
    1415             : 
    1416             :                                 // TODO: ensure that cmpTowerObstruction exists
    1417           0 :                                 cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group
    1418             : 
    1419           0 :                                 lastTowerIndex = i;
    1420           0 :                                 lastTowerControlGroup = newTowerControlGroup;
    1421             :                         }
    1422             :                 }
    1423             :                 else // failed to build wall piece, abort
    1424           0 :                         break;
    1425             :         }
    1426             : 
    1427           0 :         var lastBuiltPieceIndex = i - 1;
    1428           0 :         var wallComplete = (lastBuiltPieceIndex == pieces.length - 1);
    1429             : 
    1430             :         // At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower).
    1431             :         // Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any)
    1432             :         // as their secondary control groups.
    1433             : 
    1434           0 :         lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
    1435             : 
    1436             :         // only start off with the ending side's snapped tower's control group if we were able to build the entire wall
    1437           0 :         if (cmd.endSnappedEntity && wallComplete)
    1438             :         {
    1439           0 :                 var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
    1440           0 :                 if (!cmpSnappedEndObstruction)
    1441             :                 {
    1442           0 :                         error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component");
    1443           0 :                         return;
    1444             :                 }
    1445             : 
    1446           0 :                 lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup();
    1447             :         }
    1448             : 
    1449           0 :         for (var j = lastBuiltPieceIndex; j >= 0; --j)
    1450             :         {
    1451           0 :                 var piece = pieces[j];
    1452             : 
    1453           0 :                 if (!piece.ent)
    1454             :                 {
    1455           0 :                         error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'");
    1456           0 :                         continue;
    1457             :                 }
    1458             : 
    1459           0 :                 var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction);
    1460           0 :                 if (!cmpPieceObstruction)
    1461             :                 {
    1462           0 :                         error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component");
    1463           0 :                         continue;
    1464             :                 }
    1465             : 
    1466           0 :                 if (piece.template == cmd.wallSet.templates.tower)
    1467             :                 {
    1468             :                         // encountered a tower entity, update the last tower control group
    1469           0 :                         lastTowerControlGroup = cmpPieceObstruction.GetControlGroup();
    1470             :                 }
    1471             :                 else
    1472             :                 {
    1473             :                         // Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'.
    1474             :                         // Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group
    1475             :                         // dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'.
    1476             : 
    1477           0 :                         var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2();
    1478           0 :                         if (existingSecondaryControlGroup == INVALID_ENTITY)
    1479             :                         {
    1480           0 :                                 if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY)
    1481             :                                 {
    1482           0 :                                         cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup);
    1483             :                                 }
    1484             :                         }
    1485           0 :                         else if (existingSecondaryControlGroup != lastTowerControlGroup)
    1486             :                         {
    1487           0 :                                 error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")");
    1488           0 :                                 break;
    1489             :                         }
    1490             :                 }
    1491             :         }
    1492             : }
    1493             : 
    1494             : /**
    1495             :  * Remove the given list of entities from their current formations.
    1496             :  */
    1497             : function RemoveFromFormation(ents)
    1498             : {
    1499           0 :         let formation = ExtractFormations(ents);
    1500           0 :         for (let fid in formation.members)
    1501             :         {
    1502           0 :                 let cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
    1503           0 :                 if (cmpFormation)
    1504           0 :                         cmpFormation.RemoveMembers(formation.members[fid]);
    1505             :         }
    1506             : }
    1507             : 
    1508             : /**
    1509             :  * Returns a list of UnitAI components, each belonging either to a
    1510             :  * selected unit or to a formation entity for groups of the selected units.
    1511             :  */
    1512             : function GetFormationUnitAIs(ents, player, cmd, formationTemplate, forceTemplate)
    1513             : {
    1514             :         // If an individual was selected, remove it from any formation
    1515             :         // and command it individually.
    1516           0 :         if (ents.length == 1)
    1517             :         {
    1518           0 :                 let cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
    1519           0 :                 if (!cmpUnitAI)
    1520           0 :                         return [];
    1521             : 
    1522           0 :                 RemoveFromFormation(ents);
    1523             : 
    1524           0 :                 return [ cmpUnitAI ];
    1525             :         }
    1526             : 
    1527           0 :         let formationUnitAIs = [];
    1528             :         // Find what formations the selected entities are currently in,
    1529             :         // and default to that unless the formation is forced or it's the null formation
    1530             :         // (we want that to reset whatever formations units are in).
    1531           0 :         if (formationTemplate != NULL_FORMATION)
    1532             :         {
    1533           0 :                 let formation = ExtractFormations(ents);
    1534           0 :                 let formationIds = Object.keys(formation.members);
    1535           0 :                 if (formationIds.length == 1)
    1536             :                 {
    1537             :                         // Selected units either belong to this formation or have no formation.
    1538           0 :                         let fid = formationIds[0];
    1539           0 :                         let cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
    1540           0 :                         if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length &&
    1541             :                             cmpFormation.GetMemberCount() == formation.entities.length)
    1542             :                         {
    1543           0 :                                 cmpFormation.DeleteTwinFormations();
    1544             : 
    1545             :                                 // The whole formation was selected, so reuse its controller for this command.
    1546           0 :                                 if (!forceTemplate || formationTemplate == formation.templates[fid])
    1547             :                                 {
    1548           0 :                                         formationTemplate = formation.templates[fid];
    1549           0 :                                         formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)];
    1550             :                                 }
    1551           0 :                                 else if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate))
    1552           0 :                                         formationUnitAIs = [cmpFormation.LoadFormation(formationTemplate)];
    1553             :                         }
    1554           0 :                         else if (cmpFormation && !forceTemplate)
    1555             :                         {
    1556             :                                 // Just reuse the template.
    1557           0 :                                 formationTemplate = formation.templates[fid];
    1558             :                         }
    1559             :                 }
    1560           0 :                 else if (formationIds.length)
    1561             :                 {
    1562             :                         // Check if all entities share a common formation, if so reuse this template.
    1563           0 :                         let template = formation.templates[formationIds[0]];
    1564           0 :                         for (let i = 1; i < formationIds.length; ++i)
    1565           0 :                                 if (formation.templates[formationIds[i]] != template)
    1566             :                                 {
    1567           0 :                                         template = null;
    1568           0 :                                         break;
    1569             :                                 }
    1570           0 :                         if (template && !forceTemplate)
    1571           0 :                                 formationTemplate = template;
    1572             :                 }
    1573             :         }
    1574             : 
    1575             :         // Separate out the units that don't support the chosen formation.
    1576           0 :         let formedUnits = [];
    1577           0 :         let nonformedUnitAIs = [];
    1578           0 :         for (let ent of ents)
    1579             :         {
    1580           0 :                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
    1581           0 :                 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
    1582           0 :                 if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld())
    1583           0 :                         continue;
    1584             : 
    1585             :                 // TODO: We only check if the formation is usable by some units
    1586             :                 // if we move them to it. We should check if we can use formations
    1587             :                 // for the other cases.
    1588           0 :                 let nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == NULL_FORMATION;
    1589           0 :                 if (nullFormation || !cmpUnitAI.CanUseFormation(formationTemplate || NULL_FORMATION))
    1590             :                 {
    1591           0 :                         if (nullFormation && cmpUnitAI.GetFormationController())
    1592           0 :                                 cmpUnitAI.LeaveFormation(cmd.queued || false);
    1593           0 :                         nonformedUnitAIs.push(cmpUnitAI);
    1594             :                 }
    1595             :                 else
    1596           0 :                         formedUnits.push(ent);
    1597             :         }
    1598           0 :         if (nonformedUnitAIs.length == ents.length)
    1599             :         {
    1600             :                 // No units support the formation.
    1601           0 :                 return nonformedUnitAIs;
    1602             :         }
    1603             : 
    1604           0 :         if (!formationUnitAIs.length)
    1605             :         {
    1606             :                 // We need to give the selected units a new formation controller.
    1607             : 
    1608             :                 // TODO replace the fixed 60 with something sensible, based on vision range f.e.
    1609           0 :                 let formationSeparation = 60;
    1610           0 :                 let clusters = ClusterEntities(formedUnits, formationSeparation);
    1611           0 :                 let formationEnts = [];
    1612           0 :                 for (let cluster of clusters)
    1613             :                 {
    1614           0 :                         RemoveFromFormation(cluster);
    1615             : 
    1616           0 :                         if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate))
    1617             :                         {
    1618           0 :                                 for (let ent of cluster)
    1619           0 :                                         nonformedUnitAIs.push(Engine.QueryInterface(ent, IID_UnitAI));
    1620             : 
    1621           0 :                                 continue;
    1622             :                         }
    1623             : 
    1624             :                         // Create the new controller.
    1625           0 :                         let formationEnt = Engine.AddEntity(formationTemplate);
    1626           0 :                         let cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
    1627           0 :                         formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI));
    1628           0 :                         cmpFormation.SetFormationSeparation(formationSeparation);
    1629           0 :                         cmpFormation.SetMembers(cluster);
    1630             : 
    1631           0 :                         for (let ent of formationEnts)
    1632           0 :                                 cmpFormation.RegisterTwinFormation(ent);
    1633             : 
    1634           0 :                         formationEnts.push(formationEnt);
    1635           0 :                         let cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
    1636           0 :                         cmpOwnership.SetOwner(player);
    1637             :                 }
    1638             :         }
    1639             : 
    1640           0 :         return nonformedUnitAIs.concat(formationUnitAIs);
    1641             : }
    1642             : 
    1643             : /**
    1644             :  * Group a list of entities in clusters via single-links
    1645             :  */
    1646             : function ClusterEntities(ents, separationDistance)
    1647             : {
    1648           0 :         let clusters = [];
    1649           0 :         if (!ents.length)
    1650           0 :                 return clusters;
    1651             : 
    1652           0 :         let distSq = separationDistance * separationDistance;
    1653           0 :         let positions = [];
    1654             :         // triangular matrix with the (squared) distances between the different clusters
    1655             :         // the other half is not initialised
    1656           0 :         let matrix = [];
    1657           0 :         for (let i = 0; i < ents.length; ++i)
    1658             :         {
    1659           0 :                 matrix[i] = [];
    1660           0 :                 clusters.push([ents[i]]);
    1661           0 :                 let cmpPosition = Engine.QueryInterface(ents[i], IID_Position);
    1662           0 :                 positions.push(cmpPosition.GetPosition2D());
    1663           0 :                 for (let j = 0; j < i; ++j)
    1664           0 :                         matrix[i][j] = positions[i].distanceToSquared(positions[j]);
    1665             :         }
    1666           0 :         while (clusters.length > 1)
    1667             :         {
    1668             :                 // search two clusters that are closer than the required distance
    1669           0 :                 let closeClusters = undefined;
    1670             : 
    1671           0 :                 for (let i = matrix.length - 1; i >= 0 && !closeClusters; --i)
    1672           0 :                         for (let j = i - 1; j >= 0 && !closeClusters; --j)
    1673           0 :                                 if (matrix[i][j] < distSq)
    1674           0 :                                         closeClusters = [i,j];
    1675             : 
    1676             :                 // if no more close clusters found, just return all found clusters so far
    1677           0 :                 if (!closeClusters)
    1678           0 :                         return clusters;
    1679             : 
    1680             :                 // make a new cluster with the entities from the two found clusters
    1681           0 :                 let newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]);
    1682             : 
    1683             :                 // calculate the minimum distance between the new cluster and all other remaining
    1684             :                 // clusters by taking the minimum of the two distances.
    1685           0 :                 let distances = [];
    1686           0 :                 for (let i = 0; i < clusters.length; ++i)
    1687             :                 {
    1688           0 :                         let a = closeClusters[1];
    1689           0 :                         let b = closeClusters[0];
    1690           0 :                         if (i == a || i == b)
    1691           0 :                                 continue;
    1692           0 :                         let dist1 = matrix[a][i] !== undefined ? matrix[a][i] : matrix[i][a];
    1693           0 :                         let dist2 = matrix[b][i] !== undefined ? matrix[b][i] : matrix[i][b];
    1694           0 :                         distances.push(Math.min(dist1, dist2));
    1695             :                 }
    1696             :                 // remove the rows and columns in the matrix for the merged clusters,
    1697             :                 // and the clusters themselves from the cluster list
    1698           0 :                 clusters.splice(closeClusters[0],1);
    1699           0 :                 clusters.splice(closeClusters[1],1);
    1700           0 :                 matrix.splice(closeClusters[0],1);
    1701           0 :                 matrix.splice(closeClusters[1],1);
    1702           0 :                 for (let i = 0; i < matrix.length; ++i)
    1703             :                 {
    1704           0 :                         if (matrix[i].length > closeClusters[0])
    1705           0 :                                 matrix[i].splice(closeClusters[0],1);
    1706           0 :                         if (matrix[i].length > closeClusters[1])
    1707           0 :                                 matrix[i].splice(closeClusters[1],1);
    1708             :                 }
    1709             :                 // add a new row of distances to the matrix and the new cluster
    1710           0 :                 clusters.push(newCluster);
    1711           0 :                 matrix.push(distances);
    1712             :         }
    1713           0 :         return clusters;
    1714             : }
    1715             : 
    1716             : function GetFormationRequirements(formationTemplate)
    1717             : {
    1718           0 :         var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate);
    1719           0 :         if (!template.Formation)
    1720           0 :                 return false;
    1721             : 
    1722           0 :         return { "minCount": +template.Formation.RequiredMemberCount };
    1723             : }
    1724             : 
    1725             : 
    1726             : function CanMoveEntsIntoFormation(ents, formationTemplate)
    1727             : {
    1728             :         // TODO: should check the player's civ is allowed to use this formation
    1729             :         // See simulation/components/Player.js GetFormations() for a list of all allowed formations
    1730             : 
    1731           0 :         const requirements = GetFormationRequirements(formationTemplate);
    1732           0 :         if (!requirements)
    1733           0 :                 return false;
    1734             : 
    1735           0 :         let count = 0;
    1736           0 :         for (const ent of ents)
    1737           0 :                 if (Engine.QueryInterface(ent, IID_UnitAI)?.CanUseFormation(formationTemplate))
    1738           0 :                         ++count;
    1739             : 
    1740           0 :         return count >= requirements.minCount;
    1741             : }
    1742             : 
    1743             : /**
    1744             :  * Check if player can control this entity
    1745             :  * returns: true if the entity is owned by the player and controllable
    1746             :  *          or control all units is activated, else false
    1747             :  */
    1748             : function CanControlUnit(entity, player, controlAll)
    1749             : {
    1750           0 :         let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
    1751           0 :         let canBeControlled = IsOwnedByPlayer(player, entity) &&
    1752             :                 (!cmpIdentity || cmpIdentity.IsControllable()) ||
    1753             :                 controlAll;
    1754             : 
    1755           0 :         if (!canBeControlled)
    1756           0 :                 notifyOrderFailure(entity, player);
    1757             : 
    1758           0 :         return canBeControlled;
    1759             : }
    1760             : 
    1761             : /**
    1762             :  * @param {number} entity - The entityID to verify.
    1763             :  * @param {number} player - The playerID to check against.
    1764             :  * @return {boolean}.
    1765             :  */
    1766             : function IsOwnedByPlayerOrMutualAlly(entity, player)
    1767             : {
    1768           0 :         return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity);
    1769             : }
    1770             : 
    1771             : /**
    1772             :  * Check if player can control this entity
    1773             :  * @return {boolean} - True if the entity is valid and controlled by the player
    1774             :  *          or the entity is owned by an mutualAlly and can be controlled
    1775             :  *          or control all units is activated, else false.
    1776             :  */
    1777             : function CanPlayerOrAllyControlUnit(entity, player, controlAll)
    1778             : {
    1779           0 :         return CanControlUnit(player, entity, controlAll) ||
    1780             :                 IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity);
    1781             : }
    1782             : 
    1783             : /**
    1784             :  * @return {boolean} - Whether the owner of this entity can control the entity.
    1785             :  */
    1786             : function CanOwnerControlEntity(entity)
    1787             : {
    1788           0 :         let cmpOwner = QueryOwnerInterface(entity);
    1789           0 :         return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID());
    1790             : }
    1791             : 
    1792             : /**
    1793             :  * Filter entities which the player can control.
    1794             :  */
    1795             : function FilterEntityList(entities, player, controlAll)
    1796             : {
    1797           0 :         return entities.filter(ent => CanControlUnit(ent, player, controlAll));
    1798             : }
    1799             : 
    1800             : /**
    1801             :  * Filter entities which the player can control or are mutualAlly
    1802             :  */
    1803             : function FilterEntityListWithAllies(entities, player, controlAll)
    1804             : {
    1805           0 :         return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll));
    1806             : }
    1807             : 
    1808             : /**
    1809             :  * Incur the player with the cost of a bribe, optionally multiply the cost with
    1810             :  * the additionalMultiplier
    1811             :  */
    1812             : function IncurBribeCost(template, player, playerBribed, failedBribe)
    1813             : {
    1814           3 :         let cmpPlayerBribed = QueryPlayerIDInterface(playerBribed);
    1815           3 :         if (!cmpPlayerBribed)
    1816           0 :                 return false;
    1817             : 
    1818           3 :         let costs = {};
    1819             :         // Additional cost for this owner
    1820           3 :         let multiplier = cmpPlayerBribed.GetSpyCostMultiplier();
    1821           3 :         if (failedBribe)
    1822           0 :                 multiplier *= template.VisionSharing.FailureCostRatio;
    1823             : 
    1824           3 :         for (let res in template.Cost.Resources)
    1825           3 :                 costs[res] = Math.floor(multiplier * ApplyValueModificationsToTemplate("Cost/Resources/" + res, +template.Cost.Resources[res], player, template));
    1826             : 
    1827           3 :         let cmpPlayer = QueryPlayerIDInterface(player);
    1828           3 :         return cmpPlayer && cmpPlayer.TrySubtractResources(costs);
    1829             : }
    1830             : 
    1831           1 : Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements);
    1832           1 : Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
    1833           1 : Engine.RegisterGlobal("GetDockAngle", GetDockAngle);
    1834           1 : Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
    1835           1 : Engine.RegisterGlobal("g_Commands", g_Commands);
    1836           1 : Engine.RegisterGlobal("IncurBribeCost", IncurBribeCost);

Generated by: LCOV version 1.14