Ticket #1187: Ticket#1187.patch

File Ticket#1187.patch, 46.8 KB (added by James, 12 years ago)

A Patch to allow Tab key to show (while depressed) status bars for all friendly units on screen. (hopefully)

  • data/config/default.cfg

     
    190190hotkey.timewarp.fastforward = Space         ; If timewarp mode enabled, speed up the game
    191191hotkey.timewarp.rewind = Backspace          ; If timewarp mode enabled, go back to earlier point in the game
    192192
     193
    193194; > OVERLAY KEYS
    194195hotkey.fps.toggle = "Alt+F"                  ; Toggle frame counter
    195196hotkey.session.devcommands.toggle = "Alt+D"  ; Toggle developer commands panel
    196197hotkey.session.gui.toggle = "Alt+G"          ; Toggle visibility of session GUI
    197198hotkey.menu.toggle = "F10"                   ; Toggle in-game menu
    198199hotkey.timeelapsedcounter.toggle = "F12"     ; Toggle time elapsed counter
     200hotkey.healthbars.toggle = Tab               ; Toggle display of health bars
    199201
     202
    200203; > HOTKEYS ONLY
    201204hotkey.chat = Return                        ; Toggle chat window
    202205
  • data/mods/public/gui/session/input.js

     
    586586                inputState = INPUT_BUILDING_DRAG;
    587587                return false;
    588588            }
    589             break;
     589            break;e
    590590
    591591        case "mousebuttonup":
    592592            if (ev.button == SDL_BUTTON_LEFT)
     
    714714        else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.rewind")
    715715            Engine.RewindTimeWarp();
    716716    }
     717   
     718    // Handle the hotkey to show all status bars from friendly units
     719    if(ev.type == "hotkeydown" && ev.hotkey == "healthbars.toggle")
     720    {
     721       
     722            var ents = Engine.PickFriendlyEntitiesOnScreen(Engine.GetPlayerID()); // find all friendly units on screen
     723            Engine.GuiInterfaceCall("SetStatusBars", { "entities":ents, "enabled":true }); // show status bars for all friendly units on screen
     724    }
     725    // Hide status bars again when key is released
     726    else if(ev.type == "hotkeyup" && ev.hotkey == "healthbars.toggle")
     727    {
     728            var ents = Engine.PickFriendlyEntitiesOnScreen(Engine.GetPlayerID());
     729            var selected = g_Selection.toList();
     730            var highlighted = Engine.PickEntitiesAtPoint(mouseX, mouseY);   
     731            Engine.GuiInterfaceCall("SetStatusBars", { "entities":ents, "enabled":false }); // Hide all status bars
     732            Engine.GuiInterfaceCall("SetStatusBars", { "entities":highlighted, "enabled":true }); //Replace status bars selected/highlighted units
     733            Engine.GuiInterfaceCall("SetStatusBars", { "entities":selected, "enabled":true });
     734    }
     735   
    717736
    718737    // State-machine processing:
    719738
  • data/mods/public/simulation/components/GuiInterface.js

     
    1 function GuiInterface() {}
    2 
    3 GuiInterface.prototype.Schema =
    4     "<a:component type='system'/><empty/>";
    5 
    6 GuiInterface.prototype.Serialize = function()
    7 {
    8     // This component isn't network-synchronised so we mustn't serialise
    9     // its non-deterministic data. Instead just return an empty object.
    10     return {};
    11 };
    12 
    13 GuiInterface.prototype.Deserialize = function(obj)
    14 {
    15     this.Init();
    16 };
    17 
    18 GuiInterface.prototype.Init = function()
    19 {
    20     this.placementEntity = undefined; // = undefined or [templateName, entityID]
    21     this.rallyPoints = undefined;
    22     this.notifications = [];
    23     this.renamedEntities = [];
    24 };
    25 
    26 /*
    27  * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
    28  * from GUI scripts, and executed here with arguments (player, arg).
    29  *
    30  * CAUTION: The input to the functions in this module is not network-synchronised, so it
    31  * mustn't affect the simulation state (i.e. the data that is serialised and can affect
    32  * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
    33  */
    34 
    35 /**
    36  * Returns global information about the current game state.
    37  * This is used by the GUI and also by AI scripts.
    38  */
    39 GuiInterface.prototype.GetSimulationState = function(player)
    40 {
    41     var ret = {
    42         "players": []
    43     };
    44    
    45     var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
    46     var n = cmpPlayerMan.GetNumPlayers();
    47     for (var i = 0; i < n; ++i)
    48     {
    49         var playerEnt = cmpPlayerMan.GetPlayerByID(i);
    50         var cmpPlayerBuildLimits = Engine.QueryInterface(playerEnt, IID_BuildLimits);
    51         var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
    52        
    53         // store player ally/enemy data as arrays
    54         var allies = [];
    55         var enemies = [];
    56         for (var j = 0; j <= n; ++j)
    57         {
    58             allies[j] = cmpPlayer.IsAlly(j);
    59             enemies[j] = cmpPlayer.IsEnemy(j);
    60         }
    61         var playerData = {
    62             "name": cmpPlayer.GetName(),
    63             "civ": cmpPlayer.GetCiv(),
    64             "colour": cmpPlayer.GetColour(),
    65             "popCount": cmpPlayer.GetPopulationCount(),
    66             "popLimit": cmpPlayer.GetPopulationLimit(),
    67             "popMax": cmpPlayer.GetMaxPopulation(),
    68             "resourceCounts": cmpPlayer.GetResourceCounts(),
    69             "trainingQueueBlocked": cmpPlayer.IsTrainingQueueBlocked(),
    70             "state": cmpPlayer.GetState(),
    71             "team": cmpPlayer.GetTeam(),
    72             "phase": cmpPlayer.GetPhase(),
    73             "isAlly": allies,
    74             "isEnemy": enemies,
    75             "buildLimits": cmpPlayerBuildLimits.GetLimits(),
    76             "buildCounts": cmpPlayerBuildLimits.GetCounts()
    77         };
    78         ret.players.push(playerData);
    79     }
    80 
    81     var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    82     if (cmpRangeManager)
    83     {
    84         ret.circularMap = cmpRangeManager.GetLosCircular();
    85     }
    86    
    87     // Add timeElapsed
    88     var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    89     ret.timeElapsed = cmpTimer.GetTime();
    90 
    91     return ret;
    92 };
    93 
    94 GuiInterface.prototype.GetExtendedSimulationState = function(player)
    95 {
    96     // Get basic simulation info
    97     var ret = this.GetSimulationState();
    98 
    99     // Add statistics to each player
    100     var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
    101     var n = cmpPlayerMan.GetNumPlayers();
    102     for (var i = 0; i < n; ++i)
    103     {
    104         var playerEnt = cmpPlayerMan.GetPlayerByID(i);
    105         var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
    106         ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics();
    107     }
    108 
    109     return ret;
    110 };
    111 
    112 GuiInterface.prototype.GetRenamedEntities = function(player)
    113 {
    114     return this.renamedEntities;
    115 };
    116 
    117 GuiInterface.prototype.ClearRenamedEntities = function(player)
    118 {
    119     this.renamedEntities = [];
    120 };
    121 
    122 GuiInterface.prototype.GetEntityState = function(player, ent)
    123 {
    124     var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
    125 
    126     // All units must have a template; if not then it's a nonexistent entity id
    127     var template = cmpTempMan.GetCurrentTemplateName(ent);
    128     if (!template)
    129         return null;
    130 
    131     var ret = {
    132         "id": ent,
    133         "template": template
    134     }
    135 
    136     var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
    137     if (cmpIdentity)
    138     {
    139         ret.identity = {
    140             "rank": cmpIdentity.GetRank(),
    141             "classes": cmpIdentity.GetClassesList(),
    142             "selectionGroupName": cmpIdentity.GetSelectionGroupName()
    143         };
    144     }
    145    
    146     var cmpPosition = Engine.QueryInterface(ent, IID_Position);
    147     if (cmpPosition && cmpPosition.IsInWorld())
    148     {
    149         ret.position = cmpPosition.GetPosition();
    150     }
    151 
    152     var cmpHealth = Engine.QueryInterface(ent, IID_Health);
    153     if (cmpHealth)
    154     {
    155         ret.hitpoints = cmpHealth.GetHitpoints();
    156         ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
    157         ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
    158     }
    159 
    160     var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
    161     if (cmpAttack)
    162     {
    163         var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show?
    164         ret.attack = cmpAttack.GetAttackStrengths(type);
    165     }
    166 
    167     var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
    168     if (cmpArmour)
    169     {
    170         ret.armour = cmpArmour.GetArmourStrengths();
    171     }
    172 
    173     var cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
    174     if (cmpBuilder)
    175     {
    176         ret.buildEntities = cmpBuilder.GetEntitiesList();
    177     }
    178 
    179     var cmpTrainingQueue = Engine.QueryInterface(ent, IID_TrainingQueue);
    180     if (cmpTrainingQueue)
    181     {
    182         ret.training = {
    183             "entities": cmpTrainingQueue.GetEntitiesList(),
    184             "queue": cmpTrainingQueue.GetQueue(),
    185         };
    186     }
    187 
    188     var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
    189     if (cmpFoundation)
    190     {
    191         ret.foundation = {
    192             "progress": cmpFoundation.GetBuildPercentage()
    193         };
    194     }
    195 
    196     var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
    197     if (cmpOwnership)
    198     {
    199         ret.player = cmpOwnership.GetOwner();
    200     }
    201 
    202     var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
    203     if (cmpResourceSupply)
    204     {
    205         ret.resourceSupply = {
    206             "max": cmpResourceSupply.GetMaxAmount(),
    207             "amount": cmpResourceSupply.GetCurrentAmount(),
    208             "type": cmpResourceSupply.GetType()
    209         };
    210     }
    211 
    212     var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
    213     if (cmpResourceGatherer)
    214     {
    215         ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
    216         ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
    217     }
    218 
    219     var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
    220     if (cmpResourceDropsite)
    221     {
    222         ret.resourceDropsite = {
    223             "types": cmpResourceDropsite.GetTypes()
    224         };
    225     }
    226 
    227     var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
    228     if (cmpRallyPoint)
    229     {
    230         ret.rallyPoint = {'position': cmpRallyPoint.GetPosition()}; // undefined or {x,z} object
    231     }
    232 
    233     var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
    234     if (cmpGarrisonHolder)
    235     {
    236         ret.garrisonHolder = {
    237             "entities": cmpGarrisonHolder.GetEntities(),
    238             "allowedClasses": cmpGarrisonHolder.GetAllowedClassesList()
    239         };
    240     }
    241    
    242     var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
    243     if (cmpPromotion)
    244     {
    245         ret.promotion = {
    246             "curr": cmpPromotion.GetCurrentXp(),
    247             "req": cmpPromotion.GetRequiredXp()
    248         };
    249     }
    250    
    251     var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
    252     if (cmpUnitAI)
    253     {
    254         ret.unitAI = {
    255             // TODO: reading properties directly is kind of violating abstraction
    256             "state": cmpUnitAI.fsmStateName,
    257             "orders": cmpUnitAI.orderQueue,
    258         };
    259     }
    260 
    261     if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
    262     {
    263         var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
    264         ret.barterMarket = { "prices": cmpBarter.GetPrices() };
    265     }
    266 
    267     var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    268     ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
    269 
    270     return ret;
    271 };
    272 
    273 GuiInterface.prototype.GetTemplateData = function(player, name)
    274 {
    275     var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
    276     var template = cmpTempMan.GetTemplate(name);
    277 
    278     if (!template)
    279         return null;
    280 
    281     var ret = {};
    282 
    283     if (template.Armour)
    284     {
    285         ret.armour = {
    286             "hack": +template.Armour.Hack,
    287             "pierce": +template.Armour.Pierce,
    288             "crush": +template.Armour.Crush,
    289         };
    290     }
    291    
    292     if (template.Attack)
    293     {
    294         ret.attack = {};
    295         for (var type in template.Attack)
    296         {
    297             ret.attack[type] = {
    298                 "hack": (+template.Attack[type].Hack || 0),
    299                 "pierce": (+template.Attack[type].Pierce || 0),
    300                 "crush": (+template.Attack[type].Crush || 0),
    301             };
    302         }
    303     }
    304    
    305     if (template.Cost)
    306     {
    307         ret.cost = {};
    308         if (template.Cost.Resources.food) ret.cost.food = +template.Cost.Resources.food;
    309         if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood;
    310         if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone;
    311         if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal;
    312         if (template.Cost.Population) ret.cost.population = +template.Cost.Population;
    313         if (template.Cost.PopulationBonus) ret.cost.populationBonus = +template.Cost.PopulationBonus;
    314     }
    315    
    316     if (template.Health)
    317     {
    318         ret.health = +template.Health.Max;
    319     }
    320 
    321     if (template.Identity)
    322     {
    323         ret.selectionGroupName = template.Identity.SelectionGroupName;
    324         ret.name = {
    325             "specific": (template.Identity.SpecificName || template.Identity.GenericName),
    326             "generic": template.Identity.GenericName
    327         };
    328         ret.icon = template.Identity.Icon;
    329         ret.tooltip =  template.Identity.Tooltip;
    330     }
    331 
    332     if (template.UnitMotion)
    333     {
    334         ret.speed = {
    335             "walk": +template.UnitMotion.WalkSpeed,
    336         };
    337         if (template.UnitMotion.Run) ret.speed.run = +template.UnitMotion.Run.Speed;
    338     }
    339 
    340     return ret;
    341 };
    342 
    343 GuiInterface.prototype.PushNotification = function(notification)
    344 {
    345     this.notifications.push(notification);
    346 };
    347 
    348 GuiInterface.prototype.GetNextNotification = function()
    349 {
    350     if (this.notifications.length)
    351         return this.notifications.pop();
    352     else
    353         return "";
    354 };
    355 
    356 GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
    357 {
    358     return CanMoveEntsIntoFormation(data.ents, data.formationName);
    359 };
    360 
    361 GuiInterface.prototype.IsStanceSelected = function(player, data)
    362 {
    363     for each (var ent in data.ents)
    364     {
    365         var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
    366         if (cmpUnitAI)
    367         {
    368             if (cmpUnitAI.GetStanceName() == data.stance)
    369                 return true;
    370         }
    371     }
    372     return false;
    373 };
    374 
    375 GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
    376 {
    377     var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
    378 
    379     var playerColours = {}; // cache of owner -> colour map
    380    
    381     for each (var ent in cmd.entities)
    382     {
    383         var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
    384         if (!cmpSelectable)
    385             continue;
    386 
    387         if (cmd.alpha == 0)
    388         {
    389             cmpSelectable.SetSelectionHighlight({"r":0, "g":0, "b":0, "a":0});
    390             continue;
    391         }
    392 
    393         // Find the entity's owner's colour:
    394 
    395         var owner = -1;
    396         var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
    397         if (cmpOwnership)
    398             owner = cmpOwnership.GetOwner();
    399 
    400         var colour = playerColours[owner];
    401         if (!colour)
    402         {
    403             colour = {"r":1, "g":1, "b":1};
    404             var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player);
    405             if (cmpPlayer)
    406                 colour = cmpPlayer.GetColour();
    407             playerColours[owner] = colour;
    408         }
    409 
    410         cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":cmd.alpha});
    411     }
    412 };
    413 
    414 GuiInterface.prototype.SetStatusBars = function(player, cmd)
    415 {
    416     for each (var ent in cmd.entities)
    417     {
    418         var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
    419         if (cmpStatusBars)
    420             cmpStatusBars.SetEnabled(cmd.enabled);
    421     }
    422 };
    423 
    424 /**
    425  * Displays the rally point of a given list of entities (carried in cmd.entities).
    426  *
    427  * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
    428  * be rendered, in order to support instantaneously rendering a rally point marker at a specified location
    429  * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
    430  * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
    431  * RallyPoint component.
    432  */
    433 GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
    434 {
    435     var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
    436     var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player);
    437 
    438     // If there are some rally points already displayed, first hide them
    439     for each (var ent in this.entsRallyPointsDisplayed)
    440     {
    441         var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
    442         if (cmpRallyPointRenderer)
    443             cmpRallyPointRenderer.SetDisplayed(false);
    444     }
    445    
    446     this.entsRallyPointsDisplayed = [];
    447    
    448     // Show the rally points for the passed entities
    449     for each (var ent in cmd.entities)
    450     {
    451         var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
    452         if (!cmpRallyPointRenderer)
    453             continue;
    454        
    455         // entity must have a rally point component to display a rally point marker
    456         // (regardless of whether cmd specifies a custom location)
    457         var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
    458         if (!cmpRallyPoint)
    459             continue;
    460 
    461         // Verify the owner
    462         var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
    463         if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
    464             if (!cmpOwnership || cmpOwnership.GetOwner() != player)
    465                 continue;
    466 
    467         // If the command was passed an explicit position, use that and
    468         // override the real rally point position; otherwise use the real position
    469         var pos;
    470         if (cmd.x && cmd.z)
    471             pos = cmd;
    472         else
    473             pos = cmpRallyPoint.GetPosition(); // may return undefined if no rally point is set
    474 
    475         if (pos)
    476         {
    477             cmpRallyPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
    478             cmpRallyPointRenderer.SetDisplayed(true);
    479            
    480             // remember which entities have their rally points displayed so we can hide them again
    481             this.entsRallyPointsDisplayed.push(ent);
    482         }
    483     }
    484 };
    485 
    486 /**
    487  * Display the building placement preview.
    488  * cmd.template is the name of the entity template, or "" to disable the preview.
    489  * cmd.x, cmd.z, cmd.angle give the location.
    490  * Returns true if the placement is okay (everything is valid and the entity is not obstructed by others).
    491  */
    492 GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
    493 {
    494     // See if we're changing template
    495     if (!this.placementEntity || this.placementEntity[0] != cmd.template)
    496     {
    497         // Destroy the old preview if there was one
    498         if (this.placementEntity)
    499             Engine.DestroyEntity(this.placementEntity[1]);
    500 
    501         // Load the new template
    502         if (cmd.template == "")
    503         {
    504             this.placementEntity = undefined;
    505         }
    506         else
    507         {
    508             this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
    509         }
    510     }
    511 
    512     if (this.placementEntity)
    513     {
    514         var ent = this.placementEntity[1];
    515 
    516         // Move the preview into the right location
    517         var pos = Engine.QueryInterface(ent, IID_Position);
    518         if (pos)
    519         {
    520             pos.JumpTo(cmd.x, cmd.z);
    521             pos.SetYRotation(cmd.angle);
    522         }
    523 
    524         // Check whether it's in a visible or fogged region
    525         //  tell GetLosVisibility to force RetainInFog because preview entities set this to false,
    526         //  which would show them as hidden instead of fogged
    527         var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    528         var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
    529         var validPlacement = false;
    530 
    531         if (visible)
    532         {   // Check whether it's obstructed by other entities or invalid terrain
    533             var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
    534             if (!cmpBuildRestrictions)
    535                 error("cmpBuildRestrictions not defined");
    536 
    537             validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement(player));
    538         }
    539 
    540         var ok = (visible && validPlacement);
    541 
    542         // Set it to a red shade if this is an invalid location
    543         var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
    544         if (cmpVisual)
    545         {
    546             if (!ok)
    547                 cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1);
    548             else
    549                 cmpVisual.SetShadingColour(1, 1, 1, 1);
    550         }
    551 
    552         return ok;
    553     }
    554 
    555     return false;
    556 };
    557 
    558 GuiInterface.prototype.GetFoundationSnapData = function(player, data)
    559 {
    560     var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
    561     var template = cmpTemplateMgr.GetTemplate(data.template);
    562 
    563     if (template.BuildRestrictions.Category == "Dock")
    564     {
    565         var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
    566         var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
    567         if (!cmpTerrain || !cmpWaterManager)
    568         {
    569             return false;
    570         }
    571        
    572         // Get footprint size
    573         var halfSize = 0;
    574         if (template.Footprint.Square)
    575         {
    576             halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
    577         }
    578         else if (template.Footprint.Circle)
    579         {
    580             halfSize = template.Footprint.Circle["@radius"];
    581         }
    582        
    583         /* Find direction of most open water, algorithm:
    584          *  1. Pick points in a circle around dock
    585          *  2. If point is in water, add to array
    586          *  3. Scan array looking for consecutive points
    587          *  4. Find longest sequence of consecutive points
    588          *  5. If sequence equals all points, no direction can be determined,
    589          *      expand search outward and try (1) again
    590          *  6. Calculate angle using average of sequence
    591          */
    592         const numPoints = 16;
    593         for (var dist = 0; dist < 4; ++dist)
    594         {
    595             var waterPoints = [];
    596             for (var i = 0; i < numPoints; ++i)
    597             {
    598                 var angle = (i/numPoints)*2*Math.PI;
    599                 var d = halfSize*(dist+1);
    600                 var nx = data.x - d*Math.sin(angle);
    601                 var nz = data.z + d*Math.cos(angle);
    602                
    603                 if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
    604                 {
    605                     waterPoints.push(i);
    606                 }
    607             }
    608             var consec = [];
    609             var length = waterPoints.length;
    610             for (var i = 0; i < length; ++i)
    611             {
    612                 var count = 0;
    613                 for (var j = 0; j < (length-1); ++j)
    614                 {
    615                     if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length])
    616                     {
    617                         ++count;
    618                     }
    619                     else
    620                     {
    621                         break;
    622                     }
    623                 }
    624                 consec[i] = count;
    625             }
    626             var start = 0;
    627             var count = 0;
    628             for (var c in consec)
    629             {
    630                 if (consec[c] > count)
    631                 {
    632                     start = c;
    633                     count = consec[c];
    634                 }
    635             }
    636            
    637             // If we've found a shoreline, stop searching
    638             if (count != numPoints-1)
    639             {
    640                 return {"x": data.x, "z": data.z, "angle": -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI)};
    641             }
    642         }
    643     }
    644 
    645     return false;
    646 };
    647 
    648 GuiInterface.prototype.PlaySound = function(player, data)
    649 {
    650     // Ignore if no entity was passed
    651     if (!data.entity)
    652         return;
    653 
    654     PlaySound(data.name, data.entity);
    655 };
    656 
    657 function isIdleUnit(ent, idleClass)
    658 {
    659     var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
    660     var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
    661    
    662     // TODO: Do something with garrisoned idle units
    663     return (cmpUnitAI && cmpIdentity && cmpUnitAI.IsIdle() && !cmpUnitAI.IsGarrisoned() && idleClass && cmpIdentity.HasClass(idleClass));
    664 }
    665 
    666 GuiInterface.prototype.FindIdleUnit = function(player, data)
    667 {
    668     var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    669     var playerEntities = rangeMan.GetEntitiesByPlayer(player);
    670 
    671     // Find the first matching entity that is after the previous selection,
    672     // so that we cycle around in a predictable order
    673     for each (var ent in playerEntities)
    674     {
    675         if (ent > data.prevUnit && isIdleUnit(ent, data.idleClass))
    676             return ent;
    677     }
    678 
    679     // No idle entities left in the class
    680     return 0;
    681 };
    682 
    683 GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
    684 {
    685     var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
    686     cmpPathfinder.SetDebugOverlay(enabled);
    687 };
    688 
    689 GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
    690 {
    691     var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
    692     cmpObstructionManager.SetDebugOverlay(enabled);
    693 };
    694 
    695 GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
    696 {
    697     for each (var ent in data.entities)
    698     {
    699         var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
    700         if (cmpUnitMotion)
    701             cmpUnitMotion.SetDebugOverlay(data.enabled);
    702     }
    703 };
    704 
    705 GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
    706 {
    707     var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    708     cmpRangeManager.SetDebugOverlay(enabled);
    709 };
    710 
    711 GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
    712 {
    713     this.renamedEntities.push(msg);
    714 }
    715 
    716 // List the GuiInterface functions that can be safely called by GUI scripts.
    717 // (GUI scripts are non-deterministic and untrusted, so these functions must be
    718 // appropriately careful. They are called with a first argument "player", which is
    719 // trusted and indicates the player associated with the current client; no data should
    720 // be returned unless this player is meant to be able to see it.)
    721 var exposedFunctions = {
    722    
    723     "GetSimulationState": 1,
    724     "GetExtendedSimulationState": 1,
    725     "GetRenamedEntities": 1,
    726     "ClearRenamedEntities": 1,
    727     "GetEntityState": 1,
    728     "GetTemplateData": 1,
    729     "GetNextNotification": 1,
    730 
    731     "CanMoveEntsIntoFormation": 1,
    732     "IsStanceSelected": 1,
    733 
    734     "SetSelectionHighlight": 1,
    735     "SetStatusBars": 1,
    736     "DisplayRallyPoint": 1,
    737     "SetBuildingPlacementPreview": 1,
    738     "GetFoundationSnapData": 1,
    739     "PlaySound": 1,
    740     "FindIdleUnit": 1,
    741 
    742     "SetPathfinderDebugOverlay": 1,
    743     "SetObstructionDebugOverlay": 1,
    744     "SetMotionDebugOverlay": 1,
    745     "SetRangeDebugOverlay": 1,
    746 };
    747 
    748 GuiInterface.prototype.ScriptCall = function(player, name, args)
    749 {
    750     if (exposedFunctions[name])
    751         return this[name](player, args);
    752     else
    753         throw new Error("Invalid GuiInterface Call name \""+name+"\"");
    754 };
    755 
    756 Engine.RegisterComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
     1function GuiInterface() {}
     2
     3GuiInterface.prototype.Schema =
     4    "<a:component type='system'/><empty/>";
     5
     6GuiInterface.prototype.Serialize = function()
     7{
     8    // This component isn't network-synchronised so we mustn't serialise
     9    // its non-deterministic data. Instead just return an empty object.
     10    return {};
     11};
     12
     13GuiInterface.prototype.Deserialize = function(obj)
     14{
     15    this.Init();
     16};
     17
     18GuiInterface.prototype.Init = function()
     19{
     20    this.placementEntity = undefined; // = undefined or [templateName, entityID]
     21    this.rallyPoints = undefined;
     22    this.notifications = [];
     23    this.renamedEntities = [];
     24};
     25
     26/*
     27 * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
     28 * from GUI scripts, and executed here with arguments (player, arg).
     29 *
     30 * CAUTION: The input to the functions in this module is not network-synchronised, so it
     31 * mustn't affect the simulation state (i.e. the data that is serialised and can affect
     32 * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
     33 */
     34
     35/**
     36 * Returns global information about the current game state.
     37 * This is used by the GUI and also by AI scripts.
     38 */
     39GuiInterface.prototype.GetSimulationState = function(player)
     40{
     41    var ret = {
     42        "players": []
     43    };
     44   
     45    var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     46    var n = cmpPlayerMan.GetNumPlayers();
     47    for (var i = 0; i < n; ++i)
     48    {
     49        var playerEnt = cmpPlayerMan.GetPlayerByID(i);
     50        var cmpPlayerBuildLimits = Engine.QueryInterface(playerEnt, IID_BuildLimits);
     51        var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
     52       
     53        // store player ally/enemy data as arrays
     54        var allies = [];
     55        var enemies = [];
     56        for (var j = 0; j <= n; ++j)
     57        {
     58            allies[j] = cmpPlayer.IsAlly(j);
     59            enemies[j] = cmpPlayer.IsEnemy(j);
     60        }
     61        var playerData = {
     62            "name": cmpPlayer.GetName(),
     63            "civ": cmpPlayer.GetCiv(),
     64            "colour": cmpPlayer.GetColour(),
     65            "popCount": cmpPlayer.GetPopulationCount(),
     66            "popLimit": cmpPlayer.GetPopulationLimit(),
     67            "popMax": cmpPlayer.GetMaxPopulation(),
     68            "resourceCounts": cmpPlayer.GetResourceCounts(),
     69            "trainingQueueBlocked": cmpPlayer.IsTrainingQueueBlocked(),
     70            "state": cmpPlayer.GetState(),
     71            "team": cmpPlayer.GetTeam(),
     72            "phase": cmpPlayer.GetPhase(),
     73            "isAlly": allies,
     74            "isEnemy": enemies,
     75            "buildLimits": cmpPlayerBuildLimits.GetLimits(),
     76            "buildCounts": cmpPlayerBuildLimits.GetCounts()
     77        };
     78        ret.players.push(playerData);
     79    }
     80
     81    var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     82    if (cmpRangeManager)
     83    {
     84        ret.circularMap = cmpRangeManager.GetLosCircular();
     85    }
     86   
     87    // Add timeElapsed
     88    var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     89    ret.timeElapsed = cmpTimer.GetTime();
     90
     91    return ret;
     92};
     93
     94GuiInterface.prototype.GetExtendedSimulationState = function(player)
     95{
     96    // Get basic simulation info
     97    var ret = this.GetSimulationState();
     98
     99    // Add statistics to each player
     100    var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     101    var n = cmpPlayerMan.GetNumPlayers();
     102    for (var i = 0; i < n; ++i)
     103    {
     104        var playerEnt = cmpPlayerMan.GetPlayerByID(i);
     105        var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
     106        ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics();
     107    }
     108
     109    return ret;
     110};
     111
     112GuiInterface.prototype.GetRenamedEntities = function(player)
     113{
     114    return this.renamedEntities;
     115};
     116
     117GuiInterface.prototype.ClearRenamedEntities = function(player)
     118{
     119    this.renamedEntities = [];
     120};
     121
     122GuiInterface.prototype.GetEntityState = function(player, ent)
     123{
     124    var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
     125
     126    // All units must have a template; if not then it's a nonexistent entity id
     127    var template = cmpTempMan.GetCurrentTemplateName(ent);
     128    if (!template)
     129        return null;
     130
     131    var ret = {
     132        "id": ent,
     133        "template": template
     134    }
     135
     136    var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
     137    if (cmpIdentity)
     138    {
     139        ret.identity = {
     140            "rank": cmpIdentity.GetRank(),
     141            "classes": cmpIdentity.GetClassesList(),
     142            "selectionGroupName": cmpIdentity.GetSelectionGroupName()
     143        };
     144    }
     145   
     146    var cmpPosition = Engine.QueryInterface(ent, IID_Position);
     147    if (cmpPosition && cmpPosition.IsInWorld())
     148    {
     149        ret.position = cmpPosition.GetPosition();
     150    }
     151
     152    var cmpHealth = Engine.QueryInterface(ent, IID_Health);
     153    if (cmpHealth)
     154    {
     155        ret.hitpoints = cmpHealth.GetHitpoints();
     156        ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
     157        ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
     158    }
     159
     160    var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
     161    if (cmpAttack)
     162    {
     163        var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show?
     164        ret.attack = cmpAttack.GetAttackStrengths(type);
     165    }
     166
     167    var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
     168    if (cmpArmour)
     169    {
     170        ret.armour = cmpArmour.GetArmourStrengths();
     171    }
     172
     173    var cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
     174    if (cmpBuilder)
     175    {
     176        ret.buildEntities = cmpBuilder.GetEntitiesList();
     177    }
     178
     179    var cmpTrainingQueue = Engine.QueryInterface(ent, IID_TrainingQueue);
     180    if (cmpTrainingQueue)
     181    {
     182        ret.training = {
     183            "entities": cmpTrainingQueue.GetEntitiesList(),
     184            "queue": cmpTrainingQueue.GetQueue(),
     185        };
     186    }
     187
     188    var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
     189    if (cmpFoundation)
     190    {
     191        ret.foundation = {
     192            "progress": cmpFoundation.GetBuildPercentage()
     193        };
     194    }
     195
     196    var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
     197    if (cmpOwnership)
     198    {
     199        ret.player = cmpOwnership.GetOwner();
     200    }
     201
     202    var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
     203    if (cmpResourceSupply)
     204    {
     205        ret.resourceSupply = {
     206            "max": cmpResourceSupply.GetMaxAmount(),
     207            "amount": cmpResourceSupply.GetCurrentAmount(),
     208            "type": cmpResourceSupply.GetType()
     209        };
     210    }
     211
     212    var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
     213    if (cmpResourceGatherer)
     214    {
     215        ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
     216        ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
     217    }
     218
     219    var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
     220    if (cmpResourceDropsite)
     221    {
     222        ret.resourceDropsite = {
     223            "types": cmpResourceDropsite.GetTypes()
     224        };
     225    }
     226
     227    var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
     228    if (cmpRallyPoint)
     229    {
     230        ret.rallyPoint = {'position': cmpRallyPoint.GetPosition()}; // undefined or {x,z} object
     231    }
     232
     233    var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
     234    if (cmpGarrisonHolder)
     235    {
     236        ret.garrisonHolder = {
     237            "entities": cmpGarrisonHolder.GetEntities(),
     238            "allowedClasses": cmpGarrisonHolder.GetAllowedClassesList()
     239        };
     240    }
     241   
     242    var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
     243    if (cmpPromotion)
     244    {
     245        ret.promotion = {
     246            "curr": cmpPromotion.GetCurrentXp(),
     247            "req": cmpPromotion.GetRequiredXp()
     248        };
     249    }
     250   
     251    var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     252    if (cmpUnitAI)
     253    {
     254        ret.unitAI = {
     255            // TODO: reading properties directly is kind of violating abstraction
     256            "state": cmpUnitAI.fsmStateName,
     257            "orders": cmpUnitAI.orderQueue,
     258        };
     259    }
     260
     261    if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
     262    {
     263        var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
     264        ret.barterMarket = { "prices": cmpBarter.GetPrices() };
     265    }
     266
     267    var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     268    ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
     269
     270    return ret;
     271};
     272
     273GuiInterface.prototype.GetTemplateData = function(player, name)
     274{
     275    var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
     276    var template = cmpTempMan.GetTemplate(name);
     277
     278    if (!template)
     279        return null;
     280
     281    var ret = {};
     282
     283    if (template.Armour)
     284    {
     285        ret.armour = {
     286            "hack": +template.Armour.Hack,
     287            "pierce": +template.Armour.Pierce,
     288            "crush": +template.Armour.Crush,
     289        };
     290    }
     291   
     292    if (template.Attack)
     293    {
     294        ret.attack = {};
     295        for (var type in template.Attack)
     296        {
     297            ret.attack[type] = {
     298                "hack": (+template.Attack[type].Hack || 0),
     299                "pierce": (+template.Attack[type].Pierce || 0),
     300                "crush": (+template.Attack[type].Crush || 0),
     301            };
     302        }
     303    }
     304   
     305    if (template.Cost)
     306    {
     307        ret.cost = {};
     308        if (template.Cost.Resources.food) ret.cost.food = +template.Cost.Resources.food;
     309        if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood;
     310        if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone;
     311        if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal;
     312        if (template.Cost.Population) ret.cost.population = +template.Cost.Population;
     313        if (template.Cost.PopulationBonus) ret.cost.populationBonus = +template.Cost.PopulationBonus;
     314    }
     315   
     316    if (template.Health)
     317    {
     318        ret.health = +template.Health.Max;
     319    }
     320
     321    if (template.Identity)
     322    {
     323        ret.selectionGroupName = template.Identity.SelectionGroupName;
     324        ret.name = {
     325            "specific": (template.Identity.SpecificName || template.Identity.GenericName),
     326            "generic": template.Identity.GenericName
     327        };
     328        ret.icon = template.Identity.Icon;
     329        ret.tooltip =  template.Identity.Tooltip;
     330    }
     331
     332    if (template.UnitMotion)
     333    {
     334        ret.speed = {
     335            "walk": +template.UnitMotion.WalkSpeed,
     336        };
     337        if (template.UnitMotion.Run) ret.speed.run = +template.UnitMotion.Run.Speed;
     338    }
     339
     340    return ret;
     341};
     342
     343GuiInterface.prototype.PushNotification = function(notification)
     344{
     345    this.notifications.push(notification);
     346};
     347
     348GuiInterface.prototype.GetNextNotification = function()
     349{
     350    if (this.notifications.length)
     351        return this.notifications.pop();
     352    else
     353        return "";
     354};
     355
     356GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
     357{
     358    return CanMoveEntsIntoFormation(data.ents, data.formationName);
     359};
     360
     361GuiInterface.prototype.IsStanceSelected = function(player, data)
     362{
     363    for each (var ent in data.ents)
     364    {
     365        var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     366        if (cmpUnitAI)
     367        {
     368            if (cmpUnitAI.GetStanceName() == data.stance)
     369                return true;
     370        }
     371    }
     372    return false;
     373};
     374
     375GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
     376{
     377    var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     378
     379    var playerColours = {}; // cache of owner -> colour map
     380   
     381    for each (var ent in cmd.entities)
     382    {
     383        var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
     384        if (!cmpSelectable)
     385            continue;
     386
     387        if (cmd.alpha == 0)
     388        {
     389            cmpSelectable.SetSelectionHighlight({"r":0, "g":0, "b":0, "a":0});
     390            continue;
     391        }
     392
     393        // Find the entity's owner's colour:
     394
     395        var owner = -1;
     396        var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
     397        if (cmpOwnership)
     398            owner = cmpOwnership.GetOwner();
     399
     400        var colour = playerColours[owner];
     401        if (!colour)
     402        {
     403            colour = {"r":1, "g":1, "b":1};
     404            var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player);
     405            if (cmpPlayer)
     406                colour = cmpPlayer.GetColour();
     407            playerColours[owner] = colour;
     408        }
     409
     410        cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":cmd.alpha});
     411    }
     412};
     413
     414GuiInterface.prototype.SetStatusBars = function(player, cmd)
     415{
     416    for each (var ent in cmd.entities)
     417    {
     418        var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
     419        if (cmpStatusBars)
     420            cmpStatusBars.SetEnabled(cmd.enabled);
     421    }
     422};
     423
     424/**
     425 * Displays the rally point of a given list of entities (carried in cmd.entities).
     426 *
     427 * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
     428 * be rendered, in order to support instantaneously rendering a rally point marker at a specified location
     429 * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
     430 * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
     431 * RallyPoint component.
     432 */
     433GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
     434{
     435    var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     436    var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player);
     437
     438    // If there are some rally points already displayed, first hide them
     439    for each (var ent in this.entsRallyPointsDisplayed)
     440    {
     441        var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
     442        if (cmpRallyPointRenderer)
     443            cmpRallyPointRenderer.SetDisplayed(false);
     444    }
     445   
     446    this.entsRallyPointsDisplayed = [];
     447   
     448    // Show the rally points for the passed entities
     449    for each (var ent in cmd.entities)
     450    {
     451        var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
     452        if (!cmpRallyPointRenderer)
     453            continue;
     454       
     455        // entity must have a rally point component to display a rally point marker
     456        // (regardless of whether cmd specifies a custom location)
     457        var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
     458        if (!cmpRallyPoint)
     459            continue;
     460
     461        // Verify the owner
     462        var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
     463        if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
     464            if (!cmpOwnership || cmpOwnership.GetOwner() != player)
     465                continue;
     466
     467        // If the command was passed an explicit position, use that and
     468        // override the real rally point position; otherwise use the real position
     469        var pos;
     470        if (cmd.x && cmd.z)
     471            pos = cmd;
     472        else
     473            pos = cmpRallyPoint.GetPosition(); // may return undefined if no rally point is set
     474
     475        if (pos)
     476        {
     477            cmpRallyPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
     478            cmpRallyPointRenderer.SetDisplayed(true);
     479           
     480            // remember which entities have their rally points displayed so we can hide them again
     481            this.entsRallyPointsDisplayed.push(ent);
     482        }
     483    }
     484};
     485
     486/**
     487 * Display the building placement preview.
     488 * cmd.template is the name of the entity template, or "" to disable the preview.
     489 * cmd.x, cmd.z, cmd.angle give the location.
     490 * Returns true if the placement is okay (everything is valid and the entity is not obstructed by others).
     491 */
     492GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
     493{
     494    // See if we're changing template
     495    if (!this.placementEntity || this.placementEntity[0] != cmd.template)
     496    {
     497        // Destroy the old preview if there was one
     498        if (this.placementEntity)
     499            Engine.DestroyEntity(this.placementEntity[1]);
     500
     501        // Load the new template
     502        if (cmd.template == "")
     503        {
     504            this.placementEntity = undefined;
     505        }
     506        else
     507        {
     508            this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
     509        }
     510    }
     511
     512    if (this.placementEntity)
     513    {
     514        var ent = this.placementEntity[1];
     515
     516        // Move the preview into the right location
     517        var pos = Engine.QueryInterface(ent, IID_Position);
     518        if (pos)
     519        {
     520            pos.JumpTo(cmd.x, cmd.z);
     521            pos.SetYRotation(cmd.angle);
     522        }
     523
     524        // Check whether it's in a visible or fogged region
     525        //  tell GetLosVisibility to force RetainInFog because preview entities set this to false,
     526        //  which would show them as hidden instead of fogged
     527        var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     528        var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
     529        var validPlacement = false;
     530
     531        if (visible)
     532        {   // Check whether it's obstructed by other entities or invalid terrain
     533            var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
     534            if (!cmpBuildRestrictions)
     535                error("cmpBuildRestrictions not defined");
     536
     537            validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement(player));
     538        }
     539
     540        var ok = (visible && validPlacement);
     541
     542        // Set it to a red shade if this is an invalid location
     543        var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
     544        if (cmpVisual)
     545        {
     546            if (!ok)
     547                cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1);
     548            else
     549                cmpVisual.SetShadingColour(1, 1, 1, 1);
     550        }
     551
     552        return ok;
     553    }
     554
     555    return false;
     556};
     557
     558GuiInterface.prototype.GetFoundationSnapData = function(player, data)
     559{
     560    var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
     561    var template = cmpTemplateMgr.GetTemplate(data.template);
     562
     563    if (template.BuildRestrictions.Category == "Dock")
     564    {
     565        var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
     566        var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
     567        if (!cmpTerrain || !cmpWaterManager)
     568        {
     569            return false;
     570        }
     571       
     572        // Get footprint size
     573        var halfSize = 0;
     574        if (template.Footprint.Square)
     575        {
     576            halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
     577        }
     578        else if (template.Footprint.Circle)
     579        {
     580            halfSize = template.Footprint.Circle["@radius"];
     581        }
     582       
     583        /* Find direction of most open water, algorithm:
     584         *  1. Pick points in a circle around dock
     585         *  2. If point is in water, add to array
     586         *  3. Scan array looking for consecutive points
     587         *  4. Find longest sequence of consecutive points
     588         *  5. If sequence equals all points, no direction can be determined,
     589         *      expand search outward and try (1) again
     590         *  6. Calculate angle using average of sequence
     591         */
     592        const numPoints = 16;
     593        for (var dist = 0; dist < 4; ++dist)
     594        {
     595            var waterPoints = [];
     596            for (var i = 0; i < numPoints; ++i)
     597            {
     598                var angle = (i/numPoints)*2*Math.PI;
     599                var d = halfSize*(dist+1);
     600                var nx = data.x - d*Math.sin(angle);
     601                var nz = data.z + d*Math.cos(angle);
     602               
     603                if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
     604                {
     605                    waterPoints.push(i);
     606                }
     607            }
     608            var consec = [];
     609            var length = waterPoints.length;
     610            for (var i = 0; i < length; ++i)
     611            {
     612                var count = 0;
     613                for (var j = 0; j < (length-1); ++j)
     614                {
     615                    if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length])
     616                    {
     617                        ++count;
     618                    }
     619                    else
     620                    {
     621                        break;
     622                    }
     623                }
     624                consec[i] = count;
     625            }
     626            var start = 0;
     627            var count = 0;
     628            for (var c in consec)
     629            {
     630                if (consec[c] > count)
     631                {
     632                    start = c;
     633                    count = consec[c];
     634                }
     635            }
     636           
     637            // If we've found a shoreline, stop searching
     638            if (count != numPoints-1)
     639            {
     640                return {"x": data.x, "z": data.z, "angle": -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI)};
     641            }
     642        }
     643    }
     644
     645    return false;
     646};
     647
     648GuiInterface.prototype.PlaySound = function(player, data)
     649{
     650    // Ignore if no entity was passed
     651    if (!data.entity)
     652        return;
     653
     654    PlaySound(data.name, data.entity);
     655};
     656
     657function isIdleUnit(ent, idleClass)
     658{
     659    var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     660    var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
     661   
     662    // TODO: Do something with garrisoned idle units
     663    return (cmpUnitAI && cmpIdentity && cmpUnitAI.IsIdle() && !cmpUnitAI.IsGarrisoned() && idleClass && cmpIdentity.HasClass(idleClass));
     664}
     665
     666GuiInterface.prototype.FindIdleUnit = function(player, data)
     667{
     668    var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     669    var playerEntities = rangeMan.GetEntitiesByPlayer(player);
     670
     671    // Find the first matching entity that is after the previous selection,
     672    // so that we cycle around in a predictable order
     673    for each (var ent in playerEntities)
     674    {
     675        if (ent > data.prevUnit && isIdleUnit(ent, data.idleClass))
     676            return ent;
     677    }
     678
     679    // No idle entities left in the class
     680    return 0;
     681};
     682
     683GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
     684{
     685    var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
     686    cmpPathfinder.SetDebugOverlay(enabled);
     687};
     688
     689GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
     690{
     691    var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
     692    cmpObstructionManager.SetDebugOverlay(enabled);
     693};
     694
     695GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
     696{
     697    for each (var ent in data.entities)
     698    {
     699        var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
     700        if (cmpUnitMotion)
     701            cmpUnitMotion.SetDebugOverlay(data.enabled);
     702    }
     703};
     704
     705GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
     706{
     707    var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     708    cmpRangeManager.SetDebugOverlay(enabled);
     709};
     710
     711GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
     712{
     713    this.renamedEntities.push(msg);
     714}
     715
     716// List the GuiInterface functions that can be safely called by GUI scripts.
     717// (GUI scripts are non-deterministic and untrusted, so these functions must be
     718// appropriately careful. They are called with a first argument "player", which is
     719// trusted and indicates the player associated with the current client; no data should
     720// be returned unless this player is meant to be able to see it.)
     721var exposedFunctions = {
     722   
     723    "GetSimulationState": 1,
     724    "GetExtendedSimulationState": 1,
     725    "GetRenamedEntities": 1,
     726    "ClearRenamedEntities": 1,
     727    "GetEntityState": 1,
     728    "GetTemplateData": 1,
     729    "GetNextNotification": 1,
     730
     731    "CanMoveEntsIntoFormation": 1,
     732    "IsStanceSelected": 1,
     733
     734    "SetSelectionHighlight": 1,
     735    "SetStatusBars": 1,
     736    "DisplayRallyPoint": 1,
     737    "SetBuildingPlacementPreview": 1,
     738    "GetFoundationSnapData": 1,
     739    "PlaySound": 1,
     740    "FindIdleUnit": 1,
     741
     742    "SetPathfinderDebugOverlay": 1,
     743    "SetObstructionDebugOverlay": 1,
     744    "SetMotionDebugOverlay": 1,
     745    "SetRangeDebugOverlay": 1,
     746};
     747
     748GuiInterface.prototype.ScriptCall = function(player, name, args)
     749{
     750    if (exposedFunctions[name])
     751        return this[name](player, args);
     752    else
     753        throw new Error("Invalid GuiInterface Call name \""+name+"\"");
     754};
     755
     756Engine.RegisterComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);