Line data Source code
1 : function GuiInterface() {}
2 :
3 1 : GuiInterface.prototype.Schema =
4 : "<a:component type='system'/><empty/>";
5 :
6 1 : GuiInterface.prototype.Serialize = function()
7 : {
8 : // This component isn't network-synchronized 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 0 : return {
12 : "timeNotifications": this.timeNotifications,
13 : "timeNotificationID": this.timeNotificationID
14 : };
15 : };
16 :
17 1 : GuiInterface.prototype.Deserialize = function(data)
18 : {
19 0 : this.Init();
20 0 : this.timeNotifications = data.timeNotifications;
21 0 : this.timeNotificationID = data.timeNotificationID;
22 : };
23 :
24 1 : GuiInterface.prototype.Init = function()
25 : {
26 1 : this.placementEntity = undefined; // = undefined or [templateName, entityID]
27 1 : this.placementWallEntities = undefined;
28 1 : this.placementWallLastAngle = 0;
29 1 : this.notifications = [];
30 1 : this.renamedEntities = [];
31 1 : this.miragedEntities = [];
32 1 : this.timeNotificationID = 1;
33 1 : this.timeNotifications = [];
34 1 : this.entsRallyPointsDisplayed = [];
35 1 : this.entsWithAuraAndStatusBars = new Set();
36 1 : this.enabledVisualRangeOverlayTypes = {};
37 1 : this.templateModified = {};
38 1 : this.selectionDirty = {};
39 1 : this.obstructionSnap = new ObstructionSnap();
40 : };
41 :
42 : /*
43 : * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
44 : * from GUI scripts, and executed here with arguments (player, arg).
45 : *
46 : * CAUTION: The input to the functions in this module is not network-synchronised, so it
47 : * mustn't affect the simulation state (i.e. the data that is serialised and can affect
48 : * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
49 : */
50 :
51 : /**
52 : * Returns global information about the current game state.
53 : * This is used by the GUI and also by AI scripts.
54 : */
55 1 : GuiInterface.prototype.GetSimulationState = function()
56 : {
57 2 : let ret = {
58 : "players": []
59 : };
60 :
61 2 : let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
62 2 : let numPlayers = cmpPlayerManager.GetNumPlayers();
63 2 : for (let i = 0; i < numPlayers; ++i)
64 : {
65 4 : const playerEnt = cmpPlayerManager.GetPlayerByID(i);
66 4 : const cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
67 4 : const cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits);
68 4 : const cmpIdentity = Engine.QueryInterface(playerEnt, IID_Identity);
69 :
70 : // Work out which phase we are in.
71 4 : let phase = "";
72 4 : const cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
73 4 : if (cmpTechnologyManager)
74 : {
75 4 : if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
76 0 : phase = "city";
77 4 : else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
78 0 : phase = "town";
79 4 : else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
80 4 : phase = "village";
81 : }
82 :
83 4 : let allies = [];
84 4 : let mutualAllies = [];
85 4 : let neutrals = [];
86 4 : let enemies = [];
87 :
88 4 : for (let j = 0; j < numPlayers; ++j)
89 : {
90 8 : allies[j] = cmpPlayer.IsAlly(j);
91 8 : mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
92 8 : neutrals[j] = cmpPlayer.IsNeutral(j);
93 8 : enemies[j] = cmpPlayer.IsEnemy(j);
94 : }
95 :
96 4 : ret.players.push({
97 : "name": cmpIdentity.GetName(),
98 : "civ": cmpIdentity.GetCiv(),
99 : "color": cmpPlayer.GetColor(),
100 : "entity": cmpPlayer.entity,
101 : "controlsAll": cmpPlayer.CanControlAllUnits(),
102 : "popCount": cmpPlayer.GetPopulationCount(),
103 : "popLimit": cmpPlayer.GetPopulationLimit(),
104 : "popMax": cmpPlayer.GetMaxPopulation(),
105 : "panelEntities": cmpPlayer.GetPanelEntities(),
106 : "resourceCounts": cmpPlayer.GetResourceCounts(),
107 : "resourceGatherers": cmpPlayer.GetResourceGatherers(),
108 : "trainingBlocked": cmpPlayer.IsTrainingBlocked(),
109 : "state": cmpPlayer.GetState(),
110 : "team": cmpPlayer.GetTeam(),
111 : "teamsLocked": cmpPlayer.GetLockTeams(),
112 : "cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
113 : "disabledTemplates": cmpPlayer.GetDisabledTemplates(),
114 : "disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
115 : "hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
116 : "hasSharedLos": cmpPlayer.HasSharedLos(),
117 : "spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
118 : "phase": phase,
119 : "isAlly": allies,
120 : "isMutualAlly": mutualAllies,
121 : "isNeutral": neutrals,
122 : "isEnemy": enemies,
123 : "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
124 : "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
125 : "matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null,
126 : "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
127 : "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
128 : "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
129 : "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
130 : "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
131 : "canBarter": cmpPlayer.CanBarter(),
132 : "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer)
133 : });
134 : }
135 :
136 2 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
137 2 : if (cmpRangeManager)
138 2 : ret.circularMap = cmpRangeManager.GetLosCircular();
139 :
140 2 : let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
141 2 : if (cmpTerrain)
142 0 : ret.mapSize = cmpTerrain.GetMapSize();
143 :
144 2 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
145 2 : ret.timeElapsed = cmpTimer.GetTime();
146 :
147 2 : let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
148 2 : if (cmpCeasefireManager)
149 : {
150 0 : ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
151 0 : ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
152 : }
153 :
154 2 : let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
155 2 : if (cmpCinemaManager)
156 0 : ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
157 :
158 2 : let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
159 2 : ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
160 2 : ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
161 :
162 2 : ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation();
163 :
164 2 : for (let i = 0; i < numPlayers; ++i)
165 : {
166 4 : let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
167 4 : if (cmpPlayerStatisticsTracker)
168 4 : ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
169 : }
170 :
171 2 : return ret;
172 : };
173 :
174 : /**
175 : * Returns global information about the current game state, plus statistics.
176 : * This is used by the GUI at the end of a game, in the summary screen.
177 : * Note: Amongst statistics, the team exploration map percentage is computed from
178 : * scratch, so the extended simulation state should not be requested too often.
179 : */
180 1 : GuiInterface.prototype.GetExtendedSimulationState = function()
181 : {
182 1 : let ret = this.GetSimulationState();
183 :
184 1 : let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
185 1 : for (let i = 0; i < numPlayers; ++i)
186 : {
187 2 : let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
188 2 : if (cmpPlayerStatisticsTracker)
189 2 : ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
190 : }
191 :
192 1 : return ret;
193 : };
194 :
195 : /**
196 : * Returns the gamesettings that were chosen at the time the match started.
197 : */
198 1 : GuiInterface.prototype.GetInitAttributes = function()
199 : {
200 0 : return InitAttributes;
201 : };
202 :
203 : /**
204 : * This data will be stored in the replay metadata file after a match has been finished recording.
205 : */
206 1 : GuiInterface.prototype.GetReplayMetadata = function()
207 : {
208 0 : let extendedSimState = this.GetExtendedSimulationState();
209 0 : return {
210 : "timeElapsed": extendedSimState.timeElapsed,
211 : "playerStates": extendedSimState.players,
212 : "mapSettings": InitAttributes.settings
213 : };
214 : };
215 :
216 : /**
217 : * Called when the game ends if the current game is part of a campaign run.
218 : */
219 1 : GuiInterface.prototype.GetCampaignGameEndData = function(player)
220 : {
221 0 : let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
222 0 : if (Trigger.prototype.OnCampaignGameEnd)
223 0 : return Trigger.prototype.OnCampaignGameEnd();
224 0 : return {};
225 : };
226 :
227 1 : GuiInterface.prototype.GetRenamedEntities = function(player)
228 : {
229 0 : if (this.miragedEntities[player])
230 0 : return this.renamedEntities.concat(this.miragedEntities[player]);
231 :
232 0 : return this.renamedEntities;
233 : };
234 :
235 1 : GuiInterface.prototype.ClearRenamedEntities = function()
236 : {
237 0 : this.renamedEntities = [];
238 0 : this.miragedEntities = [];
239 : };
240 :
241 1 : GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
242 : {
243 0 : if (!this.miragedEntities[player])
244 0 : this.miragedEntities[player] = [];
245 :
246 0 : this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
247 : };
248 :
249 : /**
250 : * Get common entity info, often used in the gui.
251 : */
252 1 : GuiInterface.prototype.GetEntityState = function(player, ent)
253 : {
254 1 : if (!ent)
255 0 : return null;
256 :
257 : // All units must have a template; if not then it's a nonexistent entity id.
258 1 : const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(ent);
259 1 : if (!template)
260 0 : return null;
261 :
262 1 : const ret = {
263 : "id": ent,
264 : "player": INVALID_PLAYER,
265 : "template": template
266 : };
267 :
268 1 : const cmpAuras = Engine.QueryInterface(ent, IID_Auras);
269 1 : if (cmpAuras)
270 0 : ret.auras = cmpAuras.GetDescriptions();
271 :
272 1 : let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
273 1 : if (cmpMirage)
274 0 : ret.mirage = true;
275 :
276 1 : let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
277 1 : if (cmpIdentity)
278 1 : ret.identity = {
279 : "rank": cmpIdentity.GetRank(),
280 : "rankTechName": cmpIdentity.GetRankTechName(),
281 : "classes": cmpIdentity.GetClassesList(),
282 : "selectionGroupName": cmpIdentity.GetSelectionGroupName(),
283 : "canDelete": !cmpIdentity.IsUndeletable(),
284 : "controllable": cmpIdentity.IsControllable()
285 : };
286 :
287 1 : const cmpFormation = Engine.QueryInterface(ent, IID_Formation);
288 1 : if (cmpFormation)
289 0 : ret.formation = {
290 : "members": cmpFormation.GetMembers()
291 : };
292 :
293 1 : let cmpPosition = Engine.QueryInterface(ent, IID_Position);
294 1 : if (cmpPosition && cmpPosition.IsInWorld())
295 1 : ret.position = cmpPosition.GetPosition();
296 :
297 1 : let cmpHealth = QueryMiragedInterface(ent, IID_Health);
298 1 : if (cmpHealth)
299 : {
300 1 : ret.hitpoints = cmpHealth.GetHitpoints();
301 1 : ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
302 1 : ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured();
303 1 : ret.needsHeal = !cmpHealth.IsUnhealable();
304 : }
305 :
306 1 : let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
307 1 : if (cmpCapturable)
308 : {
309 0 : ret.capturePoints = cmpCapturable.GetCapturePoints();
310 0 : ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
311 : }
312 :
313 1 : let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
314 1 : if (cmpBuilder)
315 1 : ret.builder = true;
316 :
317 1 : let cmpMarket = QueryMiragedInterface(ent, IID_Market);
318 1 : if (cmpMarket)
319 0 : ret.market = {
320 : "land": cmpMarket.HasType("land"),
321 : "naval": cmpMarket.HasType("naval")
322 : };
323 :
324 1 : let cmpPack = Engine.QueryInterface(ent, IID_Pack);
325 1 : if (cmpPack)
326 0 : ret.pack = {
327 : "packed": cmpPack.IsPacked(),
328 : "progress": cmpPack.GetProgress()
329 : };
330 :
331 1 : let cmpPopulation = Engine.QueryInterface(ent, IID_Population);
332 1 : if (cmpPopulation)
333 0 : ret.population = {
334 : "bonus": cmpPopulation.GetPopBonus()
335 : };
336 :
337 1 : let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
338 1 : if (cmpUpgrade)
339 0 : ret.upgrade = {
340 : "upgrades": cmpUpgrade.GetUpgrades(),
341 : "progress": cmpUpgrade.GetProgress(),
342 : "template": cmpUpgrade.GetUpgradingTo(),
343 : "isUpgrading": cmpUpgrade.IsUpgrading()
344 : };
345 :
346 1 : const cmpResearcher = Engine.QueryInterface(ent, IID_Researcher);
347 1 : if (cmpResearcher)
348 0 : ret.researcher = {
349 : "technologies": cmpResearcher.GetTechnologiesList(),
350 : "techCostMultiplier": cmpResearcher.GetTechCostMultiplier()
351 : };
352 :
353 1 : let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
354 1 : if (cmpStatusEffects)
355 0 : ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
356 :
357 1 : let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
358 1 : if (cmpProductionQueue)
359 0 : ret.production = {
360 : "queue": cmpProductionQueue.GetQueue(),
361 : "autoqueue": cmpProductionQueue.IsAutoQueueing()
362 : };
363 :
364 1 : const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer);
365 1 : if (cmpTrainer)
366 0 : ret.trainer = {
367 : "entities": cmpTrainer.GetEntitiesList()
368 : };
369 :
370 1 : let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
371 1 : if (cmpTrader)
372 0 : ret.trader = {
373 : "goods": cmpTrader.GetGoods()
374 : };
375 :
376 1 : let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
377 1 : if (cmpFoundation)
378 0 : ret.foundation = {
379 : "numBuilders": cmpFoundation.GetNumBuilders(),
380 : "buildTime": cmpFoundation.GetBuildTime()
381 : };
382 :
383 1 : let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
384 1 : if (cmpRepairable)
385 0 : ret.repairable = {
386 : "numBuilders": cmpRepairable.GetNumBuilders(),
387 : "buildTime": cmpRepairable.GetBuildTime()
388 : };
389 :
390 1 : let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
391 1 : if (cmpOwnership)
392 0 : ret.player = cmpOwnership.GetOwner();
393 :
394 1 : let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
395 1 : if (cmpRallyPoint)
396 0 : ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
397 :
398 1 : let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
399 1 : if (cmpGarrisonHolder)
400 0 : ret.garrisonHolder = {
401 : "entities": cmpGarrisonHolder.GetEntities(),
402 : "buffHeal": cmpGarrisonHolder.GetHealRate(),
403 : "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
404 : "capacity": cmpGarrisonHolder.GetCapacity(),
405 : "occupiedSlots": cmpGarrisonHolder.OccupiedSlots()
406 : };
407 :
408 1 : let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
409 1 : if (cmpTurretHolder)
410 0 : ret.turretHolder = {
411 : "turretPoints": cmpTurretHolder.GetTurretPoints()
412 : };
413 :
414 1 : let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
415 1 : if (cmpTurretable)
416 0 : ret.turretable = {
417 : "ejectable": cmpTurretable.IsEjectable(),
418 : "holder": cmpTurretable.HolderID()
419 : };
420 :
421 1 : let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
422 1 : if (cmpGarrisonable)
423 0 : ret.garrisonable = {
424 : "holder": cmpGarrisonable.HolderID(),
425 : "size": cmpGarrisonable.UnitSize()
426 : };
427 :
428 1 : let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
429 1 : if (cmpUnitAI)
430 0 : ret.unitAI = {
431 : "state": cmpUnitAI.GetCurrentState(),
432 : "orders": cmpUnitAI.GetOrders(),
433 : "hasWorkOrders": cmpUnitAI.HasWorkOrders(),
434 : "canGuard": cmpUnitAI.CanGuard(),
435 : "isGuarding": cmpUnitAI.IsGuardOf(),
436 : "canPatrol": cmpUnitAI.CanPatrol(),
437 : "selectableStances": cmpUnitAI.GetSelectableStances(),
438 : "isIdle": cmpUnitAI.IsIdle(),
439 : "formations": cmpUnitAI.GetFormationsList(),
440 : "formation": cmpUnitAI.GetFormationController()
441 : };
442 :
443 1 : let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
444 1 : if (cmpGuard)
445 0 : ret.guard = {
446 : "entities": cmpGuard.GetEntities()
447 : };
448 :
449 1 : let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
450 1 : if (cmpResourceGatherer)
451 : {
452 0 : ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
453 0 : ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
454 : }
455 :
456 1 : let cmpGate = Engine.QueryInterface(ent, IID_Gate);
457 1 : if (cmpGate)
458 0 : ret.gate = {
459 : "locked": cmpGate.IsLocked()
460 : };
461 :
462 1 : let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
463 1 : if (cmpAlertRaiser)
464 0 : ret.alertRaiser = true;
465 :
466 1 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
467 1 : ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
468 :
469 1 : let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
470 1 : if (cmpAttack)
471 : {
472 0 : let types = cmpAttack.GetAttackTypes();
473 0 : if (types.length)
474 0 : ret.attack = {};
475 :
476 0 : for (let type of types)
477 : {
478 0 : ret.attack[type] = {};
479 :
480 0 : Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
481 :
482 0 : ret.attack[type].attackName = cmpAttack.GetAttackName(type);
483 :
484 0 : ret.attack[type].splash = cmpAttack.GetSplashData(type);
485 0 : if (ret.attack[type].splash)
486 0 : Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true));
487 :
488 0 : let range = cmpAttack.GetRange(type);
489 0 : ret.attack[type].minRange = range.min;
490 0 : ret.attack[type].maxRange = range.max;
491 0 : ret.attack[type].yOrigin = cmpAttack.GetAttackYOrigin(type);
492 :
493 0 : let timers = cmpAttack.GetTimers(type);
494 0 : ret.attack[type].prepareTime = timers.prepare;
495 0 : ret.attack[type].repeatTime = timers.repeat;
496 :
497 0 : if (type != "Ranged")
498 : {
499 0 : ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
500 0 : continue;
501 : }
502 :
503 0 : if (cmpPosition && cmpPosition.IsInWorld())
504 : // For units, take the range in front of it, no spread, so angle = 0,
505 : // else, take the average elevation around it: angle = 2 * pi.
506 0 : ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, ret.attack[type].yOrigin, cmpUnitAI ? 0 : 2 * Math.PI);
507 : else
508 : // Not in world, set a default?
509 0 : ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
510 : }
511 : }
512 :
513 1 : let cmpResistance = QueryMiragedInterface(ent, IID_Resistance);
514 1 : if (cmpResistance)
515 0 : ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity");
516 :
517 1 : let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
518 1 : if (cmpBuildingAI)
519 0 : ret.buildingAI = {
520 : "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
521 : "maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
522 : "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
523 : "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
524 : "arrowCount": cmpBuildingAI.GetArrowCount()
525 : };
526 :
527 1 : if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
528 0 : ret.turretParent = cmpPosition.GetTurretParent();
529 :
530 1 : let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
531 1 : if (cmpResourceSupply)
532 0 : ret.resourceSupply = {
533 : "isInfinite": cmpResourceSupply.IsInfinite(),
534 : "max": cmpResourceSupply.GetMaxAmount(),
535 : "amount": cmpResourceSupply.GetCurrentAmount(),
536 : "type": cmpResourceSupply.GetType(),
537 : "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
538 : "maxGatherers": cmpResourceSupply.GetMaxGatherers(),
539 : "numGatherers": cmpResourceSupply.GetNumGatherers()
540 : };
541 :
542 1 : let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
543 1 : if (cmpResourceDropsite)
544 0 : ret.resourceDropsite = {
545 : "types": cmpResourceDropsite.GetTypes(),
546 : "sharable": cmpResourceDropsite.IsSharable(),
547 : "shared": cmpResourceDropsite.IsShared()
548 : };
549 :
550 1 : let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
551 1 : if (cmpPromotion)
552 0 : ret.promotion = {
553 : "curr": cmpPromotion.GetCurrentXp(),
554 : "req": cmpPromotion.GetRequiredXp()
555 : };
556 :
557 1 : if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter"))
558 1 : ret.isBarterMarket = true;
559 :
560 1 : let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
561 1 : if (cmpHeal)
562 0 : ret.heal = {
563 : "health": cmpHeal.GetHealth(),
564 : "range": cmpHeal.GetRange().max,
565 : "interval": cmpHeal.GetInterval(),
566 : "unhealableClasses": cmpHeal.GetUnhealableClasses(),
567 : "healableClasses": cmpHeal.GetHealableClasses()
568 : };
569 :
570 1 : let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
571 1 : if (cmpLoot)
572 : {
573 0 : ret.loot = cmpLoot.GetResources();
574 0 : ret.loot.xp = cmpLoot.GetXp();
575 : }
576 :
577 1 : let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
578 1 : if (cmpResourceTrickle)
579 1 : ret.resourceTrickle = {
580 : "interval": cmpResourceTrickle.GetInterval(),
581 : "rates": cmpResourceTrickle.GetRates()
582 : };
583 :
584 1 : let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure);
585 1 : if (cmpTreasure)
586 0 : ret.treasure = {
587 : "collectTime": cmpTreasure.CollectionTime(),
588 : "resources": cmpTreasure.Resources()
589 : };
590 :
591 1 : let cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector);
592 1 : if (cmpTreasureCollector)
593 0 : ret.treasureCollector = true;
594 :
595 1 : let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
596 1 : if (cmpUnitMotion)
597 0 : ret.speed = {
598 : "walk": cmpUnitMotion.GetWalkSpeed(),
599 : "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier(),
600 : "acceleration": cmpUnitMotion.GetAcceleration()
601 : };
602 :
603 1 : let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep);
604 1 : if (cmpUpkeep)
605 0 : ret.upkeep = {
606 : "interval": cmpUpkeep.GetInterval(),
607 : "rates": cmpUpkeep.GetRates()
608 : };
609 :
610 1 : return ret;
611 : };
612 :
613 1 : GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
614 : {
615 0 : return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
616 : };
617 :
618 1 : GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
619 : {
620 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
621 0 : let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
622 :
623 0 : let rot = { "x": 0, "y": 0, "z": 0 };
624 0 : let pos = {
625 : "x": cmd.x,
626 : "y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
627 : "z": cmd.z
628 : };
629 :
630 0 : const yOrigin = cmd.yOrigin || 0;
631 0 : let range = cmd.range;
632 :
633 0 : return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, yOrigin, 2 * Math.PI);
634 : };
635 :
636 1 : GuiInterface.prototype.GetTemplateData = function(player, data)
637 : {
638 0 : let templateName = data.templateName;
639 0 : let owner = data.player !== undefined ? data.player : player;
640 0 : let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
641 0 : let template = cmpTemplateManager.GetTemplate(templateName);
642 :
643 0 : if (!template)
644 0 : return null;
645 :
646 0 : let aurasTemplate = {};
647 :
648 0 : if (!template.Auras)
649 0 : return GetTemplateDataHelper(template, owner, aurasTemplate, Resources);
650 :
651 0 : let auraNames = template.Auras._string.split(/\s+/);
652 :
653 0 : for (let name of auraNames)
654 : {
655 0 : let auraTemplate = AuraTemplates.Get(name);
656 0 : if (!auraTemplate)
657 0 : error("Template " + templateName + " has undefined aura " + name);
658 : else
659 0 : aurasTemplate[name] = auraTemplate;
660 : }
661 :
662 0 : return GetTemplateDataHelper(template, owner, aurasTemplate, Resources);
663 : };
664 :
665 1 : GuiInterface.prototype.AreRequirementsMet = function(player, data)
666 : {
667 0 : return !data.requirements || RequirementsHelper.AreRequirementsMet(data.requirements,
668 : data.player !== undefined ? data.player : player);
669 : };
670 :
671 : /**
672 : * Checks whether the requirements for this technology have been met.
673 : */
674 1 : GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
675 : {
676 0 : let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
677 0 : if (!cmpTechnologyManager)
678 0 : return false;
679 :
680 0 : return cmpTechnologyManager.CanResearch(data.tech);
681 : };
682 :
683 : /**
684 : * Returns technologies that are being actively researched, along with
685 : * which entity is researching them and how far along the research is.
686 : */
687 1 : GuiInterface.prototype.GetStartedResearch = function(player)
688 : {
689 0 : return QueryPlayerIDInterface(player, IID_TechnologyManager)?.GetBasicInfoOfStartedTechs() || {};
690 : };
691 :
692 : /**
693 : * Returns the battle state of the player.
694 : */
695 1 : GuiInterface.prototype.GetBattleState = function(player)
696 : {
697 0 : let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
698 0 : if (!cmpBattleDetection)
699 0 : return false;
700 :
701 0 : return cmpBattleDetection.GetState();
702 : };
703 :
704 : /**
705 : * Returns a list of ongoing attacks against the player.
706 : */
707 1 : GuiInterface.prototype.GetIncomingAttacks = function(player)
708 : {
709 0 : let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection);
710 0 : if (!cmpAttackDetection)
711 0 : return [];
712 :
713 0 : return cmpAttackDetection.GetIncomingAttacks();
714 : };
715 :
716 : /**
717 : * Used to show a red square over GUI elements you can't yet afford.
718 : */
719 1 : GuiInterface.prototype.GetNeededResources = function(player, data)
720 : {
721 0 : let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player);
722 0 : return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {};
723 : };
724 :
725 : /**
726 : * State of the templateData (player dependent): true when some template values have been modified
727 : * and need to be reloaded by the gui.
728 : */
729 1 : GuiInterface.prototype.OnTemplateModification = function(msg)
730 : {
731 0 : this.templateModified[msg.player] = true;
732 0 : this.selectionDirty[msg.player] = true;
733 : };
734 :
735 1 : GuiInterface.prototype.IsTemplateModified = function(player)
736 : {
737 0 : return this.templateModified[player] || false;
738 : };
739 :
740 1 : GuiInterface.prototype.ResetTemplateModified = function()
741 : {
742 0 : this.templateModified = {};
743 : };
744 :
745 : /**
746 : * Some changes may require an update to the selection panel,
747 : * which is cached for efficiency. Inform the GUI it needs reloading.
748 : */
749 1 : GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
750 : {
751 0 : this.selectionDirty[msg.player] = true;
752 : };
753 :
754 1 : GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg)
755 : {
756 0 : this.selectionDirty[msg.player] = true;
757 : };
758 :
759 1 : GuiInterface.prototype.SetSelectionDirty = function(player)
760 : {
761 0 : this.selectionDirty[player] = true;
762 : };
763 :
764 1 : GuiInterface.prototype.IsSelectionDirty = function(player)
765 : {
766 0 : return this.selectionDirty[player] || false;
767 : };
768 :
769 1 : GuiInterface.prototype.ResetSelectionDirty = function()
770 : {
771 0 : this.selectionDirty = {};
772 : };
773 :
774 : /**
775 : * Add a timed notification.
776 : * Warning: timed notifacations are serialised
777 : * (to also display them on saved games or after a rejoin)
778 : * so they should allways be added and deleted in a deterministic way.
779 : */
780 1 : GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
781 : {
782 0 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
783 0 : notification.endTime = duration + cmpTimer.GetTime();
784 0 : notification.id = ++this.timeNotificationID;
785 :
786 : // Let all players and observers receive the notification by default.
787 0 : if (!notification.players)
788 : {
789 0 : notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
790 0 : notification.players[0] = -1;
791 : }
792 :
793 0 : this.timeNotifications.push(notification);
794 0 : this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
795 :
796 0 : cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
797 :
798 0 : return this.timeNotificationID;
799 : };
800 :
801 1 : GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
802 : {
803 0 : this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
804 : };
805 :
806 1 : GuiInterface.prototype.GetTimeNotifications = function(player)
807 : {
808 0 : let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
809 : // Filter on players and time, since the delete timer might be executed with a delay.
810 0 : return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
811 : };
812 :
813 1 : GuiInterface.prototype.PushNotification = function(notification)
814 : {
815 0 : if (!notification.type || notification.type == "text")
816 0 : this.AddTimeNotification(notification);
817 : else
818 0 : this.notifications.push(notification);
819 : };
820 :
821 1 : GuiInterface.prototype.GetNotifications = function()
822 : {
823 0 : let n = this.notifications;
824 0 : this.notifications = [];
825 0 : return n;
826 : };
827 :
828 1 : GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
829 : {
830 0 : let cmpPlayer = QueryPlayerIDInterface(wantedPlayer);
831 0 : if (!cmpPlayer)
832 0 : return [];
833 :
834 0 : return cmpPlayer.GetFormations();
835 : };
836 :
837 1 : GuiInterface.prototype.GetFormationRequirements = function(player, data)
838 : {
839 0 : return GetFormationRequirements(data.formationTemplate);
840 : };
841 :
842 1 : GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
843 : {
844 0 : return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
845 : };
846 :
847 1 : GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
848 : {
849 0 : let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
850 0 : let template = cmpTemplateManager.GetTemplate(data.templateName);
851 :
852 0 : if (!template || !template.Formation)
853 0 : return {};
854 :
855 0 : return {
856 : "name": template.Identity.GenericName,
857 : "tooltip": template.Formation.DisabledTooltip || "",
858 : "icon": template.Identity.Icon
859 : };
860 : };
861 :
862 1 : GuiInterface.prototype.IsFormationSelected = function(player, data)
863 : {
864 0 : return data.ents.some(ent => {
865 0 : let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
866 0 : return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
867 : });
868 : };
869 :
870 1 : GuiInterface.prototype.IsStanceSelected = function(player, data)
871 : {
872 0 : for (let ent of data.ents)
873 : {
874 0 : let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
875 0 : if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
876 0 : return true;
877 : }
878 0 : return false;
879 : };
880 :
881 1 : GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
882 : {
883 0 : let buildableEnts = [];
884 0 : for (let ent of cmd.entities)
885 : {
886 0 : let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
887 0 : if (!cmpBuilder)
888 0 : continue;
889 :
890 0 : for (let building of cmpBuilder.GetEntitiesList())
891 0 : if (buildableEnts.indexOf(building) == -1)
892 0 : buildableEnts.push(building);
893 : }
894 0 : return buildableEnts;
895 : };
896 :
897 1 : GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
898 : {
899 0 : let updateEntityColor = (iids, entities) => {
900 0 : for (let ent of entities)
901 0 : for (let iid of iids)
902 : {
903 0 : let cmp = Engine.QueryInterface(ent, iid);
904 0 : if (cmp)
905 0 : cmp.UpdateColor();
906 : }
907 : };
908 :
909 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
910 0 : let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
911 0 : for (let i = 1; i < numPlayers; ++i)
912 : {
913 0 : let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
914 0 : if (!cmpPlayer)
915 0 : continue;
916 :
917 0 : cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
918 0 : if (data.displayDiplomacyColors)
919 0 : cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
920 :
921 0 : updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
922 : [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
923 : [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
924 : cmpRangeManager.GetEntitiesByPlayer(i));
925 : }
926 0 : updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
927 0 : Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
928 : };
929 :
930 1 : GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
931 : {
932 : // Cache of owner -> color map
933 0 : let playerColors = {};
934 :
935 0 : for (let ent of cmd.entities)
936 : {
937 0 : let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
938 0 : if (!cmpSelectable)
939 0 : continue;
940 :
941 : // Find the entity's owner's color.
942 0 : let owner = INVALID_PLAYER;
943 0 : let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
944 0 : if (cmpOwnership)
945 0 : owner = cmpOwnership.GetOwner();
946 :
947 0 : let color = playerColors[owner];
948 0 : if (!color)
949 : {
950 0 : color = { "r": 1, "g": 1, "b": 1 };
951 0 : let cmpPlayer = QueryPlayerIDInterface(owner);
952 0 : if (cmpPlayer)
953 0 : color = cmpPlayer.GetDisplayedColor();
954 0 : playerColors[owner] = color;
955 : }
956 :
957 0 : cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
958 :
959 0 : let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
960 0 : if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER)
961 0 : continue;
962 :
963 0 : cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
964 : }
965 : };
966 :
967 1 : GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
968 : {
969 0 : this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
970 : };
971 :
972 1 : GuiInterface.prototype.GetEntitiesWithStatusBars = function()
973 : {
974 0 : return Array.from(this.entsWithAuraAndStatusBars);
975 : };
976 :
977 1 : GuiInterface.prototype.SetStatusBars = function(player, cmd)
978 : {
979 0 : let affectedEnts = new Set();
980 0 : for (let ent of cmd.entities)
981 : {
982 0 : let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
983 0 : if (!cmpStatusBars)
984 0 : continue;
985 0 : cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience);
986 :
987 0 : let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
988 0 : if (!cmpAuras)
989 0 : continue;
990 :
991 0 : for (let name of cmpAuras.GetAuraNames())
992 : {
993 0 : if (!cmpAuras.GetOverlayIcon(name))
994 0 : continue;
995 0 : for (let e of cmpAuras.GetAffectedEntities(name))
996 0 : affectedEnts.add(e);
997 0 : if (cmd.enabled)
998 0 : this.entsWithAuraAndStatusBars.add(ent);
999 : else
1000 0 : this.entsWithAuraAndStatusBars.delete(ent);
1001 : }
1002 : }
1003 :
1004 0 : for (let ent of affectedEnts)
1005 : {
1006 0 : let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
1007 0 : if (cmpStatusBars)
1008 0 : cmpStatusBars.RegenerateSprites();
1009 : }
1010 : };
1011 :
1012 1 : GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
1013 : {
1014 0 : for (let ent of cmd.entities)
1015 : {
1016 0 : let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
1017 0 : if (cmpRangeOverlayManager)
1018 0 : cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
1019 : }
1020 : };
1021 :
1022 1 : GuiInterface.prototype.GetPlayerEntities = function(player)
1023 : {
1024 0 : return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
1025 : };
1026 :
1027 1 : GuiInterface.prototype.GetNonGaiaEntities = function()
1028 : {
1029 0 : return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
1030 : };
1031 :
1032 : /**
1033 : * Displays the rally points of a given list of entities (carried in cmd.entities).
1034 : *
1035 : * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
1036 : * be rendered, in order to support instantaneously rendering a rally point marker at a specified location
1037 : * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
1038 : * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
1039 : * RallyPoint component.
1040 : */
1041 1 : GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
1042 : {
1043 0 : let cmpPlayer = QueryPlayerIDInterface(player);
1044 :
1045 : // If there are some rally points already displayed, first hide them.
1046 0 : for (let ent of this.entsRallyPointsDisplayed)
1047 : {
1048 0 : let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
1049 0 : if (cmpRallyPointRenderer)
1050 0 : cmpRallyPointRenderer.SetDisplayed(false);
1051 : }
1052 :
1053 0 : this.entsRallyPointsDisplayed = [];
1054 :
1055 : // Show the rally points for the passed entities.
1056 0 : for (let ent of cmd.entities)
1057 : {
1058 0 : let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
1059 0 : if (!cmpRallyPointRenderer)
1060 0 : continue;
1061 :
1062 : // Entity must have a rally point component to display a rally point marker
1063 : // (regardless of whether cmd specifies a custom location).
1064 0 : let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
1065 0 : if (!cmpRallyPoint)
1066 0 : continue;
1067 :
1068 : // Verify the owner.
1069 0 : let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
1070 0 : if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
1071 0 : if (!cmpOwnership || cmpOwnership.GetOwner() != player)
1072 0 : continue;
1073 :
1074 : // If the command was passed an explicit position, use that and
1075 : // override the real rally point position; otherwise use the real position.
1076 : let pos;
1077 0 : if (cmd.x && cmd.z)
1078 0 : pos = cmd;
1079 : else
1080 : // May return undefined if no rally point is set.
1081 0 : pos = cmpRallyPoint.GetPositions()[0];
1082 :
1083 0 : if (pos)
1084 : {
1085 : // Only update the position if we changed it (cmd.queued is set).
1086 : // Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z.
1087 0 : if ("queued" in cmd)
1088 : {
1089 0 : if (cmd.queued == true)
1090 0 : cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z));
1091 : else
1092 0 : cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z));
1093 : }
1094 0 : else if (!cmpRallyPointRenderer.IsSet())
1095 : // Rebuild the renderer when not set (when reading saved game or in case of building update).
1096 0 : for (let posi of cmpRallyPoint.GetPositions())
1097 0 : cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
1098 :
1099 0 : cmpRallyPointRenderer.SetDisplayed(true);
1100 :
1101 : // Remember which entities have their rally points displayed so we can hide them again.
1102 0 : this.entsRallyPointsDisplayed.push(ent);
1103 : }
1104 : }
1105 : };
1106 :
1107 1 : GuiInterface.prototype.AddTargetMarker = function(player, cmd)
1108 : {
1109 0 : let ent = Engine.AddLocalEntity(cmd.template);
1110 0 : if (!ent)
1111 0 : return;
1112 0 : let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
1113 0 : if (cmpOwnership)
1114 0 : cmpOwnership.SetOwner(cmd.owner);
1115 0 : let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1116 0 : cmpPosition.JumpTo(cmd.x, cmd.z);
1117 : };
1118 :
1119 : /**
1120 : * Display the building placement preview.
1121 : * cmd.template is the name of the entity template, or "" to disable the preview.
1122 : * cmd.x, cmd.z, cmd.angle give the location.
1123 : *
1124 : * Returns result object from CheckPlacement:
1125 : * {
1126 : * "success": true iff the placement is valid, else false
1127 : * "message": message to display in UI for invalid placement, else ""
1128 : * "parameters": parameters to use in the message
1129 : * "translateMessage": localisation info
1130 : * "translateParameters": localisation info
1131 : * "pluralMessage": we might return a plural translation instead (optional)
1132 : * "pluralCount": localisation info (optional)
1133 : * }
1134 : */
1135 1 : GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
1136 : {
1137 0 : let result = {
1138 : "success": false,
1139 : "message": "",
1140 : "parameters": {},
1141 : "translateMessage": false,
1142 : "translateParameters": []
1143 : };
1144 :
1145 0 : if (!this.placementEntity || this.placementEntity[0] != cmd.template)
1146 : {
1147 0 : if (this.placementEntity)
1148 0 : Engine.DestroyEntity(this.placementEntity[1]);
1149 :
1150 0 : if (cmd.template == "")
1151 0 : this.placementEntity = undefined;
1152 : else
1153 0 : this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
1154 : }
1155 :
1156 0 : if (this.placementEntity)
1157 : {
1158 0 : let ent = this.placementEntity[1];
1159 :
1160 0 : let pos = Engine.QueryInterface(ent, IID_Position);
1161 0 : if (pos)
1162 : {
1163 0 : pos.JumpTo(cmd.x, cmd.z);
1164 0 : pos.SetYRotation(cmd.angle);
1165 : }
1166 :
1167 0 : let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
1168 0 : cmpOwnership.SetOwner(player);
1169 :
1170 0 : let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
1171 0 : if (!cmpBuildRestrictions)
1172 0 : error("cmpBuildRestrictions not defined");
1173 : else
1174 0 : result = cmpBuildRestrictions.CheckPlacement();
1175 :
1176 0 : let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
1177 0 : if (cmpRangeOverlayManager)
1178 0 : cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
1179 :
1180 : // Set it to a red shade if this is an invalid location.
1181 0 : let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
1182 0 : if (cmpVisual)
1183 : {
1184 0 : if (cmd.actorSeed !== undefined)
1185 0 : cmpVisual.SetActorSeed(cmd.actorSeed);
1186 :
1187 0 : if (!result.success)
1188 0 : cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
1189 : else
1190 0 : cmpVisual.SetShadingColor(1, 1, 1, 1);
1191 : }
1192 : }
1193 :
1194 0 : return result;
1195 : };
1196 :
1197 : /**
1198 : * 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
1199 : * specified. Returns an object with information about the list of entities that need to be newly constructed to complete
1200 : * 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
1201 : * them can be validly constructed.
1202 : *
1203 : * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
1204 : * another depending on things like snapping and whether some of the entities inside them can be validly positioned.
1205 : * We have:
1206 : * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
1207 : * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
1208 : * to preview the completed tower on top of its foundation.
1209 : *
1210 : * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
1211 : * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
1212 : * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
1213 : * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
1214 : * constructed.
1215 : *
1216 : * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
1217 : * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
1218 : * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
1219 : * constructed but come after said first invalid entity are also truncated away.
1220 : *
1221 : * 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
1222 : * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
1223 : * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
1224 : * argument (see below). Otherwise, it will return an object with the following information:
1225 : *
1226 : * result: {
1227 : * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
1228 : * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
1229 : * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
1230 : * but the wall construction was truncated before we could reach it, it won't be set here. Currently only
1231 : * supports towers.
1232 : * 'pieces': Array with the following data for each of the entities in the third list:
1233 : * [{
1234 : * 'template': Template name of the entity.
1235 : * 'x': X coordinate of the entity's position.
1236 : * 'z': Z coordinate of the entity's position.
1237 : * 'angle': Rotation around the Y axis of the entity (in radians).
1238 : * },
1239 : * ...]
1240 : * 'cost': { The total cost required for constructing all the pieces as listed above.
1241 : * 'food': ...,
1242 : * 'wood': ...,
1243 : * 'stone': ...,
1244 : * 'metal': ...,
1245 : * 'population': ...,
1246 : * }
1247 : * }
1248 : *
1249 : * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
1250 : * @param cmd.start Starting point of the wall segment being created.
1251 : * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
1252 : * the starting point of the wall is available at this time (e.g. while the player is still in the process
1253 : * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
1254 : * previewed.
1255 : * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
1256 : */
1257 1 : GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
1258 : {
1259 0 : let wallSet = cmd.wallSet;
1260 :
1261 : // Did the start position snap to anything?
1262 : // If we snapped, was it to an entity? If yes, hold that entity's ID.
1263 0 : let start = {
1264 : "pos": cmd.start,
1265 : "angle": 0,
1266 : "snapped": false,
1267 : "snappedEnt": INVALID_ENTITY
1268 : };
1269 :
1270 : // Did the end position snap to anything?
1271 : // If we snapped, was it to an entity? If yes, hold that entity's ID.
1272 0 : let end = {
1273 : "pos": cmd.end,
1274 : "angle": 0,
1275 : "snapped": false,
1276 : "snappedEnt": INVALID_ENTITY
1277 : };
1278 :
1279 : // --------------------------------------------------------------------------------
1280 : // Do some entity cache management and check for snapping.
1281 :
1282 0 : if (!this.placementWallEntities)
1283 0 : this.placementWallEntities = {};
1284 :
1285 0 : if (!wallSet)
1286 : {
1287 : // We're clearing the preview, clear the entity cache and bail.
1288 0 : for (let tpl in this.placementWallEntities)
1289 : {
1290 0 : for (let ent of this.placementWallEntities[tpl].entities)
1291 0 : Engine.DestroyEntity(ent);
1292 :
1293 0 : this.placementWallEntities[tpl].numUsed = 0;
1294 0 : this.placementWallEntities[tpl].entities = [];
1295 : // Keep template data around.
1296 : }
1297 :
1298 0 : return false;
1299 : }
1300 :
1301 0 : for (let tpl in this.placementWallEntities)
1302 : {
1303 0 : for (let ent of this.placementWallEntities[tpl].entities)
1304 : {
1305 0 : let pos = Engine.QueryInterface(ent, IID_Position);
1306 0 : if (pos)
1307 0 : pos.MoveOutOfWorld();
1308 : }
1309 :
1310 0 : this.placementWallEntities[tpl].numUsed = 0;
1311 : }
1312 :
1313 : // Create cache entries for templates we haven't seen before.
1314 0 : for (let type in wallSet.templates)
1315 : {
1316 0 : if (type == "curves")
1317 0 : continue;
1318 :
1319 0 : let tpl = wallSet.templates[type];
1320 0 : if (!(tpl in this.placementWallEntities))
1321 : {
1322 0 : this.placementWallEntities[tpl] = {
1323 : "numUsed": 0,
1324 : "entities": [],
1325 : "templateData": this.GetTemplateData(player, { "templateName": tpl }),
1326 : };
1327 :
1328 0 : if (!this.placementWallEntities[tpl].templateData.wallPiece)
1329 : {
1330 0 : error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
1331 0 : return false;
1332 : }
1333 : }
1334 : }
1335 :
1336 : // Prevent division by zero errors further on if the start and end positions are the same.
1337 0 : if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
1338 0 : end.pos = undefined;
1339 :
1340 : // 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
1341 : // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
1342 : // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
1343 0 : if (cmd.snapEntities)
1344 : {
1345 : // Value of 0.5 was determined through trial and error.
1346 0 : let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5;
1347 0 : let startSnapData = this.GetFoundationSnapData(player, {
1348 : "x": start.pos.x,
1349 : "z": start.pos.z,
1350 : "template": wallSet.templates.tower,
1351 : "snapEntities": cmd.snapEntities,
1352 : "snapRadius": snapRadius,
1353 : });
1354 :
1355 0 : if (startSnapData)
1356 : {
1357 0 : start.pos.x = startSnapData.x;
1358 0 : start.pos.z = startSnapData.z;
1359 0 : start.angle = startSnapData.angle;
1360 0 : start.snapped = true;
1361 :
1362 0 : if (startSnapData.ent)
1363 0 : start.snappedEnt = startSnapData.ent;
1364 : }
1365 :
1366 0 : if (end.pos)
1367 : {
1368 0 : let endSnapData = this.GetFoundationSnapData(player, {
1369 : "x": end.pos.x,
1370 : "z": end.pos.z,
1371 : "template": wallSet.templates.tower,
1372 : "snapEntities": cmd.snapEntities,
1373 : "snapRadius": snapRadius,
1374 : });
1375 :
1376 0 : if (endSnapData)
1377 : {
1378 0 : end.pos.x = endSnapData.x;
1379 0 : end.pos.z = endSnapData.z;
1380 0 : end.angle = endSnapData.angle;
1381 0 : end.snapped = true;
1382 :
1383 0 : if (endSnapData.ent)
1384 0 : end.snappedEnt = endSnapData.ent;
1385 : }
1386 : }
1387 : }
1388 :
1389 : // Clear the single-building preview entity (we'll be rolling our own).
1390 0 : this.SetBuildingPlacementPreview(player, { "template": "" });
1391 :
1392 : // --------------------------------------------------------------------------------
1393 : // Calculate wall placement and position preview entities.
1394 :
1395 0 : let result = {
1396 : "pieces": [],
1397 : "cost": { "population": 0, "time": 0 }
1398 : };
1399 0 : for (let res of Resources.GetCodes())
1400 0 : result.cost[res] = 0;
1401 :
1402 0 : let previewEntities = [];
1403 0 : if (end.pos)
1404 : // See helpers/Walls.js.
1405 0 : previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end);
1406 :
1407 : // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
1408 : // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
1409 : // an issue, because all preview entities have their obstruction components deactivated, meaning that their
1410 : // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
1411 : // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
1412 :
1413 : // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
1414 : // flag set), which is what we want. The only exception to this is when snapping to existing towers (or
1415 : // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
1416 : // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
1417 : // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
1418 : // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
1419 : // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
1420 :
1421 : // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
1422 : // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
1423 : // by the foundation it snaps to.
1424 :
1425 0 : if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
1426 : {
1427 0 : let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
1428 0 : if (previewEntities.length && startEntObstruction)
1429 0 : previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
1430 :
1431 : // If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group.
1432 0 : let startEntState = this.GetEntityState(player, start.snappedEnt);
1433 0 : if (startEntState.foundation)
1434 : {
1435 0 : let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
1436 0 : if (cmpPosition)
1437 0 : previewEntities.unshift({
1438 : "template": wallSet.templates.tower,
1439 : "pos": start.pos,
1440 : "angle": cmpPosition.GetRotation().y,
1441 : "controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
1442 : "excludeFromResult": true // Preview only, must not appear in the result.
1443 : });
1444 : }
1445 : }
1446 : else
1447 : {
1448 : // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
1449 : // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
1450 : // wall piece.
1451 :
1452 : // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
1453 : // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
1454 : // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
1455 : // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
1456 : // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
1457 : // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
1458 : // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
1459 : // the foundation's angle.
1460 :
1461 : // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
1462 : // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
1463 0 : previewEntities.unshift({
1464 : "template": wallSet.templates.tower,
1465 : "pos": start.pos,
1466 : "angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle
1467 : });
1468 : }
1469 :
1470 0 : if (end.pos)
1471 : {
1472 : // Analogous to the starting side case above.
1473 0 : if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
1474 : {
1475 0 : let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
1476 :
1477 : // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
1478 : // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
1479 : // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
1480 : // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
1481 : // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
1482 0 : if (previewEntities.length > 0 && endEntObstruction)
1483 : {
1484 0 : previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || [];
1485 0 : previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup());
1486 : }
1487 :
1488 : // If we're snapping to a foundation, add an extra preview tower and also set it to the same control group.
1489 0 : let endEntState = this.GetEntityState(player, end.snappedEnt);
1490 0 : if (endEntState.foundation)
1491 : {
1492 0 : let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
1493 0 : if (cmpPosition)
1494 0 : previewEntities.push({
1495 : "template": wallSet.templates.tower,
1496 : "pos": end.pos,
1497 : "angle": cmpPosition.GetRotation().y,
1498 : "controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
1499 : "excludeFromResult": true
1500 : });
1501 : }
1502 : }
1503 : else
1504 0 : previewEntities.push({
1505 : "template": wallSet.templates.tower,
1506 : "pos": end.pos,
1507 : "angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle
1508 : });
1509 : }
1510 :
1511 0 : let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
1512 0 : if (!cmpTerrain)
1513 : {
1514 0 : error("[SetWallPlacementPreview] System Terrain component not found");
1515 0 : return false;
1516 : }
1517 :
1518 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1519 0 : if (!cmpRangeManager)
1520 : {
1521 0 : error("[SetWallPlacementPreview] System RangeManager component not found");
1522 0 : return false;
1523 : }
1524 :
1525 : // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
1526 : // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
1527 : // but cannot validly be, constructed). See method-level documentation for more details.
1528 :
1529 0 : let allPiecesValid = true;
1530 : // Number of entities that are required to build the entire wall, regardless of validity.
1531 0 : let numRequiredPieces = 0;
1532 :
1533 0 : for (let i = 0; i < previewEntities.length; ++i)
1534 : {
1535 0 : let entInfo = previewEntities[i];
1536 :
1537 0 : let ent = null;
1538 0 : let tpl = entInfo.template;
1539 0 : let tplData = this.placementWallEntities[tpl].templateData;
1540 0 : let entPool = this.placementWallEntities[tpl];
1541 :
1542 0 : if (entPool.numUsed >= entPool.entities.length)
1543 : {
1544 0 : ent = Engine.AddLocalEntity("preview|" + tpl);
1545 0 : entPool.entities.push(ent);
1546 : }
1547 : else
1548 0 : ent = entPool.entities[entPool.numUsed];
1549 :
1550 0 : if (!ent)
1551 : {
1552 0 : error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
1553 0 : continue;
1554 : }
1555 :
1556 : // Move piece to right location.
1557 : // TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities.
1558 0 : let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1559 0 : if (cmpPosition)
1560 : {
1561 0 : cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
1562 0 : cmpPosition.SetYRotation(entInfo.angle);
1563 :
1564 : // If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces.
1565 0 : if (tpl === wallSet.templates.tower)
1566 : {
1567 0 : let terrainGroundPrev = null;
1568 0 : let terrainGroundNext = null;
1569 :
1570 0 : if (i > 0)
1571 0 : terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z);
1572 :
1573 0 : if (i < previewEntities.length - 1)
1574 0 : terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z);
1575 :
1576 0 : if (terrainGroundPrev != null || terrainGroundNext != null)
1577 : {
1578 0 : let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
1579 0 : cmpPosition.SetHeightFixed(targetY);
1580 : }
1581 : }
1582 : }
1583 :
1584 0 : let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
1585 0 : if (!cmpObstruction)
1586 : {
1587 0 : error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
1588 0 : continue;
1589 : }
1590 :
1591 : // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
1592 : // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
1593 : // first-come first-served basis; the first value in the array is always assigned as the primary control group, and
1594 : // any second value as the secondary control group.
1595 :
1596 : // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
1597 : // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
1598 : // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
1599 : // once snapped to.
1600 :
1601 0 : let primaryControlGroup = ent;
1602 0 : let secondaryControlGroup = INVALID_ENTITY;
1603 :
1604 0 : if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
1605 : {
1606 0 : if (entInfo.controlGroups.length > 2)
1607 : {
1608 0 : error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
1609 0 : break;
1610 : }
1611 :
1612 0 : primaryControlGroup = entInfo.controlGroups[0];
1613 0 : if (entInfo.controlGroups.length > 1)
1614 0 : secondaryControlGroup = entInfo.controlGroups[1];
1615 : }
1616 :
1617 0 : cmpObstruction.SetControlGroup(primaryControlGroup);
1618 0 : cmpObstruction.SetControlGroup2(secondaryControlGroup);
1619 :
1620 0 : let validPlacement = false;
1621 :
1622 0 : let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
1623 0 : cmpOwnership.SetOwner(player);
1624 :
1625 : // Check whether it's in a visible or fogged region.
1626 : // TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta.
1627 0 : let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
1628 0 : if (visible)
1629 : {
1630 0 : let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
1631 0 : if (!cmpBuildRestrictions)
1632 : {
1633 0 : error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
1634 0 : continue;
1635 : }
1636 :
1637 : // TODO: Handle results of CheckPlacement.
1638 0 : validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
1639 :
1640 : // If a wall piece has two control groups, it's likely a segment that spans
1641 : // between two existing towers. To avoid placing a duplicate wall segment,
1642 : // check for collisions with entities that share both control groups.
1643 0 : if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
1644 0 : validPlacement = cmpObstruction.CheckDuplicateFoundation();
1645 : }
1646 :
1647 0 : allPiecesValid = allPiecesValid && validPlacement;
1648 :
1649 : // The requirement below that all pieces so far have to have valid positions, rather than only this single one,
1650 : // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
1651 : // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
1652 : // through and past an existing building).
1653 :
1654 : // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
1655 : // on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
1656 :
1657 0 : if (!entInfo.excludeFromResult)
1658 0 : ++numRequiredPieces;
1659 :
1660 0 : if (allPiecesValid && !entInfo.excludeFromResult)
1661 : {
1662 0 : result.pieces.push({
1663 : "template": tpl,
1664 : "x": entInfo.pos.x,
1665 : "z": entInfo.pos.z,
1666 : "angle": entInfo.angle,
1667 : });
1668 0 : this.placementWallLastAngle = entInfo.angle;
1669 :
1670 : // Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
1671 : // copied over, so we need to fetch it from the template instead).
1672 : // TODO: We should really use a Cost object or at least some utility functions for this, this is mindless
1673 : // boilerplate that's probably duplicated in tons of places.
1674 0 : for (let res of Resources.GetCodes().concat(["population", "time"]))
1675 0 : result.cost[res] += tplData.cost[res];
1676 : }
1677 :
1678 0 : let canAfford = true;
1679 0 : let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
1680 0 : if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
1681 0 : canAfford = false;
1682 :
1683 0 : let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
1684 0 : if (cmpVisual)
1685 : {
1686 0 : if (!allPiecesValid || !canAfford)
1687 0 : cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
1688 : else
1689 0 : cmpVisual.SetShadingColor(1, 1, 1, 1);
1690 : }
1691 :
1692 0 : ++entPool.numUsed;
1693 : }
1694 :
1695 : // If any were entities required to build the wall, but none of them could be validly positioned, return failure
1696 : // (see method-level documentation).
1697 0 : if (numRequiredPieces > 0 && result.pieces.length == 0)
1698 0 : return false;
1699 :
1700 0 : if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
1701 0 : result.startSnappedEnt = start.snappedEnt;
1702 :
1703 : // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
1704 : // i.e. are included in result.pieces (see docs for the result object).
1705 0 : if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
1706 0 : result.endSnappedEnt = end.snappedEnt;
1707 :
1708 0 : return result;
1709 : };
1710 :
1711 : /**
1712 : * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
1713 : * it to (if necessary/useful).
1714 : *
1715 : * @param data.x The X position of the foundation to snap.
1716 : * @param data.z The Z position of the foundation to snap.
1717 : * @param data.template The template to get the foundation snapping data for.
1718 : * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
1719 : * around the entity. Only takes effect when used in conjunction with data.snapRadius.
1720 : * When this option is used and the foundation is found to snap to one of the entities passed in this list
1721 : * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
1722 : * holding the ID of the entity that was snapped to.
1723 : * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
1724 : * {data.x, data.z} must be located within to have it snap to that entity.
1725 : */
1726 1 : GuiInterface.prototype.GetFoundationSnapData = function(player, data)
1727 : {
1728 0 : let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
1729 0 : if (!template)
1730 : {
1731 0 : warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
1732 0 : return false;
1733 : }
1734 :
1735 0 : if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
1736 : {
1737 : // 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.
1738 : // (TODO: Break unlikely ties by choosing the lowest entity ID.)
1739 :
1740 0 : let minDist2 = -1;
1741 0 : let minDistEntitySnapData = null;
1742 0 : let radius2 = data.snapRadius * data.snapRadius;
1743 :
1744 0 : for (let ent of data.snapEntities)
1745 : {
1746 0 : let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1747 0 : if (!cmpPosition || !cmpPosition.IsInWorld())
1748 0 : continue;
1749 :
1750 0 : let pos = cmpPosition.GetPosition();
1751 0 : let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
1752 0 : if (dist2 > radius2)
1753 0 : continue;
1754 :
1755 0 : if (minDist2 < 0 || dist2 < minDist2)
1756 : {
1757 0 : minDist2 = dist2;
1758 0 : minDistEntitySnapData = {
1759 : "x": pos.x,
1760 : "z": pos.z,
1761 : "angle": cmpPosition.GetRotation().y,
1762 : "ent": ent
1763 : };
1764 : }
1765 : }
1766 :
1767 0 : if (minDistEntitySnapData != null)
1768 0 : return minDistEntitySnapData;
1769 : }
1770 :
1771 0 : if (data.snapToEdges)
1772 : {
1773 0 : let position = this.obstructionSnap.getPosition(data, template);
1774 0 : if (position)
1775 0 : return position;
1776 : }
1777 :
1778 0 : if (template.BuildRestrictions.PlacementType == "shore")
1779 : {
1780 0 : let angle = GetDockAngle(template, data.x, data.z);
1781 0 : if (angle !== undefined)
1782 0 : return {
1783 : "x": data.x,
1784 : "z": data.z,
1785 : "angle": angle
1786 : };
1787 : }
1788 :
1789 0 : return false;
1790 : };
1791 :
1792 1 : GuiInterface.prototype.PlaySoundForPlayer = function(player, data)
1793 : {
1794 0 : let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player);
1795 0 : let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound);
1796 0 : if (!cmpSound)
1797 0 : return;
1798 :
1799 0 : let soundGroup = cmpSound.GetSoundGroup(data.name);
1800 0 : if (soundGroup)
1801 0 : Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player);
1802 : };
1803 :
1804 1 : GuiInterface.prototype.PlaySound = function(player, data)
1805 : {
1806 0 : if (!data.entity)
1807 0 : return;
1808 :
1809 0 : PlaySound(data.name, data.entity);
1810 : };
1811 :
1812 : /**
1813 : * Find any idle units.
1814 : *
1815 : * @param data.idleClasses Array of class names to include.
1816 : * @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
1817 : * @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
1818 : * @param data.excludeUnits Array of units to exclude.
1819 : *
1820 : * Returns an array of idle units.
1821 : * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
1822 : */
1823 1 : GuiInterface.prototype.FindIdleUnits = function(player, data)
1824 : {
1825 0 : let idleUnits = [];
1826 : // The general case is that only the 'first' idle unit is required; filtering would examine every unit.
1827 : // This loop imitates a grouping/aggregation on the first matching idle class.
1828 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1829 0 : for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
1830 : {
1831 0 : let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
1832 0 : if (!filtered.idle)
1833 0 : continue;
1834 :
1835 : // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
1836 : // By adding to the 'end', there is no pause if the series of units loops.
1837 0 : let bucket = filtered.bucket;
1838 0 : if (bucket == 0 && data.prevUnit && entity <= data.prevUnit)
1839 0 : bucket = data.idleClasses.length;
1840 :
1841 0 : if (!idleUnits[bucket])
1842 0 : idleUnits[bucket] = [];
1843 0 : idleUnits[bucket].push(entity);
1844 :
1845 : // If enough units have been collected in the first bucket, go ahead and return them.
1846 0 : if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
1847 0 : return idleUnits[0];
1848 : }
1849 :
1850 0 : let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
1851 0 : if (data.limit && reduced.length > data.limit)
1852 0 : return reduced.slice(0, data.limit);
1853 :
1854 0 : return reduced;
1855 : };
1856 :
1857 : /**
1858 : * Discover if the player has idle units.
1859 : *
1860 : * @param data.idleClasses Array of class names to include.
1861 : * @param data.excludeUnits Array of units to exclude.
1862 : *
1863 : * Returns a boolean of whether the player has any idle units
1864 : */
1865 1 : GuiInterface.prototype.HasIdleUnits = function(player, data)
1866 : {
1867 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1868 0 : return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
1869 : };
1870 :
1871 : /**
1872 : * Whether to filter an idle unit
1873 : *
1874 : * @param unit The unit to filter.
1875 : * @param idleclasses Array of class names to include.
1876 : * @param excludeUnits Array of units to exclude.
1877 : *
1878 : * Returns an object with the following fields:
1879 : * - idle - true if the unit is considered idle by the filter, false otherwise.
1880 : * - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
1881 : */
1882 1 : GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
1883 : {
1884 0 : let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
1885 0 : if (!cmpUnitAI || !cmpUnitAI.IsIdle())
1886 0 : return { "idle": false };
1887 :
1888 0 : let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable);
1889 0 : if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned())
1890 0 : return { "idle": false };
1891 :
1892 0 : const cmpTurretable = Engine.QueryInterface(unit, IID_Turretable);
1893 0 : if (cmpTurretable && cmpTurretable.IsTurreted())
1894 0 : return { "idle": false };
1895 :
1896 0 : let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
1897 0 : if (!cmpIdentity)
1898 0 : return { "idle": false };
1899 :
1900 0 : let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
1901 0 : if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
1902 0 : return { "idle": false };
1903 :
1904 0 : return { "idle": true, "bucket": bucket };
1905 : };
1906 :
1907 1 : GuiInterface.prototype.GetTradingRouteGain = function(player, data)
1908 : {
1909 0 : if (!data.firstMarket || !data.secondMarket)
1910 0 : return null;
1911 :
1912 0 : let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market);
1913 0 : return cmpMarket && cmpMarket.CalculateTraderGain(data.secondMarket, data.template);
1914 : };
1915 :
1916 1 : GuiInterface.prototype.GetTradingDetails = function(player, data)
1917 : {
1918 0 : let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
1919 0 : if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
1920 0 : return null;
1921 :
1922 0 : let firstMarket = cmpEntityTrader.GetFirstMarket();
1923 0 : let secondMarket = cmpEntityTrader.GetSecondMarket();
1924 0 : let result = null;
1925 0 : if (data.target === firstMarket)
1926 : {
1927 0 : result = {
1928 : "type": "is first",
1929 : "hasBothMarkets": cmpEntityTrader.HasBothMarkets()
1930 : };
1931 0 : if (cmpEntityTrader.HasBothMarkets())
1932 0 : result.gain = cmpEntityTrader.GetGoods().amount;
1933 : }
1934 0 : else if (data.target === secondMarket)
1935 0 : result = {
1936 : "type": "is second",
1937 : "gain": cmpEntityTrader.GetGoods().amount,
1938 : };
1939 0 : else if (!firstMarket)
1940 0 : result = { "type": "set first" };
1941 0 : else if (!secondMarket)
1942 0 : result = {
1943 : "type": "set second",
1944 : "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
1945 : };
1946 : else
1947 0 : result = { "type": "set first" };
1948 :
1949 0 : return result;
1950 : };
1951 :
1952 1 : GuiInterface.prototype.CanAttack = function(player, data)
1953 : {
1954 0 : let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
1955 0 : return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
1956 : };
1957 :
1958 : /*
1959 : * Returns batch build time.
1960 : */
1961 1 : GuiInterface.prototype.GetBatchTime = function(player, data)
1962 : {
1963 0 : return Engine.QueryInterface(data.entity, IID_Trainer)?.GetBatchTime(data.batchSize) || 0;
1964 : };
1965 :
1966 1 : GuiInterface.prototype.IsMapRevealed = function(player)
1967 : {
1968 0 : return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
1969 : };
1970 :
1971 1 : GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
1972 : {
1973 0 : Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
1974 : };
1975 :
1976 1 : GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
1977 : {
1978 0 : Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
1979 : };
1980 :
1981 1 : GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
1982 : {
1983 0 : Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
1984 : };
1985 :
1986 1 : GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
1987 : {
1988 0 : for (let ent of data.entities)
1989 : {
1990 0 : let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
1991 0 : if (cmpUnitMotion)
1992 0 : cmpUnitMotion.SetDebugOverlay(data.enabled);
1993 : }
1994 : };
1995 :
1996 1 : GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
1997 : {
1998 0 : Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
1999 : };
2000 :
2001 1 : GuiInterface.prototype.GetTraderNumber = function(player)
2002 : {
2003 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
2004 0 : let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
2005 :
2006 0 : let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
2007 0 : let shipTrader = { "total": 0, "trading": 0 };
2008 :
2009 0 : for (let ent of traders)
2010 : {
2011 0 : let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
2012 0 : let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
2013 0 : if (!cmpIdentity || !cmpUnitAI)
2014 0 : continue;
2015 :
2016 0 : if (cmpIdentity.HasClass("Ship"))
2017 : {
2018 0 : ++shipTrader.total;
2019 0 : if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
2020 0 : ++shipTrader.trading;
2021 : }
2022 : else
2023 : {
2024 0 : ++landTrader.total;
2025 0 : if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
2026 0 : ++landTrader.trading;
2027 0 : if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
2028 : {
2029 0 : let holder = cmpUnitAI.order.data.target;
2030 0 : let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
2031 0 : if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
2032 0 : ++landTrader.garrisoned;
2033 : }
2034 : }
2035 : }
2036 :
2037 0 : return { "landTrader": landTrader, "shipTrader": shipTrader };
2038 : };
2039 :
2040 1 : GuiInterface.prototype.GetTradingGoods = function(player)
2041 : {
2042 0 : let cmpPlayer = QueryPlayerIDInterface(player);
2043 0 : if (!cmpPlayer)
2044 0 : return [];
2045 :
2046 0 : return cmpPlayer.GetTradingGoods();
2047 : };
2048 :
2049 1 : GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
2050 : {
2051 0 : this.renamedEntities.push(msg);
2052 : };
2053 :
2054 : /**
2055 : * List the GuiInterface functions that can be safely called by GUI scripts.
2056 : * (GUI scripts are non-deterministic and untrusted, so these functions must be
2057 : * appropriately careful. They are called with a first argument "player", which is
2058 : * trusted and indicates the player associated with the current client; no data should
2059 : * be returned unless this player is meant to be able to see it.)
2060 : */
2061 1 : let exposedFunctions = {
2062 :
2063 : "GetSimulationState": 1,
2064 : "GetExtendedSimulationState": 1,
2065 : "GetInitAttributes": 1,
2066 : "GetReplayMetadata": 1,
2067 : "GetCampaignGameEndData": 1,
2068 : "GetRenamedEntities": 1,
2069 : "ClearRenamedEntities": 1,
2070 : "GetEntityState": 1,
2071 : "GetMultipleEntityStates": 1,
2072 : "GetAverageRangeForBuildings": 1,
2073 : "GetTemplateData": 1,
2074 : "AreRequirementsMet": 1,
2075 : "CheckTechnologyRequirements": 1,
2076 : "GetStartedResearch": 1,
2077 : "GetBattleState": 1,
2078 : "GetIncomingAttacks": 1,
2079 : "GetNeededResources": 1,
2080 : "GetNotifications": 1,
2081 : "GetTimeNotifications": 1,
2082 :
2083 : "GetAvailableFormations": 1,
2084 : "GetFormationRequirements": 1,
2085 : "CanMoveEntsIntoFormation": 1,
2086 : "IsFormationSelected": 1,
2087 : "GetFormationInfoFromTemplate": 1,
2088 : "IsStanceSelected": 1,
2089 :
2090 : "UpdateDisplayedPlayerColors": 1,
2091 : "SetSelectionHighlight": 1,
2092 : "GetAllBuildableEntities": 1,
2093 : "SetStatusBars": 1,
2094 : "GetPlayerEntities": 1,
2095 : "GetNonGaiaEntities": 1,
2096 : "DisplayRallyPoint": 1,
2097 : "AddTargetMarker": 1,
2098 : "SetBuildingPlacementPreview": 1,
2099 : "SetWallPlacementPreview": 1,
2100 : "GetFoundationSnapData": 1,
2101 : "PlaySound": 1,
2102 : "PlaySoundForPlayer": 1,
2103 : "FindIdleUnits": 1,
2104 : "HasIdleUnits": 1,
2105 : "GetTradingRouteGain": 1,
2106 : "GetTradingDetails": 1,
2107 : "CanAttack": 1,
2108 : "GetBatchTime": 1,
2109 :
2110 : "IsMapRevealed": 1,
2111 : "SetPathfinderDebugOverlay": 1,
2112 : "SetPathfinderHierDebugOverlay": 1,
2113 : "SetObstructionDebugOverlay": 1,
2114 : "SetMotionDebugOverlay": 1,
2115 : "SetRangeDebugOverlay": 1,
2116 : "EnableVisualRangeOverlayType": 1,
2117 : "SetRangeOverlays": 1,
2118 :
2119 : "GetTraderNumber": 1,
2120 : "GetTradingGoods": 1,
2121 : "IsTemplateModified": 1,
2122 : "ResetTemplateModified": 1,
2123 : "IsSelectionDirty": 1,
2124 : "ResetSelectionDirty": 1
2125 : };
2126 :
2127 1 : GuiInterface.prototype.ScriptCall = function(player, name, args)
2128 : {
2129 0 : if (exposedFunctions[name])
2130 0 : return this[name](player, args);
2131 :
2132 0 : throw new Error("Invalid GuiInterface Call name \"" + name + "\"");
2133 : };
2134 :
2135 1 : Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
|