Cleanup of GetEntityState
[0ad.git] / binaries / data / mods / public / simulation / components / GuiInterface.js
blobbabdc5e45803f3f61d0db795c8c90cfb122b368e
1 function GuiInterface() {}
3 GuiInterface.prototype.Schema =
4         "<a:component type='system'/><empty/>";
6 GuiInterface.prototype.Serialize = function()
8         // This component isn't network-synchronised for the biggest part
9         // So most of the attributes shouldn't be serialized
10         // Return an object with a small selection of deterministic data
11         return {
12                 "timeNotifications": this.timeNotifications,
13                 "timeNotificationID": this.timeNotificationID
14         };
17 GuiInterface.prototype.Deserialize = function(data)
19         this.Init();
20         this.timeNotifications = data.timeNotifications;
21         this.timeNotificationID = data.timeNotificationID;
24 GuiInterface.prototype.Init = function()
26         this.placementEntity = undefined; // = undefined or [templateName, entityID]
27         this.placementWallEntities = undefined;
28         this.placementWallLastAngle = 0;
29         this.notifications = [];
30         this.renamedEntities = [];
31         this.miragedEntities = [];
32         this.timeNotificationID = 1;
33         this.timeNotifications = [];
34         this.entsRallyPointsDisplayed = [];
35         this.entsWithAuraAndStatusBars = new Set();
36         this.enabledVisualRangeOverlayTypes = {};
40  * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
41  * from GUI scripts, and executed here with arguments (player, arg).
42  *
43  * CAUTION: The input to the functions in this module is not network-synchronised, so it
44  * mustn't affect the simulation state (i.e. the data that is serialised and can affect
45  * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
46  */
48 /**
49  * Returns global information about the current game state.
50  * This is used by the GUI and also by AI scripts.
51  */
52 GuiInterface.prototype.GetSimulationState = function()
54         let ret = {
55                 "players": []
56         };
58         let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
59         for (let i = 0; i < numPlayers; ++i)
60         {
61                 let cmpPlayer = QueryPlayerIDInterface(i);
62                 let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
64                 // Work out what phase we are in
65                 let phase = "";
66                 let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
67                 if (cmpTechnologyManager)
68                 {
69                         if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
70                                 phase = "city";
71                         else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
72                                 phase = "town";
73                         else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
74                                 phase = "village";
75                 }
77                 // store player ally/neutral/enemy data as arrays
78                 let allies = [];
79                 let mutualAllies = [];
80                 let neutrals = [];
81                 let enemies = [];
83                 for (let j = 0; j < numPlayers; ++j)
84                 {
85                         allies[j] = cmpPlayer.IsAlly(j);
86                         mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
87                         neutrals[j] = cmpPlayer.IsNeutral(j);
88                         enemies[j] = cmpPlayer.IsEnemy(j);
89                 }
91                 ret.players.push({
92                         "name": cmpPlayer.GetName(),
93                         "civ": cmpPlayer.GetCiv(),
94                         "color": cmpPlayer.GetColor(),
95                         "controlsAll": cmpPlayer.CanControlAllUnits(),
96                         "popCount": cmpPlayer.GetPopulationCount(),
97                         "popLimit": cmpPlayer.GetPopulationLimit(),
98                         "popMax": cmpPlayer.GetMaxPopulation(),
99                         "panelEntities": cmpPlayer.GetPanelEntities(),
100                         "resourceCounts": cmpPlayer.GetResourceCounts(),
101                         "trainingBlocked": cmpPlayer.IsTrainingBlocked(),
102                         "state": cmpPlayer.GetState(),
103                         "team": cmpPlayer.GetTeam(),
104                         "teamsLocked": cmpPlayer.GetLockTeams(),
105                         "cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
106                         "disabledTemplates": cmpPlayer.GetDisabledTemplates(),
107                         "disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
108                         "hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
109                         "hasSharedLos": cmpPlayer.HasSharedLos(),
110                         "spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
111                         "phase": phase,
112                         "isAlly": allies,
113                         "isMutualAlly": mutualAllies,
114                         "isNeutral": neutrals,
115                         "isEnemy": enemies,
116                         "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
117                         "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
118                         "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
119                         "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
120                         "researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
121                         "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
122                         "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
123                         "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
124                         "canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i),
125                         "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i)
126                 });
127         }
129         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
130         if (cmpRangeManager)
131                 ret.circularMap = cmpRangeManager.GetLosCircular();
133         let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
134         if (cmpTerrain)
135                 ret.mapSize = cmpTerrain.GetMapSize();
137         // Add timeElapsed
138         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
139         ret.timeElapsed = cmpTimer.GetTime();
141         // Add ceasefire info
142         let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
143         if (cmpCeasefireManager)
144         {
145                 ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
146                 ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
147         }
149         // Add cinema path info
150         let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
151         if (cmpCinemaManager)
152                 ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
154         // Add the game type and allied victory
155         let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
156         ret.gameType = cmpEndGameManager.GetGameType();
157         ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
159         // Add basic statistics to each player
160         for (let i = 0; i < numPlayers; ++i)
161         {
162                 let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
163                 if (cmpPlayerStatisticsTracker)
164                         ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
165         }
167         return ret;
171  * Returns global information about the current game state, plus statistics.
172  * This is used by the GUI at the end of a game, in the summary screen.
173  * Note: Amongst statistics, the team exploration map percentage is computed from
174  * scratch, so the extended simulation state should not be requested too often.
175  */
176 GuiInterface.prototype.GetExtendedSimulationState = function()
178         // Get basic simulation info
179         let ret = this.GetSimulationState();
181         // Add statistics to each player
182         let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
183         for (let i = 0; i < numPlayers; ++i)
184         {
185                 let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
186                 if (cmpPlayerStatisticsTracker)
187                         ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
188         }
190         return ret;
193 GuiInterface.prototype.GetRenamedEntities = function(player)
195         if (this.miragedEntities[player])
196                 return this.renamedEntities.concat(this.miragedEntities[player]);
198         return this.renamedEntities;
201 GuiInterface.prototype.ClearRenamedEntities = function()
203         this.renamedEntities = [];
204         this.miragedEntities = [];
207 GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
209         if (!this.miragedEntities[player])
210                 this.miragedEntities[player] = [];
212         this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
216  * Get common entity info, often used in the gui
217  */
218 GuiInterface.prototype.GetEntityState = function(player, ent)
220         let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
222         // All units must have a template; if not then it's a nonexistent entity id
223         let template = cmpTemplateManager.GetCurrentTemplateName(ent);
224         if (!template)
225                 return null;
227         let ret = {
228                 "id": ent,
229                 "template": template,
231                 "alertRaiser": null,
232                 "armour": null,
233                 "attack": null,
234                 "builder": null,
235                 "buildingAI": null,
236                 "buildRate": null,
237                 "buildTime": null,
238                 "canGarrison": null,
239                 "deathDamage": null,
240                 "heal": null,
241                 "identity": null,
242                 "isBarterMarket": null,
243                 "fogging": null,
244                 "foundation": null,
245                 "garrisonHolder": null,
246                 "gate": null,
247                 "guard": null,
248                 "loot": null,
249                 "market": null,
250                 "mirage": null,
251                 "pack": null,
252                 "promotion": null,
253                 "upgrade" : null,
254                 "player": -1,
255                 "position": null,
256                 "production": null,
257                 "rallyPoint": null,
258                 "repairRate": null,
259                 "resourceCarrying": null,
260                 "resourceDropsite": null,
261                 "resourceGatherRates": null,
262                 "resourceSupply": null,
263                 "resourceTrickle": null,
264                 "rotation": null,
265                 "speed": null,
266                 "trader": null,
267                 "turretParent":null,
268                 "unitAI": null,
269                 "visibility": null,
270         };
272         let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
273         if (cmpMirage)
274                 ret.mirage = true;
276         let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
277         if (cmpIdentity)
278                 ret.identity = {
279                         "rank": cmpIdentity.GetRank(),
280                         "classes": cmpIdentity.GetClassesList(),
281                         "visibleClasses": cmpIdentity.GetVisibleClassesList(),
282                         "selectionGroupName": cmpIdentity.GetSelectionGroupName(),
283                         "canDelete": !cmpIdentity.IsUndeletable()
284                 };
286         let cmpPosition = Engine.QueryInterface(ent, IID_Position);
287         if (cmpPosition && cmpPosition.IsInWorld())
288         {
289                 ret.position = cmpPosition.GetPosition();
290                 ret.rotation = cmpPosition.GetRotation();
291         }
293         let cmpHealth = QueryMiragedInterface(ent, IID_Health);
294         if (cmpHealth)
295         {
296                 ret.hitpoints = cmpHealth.GetHitpoints();
297                 ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
298                 ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints();
299                 ret.needsHeal = !cmpHealth.IsUnhealable();
300         }
302         let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
303         if (cmpCapturable)
304         {
305                 ret.capturePoints = cmpCapturable.GetCapturePoints();
306                 ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
307         }
309         let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
310         if (cmpBuilder)
311                 ret.builder = true;
313         let cmpMarket = QueryMiragedInterface(ent, IID_Market);
314         if (cmpMarket)
315                 ret.market = {
316                         "land": cmpMarket.HasType("land"),
317                         "naval": cmpMarket.HasType("naval"),
318                 };
320         let cmpPack = Engine.QueryInterface(ent, IID_Pack);
321         if (cmpPack)
322                 ret.pack = {
323                         "packed": cmpPack.IsPacked(),
324                         "progress": cmpPack.GetProgress(),
325                 };
327         var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
328         if (cmpUpgrade)
329                 ret.upgrade = {
330                         "upgrades" : cmpUpgrade.GetUpgrades(),
331                         "progress": cmpUpgrade.GetProgress(),
332                         "template": cmpUpgrade.GetUpgradingTo()
333                 };
335         let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
336         if (cmpProductionQueue)
337                 ret.production = {
338                         "entities": cmpProductionQueue.GetEntitiesList(),
339                         "technologies": cmpProductionQueue.GetTechnologiesList(),
340                         "techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
341                         "queue": cmpProductionQueue.GetQueue()
342                 };
344         let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
345         if (cmpTrader)
346                 ret.trader = {
347                         "goods": cmpTrader.GetGoods()
348                 };
350         let cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
351         if (cmpFogging)
352                 ret.fogging = {
353                         "mirage": cmpFogging.IsMiraged(player) ? cmpFogging.GetMirage(player) : null
354                 };
356         let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
357         if (cmpFoundation)
358         {
359                 ret.foundation = {
360                         "progress": cmpFoundation.GetBuildPercentage(),
361                         "numBuilders": cmpFoundation.GetNumBuilders()
362                 };
363                 ret.buildRate = cmpFoundation.GetBuildRate();
364                 ret.buildTime = cmpFoundation.GetBuildTime();
365         }
367         let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
368         if (cmpRepairable)
369         {
370                 ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders() };
371                 ret.repairRate = cmpRepairable.GetRepairRate();
372         }
374         let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
375         if (cmpOwnership)
376                 ret.player = cmpOwnership.GetOwner();
378         let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
379         if (cmpRallyPoint)
380                 ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
382         let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
383         if (cmpGarrisonHolder)
384                 ret.garrisonHolder = {
385                         "entities": cmpGarrisonHolder.GetEntities(),
386                         "buffHeal": cmpGarrisonHolder.GetHealRate(),
387                         "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
388                         "capacity": cmpGarrisonHolder.GetCapacity(),
389                         "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
390                 };
392         ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable);
394         let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
395         if (cmpUnitAI)
396                 ret.unitAI = {
397                         "state": cmpUnitAI.GetCurrentState(),
398                         "orders": cmpUnitAI.GetOrders(),
399                         "hasWorkOrders": cmpUnitAI.HasWorkOrders(),
400                         "canGuard": cmpUnitAI.CanGuard(),
401                         "isGuarding": cmpUnitAI.IsGuardOf(),
402                         "canPatrol": cmpUnitAI.CanPatrol(),
403                         "possibleStances": cmpUnitAI.GetPossibleStances(),
404                         "isIdle":cmpUnitAI.IsIdle(),
405                 };
407         let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
408         if (cmpGuard)
409                 ret.guard = {
410                         "entities": cmpGuard.GetEntities(),
411                 };
413         let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
414         if (cmpResourceGatherer)
415         {
416                 ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
417                 ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
418         }
420         let cmpGate = Engine.QueryInterface(ent, IID_Gate);
421         if (cmpGate)
422                 ret.gate = {
423                         "locked": cmpGate.IsLocked(),
424                 };
426         let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
427         if (cmpAlertRaiser)
428                 ret.alertRaiser = {
429                         "level": cmpAlertRaiser.GetLevel(),
430                         "canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(),
431                         "hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(),
432                 };
434         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
435         ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
437         let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
438         if (cmpAttack)
439         {
440                 let types = cmpAttack.GetAttackTypes();
441                 if (types.length)
442                         ret.attack = {};
444                 for (let type of types)
445                 {
446                         ret.attack[type] = cmpAttack.GetAttackStrengths(type);
447                         ret.attack[type].splash = cmpAttack.GetSplashDamage(type);
449                         let range = cmpAttack.GetRange(type);
450                         ret.attack[type].minRange = range.min;
451                         ret.attack[type].maxRange = range.max;
453                         let timers = cmpAttack.GetTimers(type);
454                         ret.attack[type].prepareTime = timers.prepare;
455                         ret.attack[type].repeatTime = timers.repeat;
457                         if (type != "Ranged")
458                         {
459                                 // not a ranged attack, set some defaults
460                                 ret.attack[type].elevationBonus = 0;
461                                 ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
462                                 continue;
463                         }
465                         ret.attack[type].elevationBonus = range.elevationBonus;
467                         if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
468                         {
469                                 // For units, take the range in front of it, no spread. So angle = 0
470                                 ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
471                         }
472                         else if(cmpPosition && cmpPosition.IsInWorld())
473                         {
474                                 // For buildings, take the average elevation around it. So angle = 2*pi
475                                 ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
476                         }
477                         else
478                         {
479                                 // not in world, set a default?
480                                 ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
481                         }
482                 }
483         }
485         let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
486         if (cmpArmour)
487                 ret.armour = cmpArmour.GetArmourStrengths();
489         let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
490         if (cmpAuras)
491                 ret.auras = cmpAuras.GetDescriptions();
493         let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
494         if (cmpBuildingAI)
495                 ret.buildingAI = {
496                         "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
497                         "maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
498                         "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
499                         "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
500                         "arrowCount": cmpBuildingAI.GetArrowCount()
501                 };
503         let cmpDeathDamage = Engine.QueryInterface(ent, IID_DeathDamage);
504         if (cmpDeathDamage)
505                 ret.deathDamage = cmpDeathDamage.GetDeathDamageStrengths();
507         if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
508                 ret.turretParent = cmpPosition.GetTurretParent();
510         let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
511         if (cmpResourceSupply)
512                 ret.resourceSupply = {
513                         "isInfinite": cmpResourceSupply.IsInfinite(),
514                         "max": cmpResourceSupply.GetMaxAmount(),
515                         "amount": cmpResourceSupply.GetCurrentAmount(),
516                         "type": cmpResourceSupply.GetType(),
517                         "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
518                         "maxGatherers": cmpResourceSupply.GetMaxGatherers(),
519                         "numGatherers": cmpResourceSupply.GetNumGatherers()
520                 };
522         let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
523         if (cmpResourceDropsite)
524                 ret.resourceDropsite = {
525                         "types": cmpResourceDropsite.GetTypes(),
526                         "sharable": cmpResourceDropsite.IsSharable(),
527                         "shared": cmpResourceDropsite.IsShared()
528                 };
530         let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
531         if (cmpPromotion)
532                 ret.promotion = {
533                         "curr": cmpPromotion.GetCurrentXp(),
534                         "req": cmpPromotion.GetRequiredXp()
535                 };
537         if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
538                 ret.isBarterMarket = true;
540         let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
541         if (cmpHeal)
542                 ret.heal = {
543                         "hp": cmpHeal.GetHP(),
544                         "range": cmpHeal.GetRange().max,
545                         "rate": cmpHeal.GetRate(),
546                         "unhealableClasses": cmpHeal.GetUnhealableClasses(),
547                         "healableClasses": cmpHeal.GetHealableClasses(),
548                 };
550         let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
551         if (cmpLoot)
552         {
553                 ret.loot = cmpLoot.GetResources();
554                 ret.loot.xp = cmpLoot.GetXp();
555         }
557         let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
558         if (cmpResourceTrickle)
559                 ret.resourceTrickle = {
560                         "interval": cmpResourceTrickle.GetTimer(),
561                         "rates": cmpResourceTrickle.GetRates()
562                 };
564         let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
565         if (cmpUnitMotion)
566                 ret.speed = {
567                         "walk": cmpUnitMotion.GetWalkSpeed(),
568                         "run": cmpUnitMotion.GetRunSpeed()
569                 };
571         return ret;
574 GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
576         return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
579 GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
581         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
582         let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
584         let rot = { "x": 0, "y": 0, "z": 0 };
585         let pos = {
586                 "x": cmd.x,
587                 "y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
588                 "z": cmd.z
589         };
591         let elevationBonus = cmd.elevationBonus || 0;
592         let range = cmd.range;
594         return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
597 GuiInterface.prototype.GetTemplateData = function(player, templateName)
599         let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
600         let template = cmpTemplateManager.GetTemplate(templateName);
602         if (!template)
603                 return null;
605         let aurasTemplate = {};
607         if (!template.Auras)
608                 return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes);
610         let auraNames = template.Auras._string.split(/\s+/);
612         for (let name of auraNames)
613                 aurasTemplate[name] = AuraTemplates.Get(name);
615         return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes);
618 GuiInterface.prototype.IsTechnologyResearched = function(player, data)
620         if (!data.tech)
621                 return true;
623         let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
625         if (!cmpTechnologyManager)
626                 return false;
628         return cmpTechnologyManager.IsTechnologyResearched(data.tech);
631 // Checks whether the requirements for this technology have been met
632 GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
634         let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
636         if (!cmpTechnologyManager)
637                 return false;
639         return cmpTechnologyManager.CanResearch(data.tech);
642 // Returns technologies that are being actively researched, along with
643 // which entity is researching them and how far along the research is.
644 GuiInterface.prototype.GetStartedResearch = function(player)
646         let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
647         if (!cmpTechnologyManager)
648                 return {};
650         let ret = {};
651         for (let tech of cmpTechnologyManager.GetStartedTechs())
652         {
653                 ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
654                 let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
655                 if (cmpProductionQueue)
656                         ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
657                 else
658                         ret[tech].progress = 0;
659         }
660         return ret;
663 // Returns the battle state of the player.
664 GuiInterface.prototype.GetBattleState = function(player)
666         let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
668         if (!cmpBattleDetection)
669                 return false;
671         return cmpBattleDetection.GetState();
674 // Returns a list of ongoing attacks against the player.
675 GuiInterface.prototype.GetIncomingAttacks = function(player)
677         return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks();
680 // Used to show a red square over GUI elements you can't yet afford.
681 GuiInterface.prototype.GetNeededResources = function(player, data)
683         return QueryPlayerIDInterface(data.player || player).GetNeededResources(data.cost);
687  * Add a timed notification.
688  * Warning: timed notifacations are serialised
689  * (to also display them on saved games or after a rejoin)
690  * so they should allways be added and deleted in a deterministic way.
691  */
692 GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
694         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
695         notification.endTime = duration + cmpTimer.GetTime();
696         notification.id = ++this.timeNotificationID;
698         // Let all players and observers receive the notification by default
699         if (!notification.players)
700         {
701                 notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
702                 notification.players[0] = -1;
703         }
705         this.timeNotifications.push(notification);
706         this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
708         cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
710         return this.timeNotificationID;
713 GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
715         this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
718 GuiInterface.prototype.GetTimeNotifications = function(player)
720         let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
721         // filter on players and time, since the delete timer might be executed with a delay
722         return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
725 GuiInterface.prototype.PushNotification = function(notification)
727         if (!notification.type || notification.type == "text")
728                 this.AddTimeNotification(notification);
729         else
730                 this.notifications.push(notification);
733 GuiInterface.prototype.GetNotifications = function()
735         let n = this.notifications;
736         this.notifications = [];
737         return n;
740 GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
742         return QueryPlayerIDInterface(wantedPlayer).GetFormations();
745 GuiInterface.prototype.GetFormationRequirements = function(player, data)
747         return GetFormationRequirements(data.formationTemplate);
750 GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
752         return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
755 GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
757         let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
758         let template = cmpTemplateManager.GetTemplate(data.templateName);
760         if (!template || !template.Formation)
761                 return {};
763         return {
764                 "name": template.Formation.FormationName,
765                 "tooltip": template.Formation.DisabledTooltip || "",
766                 "icon": template.Formation.Icon
767         };
770 GuiInterface.prototype.IsFormationSelected = function(player, data)
772         for (let ent of data.ents)
773         {
774                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
775                 // GetLastFormationName is named in a strange way as it (also) is
776                 // the value of the current formation (see Formation.js LoadFormation)
777                 if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate)
778                         return true;
779         }
780         return false;
783 GuiInterface.prototype.IsStanceSelected = function(player, data)
785         for (let ent of data.ents)
786         {
787                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
788                 if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
789                         return true;
790         }
791         return false;
794 GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
796         let buildableEnts = [];
797         for (let ent of cmd.entities)
798         {
799                 let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
800                 if (!cmpBuilder)
801                         continue;
803                 for (let building of cmpBuilder.GetEntitiesList())
804                         if (buildableEnts.indexOf(building) == -1)
805                                 buildableEnts.push(building);
806         }
807         return buildableEnts;
811  * Updates player colors on the minimap.
812  */
813 GuiInterface.prototype.UpdateDisplayedPlayerColors = function()
815         for (let ent of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetGaiaAndNonGaiaEntities())
816         {
817                 let cmpMinimap = Engine.QueryInterface(ent, IID_Minimap);
818                 if (cmpMinimap)
819                         cmpMinimap.UpdateColor();
820         }
823 GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
825         let playerColors = {}; // cache of owner -> color map
827         for (let ent of cmd.entities)
828         {
829                 let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
830                 if (!cmpSelectable)
831                         continue;
833                 // Find the entity's owner's color:
834                 let owner = -1;
835                 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
836                 if (cmpOwnership)
837                         owner = cmpOwnership.GetOwner();
839                 let color = playerColors[owner];
840                 if (!color)
841                 {
842                         color = { "r":1, "g":1, "b":1 };
843                         let cmpPlayer = QueryPlayerIDInterface(owner);
844                         if (cmpPlayer)
845                                 color = cmpPlayer.GetColor();
846                         playerColors[owner] = color;
847                 }
849                 cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
851                 let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
852                 if (!cmpRangeOverlayManager || player != owner && player != -1)
853                         continue;
855                 cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
856         }
859 GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
861         this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
864 GuiInterface.prototype.GetEntitiesWithStatusBars = function()
866         return Array.from(this.entsWithAuraAndStatusBars);
869 GuiInterface.prototype.SetStatusBars = function(player, cmd)
871         let affectedEnts = new Set();
872         for (let ent of cmd.entities)
873         {
874                 let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
875                 if (!cmpStatusBars)
876                         continue;
877                 cmpStatusBars.SetEnabled(cmd.enabled);
879                 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
880                 if (!cmpAuras)
881                         continue;
883                 for (let name of cmpAuras.GetAuraNames())
884                 {
885                         if (!cmpAuras.GetOverlayIcon(name))
886                                 continue;
887                         for (let e of cmpAuras.GetAffectedEntities(name))
888                                 affectedEnts.add(e);
889                         if (cmd.enabled)
890                                 this.entsWithAuraAndStatusBars.add(ent);
891                         else
892                                 this.entsWithAuraAndStatusBars.delete(ent);
893                 }
894         }
896         for (let ent of affectedEnts)
897         {
898                 let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
899                 if (cmpStatusBars)
900                         cmpStatusBars.RegenerateSprites();
901         }
904 GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
906         for (let ent of cmd.entities)
907         {
908                 let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
909                 if (cmpRangeOverlayManager)
910                         cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
911         }
914 GuiInterface.prototype.GetPlayerEntities = function(player)
916         return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
919 GuiInterface.prototype.GetNonGaiaEntities = function()
921     return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
925  * Displays the rally points of a given list of entities (carried in cmd.entities).
927  * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
928  * be rendered, in order to support instantaneously rendering a rally point marker at a specified location
929  * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
930  * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
931  * RallyPoint component.
932  */
933 GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
935         let cmpPlayer = QueryPlayerIDInterface(player);
937         // If there are some rally points already displayed, first hide them
938         for (let ent of this.entsRallyPointsDisplayed)
939         {
940                 let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
941                 if (cmpRallyPointRenderer)
942                         cmpRallyPointRenderer.SetDisplayed(false);
943         }
945         this.entsRallyPointsDisplayed = [];
947         // Show the rally points for the passed entities
948         for (let ent of cmd.entities)
949         {
950                 let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
951                 if (!cmpRallyPointRenderer)
952                         continue;
954                 // entity must have a rally point component to display a rally point marker
955                 // (regardless of whether cmd specifies a custom location)
956                 let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
957                 if (!cmpRallyPoint)
958                         continue;
960                 // Verify the owner
961                 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
962                 if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
963                         if (!cmpOwnership || cmpOwnership.GetOwner() != player)
964                                 continue;
966                 // If the command was passed an explicit position, use that and
967                 // override the real rally point position; otherwise use the real position
968                 let pos;
969                 if (cmd.x && cmd.z)
970                         pos = cmd;
971                 else
972                         pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
974                 if (pos)
975                 {
976                         // Only update the position if we changed it (cmd.queued is set)
977                         if ("queued" in cmd)
978                                 if (cmd.queued == true)
979                                         cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
980                                 else
981                                         cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
983                         // rebuild the renderer when not set (when reading saved game or in case of building update)
984                         else if (!cmpRallyPointRenderer.IsSet())
985                                 for (let posi of cmpRallyPoint.GetPositions())
986                                         cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z });
988                         cmpRallyPointRenderer.SetDisplayed(true);
990                         // remember which entities have their rally points displayed so we can hide them again
991                         this.entsRallyPointsDisplayed.push(ent);
992                 }
993         }
996 GuiInterface.prototype.AddTargetMarker = function(player, cmd)
998         let ent = Engine.AddLocalEntity(cmd.template);
999         if (!ent)
1000                 return;
1002         let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1003         cmpPosition.JumpTo(cmd.x, cmd.z);
1007  * Display the building placement preview.
1008  * cmd.template is the name of the entity template, or "" to disable the preview.
1009  * cmd.x, cmd.z, cmd.angle give the location.
1011  * Returns result object from CheckPlacement:
1012  *      {
1013  *              "success":             true iff the placement is valid, else false
1014  *              "message":             message to display in UI for invalid placement, else ""
1015  *              "parameters":          parameters to use in the message
1016  *              "translateMessage":    localisation info
1017  *              "translateParameters": localisation info
1018  *              "pluralMessage":       we might return a plural translation instead (optional)
1019  *              "pluralCount":         localisation info (optional)
1020  *  }
1021  */
1022 GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
1024         let result = {
1025                 "success": false,
1026                 "message": "",
1027                 "parameters": {},
1028                 "translateMessage": false,
1029                 "translateParameters": [],
1030         };
1032         // See if we're changing template
1033         if (!this.placementEntity || this.placementEntity[0] != cmd.template)
1034         {
1035                 // Destroy the old preview if there was one
1036                 if (this.placementEntity)
1037                         Engine.DestroyEntity(this.placementEntity[1]);
1039                 // Load the new template
1040                 if (cmd.template == "")
1041                         this.placementEntity = undefined;
1042                 else
1043                         this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
1044         }
1046         if (this.placementEntity)
1047         {
1048                 let ent = this.placementEntity[1];
1050                 // Move the preview into the right location
1051                 let pos = Engine.QueryInterface(ent, IID_Position);
1052                 if (pos)
1053                 {
1054                         pos.JumpTo(cmd.x, cmd.z);
1055                         pos.SetYRotation(cmd.angle);
1056                 }
1058                 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
1059                 cmpOwnership.SetOwner(player);
1061                 // Check whether building placement is valid
1062                 let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
1063                 if (!cmpBuildRestrictions)
1064                         error("cmpBuildRestrictions not defined");
1065                 else
1066                         result = cmpBuildRestrictions.CheckPlacement();
1068                 let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
1069                 if (cmpRangeOverlayManager)
1070                         cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
1072                 // Set it to a red shade if this is an invalid location
1073                 let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
1074                 if (cmpVisual)
1075                 {
1076                         if (cmd.actorSeed !== undefined)
1077                                 cmpVisual.SetActorSeed(cmd.actorSeed);
1079                         if (!result.success)
1080                                 cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
1081                         else
1082                                 cmpVisual.SetShadingColor(1, 1, 1, 1);
1083                 }
1084         }
1086         return result;
1090  * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
1091  * specified. Returns an object with information about the list of entities that need to be newly constructed to complete
1092  * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
1093  * them can be validly constructed.
1095  * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
1096  * another depending on things like snapping and whether some of the entities inside them can be validly positioned.
1097  * We have:
1098  *    - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
1099  *      entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
1100  *      to preview the completed tower on top of its foundation.
1102  *    - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
1103  *      any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
1104  *      towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
1105  *      snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
1106  *      constructed.
1108  *    - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
1109  *      as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
1110  *      e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
1111  *      constructed but come after said first invalid entity are also truncated away.
1113  * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
1114  * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
1115  * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
1116  * argument (see below). Otherwise, it will return an object with the following information:
1118  * result: {
1119  *   'startSnappedEnt':   ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
1120  *   'endSnappedEnt':     ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
1121  *                        can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
1122  *                        but the wall construction was truncated before we could reach it, it won't be set here. Currently only
1123  *                        supports towers.
1124  *   'pieces':            Array with the following data for each of the entities in the third list:
1125  *    [{
1126  *       'template':      Template name of the entity.
1127  *       'x':             X coordinate of the entity's position.
1128  *       'z':             Z coordinate of the entity's position.
1129  *       'angle':         Rotation around the Y axis of the entity (in radians).
1130  *     },
1131  *     ...]
1132  *   'cost': {            The total cost required for constructing all the pieces as listed above.
1133  *     'food': ...,
1134  *     'wood': ...,
1135  *     'stone': ...,
1136  *     'metal': ...,
1137  *     'population': ...,
1138  *     'populationBonus': ...,
1139  *   }
1140  * }
1142  * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
1143  * @param cmd.start Starting point of the wall segment being created.
1144  * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
1145  *                 the starting point of the wall is available at this time (e.g. while the player is still in the process
1146  *                 of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
1147  *                 previewed.
1148  * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
1149  */
1150 GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
1152         let wallSet = cmd.wallSet;
1154         let start = {
1155                 "pos": cmd.start,
1156                 "angle": 0,
1157                 "snapped": false,                       // did the start position snap to anything?
1158                 "snappedEnt": INVALID_ENTITY,           // if we snapped, was it to an entity? if yes, holds that entity's ID
1159         };
1161         let end = {
1162                 "pos": cmd.end,
1163                 "angle": 0,
1164                 "snapped": false,                       // did the start position snap to anything?
1165                 "snappedEnt": INVALID_ENTITY,           // if we snapped, was it to an entity? if yes, holds that entity's ID
1166         };
1168         // --------------------------------------------------------------------------------
1169         // do some entity cache management and check for snapping
1171         if (!this.placementWallEntities)
1172                 this.placementWallEntities = {};
1174         if (!wallSet)
1175         {
1176                 // we're clearing the preview, clear the entity cache and bail
1177                 for (let tpl in this.placementWallEntities)
1178                 {
1179                         for (let ent of this.placementWallEntities[tpl].entities)
1180                                 Engine.DestroyEntity(ent);
1182                         this.placementWallEntities[tpl].numUsed = 0;
1183                         this.placementWallEntities[tpl].entities = [];
1184                         // keep template data around
1185                 }
1187                 return false;
1188         }
1190         // Move all existing cached entities outside of the world and reset their use count
1191         for (let tpl in this.placementWallEntities)
1192         {
1193                 for (let ent of this.placementWallEntities[tpl].entities)
1194                 {
1195                         let pos = Engine.QueryInterface(ent, IID_Position);
1196                         if (pos)
1197                                 pos.MoveOutOfWorld();
1198                 }
1200                 this.placementWallEntities[tpl].numUsed = 0;
1201         }
1203         // Create cache entries for templates we haven't seen before
1204         for (let type in wallSet.templates)
1205         {
1206                 if (type == "curves")
1207                         continue;
1209                 let tpl = wallSet.templates[type];
1210                 if (!(tpl in this.placementWallEntities))
1211                 {
1212                         this.placementWallEntities[tpl] = {
1213                                 "numUsed": 0,
1214                                 "entities": [],
1215                                 "templateData": this.GetTemplateData(player, tpl),
1216                         };
1218                         // ensure that the loaded template data contains a wallPiece component
1219                         if (!this.placementWallEntities[tpl].templateData.wallPiece)
1220                         {
1221                                 error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
1222                                 return false;
1223                         }
1224                 }
1225         }
1227         // prevent division by zero errors further on if the start and end positions are the same
1228         if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
1229                 end.pos = undefined;
1231         // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
1232         // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
1233         // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
1234         if (cmd.snapEntities)
1235         {
1236                 let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
1237                 let startSnapData = this.GetFoundationSnapData(player, {
1238                         "x": start.pos.x,
1239                         "z": start.pos.z,
1240                         "template": wallSet.templates.tower,
1241                         "snapEntities": cmd.snapEntities,
1242                         "snapRadius": snapRadius,
1243                 });
1245                 if (startSnapData)
1246                 {
1247                         start.pos.x = startSnapData.x;
1248                         start.pos.z = startSnapData.z;
1249                         start.angle = startSnapData.angle;
1250                         start.snapped = true;
1252                         if (startSnapData.ent)
1253                                 start.snappedEnt = startSnapData.ent;
1254                 }
1256                 if (end.pos)
1257                 {
1258                         let endSnapData = this.GetFoundationSnapData(player, {
1259                                 "x": end.pos.x,
1260                                 "z": end.pos.z,
1261                                 "template": wallSet.templates.tower,
1262                                 "snapEntities": cmd.snapEntities,
1263                                 "snapRadius": snapRadius,
1264                         });
1266                         if (endSnapData)
1267                         {
1268                                 end.pos.x = endSnapData.x;
1269                                 end.pos.z = endSnapData.z;
1270                                 end.angle = endSnapData.angle;
1271                                 end.snapped = true;
1273                                 if (endSnapData.ent)
1274                                         end.snappedEnt = endSnapData.ent;
1275                         }
1276                 }
1277         }
1279         // clear the single-building preview entity (we'll be rolling our own)
1280         this.SetBuildingPlacementPreview(player, { "template": "" });
1282         // --------------------------------------------------------------------------------
1283         // calculate wall placement and position preview entities
1285         let result = {
1286                 "pieces": [],
1287                 "cost": { "population": 0, "populationBonus": 0, "time": 0 },
1288         };
1289         for (let res of Resources.GetCodes())
1290                 result.cost[res] = 0;
1292         let previewEntities = [];
1293         if (end.pos)
1294                 previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
1296         // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
1297         // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
1298         // an issue, because all preview entities have their obstruction components deactivated, meaning that their
1299         // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
1300         // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
1302         // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
1303         // flag set), which is what we want. The only exception to this is when snapping to existing towers (or
1304         // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
1305         // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
1306         // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
1307         // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
1308         // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
1310         // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
1311         // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
1312         // by the foundation it snaps to.
1314         if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
1315         {
1316                 let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
1317                 if (previewEntities.length > 0 && startEntObstruction)
1318                         previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
1320                 // if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
1321                 let startEntState = this.GetEntityState(player, start.snappedEnt);
1322                 if (startEntState.foundation)
1323                 {
1324                         let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
1325                         if (cmpPosition)
1326                                 previewEntities.unshift({
1327                                         "template": wallSet.templates.tower,
1328                                         "pos": start.pos,
1329                                         "angle": cmpPosition.GetRotation().y,
1330                                         "controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
1331                                         "excludeFromResult": true, // preview only, must not appear in the result
1332                                 });
1333                 }
1334         }
1335         else
1336         {
1337                 // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
1338                 // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
1339                 // wall piece.
1341                 // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
1342                 // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
1343                 // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
1344                 // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
1345                 // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
1346                 // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
1347                 // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
1348                 // the foundation's angle.
1350                 // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
1351                 // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
1352                 previewEntities.unshift({
1353                         "template": wallSet.templates.tower,
1354                         "pos": start.pos,
1355                         "angle": previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle
1356                 });
1357         }
1359         if (end.pos)
1360         {
1361                 // Analogous to the starting side case above
1362                 if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
1363                 {
1364                         let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
1366                         // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
1367                         // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
1368                         // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
1369                         // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
1370                         // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
1371                         if (previewEntities.length > 0 && endEntObstruction)
1372                         {
1373                                 previewEntities[previewEntities.length-1].controlGroups = previewEntities[previewEntities.length-1].controlGroups || [];
1374                                 previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup());
1375                         }
1377                         // if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
1378                         let endEntState = this.GetEntityState(player, end.snappedEnt);
1379                         if (endEntState.foundation)
1380                         {
1381                                 let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
1382                                 if (cmpPosition)
1383                                         previewEntities.push({
1384                                                 "template": wallSet.templates.tower,
1385                                                 "pos": end.pos,
1386                                                 "angle": cmpPosition.GetRotation().y,
1387                                                 "controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
1388                                                 "excludeFromResult": true
1389                                         });
1390                         }
1391                 }
1392                 else
1393                         previewEntities.push({
1394                                 "template": wallSet.templates.tower,
1395                                 "pos": end.pos,
1396                                 "angle": previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle
1397                         });
1398         }
1400         let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
1401         if (!cmpTerrain)
1402         {
1403                 error("[SetWallPlacementPreview] System Terrain component not found");
1404                 return false;
1405         }
1407         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1408         if (!cmpRangeManager)
1409         {
1410                 error("[SetWallPlacementPreview] System RangeManager component not found");
1411                 return false;
1412         }
1414         // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
1415         // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
1416         // but cannot validly be, constructed). See method-level documentation for more details.
1418         let allPiecesValid = true;
1419         let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
1421         for (let i = 0; i < previewEntities.length; ++i)
1422         {
1423                 let entInfo = previewEntities[i];
1425                 let ent = null;
1426                 let tpl = entInfo.template;
1427                 let tplData = this.placementWallEntities[tpl].templateData;
1428                 let entPool = this.placementWallEntities[tpl];
1430                 if (entPool.numUsed >= entPool.entities.length)
1431                 {
1432                         // allocate new entity
1433                         ent = Engine.AddLocalEntity("preview|" + tpl);
1434                         entPool.entities.push(ent);
1435                 }
1436                 else
1437                          // reuse an existing one
1438                         ent = entPool.entities[entPool.numUsed];
1440                 if (!ent)
1441                 {
1442                         error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
1443                         continue;
1444                 }
1446                 // move piece to right location
1447                 // TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities
1448                 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1449                 if (cmpPosition)
1450                 {
1451                         cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
1452                         cmpPosition.SetYRotation(entInfo.angle);
1454                         // if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces
1455                         if (tpl === wallSet.templates.tower)
1456                         {
1457                                 let terrainGroundPrev = null;
1458                                 let terrainGroundNext = null;
1460                                 if (i > 0)
1461                                         terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z);
1463                                 if (i < previewEntities.length - 1)
1464                                         terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z);
1466                                 if (terrainGroundPrev != null || terrainGroundNext != null)
1467                                 {
1468                                         let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
1469                                         cmpPosition.SetHeightFixed(targetY);
1470                                 }
1471                         }
1472                 }
1474                 let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
1475                 if (!cmpObstruction)
1476                 {
1477                         error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
1478                         continue;
1479                 }
1481                 // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
1482                 // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
1483                 // first-come first-served basis; the first value in the array is always assigned as the primary control group, and
1484                 // any second value as the secondary control group.
1486                 // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
1487                 // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
1488                 // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
1489                 // once snapped to.
1491                 let primaryControlGroup = ent;
1492                 let secondaryControlGroup = INVALID_ENTITY;
1494                 if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
1495                 {
1496                         if (entInfo.controlGroups.length > 2)
1497                         {
1498                                 error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
1499                                 break;
1500                         }
1502                         primaryControlGroup = entInfo.controlGroups[0];
1503                         if (entInfo.controlGroups.length > 1)
1504                                 secondaryControlGroup = entInfo.controlGroups[1];
1505                 }
1507                 cmpObstruction.SetControlGroup(primaryControlGroup);
1508                 cmpObstruction.SetControlGroup2(secondaryControlGroup);
1510                 // check whether this wall piece can be validly positioned here
1511                 let validPlacement = false;
1513                 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
1514                 cmpOwnership.SetOwner(player);
1516                 // Check whether it's in a visible or fogged region
1517                 // TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
1518                 let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
1519                 if (visible)
1520                 {
1521                         let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
1522                         if (!cmpBuildRestrictions)
1523                         {
1524                                 error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
1525                                 continue;
1526                         }
1528                         // TODO: Handle results of CheckPlacement
1529                         validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
1531                         // If a wall piece has two control groups, it's likely a segment that spans
1532                         // between two existing towers. To avoid placing a duplicate wall segment,
1533                         // check for collisions with entities that share both control groups.
1534                         if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
1535                                 validPlacement = cmpObstruction.CheckDuplicateFoundation();
1536                 }
1538                 allPiecesValid = allPiecesValid && validPlacement;
1540                 // The requirement below that all pieces so far have to have valid positions, rather than only this single one,
1541                 // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
1542                 // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
1543                 // through and past an existing building).
1545                 // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
1546                 // on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
1548                 if (!entInfo.excludeFromResult)
1549                         ++numRequiredPieces;
1551                 if (allPiecesValid && !entInfo.excludeFromResult)
1552                 {
1553                         result.pieces.push({
1554                                 "template": tpl,
1555                                 "x": entInfo.pos.x,
1556                                 "z": entInfo.pos.z,
1557                                 "angle": entInfo.angle,
1558                         });
1559                         this.placementWallLastAngle = entInfo.angle;
1561                         // grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
1562                         // copied over, so we need to fetch it from the template instead).
1563                         // TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
1564                         // boilerplate that's probably duplicated in tons of places.
1565                         for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"]))
1566                                 result.cost[res] += tplData.cost[res];
1567                 }
1569                 let canAfford = true;
1570                 let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
1571                 if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
1572                         canAfford = false;
1574                 let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
1575                 if (cmpVisual)
1576                 {
1577                         if (!allPiecesValid || !canAfford)
1578                                 cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
1579                         else
1580                                 cmpVisual.SetShadingColor(1, 1, 1, 1);
1581                 }
1583                 ++entPool.numUsed;
1584         }
1586         // If any were entities required to build the wall, but none of them could be validly positioned, return failure
1587         // (see method-level documentation).
1588         if (numRequiredPieces > 0 && result.pieces.length == 0)
1589                 return false;
1591         if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
1592                 result.startSnappedEnt = start.snappedEnt;
1594         // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
1595         // i.e. are included in result.pieces (see docs for the result object).
1596         if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
1597                 result.endSnappedEnt = end.snappedEnt;
1599         return result;
1603  * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
1604  * it to (if necessary/useful).
1606  * @param data.x            The X position of the foundation to snap.
1607  * @param data.z            The Z position of the foundation to snap.
1608  * @param data.template     The template to get the foundation snapping data for.
1609  * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
1610  *                            around the entity. Only takes effect when used in conjunction with data.snapRadius.
1611  *                          When this option is used and the foundation is found to snap to one of the entities passed in this list
1612  *                            (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
1613  *                            holding the ID of the entity that was snapped to.
1614  * @param data.snapRadius   Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
1615  *                            {data.x, data.z} must be located within to have it snap to that entity.
1616  */
1617 GuiInterface.prototype.GetFoundationSnapData = function(player, data)
1619         let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
1620         if (!template)
1621         {
1622                 warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
1623                 return false;
1624         }
1626         if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
1627         {
1628                 // see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest
1629                 // (TODO: break unlikely ties by choosing the lowest entity ID)
1631                 let minDist2 = -1;
1632                 let minDistEntitySnapData = null;
1633                 let radius2 = data.snapRadius * data.snapRadius;
1635                 for (let ent of data.snapEntities)
1636                 {
1637                         let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1638                         if (!cmpPosition || !cmpPosition.IsInWorld())
1639                                 continue;
1641                         let pos = cmpPosition.GetPosition();
1642                         let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
1643                         if (dist2 > radius2)
1644                                 continue;
1646                         if (minDist2 < 0 || dist2 < minDist2)
1647                         {
1648                                 minDist2 = dist2;
1649                                 minDistEntitySnapData = {
1650                                                 "x": pos.x,
1651                                                 "z": pos.z,
1652                                                 "angle": cmpPosition.GetRotation().y,
1653                                                 "ent": ent
1654                                 };
1655                         }
1656                 }
1658                 if (minDistEntitySnapData != null)
1659                         return minDistEntitySnapData;
1660         }
1662         if (template.BuildRestrictions.PlacementType == "shore")
1663         {
1664                 let angle = GetDockAngle(template, data.x, data.z);
1665                 if (angle !== undefined)
1666                         return {
1667                                 "x": data.x,
1668                                 "z": data.z,
1669                                 "angle": angle
1670                         };
1671         }
1673         return false;
1676 GuiInterface.prototype.PlaySound = function(player, data)
1678         if (!data.entity)
1679                 return;
1681         PlaySound(data.name, data.entity);
1685  * Find any idle units.
1687  * @param data.idleClasses              Array of class names to include.
1688  * @param data.prevUnit         The previous idle unit, if calling a second time to iterate through units.  May be left undefined.
1689  * @param data.limit                    The number of idle units to return.  May be left undefined (will return all idle units).
1690  * @param data.excludeUnits     Array of units to exclude.
1692  * Returns an array of idle units.
1693  * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
1694  */
1695 GuiInterface.prototype.FindIdleUnits = function(player, data)
1697         let idleUnits = [];
1698         // The general case is that only the 'first' idle unit is required; filtering would examine every unit.
1699         // This loop imitates a grouping/aggregation on the first matching idle class.
1700         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1701         for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
1702         {
1703                 let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
1704                 if (!filtered.idle)
1705                         continue;
1707                 // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
1708                 // By adding to the 'end', there is no pause if the series of units loops.
1709                 var bucket = filtered.bucket;
1710                 if(bucket == 0 && data.prevUnit && entity <= data.prevUnit)
1711                         bucket = data.idleClasses.length;
1713                 if (!idleUnits[bucket])
1714                         idleUnits[bucket] = [];
1715                 idleUnits[bucket].push(entity);
1717                 // If enough units have been collected in the first bucket, go ahead and return them.
1718                 if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
1719                         return idleUnits[0];
1720         }
1722         let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
1723         if (data.limit && reduced.length > data.limit)
1724                 return reduced.slice(0, data.limit);
1726         return reduced;
1730  * Discover if the player has idle units.
1732  * @param data.idleClasses      Array of class names to include.
1733  * @param data.excludeUnits     Array of units to exclude.
1735  * Returns a boolean of whether the player has any idle units
1736  */
1737 GuiInterface.prototype.HasIdleUnits = function(player, data)
1739         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1740         return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
1744  * Whether to filter an idle unit
1746  * @param unit                  The unit to filter.
1747  * @param idleclasses   Array of class names to include.
1748  * @param excludeUnits  Array of units to exclude.
1750  * Returns an object with the following fields:
1751  *      - idle - true if the unit is considered idle by the filter, false otherwise.
1752  *      - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
1753  */
1754 GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
1756         let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
1757         if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
1758                 return { "idle": false };
1760         let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
1761         if(!cmpIdentity)
1762                 return { "idle": false };
1764         let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
1765         if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
1766                 return { "idle": false };
1768         return { "idle": true, "bucket": bucket };
1771 GuiInterface.prototype.GetTradingRouteGain = function(player, data)
1773         if (!data.firstMarket || !data.secondMarket)
1774                 return null;
1776         return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
1779 GuiInterface.prototype.GetTradingDetails = function(player, data)
1781         let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
1782         if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
1783                 return null;
1785         let firstMarket = cmpEntityTrader.GetFirstMarket();
1786         let secondMarket = cmpEntityTrader.GetSecondMarket();
1787         let result = null;
1788         if (data.target === firstMarket)
1789         {
1790                 result = {
1791                         "type": "is first",
1792                         "hasBothMarkets": cmpEntityTrader.HasBothMarkets()
1793                 };
1794                 if (cmpEntityTrader.HasBothMarkets())
1795                         result.gain = cmpEntityTrader.GetGoods().amount;
1796         }
1797         else if (data.target === secondMarket)
1798         {
1799                 result = {
1800                         "type": "is second",
1801                         "gain": cmpEntityTrader.GetGoods().amount,
1802                 };
1803         }
1804         else if (!firstMarket)
1805         {
1806                 result = { "type": "set first" };
1807         }
1808         else if (!secondMarket)
1809         {
1810                 result = {
1811                         "type": "set second",
1812                         "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
1813                 };
1814         }
1815         else
1816         {
1817                 // Else both markets are not null and target is different from them
1818                 result = { "type": "set first" };
1819         }
1820         return result;
1823 GuiInterface.prototype.CanAttack = function(player, data)
1825         let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
1826         return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
1830  * Returns batch build time.
1831  */
1832 GuiInterface.prototype.GetBatchTime = function(player, data)
1834         let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
1835         if (!cmpProductionQueue)
1836                 return 0;
1838         return cmpProductionQueue.GetBatchTime(data.batchSize);
1841 GuiInterface.prototype.IsMapRevealed = function(player)
1843         return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
1846 GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
1848         Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
1851 GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
1853         Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
1856 GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
1858         Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
1861 GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
1863         for (let ent of data.entities)
1864         {
1865                 let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
1866                 if (cmpUnitMotion)
1867                         cmpUnitMotion.SetDebugOverlay(data.enabled);
1868         }
1871 GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
1873         Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
1876 GuiInterface.prototype.GetTraderNumber = function(player)
1878         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1879         let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
1881         let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
1882         let shipTrader = { "total": 0, "trading": 0 };
1884         for (let ent of traders)
1885         {
1886                 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
1887                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
1888                 if (!cmpIdentity || !cmpUnitAI)
1889                         continue;
1891                 if (cmpIdentity.HasClass("Ship"))
1892                 {
1893                         ++shipTrader.total;
1894                         if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
1895                                 ++shipTrader.trading;
1896                 }
1897                 else
1898                 {
1899                         ++landTrader.total;
1900                         if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
1901                                 ++landTrader.trading;
1902                         if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
1903                         {
1904                                 let holder = cmpUnitAI.order.data.target;
1905                                 let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
1906                                 if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
1907                                         ++landTrader.garrisoned;
1908                         }
1909                 }
1910         }
1912         return { "landTrader": landTrader, "shipTrader": shipTrader };
1915 GuiInterface.prototype.GetTradingGoods = function(player)
1917         return QueryPlayerIDInterface(player).GetTradingGoods();
1920 GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
1922         this.renamedEntities.push(msg);
1925 // List the GuiInterface functions that can be safely called by GUI scripts.
1926 // (GUI scripts are non-deterministic and untrusted, so these functions must be
1927 // appropriately careful. They are called with a first argument "player", which is
1928 // trusted and indicates the player associated with the current client; no data should
1929 // be returned unless this player is meant to be able to see it.)
1930 let exposedFunctions = {
1932         "GetSimulationState": 1,
1933         "GetExtendedSimulationState": 1,
1934         "GetRenamedEntities": 1,
1935         "ClearRenamedEntities": 1,
1936         "GetEntityState": 1,
1937         "GetMultipleEntityStates": 1,
1938         "GetAverageRangeForBuildings": 1,
1939         "GetTemplateData": 1,
1940         "IsTechnologyResearched": 1,
1941         "CheckTechnologyRequirements": 1,
1942         "GetStartedResearch": 1,
1943         "GetBattleState": 1,
1944         "GetIncomingAttacks": 1,
1945         "GetNeededResources": 1,
1946         "GetNotifications": 1,
1947         "GetTimeNotifications": 1,
1949         "GetAvailableFormations": 1,
1950         "GetFormationRequirements": 1,
1951         "CanMoveEntsIntoFormation": 1,
1952         "IsFormationSelected": 1,
1953         "GetFormationInfoFromTemplate": 1,
1954         "IsStanceSelected": 1,
1956         "UpdateDisplayedPlayerColors": 1,
1957         "SetSelectionHighlight": 1,
1958         "GetAllBuildableEntities": 1,
1959         "SetStatusBars": 1,
1960         "GetPlayerEntities": 1,
1961         "GetNonGaiaEntities": 1,
1962         "DisplayRallyPoint": 1,
1963         "AddTargetMarker": 1,
1964         "SetBuildingPlacementPreview": 1,
1965         "SetWallPlacementPreview": 1,
1966         "GetFoundationSnapData": 1,
1967         "PlaySound": 1,
1968         "FindIdleUnits": 1,
1969         "HasIdleUnits": 1,
1970         "GetTradingRouteGain": 1,
1971         "GetTradingDetails": 1,
1972         "CanAttack": 1,
1973         "GetBatchTime": 1,
1975         "IsMapRevealed": 1,
1976         "SetPathfinderDebugOverlay": 1,
1977         "SetPathfinderHierDebugOverlay": 1,
1978         "SetObstructionDebugOverlay": 1,
1979         "SetMotionDebugOverlay": 1,
1980         "SetRangeDebugOverlay": 1,
1981         "EnableVisualRangeOverlayType": 1,
1982         "SetRangeOverlays": 1,
1984         "GetTraderNumber": 1,
1985         "GetTradingGoods": 1,
1988 GuiInterface.prototype.ScriptCall = function(player, name, args)
1990         if (exposedFunctions[name])
1991                 return this[name](player, args);
1993         throw new Error("Invalid GuiInterface Call name \""+name+"\"");
1996 Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);