Ticket #786: 786_walls_16apr12.patch

File 786_walls_16apr12.patch, 179.1 KB (added by vts, 12 years ago)

WIP patch of the wall system. Start with -quickstart -autostart="WallTest" to play with it.

  • binaries/data/mods/public/gui/session/input.js

    diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js
    index 23ed23a..8034091 100644
    a b const ACTION_GARRISON = 1;  
    1717const ACTION_REPAIR = 2;
    1818var preSelectedAction = ACTION_NONE;
    1919
    20 var INPUT_NORMAL = 0;
    21 var INPUT_SELECTING = 1;
    22 var INPUT_BANDBOXING = 2;
    23 var INPUT_BUILDING_PLACEMENT = 3;
    24 var INPUT_BUILDING_CLICK = 4;
    25 var INPUT_BUILDING_DRAG = 5;
    26 var INPUT_BATCHTRAINING = 6;
    27 var INPUT_PRESELECTEDACTION = 7;
     20const INPUT_NORMAL = 0;
     21const INPUT_SELECTING = 1;
     22const INPUT_BANDBOXING = 2;
     23const INPUT_BUILDING_PLACEMENT = 3;
     24const INPUT_BUILDING_CLICK = 4;
     25const INPUT_BUILDING_DRAG = 5;
     26const INPUT_BATCHTRAINING = 6;
     27const INPUT_PRESELECTEDACTION = 7;
     28const INPUT_BUILDING_WALL_CLICK = 8;
     29const INPUT_BUILDING_WALL_PATHING = 9;
    2830
    2931var inputState = INPUT_NORMAL;
    3032
    31 var defaultPlacementAngle = Math.PI*3/4;
    32 var placementAngle = undefined;
    33 var placementPosition = undefined;
    34 var placementEntity = undefined;
     33const defaultPlacementAngle = Math.PI*3/4;
     34var placementSupport = new PlacementSupport();
    3535
    3636var mouseX = 0;
    3737var mouseY = 0;
    function updateCursorAndTooltip()  
    8888function updateBuildingPlacementPreview()
    8989{
    9090    // The preview should be recomputed every turn, so that it responds
    91     // to obstructions/fog/etc moving underneath it
     91    // to obstructions/fog/etc moving underneath it, or in the case of
     92    // the wall previews, in response to new tower foundations getting
     93    // constructed for it to snap to.
    9294
    93     if (placementEntity && placementPosition)
     95    if (placementSupport.mode === "building")
    9496    {
    95         return Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
    96             "template": placementEntity,
    97             "x": placementPosition.x,
    98             "z": placementPosition.z,
    99             "angle": placementAngle
    100         });
     97        if (placementSupport.template && placementSupport.position)
     98        {
     99            return Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
     100                "template": placementSupport.template,
     101                "x": placementSupport.position.x,
     102                "z": placementSupport.position.z,
     103                "angle": placementSupport.angle,
     104            });
     105        }
     106    }
     107    else if (placementSupport.mode === "wall")
     108    {
     109        if (placementSupport.wallSet && placementSupport.position)
     110        {
     111            // Fetch an updated list of snapping candidate entities
     112            placementSupport.wallSnapEntities = Engine.PickSimilarFriendlyEntities(
     113                placementSupport.wallSet.tower,
     114                placementSupport.wallSnapEntitiesIncludeOffscreen,
     115                true, // require exact template match
     116                true  // include foundations
     117            );
     118           
     119            return Engine.GuiInterfaceCall("SetWallPlacementPreview", {
     120                "wallSet": placementSupport.wallSet,
     121                "start": placementSupport.position,
     122                "end": placementSupport.wallEndPosition,
     123                "snapEntities": placementSupport.wallSnapEntities,  // snapping entities (towers) for starting a wall segment
     124            });
     125        }
    101126    }
    102127
    103128    return false;
    104129}
    105130
    106 function resetPlacementEntity()
    107 {
    108     Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
    109     placementEntity = undefined;
    110     placementPosition = undefined;
    111     placementAngle = undefined;
    112 }
    113 
    114131function findGatherType(gatherer, supply)
    115132{
    116133    if (!gatherer || !supply)
    var dragStart; // used for remembering mouse coordinates at start of drag operat  
    434451
    435452function tryPlaceBuilding(queued)
    436453{
     454    if (placementSupport.mode !== "building")
     455    {
     456        error("[tryPlaceBuilding] Called while in '"+placementSupport.mode+"' placement mode instead of 'building'");
     457        return false;
     458    }
     459   
    437460    var selection = g_Selection.toList();
    438461
    439462    // Use the preview to check it's a valid build location
    function tryPlaceBuilding(queued)  
    447470    // Start the construction
    448471    Engine.PostNetworkCommand({
    449472        "type": "construct",
    450         "template": placementEntity,
    451         "x": placementPosition.x,
    452         "z": placementPosition.z,
    453         "angle": placementAngle,
     473        "template": placementSupport.template,
     474        "x": placementSupport.position.x,
     475        "z": placementSupport.position.z,
     476        "angle": placementSupport.angle,
    454477        "entities": selection,
    455478        "autorepair": true,
    456479        "autocontinue": true,
    function tryPlaceBuilding(queued)  
    459482    Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
    460483
    461484    if (!queued)
    462         resetPlacementEntity();
     485        placementSupport.Reset();
     486
     487    return true;
     488}
     489
     490function tryPlaceWall()
     491{
     492    if (placementSupport.mode !== "wall")
     493    {
     494        error("[tryPlaceWall] Called while in '" + placementSupport.mode + "' placement mode; expected 'wall' mode");
     495        return false;
     496    }
     497   
     498    var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...)
     499    if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object"))
     500    {
     501        error("[tryPlaceWall] Unexpected return value from updateBuildingPlacementPreview: '" + uneval(placementInfo) + "'; expected either 'false' or 'object'");
     502        return false;
     503    }
     504   
     505    if (!wallPlacementInfo)
     506        return false;
     507   
     508    var selection = g_Selection.toList();
     509    var cmd = {
     510        "type": "construct-wall",
     511        "autorepair": true,
     512        "autocontinue": true,
     513        "queued": true,
     514        "entities": selection,
     515        "wallSet": placementSupport.wallSet,
     516        "pieces": wallPlacementInfo.pieces,
     517        "startSnappedEntity": wallPlacementInfo.startSnappedEnt,
     518        "endSnappedEntity": wallPlacementInfo.endSnappedEnt,
     519    };
     520   
     521    //warn("-------------");
     522    //for (var k in cmd)
     523    //  warn("  " + k + ": " + uneval(cmd[k]));
     524    //warn("-------------");
     525   
     526    /*for each (var entInfo in wallPlacementInfo.pieces)
     527    {
     528        cmd.pieces.push({
     529            "template": entInfo.template,
     530            "x": entInfo.x,
     531            "z": entInfo.z,
     532            "angle": entInfo.angle,
     533        });
     534    }*/
     535   
     536    Engine.PostNetworkCommand(cmd);
     537    Engine.GuiInterfaceCall("PlaySound", {"name": "order_repair", "entity": selection[0] });
    463538
    464539    return true;
    465540}
    function handleInputBeforeGui(ev, hoveredObject)  
    659734            if (ev.button == SDL_BUTTON_RIGHT)
    660735            {
    661736                // Cancel building
    662                 resetPlacementEntity();
     737                placementSupport.Reset();
    663738                inputState = INPUT_NORMAL;
    664739                return true;
    665740            }
    function handleInputBeforeGui(ev, hoveredObject)  
    667742        }
    668743        break;
    669744
     745    case INPUT_BUILDING_WALL_CLICK:
     746        // User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point
     747        // by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode.
     748        switch (ev.type)
     749        {
     750        case "mousebuttonup":
     751            if (ev.button === SDL_BUTTON_LEFT)
     752            {
     753                inputState = INPUT_BUILDING_WALL_PATHING; warn("-> INPUT_BUILDING_WALL_PATHING");
     754                return true;
     755            }
     756            break;
     757           
     758        case "mousebuttondown":
     759            if (ev.button == SDL_BUTTON_RIGHT)
     760            {
     761                // Cancel building
     762                placementSupport.Reset();
     763                updateBuildingPlacementPreview();
     764               
     765                inputState = INPUT_NORMAL; warn("-> INPUT_NORMAL");
     766                return true;
     767            }
     768            break;
     769        }
     770        break;
     771   
     772    case INPUT_BUILDING_WALL_PATHING:
     773        // User has chosen a starting point for constructing the wall, and is now looking to set the endpoint.
     774        // Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to
     775        // normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the
     776        // user to continue building walls.
     777        switch (ev.type)
     778        {
     779            case "mousemotion":
     780                placementSupport.wallEndPosition = Engine.GetTerrainAtPoint(ev.x, ev.y);
     781               
     782                // Update the building placement preview, and by extension, the list of snapping candidate entities for both (!)
     783                // the ending point and the starting point to snap to.
     784                //
     785                // TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case
     786                // where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a
     787                // foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on
     788                // the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers
     789                // in them. Might be useful to query only for entities within a certain range around the starting point and ending
     790                // points.
     791               
     792                placementSupport.wallSnapEntitiesIncludeOffscreen = true;
     793                updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
     794                break;
     795               
     796            case "mousebuttondown":
     797                if (ev.button == SDL_BUTTON_LEFT)
     798                {
     799                    if (tryPlaceWall())
     800                    {
     801                        if (Engine.HotkeyIsPressed("session.queue"))
     802                        {
     803                            // continue building, just set a new starting position where we left off
     804                            placementSupport.position = placementSupport.wallEndPosition;
     805                            placementSupport.wallEndPosition = undefined;
     806                           
     807                            inputState = INPUT_BUILDING_WALL_CLICK; warn("-> INPUT_BUILDING_WALL_CLICK");
     808                        }
     809                        else
     810                        {
     811                            placementSupport.Reset();
     812                            inputState = INPUT_NORMAL; warn("-> INPUT_NORMAL");
     813                        }
     814                    }
     815                    else
     816                    {
     817                        // TODO: display some useful error message to the player
     818                        error("Cannot build wall here!");
     819                    }
     820                   
     821                    updateBuildingPlacementPreview();
     822                    return true;
     823                }
     824                else if (ev.button == SDL_BUTTON_RIGHT)
     825                {
     826                    // reset to normal input mode
     827                    placementSupport.Reset();
     828                    updateBuildingPlacementPreview();
     829                   
     830                    inputState = INPUT_NORMAL; warn("-> INPUT_NORMAL");
     831                    return true;
     832                }
     833                break;
     834        }
     835        break;
     836       
    670837    case INPUT_BUILDING_DRAG:
    671838        switch (ev.type)
    672839        {
    function handleInputBeforeGui(ev, hoveredObject)  
    678845            {
    679846                // Rotate in the direction of the mouse
    680847                var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
    681                 placementAngle = Math.atan2(target.x - placementPosition.x, target.z - placementPosition.z);
     848                placementSupport.angle = Math.atan2(target.x - placementSupport.position.x, target.z - placementSupport.position.z);
    682849            }
    683850            else
    684851            {
    685852                // If the mouse is near the center, snap back to the default orientation
    686                 placementAngle = defaultPlacementAngle;
     853                placementSupport.SetDefaultAngle();
    687854            }
    688855
    689856            var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
    690                 "template": placementEntity,
    691                 "x": placementPosition.x,
    692                 "z": placementPosition.z
     857                "template": placementSupport.template,
     858                "x": placementSupport.position.x,
     859                "z": placementSupport.position.z
    693860            });
    694861            if (snapData)
    695862            {
    696                 placementAngle = snapData.angle;
    697                 placementPosition.x = snapData.x;
    698                 placementPosition.z = snapData.z;
     863                placementSupport.angle = snapData.angle;
     864                placementSupport.position.x = snapData.x;
     865                placementSupport.position.z = snapData.z;
    699866            }
    700867
    701868            updateBuildingPlacementPreview();
    function handleInputBeforeGui(ev, hoveredObject)  
    725892            if (ev.button == SDL_BUTTON_RIGHT)
    726893            {
    727894                // Cancel building
    728                 resetPlacementEntity();
     895                placementSupport.Reset();
    729896                inputState = INPUT_NORMAL;
    730897                return true;
    731898            }
    function handleInputAfterGui(ev)  
    819986                break;
    820987        }
    821988        break;
     989       
    822990    case INPUT_PRESELECTEDACTION:
    823991        switch (ev.type)
    824992        {
    function handleInputAfterGui(ev)  
    8491017            }
    8501018        }
    8511019        break;
     1020       
    8521021    case INPUT_SELECTING:
    8531022        switch (ev.type)
    8541023        {
    function handleInputAfterGui(ev)  
    9221091                    }
    9231092
    9241093                    // TODO: Should we handle "control all units" here as well?
    925                     ents = Engine.PickSimilarFriendlyEntities(templateToMatch, showOffscreen, matchRank);
     1094                    ents = Engine.PickSimilarFriendlyEntities(templateToMatch, showOffscreen, matchRank, false);
    9261095                }
    9271096                else
    9281097                {
    function handleInputAfterGui(ev)  
    9611130        switch (ev.type)
    9621131        {
    9631132        case "mousemotion":
    964             placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y);
    965             var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
    966                 "template": placementEntity,
    967                 "x": placementPosition.x,
    968                 "z": placementPosition.z
    969             });
    970             if (snapData)
     1133           
     1134            placementSupport.position = Engine.GetTerrainAtPoint(ev.x, ev.y);
     1135           
     1136            if (placementSupport.mode === "wall")
    9711137            {
    972                 placementAngle = snapData.angle;
    973                 placementPosition.x = snapData.x;
    974                 placementPosition.z = snapData.z;
     1138                // Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is
     1139                // still selecting a starting point (which must necessarily be on-screen). (The update itself happens in the
     1140                // call to updateBuildingPlacementPreview below).
     1141                placementSupport.wallSnapEntitiesIncludeOffscreen = false;
     1142            }
     1143            else
     1144            {
     1145                var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
     1146                    "template": placementSupport.template,
     1147                    "x": placementSupport.position.x,
     1148                    "z": placementSupport.position.z,
     1149                });
     1150                if (snapData)
     1151                {
     1152                    placementSupport.angle = snapData.angle;
     1153                    placementSupport.position.x = snapData.x;
     1154                    placementSupport.position.z = snapData.z;
     1155                }
    9751156            }
    9761157
    977             updateBuildingPlacementPreview();
    978 
     1158            updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
    9791159            return false; // continue processing mouse motion
    9801160
    9811161        case "mousebuttondown":
    9821162            if (ev.button == SDL_BUTTON_LEFT)
    9831163            {
    984                 placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y);
    985                 dragStart = [ ev.x, ev.y ];
    986                 inputState = INPUT_BUILDING_CLICK;
     1164                if (placementSupport.mode === "wall")
     1165                {
     1166                    var validPlacement = updateBuildingPlacementPreview();
     1167                    if (validPlacement !== false)
     1168                    {
     1169                        inputState = INPUT_BUILDING_WALL_CLICK;
     1170                    }
     1171                    else
     1172                    {
     1173                        // TODO: add a user-friendly notification
     1174                        error("Not a valid starting location!");
     1175                    }
     1176                }
     1177                else
     1178                {
     1179                    placementSupport.position = Engine.GetTerrainAtPoint(ev.x, ev.y);
     1180                    dragStart = [ ev.x, ev.y ];
     1181                    inputState = INPUT_BUILDING_CLICK;
     1182                }
    9871183                return true;
    9881184            }
    9891185            else if (ev.button == SDL_BUTTON_RIGHT)
    9901186            {
    9911187                // Cancel building
    992                 resetPlacementEntity();
     1188                placementSupport.Reset();
    9931189                inputState = INPUT_NORMAL;
    9941190                return true;
    9951191            }
    function handleInputAfterGui(ev)  
    10021198            switch (ev.hotkey)
    10031199            {
    10041200            case "session.rotate.cw":
    1005                 placementAngle += rotation_step;
     1201                placementSupport.angle += rotation_step;
    10061202                updateBuildingPlacementPreview();
    10071203                break;
    10081204            case "session.rotate.ccw":
    1009                 placementAngle -= rotation_step;
     1205                placementSupport.angle -= rotation_step;
    10101206                updateBuildingPlacementPreview();
    10111207                break;
    10121208            }
    function handleMinimapEvent(target)  
    11441340}
    11451341
    11461342// Called by GUI when user clicks construction button
    1147 function startBuildingPlacement(buildEntType)
     1343// @param buildTemplate Template name of the entity the user wants to build
     1344function startBuildingPlacement(buildTemplate)
    11481345{
    1149     placementEntity = buildEntType;
    1150     placementAngle = defaultPlacementAngle;
    1151     inputState = INPUT_BUILDING_PLACEMENT;
     1346    // TODO: we should clear any highlight selection rings here. If the mouse was over an entity before going onto the GUI
     1347    // to start building a structure, then the highlight selection rings are kept during the construction of the building.
     1348    // Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing.
     1349   
     1350    placementSupport.SetDefaultAngle();
     1351   
     1352    // find out if we're building a wall, and change the entity appropriately if so
     1353    var templateData = GetTemplateData(buildTemplate);
     1354    if (templateData.wallSet)
     1355    {
     1356        placementSupport.mode = "wall";
     1357        placementSupport.wallSet = templateData.wallSet;
     1358        inputState = INPUT_BUILDING_PLACEMENT;
     1359    }
     1360    else
     1361    {
     1362        placementSupport.mode = "building";
     1363        placementSupport.template = buildTemplate;
     1364        inputState = INPUT_BUILDING_PLACEMENT;
     1365    }
    11521366}
    11531367
    11541368// Called by GUI when user changes preferred trading goods
  • new file inaries/data/mods/public/gui/session/placement.js

    diff --git a/binaries/data/mods/public/gui/session/placement.js b/binaries/data/mods/public/gui/session/placement.js
    new file mode 100644
    index 0000000..29d5d04
    - +  
     1function PlacementSupport()
     2{
     3    this.Reset();
     4}
     5
     6PlacementSupport.DEFAULT_ANGLE = Math.PI*3/4;
     7
     8/**
     9 * Resets the building placement support state. Use this to cancel construction of an entity.
     10 */
     11PlacementSupport.prototype.Reset = function()
     12{
     13    this.mode = null;
     14    this.position = null;
     15    this.template = null;
     16    this.wallSet = null;             // maps types of wall pieces ("tower", "long", "short", ...) to template names
     17    this.wallSnapEntities = null;    // list of candidate entities to snap the starting and (!) ending positions to when building walls
     18    this.wallEndPosition = null;
     19    this.wallSnapEntitiesIncludeOffscreen = false; // should the next update of the snap candidate list include offscreen towers?
     20   
     21    this.SetDefaultAngle();
     22   
     23    warn("PlacementSupport::Reset");
     24    Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
     25    Engine.GuiInterfaceCall("SetWallPlacementPreview", {"wallSet": null});
     26};
     27
     28PlacementSupport.prototype.SetDefaultAngle = function()
     29{
     30    this.angle = PlacementSupport.DEFAULT_ANGLE;
     31};
     32 No newline at end of file
  • binaries/data/mods/public/gui/session/session.js

    diff --git a/binaries/data/mods/public/gui/session/session.js b/binaries/data/mods/public/gui/session/session.js
    index 8ecf776..c981c18 100644
    a b function getSavedGameData()  
    169169}
    170170
    171171var lastTickTime = new Date;
     172
     173/**
     174 * Called every frame.
     175 */
    172176function onTick()
    173177{
    174178    var now = new Date;
    function onTick()  
    187191
    188192    updateCursorAndTooltip();
    189193
    190     // If the selection changed, we need to regenerate the sim display
     194    // If the selection changed, we need to regenerate the sim display (the display depends on both the
     195    // simulation state and the current selection).
    191196    if (g_Selection.dirty)
    192197    {
    193198        onSimulationUpdate();
    function checkPlayerState()  
    252257    }
    253258}
    254259
     260/**
     261 * Recomputes GUI state that depends on simulation state or selection state. Called directly every simulation
     262 * update (see session.xml), or from onTick when the selection has changed.
     263 */
    255264function onSimulationUpdate()
    256265{
    257266    g_Selection.dirty = false;
  • binaries/data/mods/public/gui/session/session.xml

    diff --git a/binaries/data/mods/public/gui/session/session.xml b/binaries/data/mods/public/gui/session/session.xml
    index 5de7e23..f5a37f3 100644
    a b  
    99    <script file="gui/common/timer.js"/>
    1010    <script file="gui/session/session.js"/>
    1111    <script file="gui/session/selection.js"/>
     12    <script file="gui/session/placement.js"/>
    1213    <script file="gui/session/input.js"/>
    1314    <script file="gui/session/menu.js"/>
    1415    <script file="gui/session/selection_details.js"/>
  • binaries/data/mods/public/gui/session/unit_commands.js

    diff --git a/binaries/data/mods/public/gui/session/unit_commands.js b/binaries/data/mods/public/gui/session/unit_commands.js
    index 8e8e2da..0d84dde 100644
    a b function layoutButtonRow(rowNumber, guiName, buttonSideLength, buttonSpacer, sta  
    120120    }
    121121}
    122122
    123 // Sets up "unit panels" - the panels with rows of icons (Helper function for updateUnitDisplay)
     123/**
     124 * Helper function for updateUnitCommands; sets up "unit panels" (i.e. panels with rows of icons) for the currently selected
     125 * unit.
     126 *
     127 * @param guiName Short identifier string of this panel; see constants defined at the top of this file.
     128 * @param usedPanels Output object; usedPanels[guiName] will be set to 1 to indicate that this panel was used during this
     129 *                     run of updateUnitCommands and should not be hidden. TODO: why is this done this way instead of having
     130 *                     updateUnitCommands keep track of this?
     131 * @param unitEntState Entity state of the (first) selected unit.
     132 * @param items Panel-specific data to construct the icons with.
     133 * @param callback Callback function to argument to execute when an item's icon gets clicked. Takes a single 'item' argument.
     134 */
    124135function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
    125136{
    126137    usedPanels[guiName] = 1;
    function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)  
    183194    for (i = 0; i < numberOfItems; i++)
    184195    {
    185196        var item = items[i];
    186         var entType = ((guiName == "Queue")? item.template : item);
     197        var entType = ((guiName == "Queue") ? item.template : item);
    187198        var template;
    188199        if (guiName != "Formation" && guiName != "Command" && guiName != "Stance")
    189200        {
    function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)  
    236247                var [batchSize, batchIncrement] = getTrainingQueueBatchStatus(unitEntState.id, entType);
    237248                var trainNum = batchSize ? batchSize+batchIncrement : batchIncrement;
    238249
    239                 tooltip += "\n" + getEntityCost(template);
     250                tooltip += "\n" + getEntityCostTooltip(template);
    240251
    241252                if (template.health)
    242253                    tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + template.health;
    function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)  
    256267                if (template.tooltip)
    257268                    tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]";
    258269
    259                 tooltip += "\n" + getEntityCost(template);
     270                tooltip += "\n" + getEntityCostTooltip(template); // see utility_functions.js
     271                tooltip += getPopulationBonusTooltip(template); // see utility_functions.js
    260272
    261                 tooltip += getPopulationBonus(template);
    262273                if (template.health)
    263274                    tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + template.health;
    264275
    function setupUnitBarterPanel(unitEntState)  
    483494    }
    484495}
    485496
    486 // Updates right Unit Commands Panel - runs in the main session loop via updateSelectionDetails()
     497/**
     498 * Updates the right hand side "Unit Commands" panel. Runs in the main session loop via updateSelectionDetails().
     499 * Delegates to setupUnitPanel to set up individual subpanels, appropriately activated depending on the selected
     500 * unit's state.
     501 *
     502 * @param entState Entity state of the (first) selected unit.
     503 * @param supplementalDetailsPanel Reference to the "supplementalSelectionDetails" GUI Object
     504 * @param selection Array of currently selected entity IDs.
     505 */
    487506function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection)
    488507{
    489508    // Panels that are active
  • binaries/data/mods/public/gui/session/utility_functions.js

    diff --git a/binaries/data/mods/public/gui/session/utility_functions.js b/binaries/data/mods/public/gui/session/utility_functions.js
    index 0d95a70..bbc81f7 100644
    a b function getEntityCommandsList(entState)  
    193193    return commands;
    194194}
    195195
    196 function getEntityCost(template)
     196/**
     197 * Helper function for getEntityCostTooltip.
     198 */
     199function getEntityCostComponentsTooltipString(template)
    197200{
    198     var cost = "";
    199     if (template.cost)
     201    var costs = [];
     202    if (template.cost.food) costs.push(template.cost.food + " [font=\"serif-12\"]Food[/font]");
     203    if (template.cost.wood) costs.push(template.cost.wood + " [font=\"serif-12\"]Wood[/font]");
     204    if (template.cost.metal) costs.push(template.cost.metal + " [font=\"serif-12\"]Metal[/font]");
     205    if (template.cost.stone) costs.push(template.cost.stone + " [font=\"serif-12\"]Stone[/font]");
     206    if (template.cost.population) costs.push(template.cost.population + " [font=\"serif-12\"]Population[/font]");
     207    return costs;
     208}
     209
     210/**
     211 * Returns the cost information to display in the specified entity's construction button tooltip.
     212 */
     213function getEntityCostTooltip(template)
     214{
     215    var cost = "[font=\"serif-bold-13\"]Costs:[/font] ";
     216   
     217    // Entities with a wallset component are proxies for initiating wall placement and as such do not have a cost of
     218    // their own; the individual wall pieces within it do.
     219    if (template.wallSet)
     220    {
     221        var templateLong = GetTemplateData(template.wallSet.long);
     222        var templateMedium = GetTemplateData(template.wallSet.medium);
     223        var templateShort = GetTemplateData(template.wallSet.short);
     224        var templateTower = GetTemplateData(template.wallSet.tower);
     225       
     226        // TODO: the costs of the wall segments should be the same, and for now we will assume they are (ideally we
     227        // should take the average here or something).
     228        var wallCosts = getEntityCostComponentsTooltipString(templateLong);
     229        var towerCosts = getEntityCostComponentsTooltipString(templateTower);
     230       
     231        cost += "\n";
     232        cost += " Walls:  " + wallCosts.join(", ") + "\n";
     233        cost += " Towers: " + towerCosts.join(", ");
     234    }
     235    else if (template.cost)
    200236    {
    201         var costs = [];
    202         if (template.cost.food) costs.push(template.cost.food + " [font=\"serif-12\"]Food[/font]");
    203         if (template.cost.wood) costs.push(template.cost.wood + " [font=\"serif-12\"]Wood[/font]");
    204         if (template.cost.metal) costs.push(template.cost.metal + " [font=\"serif-12\"]Metal[/font]");
    205         if (template.cost.stone) costs.push(template.cost.stone + " [font=\"serif-12\"]Stone[/font]");
    206         if (template.cost.population) costs.push(template.cost.population + " [font=\"serif-12\"]Population[/font]");
    207 
    208         cost += "[font=\"serif-bold-13\"]Costs:[/font] " + costs.join(", ");
     237        var costs = getEntityCostComponentsTooltipString(template);
     238        cost += costs.join(", ");
    209239    }
     240    else
     241    {
     242        cost = ""; // cleaner than duplicating the serif-bold-13 stuff
     243    }
     244   
    210245    return cost;
    211246}
    212247
    213 function getPopulationBonus(template)
     248/**
     249 * Returns the population bonus information to display in the specified entity's construction button tooltip.
     250 */
     251function getPopulationBonusTooltip(template)
    214252{
    215253    var popBonus = "";
    216     if (template.cost.populationBonus)
     254    if (template.cost && template.cost.populationBonus)
    217255        popBonus = "\n[font=\"serif-bold-13\"]Population Bonus:[/font] " + template.cost.populationBonus;
    218256    return popBonus;
    219257}
  • new file inaries/data/mods/public/maps/scenarios/WallTest.xml

    diff --git a/binaries/data/mods/public/maps/scenarios/WallTest.pmp b/binaries/data/mods/public/maps/scenarios/WallTest.pmp
    new file mode 100644
    index 0000000..bf23296
    Binary files /dev/null and b/binaries/data/mods/public/maps/scenarios/WallTest.pmp differ
    diff --git a/binaries/data/mods/public/maps/scenarios/WallTest.xml b/binaries/data/mods/public/maps/scenarios/WallTest.xml
    new file mode 100644
    index 0000000..44428a8
    - +  
     1<?xml version="1.0" encoding="UTF-8"?>
     2
     3<Scenario version="5">
     4    <Environment>
     5        <LightingModel>standard</LightingModel>
     6        <SkySet>default</SkySet>
     7        <SunColour r="0.74902" g="0.74902" b="0.74902"/>
     8        <SunElevation angle="0.785398"/>
     9        <SunRotation angle="-0.785396"/>
     10        <TerrainAmbientColour r="0.501961" g="0.501961" b="0.501961"/>
     11        <UnitsAmbientColour r="0.501961" g="0.501961" b="0.501961"/>
     12        <Water>
     13            <WaterBody>
     14                <Type>default</Type>
     15                <Colour r="0.294118" g="0.34902" b="0.694118"/>
     16                <Height>5</Height>
     17                <Shininess>150</Shininess>
     18                <Waviness>8</Waviness>
     19                <Murkiness>0.45</Murkiness>
     20                <Tint r="0.27451" g="0.294118" b="0.584314"/>
     21                <ReflectionTint r="0.27451" g="0.294118" b="0.584314"/>
     22                <ReflectionTintStrength>0</ReflectionTintStrength>
     23            </WaterBody>
     24        </Water>
     25    </Environment>
     26    <Camera>
     27        <Position x="292.862" y="79.7402" z="170.777"/>
     28        <Rotation angle="0"/>
     29        <Declination angle="0.610865"/>
     30    </Camera>
     31    <ScriptSettings><![CDATA[
     32{
     33  "CircularMap": true,
     34  "Description": "Give an interesting description of your map.",
     35  "GameType": "conquest",
     36  "Keywords": [],
     37  "LockTeams": false,
     38  "Name": "WallTest",
     39  "PlayerData": [
     40    {
     41      "AI": "",
     42      "Civ": "hele",
     43      "Colour": {
     44        "b": 200,
     45        "g": 46,
     46        "r": 46
     47      },
     48      "Name": "Player 1",
     49      "PopulationLimit": 100000,
     50      "Resources": {
     51        "food": 100000,
     52        "metal": 100000,
     53        "stone": 100000,
     54        "wood": 100000
     55      },
     56      "StartingCamera": {
     57        "Position": {
     58          "x": 292.862,
     59          "y": 22.3825,
     60          "z": 252.692
     61        },
     62        "Rotation": {
     63          "x": 35,
     64          "y": 0,
     65          "z": 0
     66        }
     67      },
     68      "Team": -1
     69    },
     70    {
     71      "AI": "qbot",
     72      "Civ": "hele",
     73      "Colour": {
     74        "b": 20,
     75        "g": 20,
     76        "r": 150
     77      },
     78      "Name": "Player 2",
     79      "PopulationLimit": 2,
     80      "Resources": {
     81        "food": 5000,
     82        "metal": 5000,
     83        "stone": 5000,
     84        "wood": 5000
     85      },
     86      "Team": -1
     87    }
     88  ],
     89  "RevealMap": false
     90}
     91]]></ScriptSettings>
     92    <Entities>
     93        <Entity uid="11">
     94            <Template>structures/celt_civil_centre</Template>
     95            <Player>1</Player>
     96            <Position x="286.75593" z="268.53187"/>
     97            <Orientation y="2.35621"/>
     98        </Entity>
     99        <Entity uid="12">
     100            <Template>structures/pers_civil_centre</Template>
     101            <Player>2</Player>
     102            <Position x="751.73603" z="789.12446"/>
     103            <Orientation y="2.35621"/>
     104        </Entity>
     105        <Entity uid="13">
     106            <Template>other/palisades_rocks_tower</Template>
     107            <Player>1</Player>
     108            <Position x="280.52155" z="152.9048"/>
     109            <Orientation y="2.35621"/>
     110        </Entity>
     111        <Entity uid="14">
     112            <Template>other/palisades_rocks_tower</Template>
     113            <Player>1</Player>
     114            <Position x="405.53681" z="237.12357"/>
     115            <Orientation y="2.35621"/>
     116        </Entity>
     117        <Entity uid="15">
     118            <Template>other/palisades_rocks_tower</Template>
     119            <Player>1</Player>
     120            <Position x="273.05485" z="368.58619"/>
     121            <Orientation y="2.35621"/>
     122        </Entity>
     123        <Entity uid="16">
     124            <Template>other/palisades_rocks_tower</Template>
     125            <Player>1</Player>
     126            <Position x="162.9825" z="248.80494"/>
     127            <Orientation y="2.35621"/>
     128        </Entity>
     129        <Entity uid="56">
     130            <Template>units/celt_fanatic</Template>
     131            <Player>1</Player>
     132            <Position x="297.80228" z="238.4332"/>
     133            <Orientation y="2.35621"/>
     134        </Entity>
     135        <Entity uid="57">
     136            <Template>units/celt_fanatic</Template>
     137            <Player>1</Player>
     138            <Position x="300.73261" z="240.50628"/>
     139            <Orientation y="2.35621"/>
     140        </Entity>
     141        <Entity uid="58">
     142            <Template>units/celt_fanatic</Template>
     143            <Player>1</Player>
     144            <Position x="304.17972" z="243.5565"/>
     145            <Orientation y="2.35621"/>
     146        </Entity>
     147        <Entity uid="59">
     148            <Template>units/celt_fanatic</Template>
     149            <Player>1</Player>
     150            <Position x="308.13715" z="246.98673"/>
     151            <Orientation y="2.35621"/>
     152        </Entity>
     153        <Entity uid="60">
     154            <Template>units/celt_fanatic</Template>
     155            <Player>1</Player>
     156            <Position x="312.3144" z="251.31986"/>
     157            <Orientation y="2.35621"/>
     158        </Entity>
     159        <Entity uid="61">
     160            <Template>units/celt_fanatic</Template>
     161            <Player>1</Player>
     162            <Position x="317.41407" z="256.17164"/>
     163            <Orientation y="2.35621"/>
     164        </Entity>
     165    </Entities>
     166    <Paths/>
     167</Scenario>
     168 No newline at end of file
  • binaries/data/mods/public/simulation/components/BuildRestrictions.js

    diff --git a/binaries/data/mods/public/simulation/components/BuildRestrictions.js b/binaries/data/mods/public/simulation/components/BuildRestrictions.js
    index b9d1e87..b50999c 100644
    a b BuildRestrictions.prototype.Init = function()  
    7171    this.territories = this.template.Territory.split(/\s+/);
    7272};
    7373
     74/**
     75 * Returns true iff this entity can be built at its current position.
     76 */
    7477BuildRestrictions.prototype.CheckPlacement = function(player)
    7578{
    7679    // TODO: Return error code for invalid placement, which can be handled by the UI
  • binaries/data/mods/public/simulation/components/Foundation.js

    diff --git a/binaries/data/mods/public/simulation/components/Foundation.js b/binaries/data/mods/public/simulation/components/Foundation.js
    index 14029da..f63a1e3 100644
    a b Foundation.prototype.Build = function(builderEnt, work)  
    187187        cmpBuildingPosition.SetXZRotation(rot.x, rot.z);
    188188        // TODO: should add a ICmpPosition::CopyFrom() instead of all this
    189189
     190        // ----------------------------------------------------------------------
     191       
    190192        var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    191193        var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership);
    192194        cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner());
    193195       
     196        // ----------------------------------------------------------------------
     197       
     198        // Copy over the obstruction control group IDs from the foundation entities. This is needed to ensure that when a foundation
     199        // is completed and replaced by a new entity, it remains in the same control group(s) as any other foundation entities that
     200        // may surround it. This is the mechanism that is used to e.g. enable wall pieces to be built closely together, ignoring their
     201        // mutual obstruction shapes (since they would otherwise be prevented from being built so closely together). If the control
     202        // groups are not copied over, the new entity will default to a new control group containing only itself, and will hence block
     203        // construction of any surrounding foundations that it was previously in the same control group with.
     204       
     205        // Note that this will result in the completed building entities having control group IDs that equal entity IDs of old (and soon
     206        // to be deleted) foundation entities. This should not have any consequences, however, since the control group IDs are only meant
     207        // to be unique identifiers, which is still true when reusing the old ones.
     208       
     209        var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
     210        var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction);
     211        cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
     212        cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
     213       
     214        // ----------------------------------------------------------------------
     215       
    194216        var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
    195217        cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter();
    196218
    197219        var cmpIdentity = Engine.QueryInterface(building, IID_Identity);
    198         if (cmpIdentity.GetClassesList().indexOf("CivCentre") != -1) cmpPlayerStatisticsTracker.IncreaseBuiltCivCentresCounter();
     220        if (cmpIdentity.GetClassesList().indexOf("CivCentre") != -1)
     221            cmpPlayerStatisticsTracker.IncreaseBuiltCivCentresCounter();
    199222       
    200223        var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
    201224        var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health);
    Foundation.prototype.Build = function(builderEnt, work)  
    203226
    204227        PlaySound("constructed", building);
    205228
    206         Engine.PostMessage(this.entity, MT_ConstructionFinished,
    207             { "entity": this.entity, "newentity": building });
    208 
     229        Engine.PostMessage(this.entity, MT_ConstructionFinished, { "entity": this.entity, "newentity": building });
    209230        Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: building });
    210231
    211232        Engine.DestroyEntity(this.entity);
  • binaries/data/mods/public/simulation/components/GuiInterface.js

    diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js
    index 2c4a82f..47058e7 100644
    a b GuiInterface.prototype.Deserialize = function(obj)  
    1818GuiInterface.prototype.Init = function()
    1919{
    2020    this.placementEntity = undefined; // = undefined or [templateName, entityID]
     21    this.placementWallEntities = undefined;
     22    this.placementWallLastAngle = 0;
    2123    this.rallyPoints = undefined;
    2224    this.notifications = [];
    2325    this.renamedEntities = [];
    GuiInterface.prototype.GetEntityState = function(player, ent)  
    131133    var ret = {
    132134        "id": ent,
    133135        "template": template
    134     }
     136    };
    135137
    136138    var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
    137139    if (cmpIdentity)
    GuiInterface.prototype.GetEntityState = function(player, ent)  
    202204        };
    203205    }
    204206
     207    var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
     208    if (cmpObstruction)
     209    {
     210        ret.obstruction = {
     211            "controlGroup": cmpObstruction.GetControlGroup(),
     212            "controlGroup2": cmpObstruction.GetControlGroup2(),
     213        };
     214    }
     215
    205216    var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
    206217    if (cmpOwnership)
    207218    {
    GuiInterface.prototype.GetTemplateData = function(player, name)  
    311322        }
    312323    }
    313324   
     325    if (template.BuildRestrictions)
     326    {
     327        // required properties
     328        ret.buildRestrictions = {
     329            "placementType": template.BuildRestrictions.PlacementType,
     330            "territory": template.BuildRestrictions.Territory,
     331            "category": template.BuildRestrictions.Category,
     332        };
     333       
     334        // optional properties
     335        if (template.BuildRestrictions.Distance)
     336        {
     337            ret.buildRestrictions.distance = {
     338                "fromCategory": template.BuildRestrictions.Distance.FromCategory,
     339            };
     340            if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = +template.BuildRestrictions.Distance.MinDistance;
     341            if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = +template.BuildRestrictions.Distance.MaxDistance;
     342        }
     343    }
     344   
    314345    if (template.Cost)
    315346    {
    316347        ret.cost = {};
    GuiInterface.prototype.GetTemplateData = function(player, name)  
    322353        if (template.Cost.PopulationBonus) ret.cost.populationBonus = +template.Cost.PopulationBonus;
    323354    }
    324355   
     356    if (template.Footprint)
     357    {
     358        ret.footprint = {"height": template.Footprint.Height};
     359       
     360        if (template.Footprint.Square)
     361            ret.footprint.square = {"width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"]};
     362        else if (template.Footprint.Circle)
     363            ret.footprint.circle = {"radius": +template.Footprint.Circle["@radius"]};
     364        else
     365            warn("[GetTemplateData] Unrecognized Footprint type");
     366    }
     367   
     368    if (template.Obstruction)
     369    {
     370        ret.obstruction = {
     371            "active": ("" + template.Obstruction.Active == "true"),
     372            "blockMovement": ("" + template.Obstruction.BlockMovement == "true"),
     373            "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"),
     374            "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"),
     375            "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"),
     376            "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"),
     377            "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"),
     378            "shape": {}
     379        };
     380       
     381        if (template.Obstruction.Static)
     382        {
     383            ret.obstruction.shape.type = "static";
     384            ret.obstruction.shape.width = +template.Obstruction.Static["@width"];
     385            ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"];
     386        }
     387        else
     388        {
     389            ret.obstruction.shape.type = "unit";
     390            ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"];
     391        }
     392    }
     393   
    325394    if (template.Health)
    326395    {
    327396        ret.health = +template.Health.Max;
    GuiInterface.prototype.GetTemplateData = function(player, name)  
    346415        if (template.UnitMotion.Run) ret.speed.run = +template.UnitMotion.Run.Speed;
    347416    }
    348417
     418    if (template.WallSet)
     419    {
     420        ret.wallSet = {
     421            "long": template.WallSet.WallLong,
     422            "medium": template.WallSet.WallMedium,
     423            "short": template.WallSet.WallShort,
     424            "tower": template.WallSet.Tower,
     425            "gate": template.WallSet.Gate,
     426        };
     427    }
     428   
     429    if (template.WallPiece)
     430    {
     431        ret.wallPiece = {"length": +template.WallPiece.Length};
     432    }
     433
    349434    return ret;
    350435};
    351436
    GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)  
    517602 * Display the building placement preview.
    518603 * cmd.template is the name of the entity template, or "" to disable the preview.
    519604 * cmd.x, cmd.z, cmd.angle give the location.
     605 *
    520606 * Returns true if the placement is okay (everything is valid and the entity is not obstructed by others).
    521607 */
    522608GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
    GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)  
    585671    return false;
    586672};
    587673
     674/**
     675 * 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
     676 * specified. Returns an object with information about the list of entities that need to be newly constructed to complete
     677 * 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
     678 * them can be validly constructed.
     679 *
     680 * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
     681 * another depending on things like snapping and whether some of the entities inside them can be validly positioned.
     682 * We have:
     683 *    - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
     684 *      entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
     685 *      to preview the completed tower on top of its foundation.
     686 *     
     687 *    - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
     688 *      any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
     689 *      towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
     690 *      snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
     691 *      constructed.
     692 *     
     693 *    - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
     694 *      as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
     695 *      e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
     696 *      constructed but come after said first invalid entity are also truncated away.
     697 *
     698 * 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
     699 * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
     700 * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
     701 * argument (see below). Otherwise, it will return an object with the following information:
     702 *
     703 * result: {
     704 *   'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
     705 *   'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
     706 *                    can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
     707 *                    but the wall construction was truncated before we could reach it, it won't be set here. Currently only
     708 *                    supports towers.
     709 *   'pieces': Array with the following data for each of the entities in the third list:
     710 *    [{
     711 *       'template': Template name of the entity.
     712 *       'x': X coordinate of the entity's position.
     713 *       'z': Z coordinate of the entity's position.
     714 *       'angle': Rotation around the Y axis of the entity (in radians).
     715 *     },
     716 *     ...]
     717 * }
     718 *
     719 * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
     720 * @param cmd.start Starting point of the wall segment being created.
     721 * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
     722 *                 the starting point of the wall is available at this time (e.g. while the player is still in the process
     723 *                 of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
     724 *                 previewed.
     725 * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
     726 */
     727GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
     728{
     729    var wallSet = cmd.wallSet;
     730   
     731    var start = {
     732        "pos": cmd.start,
     733        "angle": 0,
     734        "snapped": false,                       // did the start position snap to anything?
     735        "snappedEnt": INVALID_ENTITY,           // if we snapped, was it to an entity? if yes, holds that entity's ID
     736    };
     737   
     738    var end = {
     739        "pos": cmd.end,
     740        "angle": 0,
     741        "snapped": false,                       // did the start position snap to anything?
     742        "snappedEnt": INVALID_ENTITY,           // if we snapped, was it to an entity? if yes, holds that entity's ID
     743    };
     744   
     745    // --------------------------------------------------------------------------------
     746    // do some entity cache management and check for snapping
     747   
     748    if (!this.placementWallEntities)
     749        this.placementWallEntities = {};
     750   
     751    if (!wallSet)
     752    {
     753        // we're clearing the preview, clear the entity cache and bail
     754        var numCleared = 0;
     755        for (var tpl in this.placementWallEntities)
     756        {
     757            for each (var ent in this.placementWallEntities[tpl].entities)
     758                Engine.DestroyEntity(ent);
     759           
     760            this.placementWallEntities[tpl].numUsed = 0;
     761            this.placementWallEntities[tpl].entities = [];
     762            // keep template data around
     763        }
     764       
     765        return false;
     766    }
     767    else
     768    {
     769        // Move all existing cached entities outside of the world and reset their use count
     770        for (var tpl in this.placementWallEntities)
     771        {
     772            for each (var ent in this.placementWallEntities[tpl].entities)
     773            {
     774                var pos = Engine.QueryInterface(ent, IID_Position);
     775                if (pos)
     776                    pos.MoveOutOfWorld();
     777            }
     778           
     779            this.placementWallEntities[tpl].numUsed = 0;
     780        }
     781       
     782        // Create cache entries for templates we haven't seen before
     783        for each (var tpl in wallSet)
     784        {
     785            if (!(tpl in this.placementWallEntities))
     786            {
     787                //warn("[SetWallPlacementPreview] Initializing wall data for '" + tpl + "'");
     788                this.placementWallEntities[tpl] = {
     789                    "numUsed": 0,
     790                    "entities": [],
     791                    "templateData": this.GetTemplateData(player, tpl),
     792                };
     793               
     794                // ensure that the loaded template data contains a wallPiece component
     795                if (!this.placementWallEntities[tpl].templateData.wallPiece)
     796                {
     797                    error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
     798                    return false;
     799                }
     800            }
     801        }
     802    }
     803       
     804   
     805    // prevent division by zero errors further on if the start and end positions are the same
     806    if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
     807        end.pos = undefined;
     808   
     809    // 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
     810    // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
     811    // data (if any) to determine whether it snapped to an entity, and to which one (see GetFoundationSnapData).
     812    if (cmd.snapEntities)
     813    {
     814        var snapRadius = this.placementWallEntities[wallSet.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
     815        var startSnapData = this.GetFoundationSnapData(player, {
     816            "x": start.pos.x,
     817            "z": start.pos.z,
     818            "template": wallSet.tower,
     819            "snapEntities": cmd.snapEntities,
     820            "snapRadius": snapRadius,
     821        });
     822       
     823        if (startSnapData)
     824        {
     825            start.pos.x = startSnapData.x;
     826            start.pos.z = startSnapData.z;
     827            start.angle = startSnapData.angle;
     828            start.snapped = true;
     829           
     830            if (startSnapData.ent)
     831                start.snappedEnt = startSnapData.ent;
     832        }
     833   
     834        if (end.pos)
     835        {
     836            var endSnapData = this.GetFoundationSnapData(player, {
     837                "x": end.pos.x,
     838                "z": end.pos.z,
     839                "template": wallSet.tower,
     840                "snapEntities": cmd.snapEntities,
     841                "snapRadius": snapRadius,
     842            });
     843           
     844            if (endSnapData)
     845            {
     846                end.pos.x = endSnapData.x;
     847                end.pos.z = endSnapData.z;
     848                end.angle = endSnapData.angle;
     849                end.snapped = true;
     850               
     851                if (endSnapData.ent)
     852                    end.snappedEnt = endSnapData.ent;
     853            }
     854        }
     855    }
     856   
     857    // clear the single-building preview entity (we'll be rolling our own)
     858    this.SetBuildingPlacementPreview(player, {"template": ""});
     859   
     860    // --------------------------------------------------------------------------------
     861    // calculate wall placement and position preview entities
     862   
     863    var result = {"pieces": []};
     864   
     865    var previewEntities = [];
     866    if (end.pos)
     867        previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
     868   
     869    // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
     870    // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
     871    // an issue, because all preview entities have their obstruction components deactivated, meaning that their
     872    // obstruction shapes do not register in the simulation and hence cannot affect it. This means that the preview
     873    // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
     874   
     875    // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
     876    // flag set), which is what we want. The only exception to this is when snapping to existing towers (or
     877    // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
     878    // existing tower/foundation and be shaded red to indicate that they cannot be placed there. To prevent this,
     879    // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
     880    // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
     881    // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
     882   
     883    // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
     884    // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
     885    // by the foundation it snaps to.
     886   
     887    if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
     888    {
     889        var startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
     890        if (previewEntities.length > 0 && startEntObstruction)
     891            previewEntities[0].controlGroup = startEntObstruction.GetControlGroup();
     892       
     893        // if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
     894        var startEntState = this.GetEntityState(player, start.snappedEnt);
     895        if (startEntState.foundation)
     896        {
     897            var cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
     898            if (cmpPosition)
     899            {
     900                previewEntities.unshift({
     901                    "template": wallSet.tower,
     902                    "pos": start.pos,
     903                    "angle": cmpPosition.GetRotation().y,
     904                    "controlGroup": (startEntObstruction ? startEntObstruction.GetControlGroup() : undefined),
     905                    "excludeFromResult": true,
     906                });
     907            }
     908        }
     909    }
     910    else
     911    {
     912        // Didn't snap to an existing entity, add the starting tower manually. Reuse the placement angle that was last
     913        // seen on a validly positioned wall piece, for better visual consistency and to prevent odd-looking jumps in
     914        // rotation when shift-clicking to build a wall.
     915       
     916        // (Consider what happens if we used a constant of, say, 0 instead. Issuing the build command for a wall is
     917        // asynchronous, so when the preview updates after shift-clicking, the foundation is typically not there yet in
     918        // the simulation. This means they cannot possibly be picked in the list of candidate entities for snapping, and
     919        // so we hit this case and rotate the preview to 0 radians. Then, after one or two simulation updates or so, the
     920        // foundation registers and onSimulationUpdate in session.js updates the preview again. It first grabs a new list
     921        // of snapping candidates, which this time does contain the new foundations; so we snap to the entity, and rotate
     922        // the preview back to the foundation's angle. The result is a noticeable rotation to 0 and back, which is
     923        // undesirable. So, for a split second there until the simulation updates, we fake it by reusing the last angle,
     924        // and hope the player doesn't notice).
     925        previewEntities.unshift({
     926            "template": wallSet.tower,
     927            "pos": start.pos,
     928            "angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle)
     929        });
     930    }
     931   
     932    if (end.pos)
     933    {
     934        // Analogous to the starting side case above
     935        if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
     936        {
     937            var endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
     938           
     939            // TODO: technically it's possible (but highly unlikely) for the last entity in previewEntities to be the same as the first,
     940            // i.e. the same wall piece snapping to both a starting and an ending tower. For this to happen, the wall piece would have to
     941            // be exactly as long as the distance between start.pos and end.pos, but it's possible, so we should probably turn
     942            // '.controlGroup' into '.controlGroups' and append to it, and assign them to the primary and secondary control groups if there
     943            // are two (there can only be 0, 1 or 2).
     944            if (previewEntities.length > 0 && endEntObstruction)
     945                previewEntities[previewEntities.length-1].controlGroup = endEntObstruction.GetControlGroup();
     946           
     947            // if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
     948            var endEntState = this.GetEntityState(player, end.snappedEnt);
     949            if (endEntState.foundation)
     950            {
     951                var cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
     952                if (cmpPosition)
     953                {
     954                    previewEntities.push({
     955                        "template": wallSet.tower,
     956                        "pos": end.pos,
     957                        "angle": cmpPosition.GetRotation().y,
     958                        "controlGroup": (endEntObstruction ? endEntObstruction.GetControlGroup() : undefined),
     959                        "excludeFromResult": true
     960                    });
     961                }
     962            }
     963        }
     964        else
     965        {
     966            previewEntities.push({
     967                "template": wallSet.tower,
     968                "pos": end.pos,
     969                "angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle)
     970            });
     971        }
     972    }
     973   
     974    // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed to build at
     975    // least a part of the wall (meaning that the subset is truncated after the first entity that needs to be, but cannot validly be,
     976    // constructed).
     977   
     978    var allPiecesValid = true;
     979    var numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
     980   
     981    for (var i = 0; i < previewEntities.length; ++i)
     982    {
     983        var entInfo = previewEntities[i];
     984       
     985        var ent = null;
     986        var tpl = entInfo.template;
     987        var entPool = this.placementWallEntities[tpl];
     988       
     989        if (entPool.numUsed >= entPool.entities.length)
     990        {
     991            // allocate new entity
     992            //warn("Allocating new entity of template '" + tpl + "'");
     993            ent = Engine.AddLocalEntity("preview|" + tpl);
     994            entPool.entities.push(ent);
     995        }
     996        else
     997        {
     998             // reuse an existing one
     999            ent = entPool.entities[entPool.numUsed];
     1000        }
     1001       
     1002        if (!ent)
     1003        {
     1004            error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
     1005            continue;
     1006        }
     1007       
     1008        // move piece to right location
     1009        // TODO: reuse SetBuildingPlacementReview for this, multiplexed to be able to deal with multiple entities?
     1010       
     1011        var cmpPosition = Engine.QueryInterface(ent, IID_Position);
     1012        if (cmpPosition)
     1013        {
     1014            cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
     1015            cmpPosition.SetYRotation(entInfo.angle);
     1016        }
     1017       
     1018        var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
     1019        if (!cmpObstruction)
     1020        {
     1021            error("[SetWallPlacementPreview] Preview entity of template " + tpl + " does not have an obstruction component!");
     1022            continue;
     1023        }
     1024       
     1025        if (entInfo.controlGroup)
     1026        {
     1027            cmpObstruction.SetControlGroup(entInfo.controlGroup);
     1028        }
     1029        else
     1030        {
     1031            // Reset the control group (remember, we're reusing entities; without this, an ending wall segment that was
     1032            // at one point snapped to an existing tower, and is subsequently reused to be the same ending segment (only
     1033            // this time not snapped), would no longer be capable of being obstructed by the same tower)
     1034            cmpObstruction.SetControlGroup(ent);
     1035        }
     1036       
     1037        // check whether this wall piece can be validly positioned here
     1038        var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
     1039        if (!cmpBuildRestrictions)
     1040        {
     1041            error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for template '" + tpl + "'");
     1042            continue;
     1043        }
     1044       
     1045        var validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement(player));
     1046        allPiecesValid = allPiecesValid && validPlacement;
     1047       
     1048        var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
     1049        if (cmpVisual)
     1050        {
     1051            if (!validPlacement)
     1052                cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1);
     1053            else
     1054                cmpVisual.SetShadingColour(1, 1, 1, 1);
     1055        }
     1056       
     1057        // The requirement that all pieces so far have to have valid positions, rather than only this single one,
     1058        // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
     1059        // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
     1060        // through and past an existing building).
     1061       
     1062        // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
     1063        // on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
     1064       
     1065        if (!entInfo.excludeFromResult)
     1066            numRequiredPieces++;
     1067       
     1068        if (allPiecesValid && !entInfo.excludeFromResult)
     1069        {
     1070            result.pieces.push({
     1071                "template": tpl,
     1072                "x": entInfo.pos.x,
     1073                "z": entInfo.pos.z,
     1074                "angle": entInfo.angle,
     1075            });
     1076            this.placementWallLastAngle = entInfo.angle;
     1077        }
     1078       
     1079        entPool.numUsed++;
     1080    }
     1081   
     1082    // If any were entities required to build the wall, but none of them could be validly positioned, return failure
     1083    // (see method documentation).
     1084    if (numRequiredPieces > 0 && result.pieces.length <= 0)
     1085        return false;
     1086   
     1087    if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
     1088        result.startSnappedEnt = start.snappedEnt;
     1089   
     1090    // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
     1091    // i.e. are included in result.pieces (see docs for the result object).
     1092    if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
     1093        result.endSnappedEnt = end.snappedEnt;
     1094   
     1095    return result;
     1096};
     1097
     1098/**
     1099 * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
     1100 * it to (if necessary/useful).
     1101 *
     1102 * @param data.x            The X position of the foundation to snap.
     1103 * @param data.z            The Z position of the foundation to snap.
     1104 * @param data.template     The template to get the foundation snapping data for.
     1105 * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
     1106 *                            around the entity. Only takes effect when used in conjunction with data.snapRadius.
     1107 *                          When this option is used and the foundation is found to snap to one of the entities passed in this list
     1108 *                            (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
     1109 *                            holding the ID of the entity that was snapped to.
     1110 * @param data.snapRadius   Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
     1111 *                            {data.x, data.z} must be located within to have it snap to that entity.
     1112 */
    5881113GuiInterface.prototype.GetFoundationSnapData = function(player, data)
    5891114{
    5901115    var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
    5911116    var template = cmpTemplateMgr.GetTemplate(data.template);
    5921117
     1118    if (!template)
     1119    {
     1120        warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
     1121        return false;
     1122    }
     1123   
     1124    if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
     1125    {
     1126        // 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
     1127        // (TODO: break unlikely ties by choosing the lowest entity ID)
     1128       
     1129        var minDist2 = -1;
     1130        var minDistEntitySnapData = null;
     1131        var radius2 = data.snapRadius * data.snapRadius;
     1132       
     1133        for each (ent in data.snapEntities)
     1134        {
     1135            var cmpPosition = Engine.QueryInterface(ent, IID_Position);
     1136            if (!cmpPosition || !cmpPosition.IsInWorld())
     1137                continue;
     1138           
     1139            var pos = cmpPosition.GetPosition();
     1140            var dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
     1141            if (dist2 > radius2)
     1142                continue;
     1143           
     1144            if (minDist2 < 0 || dist2 < minDist2)
     1145            {
     1146                minDist2 = dist2;
     1147                minDistEntitySnapData = {"x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent};
     1148            }
     1149        }
     1150       
     1151        if (minDistEntitySnapData != null)
     1152            return minDistEntitySnapData;
     1153    }
     1154   
    5931155    if (template.BuildRestrictions.Category == "Dock")
    5941156    {
    5951157        var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
    var exposedFunctions = {  
    8131375    "SetStatusBars": 1,
    8141376    "DisplayRallyPoint": 1,
    8151377    "SetBuildingPlacementPreview": 1,
     1378    "SetWallPlacementPreview": 1,
    8161379    "GetFoundationSnapData": 1,
    8171380    "PlaySound": 1,
    8181381    "FindIdleUnit": 1,
  • new file inaries/data/mods/public/simulation/components/WallPiece.js

    diff --git a/binaries/data/mods/public/simulation/components/WallPiece.js b/binaries/data/mods/public/simulation/components/WallPiece.js
    new file mode 100644
    index 0000000..f55f4d0
    - +  
     1function WallPiece() {}
     2
     3WallPiece.prototype.Schema =
     4    "<a:help></a:help>" +
     5    "<a:example>" +
     6    "</a:example>" +
     7    "<element name='Length'>" +
     8        "<ref name='nonNegativeDecimal'/>" +
     9    "</element>";
     10
     11
     12WallPiece.prototype.Init = function()
     13{
     14};
     15
     16Engine.RegisterComponentType(IID_WallPiece, "WallPiece", WallPiece);
  • new file inaries/data/mods/public/simulation/components/WallSet.js

    diff --git a/binaries/data/mods/public/simulation/components/WallSet.js b/binaries/data/mods/public/simulation/components/WallSet.js
    new file mode 100644
    index 0000000..79b673b
    - +  
     1function WallSet() {}
     2
     3WallSet.prototype.Schema =
     4    "<a:help></a:help>" +
     5    "<a:example>" +
     6    "</a:example>" +
     7    "<interleave>" +
     8        "<element name='WallLong'>" +
     9            "<text/>" +
     10        "</element>" +
     11        "<element name='WallMedium'>" +
     12            "<text/>" +
     13        "</element>" +
     14        "<element name='WallShort'>" +
     15            "<text/>" +
     16        "</element>" +
     17        "<element name='Tower'>" +
     18            "<text/>" +
     19        "</element>" +
     20        "<element name='Gate'>" +
     21            "<text/>" +
     22        "</element>" +
     23    "</interleave>";
     24
     25
     26WallSet.prototype.Init = function()
     27{
     28};
     29
     30WallSet.prototype.Serialize = null;
     31
     32Engine.RegisterComponentType(IID_WallSet, "WallSet", WallSet);
  • new file inaries/data/mods/public/simulation/components/interfaces/WallPiece.js

    diff --git a/binaries/data/mods/public/simulation/components/interfaces/WallPiece.js b/binaries/data/mods/public/simulation/components/interfaces/WallPiece.js
    new file mode 100644
    index 0000000..ad4ae36
    - +  
     1Engine.RegisterInterface("WallPiece");
     2 No newline at end of file
  • new file inaries/data/mods/public/simulation/components/interfaces/WallSet.js

    diff --git a/binaries/data/mods/public/simulation/components/interfaces/WallSet.js b/binaries/data/mods/public/simulation/components/interfaces/WallSet.js
    new file mode 100644
    index 0000000..10e0cfc
    - +  
     1Engine.RegisterInterface("WallSet");
     2 No newline at end of file
  • binaries/data/mods/public/simulation/helpers/Commands.js

    diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js
    index 4d07c2e..0724af0 100644
    a b function ProcessCommand(player, cmd)  
    88    var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
    99    if (!cmpPlayerMan || player < 0)
    1010        return;
     11   
    1112    var playerEnt = cmpPlayerMan.GetPlayerByID(player);
    1213    if (playerEnt == INVALID_ENTITY)
    1314        return;
     15   
    1416    var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
    1517    if (!cmpPlayer)
    1618        return;
     19   
    1720    var controlAllUnits = cmpPlayer.CanControlAllUnits();
    1821
    1922    // Note: checks of UnitAI targets are not robust enough here, as ownership
    function ProcessCommand(player, cmd)  
    146149        break;
    147150
    148151    case "construct":
    149         // Message structure:
    150         // {
    151         //   "type": "construct",
    152         //   "entities": [...],
    153         //   "template": "...",
    154         //   "x": ...,
    155         //   "z": ...,
    156         //   "angle": ...,
    157         //   "autorepair": true, // whether to automatically start constructing/repairing the new foundation
    158         //   "autocontinue": true, // whether to automatically gather/build/etc after finishing this
    159         //   "queued": true,
    160         // }
    161 
    162         /*
    163          * Construction process:
    164          *  . Take resources away immediately.
    165          *  . Create a foundation entity with 1hp, 0% build progress.
    166          *  . Increase hp and build progress up to 100% when people work on it.
    167          *  . If it's destroyed, an appropriate fraction of the resource cost is refunded.
    168          *  . If it's completed, it gets replaced with the real building.
    169          */
    170          
    171         // Check that we can control these units
    172         var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
    173         if (!entities.length)
    174             break;
    175 
    176         // Tentatively create the foundation (we might find later that it's a invalid build command)
    177         var ent = Engine.AddEntity("foundation|" + cmd.template);
    178         if (ent == INVALID_ENTITY)
    179         {
    180             // Error (e.g. invalid template names)
    181             error("Error creating foundation entity for '" + cmd.template + "'");
    182             break;
    183         }
    184 
    185         // Move the foundation to the right place
    186         var cmpPosition = Engine.QueryInterface(ent, IID_Position);
    187         cmpPosition.JumpTo(cmd.x, cmd.z);
    188         cmpPosition.SetYRotation(cmd.angle);
    189 
    190         // Check whether it's obstructed by other entities or invalid terrain
    191         var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
    192         if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player))
    193         {
    194             if (g_DebugCommands)
    195             {
    196                 warn("Invalid command: build restrictions check failed for player "+player+": "+uneval(cmd));
    197             }
    198 
    199             var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
    200             cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" });
    201 
    202             // Remove the foundation because the construction was aborted
    203             Engine.DestroyEntity(ent);
    204             break;
    205         }
    206 
    207         // Check build limits
    208         var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits);
    209         if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
    210         {
    211             if (g_DebugCommands)
    212             {
    213                 warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
    214             }
    215 
    216             // TODO: The UI should tell the user they can't build this (but we still need this check)
    217 
    218             // Remove the foundation because the construction was aborted
    219             Engine.DestroyEntity(ent);
    220             break;
    221         }
    222 
    223         // TODO: AI has no visibility info
    224         if (!cmpPlayer.IsAI())
    225         {
    226             // Check whether it's in a visible or fogged region
    227             //  tell GetLosVisibility to force RetainInFog because preview entities set this to false,
    228             //  which would show them as hidden instead of fogged
    229             var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    230             var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
    231             if (!visible)
    232             {
    233                 if (g_DebugCommands)
    234                 {
    235                     warn("Invalid command: foundation visibility check failed for player "+player+": "+uneval(cmd));
    236                 }
    237 
    238                 var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
    239                 cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was not visible" });
    240 
    241                 Engine.DestroyEntity(ent);
    242                 break;
    243             }
    244         }
    245 
    246         var cmpCost = Engine.QueryInterface(ent, IID_Cost);
    247         if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
    248         {
    249             if (g_DebugCommands)
    250             {
    251                 warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
    252             }
    253 
    254             Engine.DestroyEntity(ent);
    255             break;
    256         }
    257 
    258         // Make it owned by the current player
    259         var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
    260         cmpOwnership.SetOwner(player);
    261 
    262         // Initialise the foundation
    263         var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
    264         cmpFoundation.InitialiseConstruction(player, cmd.template);
    265 
    266         // Tell the units to start building this new entity
    267         if (cmd.autorepair)
    268         {
    269             ProcessCommand(player, {
    270                 "type": "repair",
    271                 "entities": entities,
    272                 "target": ent,
    273                 "autocontinue": cmd.autocontinue,
    274                 "queued": cmd.queued
    275             });
    276         }
     152        TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd);
     153        break;
    277154
     155    case "construct-wall":
     156        TryConstructWall(player, cmpPlayer, controlAllUnits, cmd);
    278157        break;
    279158
    280159    case "delete-entities":
    function ExtractFormations(ents)  
    467346}
    468347
    469348/**
     349 * Attempts to construct a building using the specified parameters.
     350 * Returns true on success, false on failure.
     351 */
     352function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
     353{
     354    // Message structure:
     355    // {
     356    //   "type": "construct",
     357    //   "entities": [...],                 // entities that will be ordered to construct the building (if applicable)
     358    //   "template": "...",                 // template name of the entity being constructed
     359    //   "x": ...,
     360    //   "z": ...,
     361    //   "angle": ...,
     362    //   "autorepair": true,                // whether to automatically start constructing/repairing the new foundation
     363    //   "autocontinue": true,              // whether to automatically gather/build/etc after finishing this
     364    //   "queued": true,                    // whether to add the construction/repairing of this foundation to entities' queue (if applicable)
     365    //   "obstructionControlGroup": ...,    // Optional; the obstruction control group ID that should be set for this building prior to obstruction
     366    //                                      // testing to determine placement validity. If specified, must be a valid control group ID (> 0).
     367    //   "obstructionControlGroup2": ...,   // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction
     368    //                                      // testing to determine placement validity. May be INVALID_ENTITY.
     369    // }
     370   
     371    /*
     372     * Construction process:
     373     *  . Take resources away immediately.
     374     *  . Create a foundation entity with 1hp, 0% build progress.
     375     *  . Increase hp and build progress up to 100% when people work on it.
     376     *  . If it's destroyed, an appropriate fraction of the resource cost is refunded.
     377     *  . If it's completed, it gets replaced with the real building.
     378     */
     379   
     380    // Check whether we can control these units
     381    var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
     382    if (!entities.length)
     383        return false;
     384   
     385    // Tentatively create the foundation (we might find later that it's a invalid build command)
     386    var ent = Engine.AddEntity("foundation|" + cmd.template);
     387    if (ent == INVALID_ENTITY)
     388    {
     389        // Error (e.g. invalid template names)
     390        error("Error creating foundation entity for '" + cmd.template + "'");
     391        return false;
     392    }
     393   
     394    // Move the foundation to the right place
     395    var cmpPosition = Engine.QueryInterface(ent, IID_Position);
     396    cmpPosition.JumpTo(cmd.x, cmd.z);
     397    cmpPosition.SetYRotation(cmd.angle);
     398   
     399    // Set the obstruction control group if needed
     400    if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2)
     401    {
     402        var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
     403       
     404        // primary control group must always be valid
     405        if (cmd.obstructionControlGroup)
     406        {
     407            if (cmd.obstructionControlGroup <= 0)
     408                warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0");
     409           
     410            cmpObstruction.SetControlGroup(cmd.obstructionControlGroup);
     411        }
     412       
     413        if (cmd.obstructionControlGroup2)
     414            cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2);
     415    }
     416   
     417    // Check whether it's obstructed by other entities or invalid terrain
     418    var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
     419    if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player))
     420    {
     421        if (g_DebugCommands)
     422        {
     423            warn("Invalid command: build restrictions check failed for player "+player+": "+uneval(cmd));
     424        }
     425       
     426        var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     427        cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" });
     428       
     429        // Remove the foundation because the construction was aborted
     430        Engine.DestroyEntity(ent);
     431        return false;
     432    }
     433   
     434    // Check build limits
     435    var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits);
     436    if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
     437    {
     438        if (g_DebugCommands)
     439        {
     440            warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
     441        }
     442       
     443        // TODO: The UI should tell the user they can't build this (but we still need this check)
     444       
     445        // Remove the foundation because the construction was aborted
     446        Engine.DestroyEntity(ent);
     447        return false;
     448    }
     449   
     450    // TODO: AI has no visibility info
     451    if (!cmpPlayer.IsAI())
     452    {
     453        // Check whether it's in a visible or fogged region
     454        //  tell GetLosVisibility to force RetainInFog because preview entities set this to false,
     455        //  which would show them as hidden instead of fogged
     456        var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     457        var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
     458        if (!visible)
     459        {
     460            if (g_DebugCommands)
     461            {
     462                warn("Invalid command: foundation visibility check failed for player "+player+": "+uneval(cmd));
     463            }
     464           
     465            var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     466            cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was not visible" });
     467           
     468            Engine.DestroyEntity(ent);
     469            return false;
     470        }
     471    }
     472   
     473    var cmpCost = Engine.QueryInterface(ent, IID_Cost);
     474    if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
     475    {
     476        if (g_DebugCommands)
     477        {
     478            warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
     479        }
     480       
     481        Engine.DestroyEntity(ent);
     482        return false;
     483    }
     484   
     485    // Make it owned by the current player
     486    var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
     487    cmpOwnership.SetOwner(player);
     488   
     489    // Initialise the foundation
     490    var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
     491    cmpFoundation.InitialiseConstruction(player, cmd.template);
     492   
     493    // Tell the units to start building this new entity
     494    if (cmd.autorepair)
     495    {
     496        ProcessCommand(player, {
     497            "type": "repair",
     498            "entities": entities,
     499            "target": ent,
     500            "autocontinue": cmd.autocontinue,
     501            "queued": cmd.queued
     502        });
     503    }
     504   
     505    return ent;
     506}
     507
     508function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd)
     509{
     510    // Message structure:
     511    // {
     512    //   "type": "construct-wall",
     513    //   "entities": [...],    // entities that will be ordered to construct the wall (if applicable)
     514    //   "pieces": [
     515    //      {
     516    //          "template": "...",    // template name of the wall piece entity being constructed (e.g. towers, long/short/med segments, ...)
     517    //          "x": ...,
     518    //          "z": ...,
     519    //          "angle": ...,
     520    //      },
     521    //      ...
     522    //   ],
     523    //   "wallSet": {
     524    //      "tower": [tower template name],
     525    //      "long":  [long wall segment template name],
     526    //      ...
     527    //      (TODO: we only really need the tower template name here)
     528    //   },
     529    //   "startSnappedEntity":    // optional; entity ID of tower being snapped to at the starting side of the wall
     530    //   "endSnappedEntity":      // optional; entity ID of tower being snapped to at the ending side of the wall
     531    //   "autorepair": true,      // whether to automatically start constructing/repairing the new foundation
     532    //   "autocontinue": true,    // whether to automatically gather/build/etc after finishing this
     533    //   "queued": true,          // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable)
     534    // }
     535   
     536    if (cmd.pieces.length <= 0)
     537        return;
     538   
     539    if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.tower)
     540    {
     541        error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.tower + ") when snapping at the starting side");
     542        return;
     543    }
     544   
     545    if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.tower)
     546    {
     547        error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.tower + ") when snapping at the ending side");
     548        return;
     549    }
     550   
     551    // Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement
     552    // and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control
     553    // groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing
     554    // towers in the case of snapping). The towers themselves all keep their default unique control groups.
     555   
     556    // To support this, every non-tower piece registers the entity ID of the tower foundations that neighbour it on either side.
     557    // We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the
     558    // wall segments may/will need the entity IDs of towers that come afterwards.
     559   
     560    // So, build it in two passes:
     561    //    - In the first pass, go left to right (i.e. from start to end position), and construct wall piece foundations as
     562    //        far as we can without running into a piece that cannot be built (e.g. because it is obstructed). At each step,
     563    //        set the left-neighbouring tower ID as the primary control group, thus allowing it to be built overlapping it.
     564    //      If we encounter a new tower along the way (which will gain its own control group), first build it using temporarily
     565    //        the same control group of the previous (non-tower) piece, then set the previous piece's secondary control group
     566    //        to the tower's entity ID, and then restore the primary control group of the constructed tower back its original
     567    //        (unique) value. (If we hadn't first temporarily given it the previous piece's control group, it wouldn't've been
     568    //        able to be placed while overlapping the previous piece).
     569    //      If we hit a piece that can't be built, backtrack to the last tower that was successfully built, deleting any
     570    //        non-tower pieces that were built after it (we want walls to always end in a tower so we can always extend it
     571    //        by snapping onto it).
     572    //     
     573    //    - In the second pass, go right to left from said last successfully placed wall piece (which might be a tower we
     574    //      backtracked to), this time registering the right neighbouring tower in each non-tower piece.
     575   
     576    // first pass; L -> R
     577   
     578    var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces
     579    var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
     580   
     581    if (cmd.startSnappedEntity)
     582    {
     583        var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction);
     584        if (!cmpSnappedStartObstruction)
     585        {
     586            error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component");
     587            return;
     588        }
     589       
     590        lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup();
     591        //warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup);
     592    }
     593   
     594    var i = 0;
     595    for (; i < cmd.pieces.length; ++i)
     596    {
     597        var piece = cmd.pieces[i];
     598       
     599        //warn("---[>>]--- iteration " + i);
     600       
     601        // except for if we don't do start position snapping and the first entity we build is therefore a tower,
     602        // 'lastTowerControlGroup' must always be defined and valid here
     603        if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
     604        {
     605            if (!(i == 0 && piece.template == cmd.wallSet.tower && !cmd.startSnappedEntity))
     606            {
     607                error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass)");
     608                break;
     609            }
     610        }
     611       
     612        var constructPieceCmd = {
     613            "type": "construct",
     614            "entities": cmd.entities,
     615            "template": piece.template,
     616            "x": piece.x,
     617            "z": piece.z,
     618            "angle": piece.angle,
     619            "autorepair": cmd.autorepair,
     620            "autocontinue": cmd.autocontinue,
     621            "queued": cmd.queued,
     622            // regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed
     623            // using the control group of the last tower (see algorithm outline above)
     624            "obstructionControlGroup": lastTowerControlGroup, // allowed to be null; should be the case when the first entity is a tower
     625        };
     626       
     627        // if we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's
     628        // control group directly at construction time (instead of setting it in the second pass) to allow it to be built with
     629        // overlap to the snapped entity
     630        if (cmd.endSnappedEntity && i == cmd.pieces.length - 1)
     631        {
     632            var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
     633            // TODO: ensure cmpEndSnappedObstruction exists
     634            constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup();
     635        }
     636       
     637        //for (var k in constructPieceCmd)
     638        //  warn("   " + k + ": " + uneval(constructPieceCmd[k]));
     639        //warn("   ");
     640       
     641        var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd);
     642        if (pieceEntityId)
     643        {
     644            //warn("   successfully built wall piece; ID = " + pieceEntityId);
     645           
     646            // wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later
     647            piece.ent = pieceEntityId;
     648           
     649            // if we built a tower, do the control group dance (see algorithm outline above) and update lastTowerControlGroup
     650            // and lastTowerIndex
     651            if (piece.template == cmd.wallSet.tower)
     652            {
     653                var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction);
     654                var newTowerControlGroup = pieceEntityId;
     655               
     656                if (i > 0)
     657                {
     658                    //warn("   updating previous wall piece's secondary control group to " + newTowerControlGroup);
     659                    var cmpPreviousObstruction = Engine.QueryInterface(cmd.pieces[i-1].ent, IID_Obstruction);
     660                    // TODO: ensure that cmpPreviousObstruction exists
     661                    // TODO: ensure that the previous obstruction does not yet have a secondary control group set
     662                    cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup);
     663                }
     664               
     665                // TODO: ensure that cmpTowerObstruction exists
     666                cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group
     667                //warn("   updating tower control group to " + newTowerControlGroup);
     668               
     669                lastTowerIndex = i;// warn("   updated lastTowerIndex to " + lastTowerIndex);
     670                lastTowerControlGroup = newTowerControlGroup;// warn("   updated lastTowerControlGroup to " + newTowerControlGroup);
     671            }
     672        }
     673        else
     674        {
     675            //error("   (!!) failed to build wall piece, backtracking to last tower index " + lastTowerIndex);
     676           
     677            // wall piece foundation failed to build, backtrack and delete entities until the last tower that was succesfully built
     678            var j = i - 1;
     679            for (; (j >= 0 && cmd.pieces[j].template !== cmd.wallSet.tower); --j)
     680            {
     681                // the previous pieces should all have their entity ID registered; if not, something's wrong
     682                if (!cmd.pieces[j].ent)
     683                {
     684                    error("[TryConstructWall] Cannot backtrack to delete dangling wall pieces after placement failure; no registered entity ID");
     685                    continue;
     686                }
     687               
     688                //warn("   destroying backtracked entity " + cmd.pieces[j].ent + " of template " + cmd.pieces[j].template);
     689                Engine.DestroyEntity(cmd.pieces[j].ent);
     690            }
     691           
     692            i = j + 1; // compensate for the -1 subtracted by lastBuiltPieceIndex below
     693            break;
     694        }
     695    }
     696   
     697    var lastBuiltPieceIndex = i - 1;
     698    var wallComplete = (lastBuiltPieceIndex == cmd.pieces.length - 1);
     699   
     700    //warn("lastBuiltPieceIndex = " + lastBuiltPieceIndex);
     701    //warn("wall complete? " + (wallComplete ? "yes" : "no"));
     702   
     703    // At this point, 'i' is the index of the last wall piece that was successfully constructed (taking into account backtracking
     704    // to the last tower upon foundation placement failure). Now do the second pass going right-to-left, registering the control
     705    // groups of the towers to the right of each piece as their secondary control groups.
     706   
     707    lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
     708   
     709    // only start off with the ending side's snapped tower's control group if we were able to build the entire wall
     710    if (cmd.endSnappedEntity && wallComplete)
     711    {
     712        var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
     713        if (!cmpSnappedEndObstruction)
     714        {
     715            error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component");
     716            return;
     717        }
     718       
     719        lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup();
     720    }
     721   
     722    for (var j = lastBuiltPieceIndex; j >= 0; --j)
     723    {
     724        var piece = cmd.pieces[j];
     725       
     726        //warn("---[<<]--- iteration " + j);
     727       
     728        // In general, lastTowerControlGroup must always be defined and valid here, except when the last successfully built
     729        // piece is a tower that we didn't snap to. This can happen:
     730        //    - If we didn't do any end-side snapping and the last successfully built piece is therefore a tower. Note
     731        //      that in this case it doesn't matter if we did or did not complete the entire wall.
     732        //    - If we did do end-side snapping, but we didn't complete the entire wall and had to end at an intermediate
     733        //      tower.
     734       
     735        if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
     736        {
     737            var ok = (j == lastBuiltPieceIndex && ((!cmd.endSnappedEntity && piece.template == cmd.wallSet.tower) ||
     738                                                   ( cmd.endSnappedEntity && piece.template == cmd.wallSet.tower && !wallComplete)));
     739            if (!ok)
     740            {
     741                error("[TryConstructWall] Expected last tower control group to be available, none found (2nd pass)");
     742                break;
     743            }
     744        }
     745       
     746        var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction);
     747        // TODO: make sure piece.ent/cmpPieceObstruction exists
     748       
     749        if (piece.template == cmd.wallSet.tower)
     750        {
     751            // encountered a tower entity, update the last tower control group
     752            lastTowerControlGroup = cmpPieceObstruction.GetControlGroup();
     753            //warn("   updated last tower control group to " + lastTowerControlGroup);
     754        }
     755        else
     756        {
     757            // Encountered a non-tower entity, update its secondary control group.
     758            // Note that the wall piece may already have its secondary control group set to the tower's entity ID from the
     759            // first pass, in which case we should validate it against lastTowerControlGroup.
     760            var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2();
     761            if (existingSecondaryControlGroup == INVALID_ENTITY)
     762            {
     763                cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup);
     764                //warn("   set wall piece secondary control group to " + lastTowerControlGroup);
     765            }
     766            else if (existingSecondaryControlGroup != lastTowerControlGroup)
     767            {
     768                error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass)");
     769                break;
     770            }
     771        }
     772    }
     773   
     774    //warn("--- Done ---");
     775    warn("Built " + (lastBuiltPieceIndex + 1) + "/" + cmd.pieces.length + " pieces");
     776    //warn("------------");
     777}
     778
     779/**
    470780 * Remove the given list of entities from their current formations.
    471781 */
    472782function RemoveFromFormation(ents)
  • new file inaries/data/mods/public/simulation/helpers/Walls.js

    diff --git a/binaries/data/mods/public/simulation/helpers/Walls.js b/binaries/data/mods/public/simulation/helpers/Walls.js
    new file mode 100644
    index 0000000..3dd66ea
    - +  
     1/**
     2 * Returns the wall piece entities needed to construct a wall between start.pos and end.pos. Assumes start.pos != end.pos.
     3 * The result is an array of objects, each one containing the following information about a single wall piece entity:
     4 *   - 'template': the template name of the entity
     5 *   - 'pos': position of the entity, as an object with keys 'x' and 'z'
     6 *   - 'angle': orientation of the entity, as an angle in radians
     7 *
     8 * All the pieces in the resulting array are ordered left-to-right (or right-to-left) as they appear in the physical wall.
     9 *
     10 * @param placementData Object that associates the wall piece template names with information about those kinds of pieces.
     11 *                        Expects placementData[templateName].templateData to contain the parsed template information about
     12 *                        the template whose filename is <i>templateName</i>.
     13 * @param wallSet Object that associates template names with the kinds of wall pieces that supported. Expected to contain
     14 *                  template names for keys "long" (long wall segment), "medium" (medium wall segment), "short" (short wall
     15 *                  segment), "tower" (intermediate tower between wall segments), "gate" (replacement for long walls).
     16 * @param start Object holding the starting position of the wall. Must contain keys 'x' and 'z'.
     17 * @param end   Object holding the ending position of the wall. Must contains keys 'x' and 'z'.
     18 */
     19function GetWallPlacement(placementData, wallSet, start, end)
     20{
     21    var result = [];
     22   
     23    var candidateSegments = [
     24        {"template": wallSet.long,   "len": placementData[wallSet.long].templateData.wallPiece.length},
     25        {"template": wallSet.medium, "len": placementData[wallSet.medium].templateData.wallPiece.length},
     26        {"template": wallSet.short,  "len": placementData[wallSet.short].templateData.wallPiece.length},
     27    ];
     28   
     29    var towerWidth = placementData[wallSet.tower].templateData.wallPiece.length;
     30   
     31    var dir = {"x": end.pos.x - start.pos.x, "z": end.pos.z - start.pos.z};
     32    var len = Math.sqrt(dir.x * dir.x + dir.z * dir.z);
     33   
     34    if (len <= towerWidth)
     35        // we'll need room for at least our starting and ending towers to fit next to eachother
     36        return result;
     37   
     38    var placement = GetWallSegmentsRec(len, candidateSegments, 0.05, 0.8, towerWidth, 0, []);
     39   
     40    // TODO: make sure intermediate towers are spaced out far enough for their obstructions to not overlap, implying that
     41    // tower's wallpiece lengths should be > their obstruction width, which is undesirable because it prevents towers with
     42    // wide bases
     43    if (placement)
     44    {
     45        var placedEntities = placement.segments; // list of chosen candidate segments
     46        var r = placement.r; // remaining distance to target without towers (must be <= (N-1) * towerWidth)
     47        var s = r / (2 * placedEntities.length); // spacing
     48       
     49        var dirNormalized = {"x": dir.x / len, "z": dir.z / len};
     50        var angle = -Math.atan2(dir.z, dir.x);    // angle of this wall segment (relative to world-space X/Z axes)
     51       
     52        var progress = 0;
     53        for (var i = 0; i < placedEntities.length; i++)
     54        {
     55            var placedEntity = placedEntities[i];
     56            var targetX = start.pos.x + (progress + s + placedEntity.len/2) * dirNormalized.x;
     57            var targetZ = start.pos.z + (progress + s + placedEntity.len/2) * dirNormalized.z;
     58           
     59            result.push({
     60                "template": placedEntity.template,
     61                "pos": {"x": targetX, "z": targetZ},
     62                "angle": angle,
     63            });
     64           
     65            if (i < placedEntities.length - 1)
     66            {
     67                var towerX = start.pos.x + (progress + placedEntity.len + 2*s) * dirNormalized.x;
     68                var towerZ = start.pos.z + (progress + placedEntity.len + 2*s) * dirNormalized.z;
     69               
     70                result.push({
     71                    "template": wallSet.tower,
     72                    "pos": {"x": towerX, "z": towerZ},
     73                    "angle": angle,
     74                });
     75            }
     76           
     77            progress += placedEntity.len + 2*s;
     78        }
     79    }
     80    else
     81    {
     82        error("No placement possible");
     83    }
     84   
     85    return result;
     86}
     87
     88/**
     89 * Helper function for GetWallPlacement. Finds a list of wall segments and the corresponding remaining spacing/overlap
     90 * distance "r" that will suffice to construct a wall of the given distance. It is understood that two extra towers will
     91 * be placed centered at the starting and ending points of the wall.
     92 *
     93 * @param d Total distance between starting and ending points (constant throughout calls).
     94 * @param candidateSegments List of candidate segments (constant throughout calls). Should be ordered longer-to-shorter
     95 *                            for better execution speed.
     96 * @param minOverlap Minimum overlap factor (constant throughout calls). Must have a value between 0 (meaning walls are
     97 *                     not allowed to overlap towers) and 1 (meaning they're allowed to overlap towers entirely).
     98 *                     Must be <= maxOverlap.
     99 * @param maxOverlap Maximum overlap factor (constant throughout calls). Must have a value between 0 (meaning walls are
     100 *                     not allowed to overlap towers) and 1 (meaning they're allowed to overlap towers entirely).
     101 *                     Must be >= minOverlap.
     102 * @param t Length of a single tower (constant throughout calls). Acts as buffer space for wall segments (see comments).
     103 * @param distSoFar Sum of all the wall segments' lengths in 'segments'.
     104 * @param segments Current list of wall segments placed.
     105 */
     106function GetWallSegmentsRec(d, candidateSegments, minOverlap, maxOverlap, t, distSoFar, segments)
     107{
     108    // The idea is to find a number N of wall segments (excluding towers) so that the sum of their lengths adds up to a
     109    // value that is within certain bounds of the distance 'd' between the starting and ending points of the wall. This
     110    // creates either a positive or negative 'buffer' of space, that can be compensated for by spacing the wall segments
     111    // out away from each other, or inwards, overlapping each other. The spaces or overlaps can then be covered up by
     112    // placing towers on top of them. In this way, the same set of wall segments can be used to span a wider range of
     113    // target distances.
     114    //
     115    // In this function, it is understood that two extra towers will be placed centered at the starting and ending points.
     116    // They are allowed to contribute to the buffer space.
     117    //
     118    // The buffer space equals the difference between d and the sum of the lengths of all the wall segments, and is denoted
     119    // 'r' for 'remaining space'. Positive values of r mean that the walls will need to be spaced out, negative values of r
     120    // mean that they will need to overlap. Clearly, there are limits to how far wall segments can be spaced out or
     121    // overlapped, depending on how much 'buffer space' each tower provides, and how far 'into' towers the wall segments are
     122    // allowed to overlap.
     123    //
     124    // Let 't' signify the width of a tower. When there are N wall segments, then the maximum distance that can be covered
     125    // using only these walls (plus the towers covering up any gaps) is achieved when the walls and towers touch outer-border-
     126    // to-outer-border. Therefore, the maximum value of r is then given by:
     127    //
     128    //   rMax = t/2 + (N-1)*t + t/2
     129    //        = N*t
     130    //
     131    // where the two half-tower widths are buffer space contributed by the implied towers on the starting and ending points.
     132    // Similarly, a value rMin = -N*t can be derived for the minimal value of r. Note that a value of r = 0 means that the
     133    // wall segment lengths add up to exactly d, meaning that each one starts and ends right in the center of a tower.
     134    //
     135    // Thus, we establish:
     136    //   -Nt <= r <= Nt
     137    //
     138    // We can further generalize this by adding in parameters to control the depth to within which wall segments are allowed to
     139    // overlap with a tower. The bounds above assume that a wall segment is allowed to overlap across the entire range of 0
     140    // (not overlapping at all, as in the upper boundary) to 1 (overlapping maximally, as in the lower boundary).
     141    //
     142    // By requiring that walls overlap towers to a degree of at least 0 < minOverlap <= 1, it is clear that this lowers the
     143    // distance that can be maximally reached by the same set of wall segments, compared to the value of minOverlap = 0 that
     144    // we assumed to initially find Nt.
     145    //
     146    // Consider a value of minOverlap = 0.5, meaning that any wall segment must protrude at least halfway into towers; in this
     147    // situation, wall segments must at least touch boundaries or overlap mutually, implying that the sum of their lengths
     148    // must equal or exceed 'd', establishing an upper bound of 0 for r.
     149    // Similarly, consider a value of minOverlap = 1, meaning that any wall segment must overlap towers maximally; this situation
     150    // is equivalent to the one for finding the lower bound -Nt on r.
     151    //
     152    // With the implicit value minOverlap = 0 that yielded the upper bound Nt above, simple interpolation and a similar exercise
     153    // for maxOverlap, we find:
     154    //   (1-2*maxOverlap) * Nt <= r <= (1-2*minOverlap) * Nt
     155    //
     156    // To find N segments that satisfy this requirement, we try placing L, M and S wall segments in turn and continue recursively
     157    // as long as the value of r is not within the bounds. If continuing recursively returns an impossible configuration, we
     158    // backtrack and try a wall segment of the next length instead. Note that we should prefer to use the long segments first since
     159    // they can be replaced by gates.
     160   
     161    for each (var candSegment in candidateSegments)
     162    {
     163        segments.push(candSegment);
     164       
     165        var newDistSoFar = distSoFar + candSegment.len;
     166        var r = d - newDistSoFar;
     167       
     168        // TODO: these don't have to be recalculated every iteration
     169        var rLowerBound = (1 - 2 * maxOverlap) * segments.length * t;
     170        var rUpperBound = (1 - 2 * minOverlap) * segments.length * t;
     171       
     172        if (r < rLowerBound)
     173        {
     174            // we've allocated too much wall length, pop the last segment and try the next
     175            //warn("Distance so far exceeds target, trying next level");
     176            segments.pop();
     177            continue;
     178        }
     179        else if (r > rUpperBound)
     180        {
     181            var recursiveResult = GetWallSegmentsRec(d, candidateSegments, minOverlap, maxOverlap, t, newDistSoFar, segments);
     182            if (!recursiveResult)
     183            {
     184                // recursive search with this piece yielded no results, pop it and try the next one
     185                segments.pop();
     186                continue;
     187            }
     188            else
     189                return recursiveResult;
     190        }
     191        else
     192        {
     193            // found a placement
     194            return {"segments": segments, "r": r};
     195        }
     196    }
     197   
     198    // no placement possible :(
     199    return false;
     200}
     201
     202Engine.RegisterGlobal("GetWallPlacement", GetWallPlacement);
  • binaries/data/mods/public/simulation/templates/other/palisades_rocks_gate.xml

    diff --git a/binaries/data/mods/public/simulation/templates/other/palisades_rocks_gate.xml b/binaries/data/mods/public/simulation/templates/other/palisades_rocks_gate.xml
    index 5a882ef..0040043 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>props/special/palisade_rocks_gate.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>11.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/other/palisades_rocks_long.xml

    diff --git a/binaries/data/mods/public/simulation/templates/other/palisades_rocks_long.xml b/binaries/data/mods/public/simulation/templates/other/palisades_rocks_long.xml
    index 8af8b59..fe04d11 100644
    a b  
    2222  <VisualActor>
    2323    <Actor>props/special/palisade_rocks_long.xml</Actor>
    2424  </VisualActor>
     25  <WallPiece>
     26    <Length>11.0</Length>
     27  </WallPiece>
    2528</Entity>
  • binaries/data/mods/public/simulation/templates/other/palisades_rocks_medium.xml

    diff --git a/binaries/data/mods/public/simulation/templates/other/palisades_rocks_medium.xml b/binaries/data/mods/public/simulation/templates/other/palisades_rocks_medium.xml
    index db4a799..dca7de4 100644
    a b  
    2222  <VisualActor>
    2323    <Actor>props/special/palisade_rocks_medium.xml</Actor>
    2424  </VisualActor>
     25  <WallPiece>
     26    <Length>8.0</Length>
     27  </WallPiece>
    2528</Entity>
  • binaries/data/mods/public/simulation/templates/other/palisades_rocks_short.xml

    diff --git a/binaries/data/mods/public/simulation/templates/other/palisades_rocks_short.xml b/binaries/data/mods/public/simulation/templates/other/palisades_rocks_short.xml
    index f7a6bcf..bfd3bb3 100644
    a b  
    2222  <VisualActor>
    2323    <Actor>props/special/palisade_rocks_short.xml</Actor>
    2424  </VisualActor>
     25  <WallPiece>
     26    <Length>4.0</Length>
     27  </WallPiece>
    2528</Entity>
    26 
  • binaries/data/mods/public/simulation/templates/other/palisades_rocks_tower.xml

    diff --git a/binaries/data/mods/public/simulation/templates/other/palisades_rocks_tower.xml b/binaries/data/mods/public/simulation/templates/other/palisades_rocks_tower.xml
    index 8a56c70..f2b70fa 100644
    a b  
    2222  <VisualActor>
    2323    <Actor>props/special/palisade_rocks_tower.xml</Actor>
    2424  </VisualActor>
     25  <WallPiece>
     26    <Length>4.0</Length>
     27  </WallPiece>
    2528</Entity>
  • binaries/data/mods/public/simulation/templates/structures/celt_wall_gate.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/celt_wall_gate.xml b/binaries/data/mods/public/simulation/templates/structures/celt_wall_gate.xml
    index 0675ccc..63fa2e6 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/celts/wall_gate.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>22.0</Length>
     20  </WallPiece>
    1821</Entity>
  • binaries/data/mods/public/simulation/templates/structures/celt_wall_long.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/celt_wall_long.xml b/binaries/data/mods/public/simulation/templates/structures/celt_wall_long.xml
    index 41d1fdc..a9a4451 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/celts/wall_long.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>22.0</Length>
     20  </WallPiece>
    1821</Entity>
  • binaries/data/mods/public/simulation/templates/structures/celt_wall_medium.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/celt_wall_medium.xml b/binaries/data/mods/public/simulation/templates/structures/celt_wall_medium.xml
    index 4b51497..851af02 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/celts/wall_medium.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>22.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/celt_wall_short.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/celt_wall_short.xml b/binaries/data/mods/public/simulation/templates/structures/celt_wall_short.xml
    index 1f17003..66523a9 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/celts/wall_short.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>22.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/celt_wall_tower.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/celt_wall_tower.xml b/binaries/data/mods/public/simulation/templates/structures/celt_wall_tower.xml
    index d67c982..8ebcd99 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/celts/wall_tower.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>6.0</Length>
     20  </WallPiece>
    1821</Entity>
  • binaries/data/mods/public/simulation/templates/structures/hele_wall_gate.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/hele_wall_gate.xml b/binaries/data/mods/public/simulation/templates/structures/hele_wall_gate.xml
    index 5726f89..1c1ed7b 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_gate.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>35.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/hele_wall_long.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/hele_wall_long.xml b/binaries/data/mods/public/simulation/templates/structures/hele_wall_long.xml
    index 280419e..a254cd2 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_long.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>34.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/hele_wall_med.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/hele_wall_med.xml b/binaries/data/mods/public/simulation/templates/structures/hele_wall_med.xml
    index 13550bc..cfd2034 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_medium.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>21.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/hele_wall_medium.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/hele_wall_medium.xml b/binaries/data/mods/public/simulation/templates/structures/hele_wall_medium.xml
    index 2db9bd1..1f227a8 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_medium.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>21.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/hele_wall_short.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/hele_wall_short.xml b/binaries/data/mods/public/simulation/templates/structures/hele_wall_short.xml
    index e3be746..c7ecf13 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_short.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>10.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/hele_wall_tower.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/hele_wall_tower.xml b/binaries/data/mods/public/simulation/templates/structures/hele_wall_tower.xml
    index c5e81aa..0a19abf 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_tower.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>5.0</Length>
     26  </WallPiece>
    2427</Entity>
     28 No newline at end of file
  • binaries/data/mods/public/simulation/templates/structures/iber_wall_gate.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/iber_wall_gate.xml b/binaries/data/mods/public/simulation/templates/structures/iber_wall_gate.xml
    index 1030027..d005300 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/iberians/wall_gate.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>32.0</Length>
     20  </WallPiece>
    1821</Entity>
  • binaries/data/mods/public/simulation/templates/structures/iber_wall_long.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/iber_wall_long.xml b/binaries/data/mods/public/simulation/templates/structures/iber_wall_long.xml
    index 8fd77e4..e339f42 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/iberians/wall_long.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>34.0</Length>
     20  </WallPiece>
    1821</Entity>
  • binaries/data/mods/public/simulation/templates/structures/iber_wall_medium.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/iber_wall_medium.xml b/binaries/data/mods/public/simulation/templates/structures/iber_wall_medium.xml
    index 9bbdf5d..3435685 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/iberians/wall_medium.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>23.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/iber_wall_short.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/iber_wall_short.xml b/binaries/data/mods/public/simulation/templates/structures/iber_wall_short.xml
    index fa32ee1..2436a59 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/iberians/wall_short.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>10.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/iber_wall_tower.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/iber_wall_tower.xml b/binaries/data/mods/public/simulation/templates/structures/iber_wall_tower.xml
    index 264de9e..8f368de 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/iberians/wall_tower.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>8.5</Length>
     20  </WallPiece>
    1821</Entity>
  • binaries/data/mods/public/simulation/templates/structures/pers_wall_gate.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/pers_wall_gate.xml b/binaries/data/mods/public/simulation/templates/structures/pers_wall_gate.xml
    index 7218abe..d423d91 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/persians/wall_gate.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>34.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/pers_wall_long.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/pers_wall_long.xml b/binaries/data/mods/public/simulation/templates/structures/pers_wall_long.xml
    index 48cad21..c2e7dd1 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/persians/wall_long.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>34.0</Length>
     26  </WallPiece>
    2427</Entity>
     28 No newline at end of file
  • binaries/data/mods/public/simulation/templates/structures/pers_wall_medium.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/pers_wall_medium.xml b/binaries/data/mods/public/simulation/templates/structures/pers_wall_medium.xml
    index e983e18..8a9ef69 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/persians/wall_medium.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>21.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/pers_wall_short.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/pers_wall_short.xml b/binaries/data/mods/public/simulation/templates/structures/pers_wall_short.xml
    index d92c6b6..c5a096c 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/persians/wall_short.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>10.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/pers_wall_tower.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/pers_wall_tower.xml b/binaries/data/mods/public/simulation/templates/structures/pers_wall_tower.xml
    index 75cadce..ec80dcd 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/persians/wall_tower.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>5.5</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/rome_wall_gate.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_wall_gate.xml b/binaries/data/mods/public/simulation/templates/structures/rome_wall_gate.xml
    index ad4b54e..20fc90a 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/romans/wall_gate.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>34.0</Length>
     20  </WallPiece>
    1821</Entity>
  • binaries/data/mods/public/simulation/templates/structures/rome_wall_long.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_wall_long.xml b/binaries/data/mods/public/simulation/templates/structures/rome_wall_long.xml
    index ae4a5ef..b0003b8 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/romans/wall_long.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>34.0</Length>
     20  </WallPiece>
    1821</Entity>
  • binaries/data/mods/public/simulation/templates/structures/rome_wall_medium.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_wall_medium.xml b/binaries/data/mods/public/simulation/templates/structures/rome_wall_medium.xml
    index 169ccbe..bc67e85 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/romans/wall_medium.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>22.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/rome_wall_short.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_wall_short.xml b/binaries/data/mods/public/simulation/templates/structures/rome_wall_short.xml
    index 8a79992..58ab55b 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/romans/wall_short.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>10.0</Length>
     26  </WallPiece>
    2427</Entity>
  • binaries/data/mods/public/simulation/templates/structures/rome_wall_tower.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_wall_tower.xml b/binaries/data/mods/public/simulation/templates/structures/rome_wall_tower.xml
    index 6f68a9c..252eec4 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/romans/wall_tower.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>6.5</Length>
     20  </WallPiece>
    1821</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/wallsets/celt_palisade.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/wallsets/celt_palisade.xml b/binaries/data/mods/public/simulation/templates/structures/wallsets/celt_palisade.xml
    new file mode 100644
    index 0000000..ff082ce
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<!-- Abstract entity to serve as a bare-minimum dummy constructable entity to initiate wall placement.
     3Defines the set of actual entities that are part of the same wall construction system (i.e., towers,
     4gates, wall segments of various length, etc.) -->
     5<Entity>
     6  <Identity>
     7    <Icon>gaia/special_palisade.png</Icon>
     8    <Civ>celt</Civ>
     9    <SpecificName>WALLSET</SpecificName>
     10    <History>WALLSET</History>
     11    <!-- TODO: Undesirable copy/pasta from the wall templates :( -->
     12    <Classes datatype="tokens">Town Wall</Classes>
     13    <GenericName>Palisade Wallset</GenericName>
     14    <Tooltip>WALLSET</Tooltip>
     15  </Identity>
     16  <WallSet>
     17    <Tower>other/palisades_rocks_tower</Tower>
     18    <Gate>other/palisades_rocks_gate</Gate>
     19    <WallLong>other/palisades_rocks_long</WallLong>
     20    <WallMedium>other/palisades_rocks_medium</WallMedium>
     21    <WallShort>other/palisades_rocks_short</WallShort>
     22  </WallSet>
     23</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/wallsets/celt_stone.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/wallsets/celt_stone.xml b/binaries/data/mods/public/simulation/templates/structures/wallsets/celt_stone.xml
    new file mode 100644
    index 0000000..c3baf09
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<!-- Abstract entity to serve as a bare-minimum dummy constructable entity to initiate wall placement.
     3Defines the set of actual entities that are part of the same wall construction system (i.e., towers,
     4gates, wall segments of various length, etc.) -->
     5<Entity>
     6  <Identity>
     7    <Icon>structures/wall.png</Icon>
     8    <Civ>iber</Civ>
     9    <SpecificName>Celt Wallset</SpecificName>
     10    <!-- TODO: Undesirable copy/pasta from the wall templates :( -->
     11    <Classes datatype="tokens">Town Wall</Classes>
     12    <GenericName>City Wall</GenericName>
     13    <Tooltip>(Celt WallSet)</Tooltip>
     14  </Identity>
     15  <WallSet>
     16    <Tower>structures/celt_wall_tower</Tower>
     17    <Gate>structures/celt_wall_gate</Gate>
     18    <WallLong>structures/celt_wall_long</WallLong>
     19    <WallMedium>structures/celt_wall_medium</WallMedium>
     20    <WallShort>structures/celt_wall_short</WallShort>
     21  </WallSet>
     22</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/wallsets/hele_stone.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/wallsets/hele_stone.xml b/binaries/data/mods/public/simulation/templates/structures/wallsets/hele_stone.xml
    new file mode 100644
    index 0000000..71a6e26
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<!-- Abstract entity to serve as a bare-minimum dummy constructable entity to initiate wall placement.
     3Defines the set of actual entities that are part of the same wall construction system (i.e., towers,
     4gates, wall segments of various length, etc.) -->
     5<Entity>
     6  <Identity>
     7    <Icon>structures/wall.png</Icon>
     8    <Civ>hele</Civ>
     9    <SpecificName>HELE WALLSET</SpecificName>
     10    <History>All Hellenic cities were surrounded by stone walls for protection against enemy raids. Some of these fortifications, like the Athenian Long Walls, for example, were massive structures.</History>
     11    <!-- TODO: Undesirable copy/pasta from the wall templates :( -->
     12    <Classes datatype="tokens">Town Wall</Classes>
     13    <GenericName>City Wall</GenericName>
     14    <Tooltip>HELE WALLSET</Tooltip>
     15  </Identity>
     16  <WallSet>
     17    <Tower>structures/hele_wall_tower</Tower>
     18    <Gate>structures/hele_wall_gate</Gate>
     19    <WallLong>structures/hele_wall_long</WallLong>
     20    <WallMedium>structures/hele_wall_med</WallMedium>
     21    <WallShort>structures/hele_wall_short</WallShort>
     22  </WallSet>
     23</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/wallsets/iber_stone.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/wallsets/iber_stone.xml b/binaries/data/mods/public/simulation/templates/structures/wallsets/iber_stone.xml
    new file mode 100644
    index 0000000..669298e
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<!-- Abstract entity to serve as a bare-minimum dummy constructable entity to initiate wall placement.
     3Defines the set of actual entities that are part of the same wall construction system (i.e., towers,
     4gates, wall segments of various length, etc.) -->
     5<Entity>
     6  <Identity>
     7    <Icon>structures/wall.png</Icon>
     8    <Civ>iber</Civ>
     9    <SpecificName>Iber Wallset</SpecificName>
     10    <!-- TODO: Undesirable copy/pasta from the wall templates :( -->
     11    <Classes datatype="tokens">Town Wall</Classes>
     12    <GenericName>City Wall</GenericName>
     13    <Tooltip>(Iber WallSet)</Tooltip>
     14  </Identity>
     15  <WallSet>
     16    <Tower>structures/iber_wall_tower</Tower>
     17    <Gate>structures/iber_wall_gate</Gate>
     18    <WallLong>structures/iber_wall_long</WallLong>
     19    <WallMedium>structures/iber_wall_medium</WallMedium>
     20    <WallShort>structures/iber_wall_short</WallShort>
     21  </WallSet>
     22</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/wallsets/pers_stone.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/wallsets/pers_stone.xml b/binaries/data/mods/public/simulation/templates/structures/wallsets/pers_stone.xml
    new file mode 100644
    index 0000000..4ec9533
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<!-- Abstract entity to serve as a bare-minimum dummy constructable entity to initiate wall placement.
     3Defines the set of actual entities that are part of the same wall construction system (i.e., towers,
     4gates, wall segments of various length, etc.) -->
     5<Entity>
     6  <Identity>
     7    <Icon>structures/wall.png</Icon>
     8    <Civ>iber</Civ>
     9    <SpecificName>Persian WallSet</SpecificName>
     10    <!-- TODO: Undesirable copy/pasta from the wall templates :( -->
     11    <Classes datatype="tokens">Town Wall</Classes>
     12    <GenericName>City Wall</GenericName>
     13    <Tooltip>Persian WallSet</Tooltip>
     14  </Identity>
     15  <WallSet>
     16    <Tower>structures/pers_wall_tower</Tower>
     17    <Gate>structures/pers_wall_gate</Gate>
     18    <WallLong>structures/pers_wall_long</WallLong>
     19    <WallMedium>structures/pers_wall_medium</WallMedium>
     20    <WallShort>structures/pers_wall_short</WallShort>
     21  </WallSet>
     22</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/wallsets/rome_stone.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/wallsets/rome_stone.xml b/binaries/data/mods/public/simulation/templates/structures/wallsets/rome_stone.xml
    new file mode 100644
    index 0000000..d5ebab1
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<!-- Abstract entity to serve as a bare-minimum dummy constructable entity to initiate wall placement.
     3Defines the set of actual entities that are part of the same wall construction system (i.e., towers,
     4gates, wall segments of various length, etc.) -->
     5<Entity>
     6  <Identity>
     7    <Icon>structures/wall.png</Icon>
     8    <Civ>iber</Civ>
     9    <SpecificName>Roman WallSet</SpecificName>
     10    <!-- TODO: Undesirable copy/pasta from the wall templates :( -->
     11    <Classes datatype="tokens">Town Wall</Classes>
     12    <GenericName>City Wall</GenericName>
     13    <Tooltip>Roman WallSet</Tooltip>
     14  </Identity>
     15  <WallSet>
     16    <Tower>structures/rome_wall_tower</Tower>
     17    <Gate>structures/rome_wall_gate</Gate>
     18    <WallLong>structures/rome_wall_long</WallLong>
     19    <WallMedium>structures/rome_wall_medium</WallMedium>
     20    <WallShort>structures/rome_wall_short</WallShort>
     21  </WallSet>
     22</Entity>
  • binaries/data/mods/public/simulation/templates/template_unit_infantry.xml

    diff --git a/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml b/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml
    index 4d51082..3ef5b3a 100644
    a b  
    2020      structures/{civ}_dock
    2121      structures/{civ}_outpost
    2222      structures/{civ}_defense_tower
    23       structures/{civ}_wall
    24       structures/{civ}_wall_tower
    25       structures/{civ}_wall_gate
     23      <!-- TODO: add stone wallset here? do all civs have stone wallsets? -->
    2624      structures/{civ}_fortress
    2725    </Entities>
    2826  </Builder>
  • binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml

    diff --git a/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml b/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml
    index c90ad5f..8db4942 100644
    a b  
    1919      structures/{civ}_barracks
    2020      structures/{civ}_dock
    2121      structures/{civ}_scout_tower
    22       structures/{civ}_wall
    23       structures/{civ}_wall_tower
    24       structures/{civ}_wall_gate
     22      special/wallsets/{civ}_land
    2523      structures/{civ}_fortress
    2624    </Entities>
    2725  </Builder>
  • binaries/data/mods/public/simulation/templates/units/celt_fanatic.xml

    diff --git a/binaries/data/mods/public/simulation/templates/units/celt_fanatic.xml b/binaries/data/mods/public/simulation/templates/units/celt_fanatic.xml
    index 096de32..ffd1fca 100644
    a b  
    1212      <Hack>60.0</Hack>
    1313    </Charge>
    1414  </Attack>
     15  <!-- TODO: remove me; temporarily here for ease of playing with the wall system -->
     16  <Builder>
     17    <Rate>50.0</Rate>
     18    <Entities datatype="tokens">
     19      structures/wallsets/{civ}_palisade
     20      structures/wallsets/iber_stone
     21      structures/wallsets/pers_stone
     22      structures/wallsets/hele_stone
     23      structures/wallsets/rome_stone
     24    </Entities>
     25  </Builder>
    1526  <Cost>
    1627    <Resources>
    1728      <food>0</food>
  • source/graphics/Overlay.h

    diff --git a/binaries/system/ActorEditor.exe b/binaries/system/ActorEditor.exe
    deleted file mode 100644
    index 15cdfc5..0000000
    Binary files a/binaries/system/ActorEditor.exe and /dev/null differ
    diff --git a/source/graphics/Overlay.h b/source/graphics/Overlay.h
    index ce6f7c4..6d0dfe7 100644
    a b  
    2121#include "graphics/RenderableObject.h"
    2222#include "graphics/Texture.h"
    2323#include "maths/Vector3D.h"
     24#include "maths/FixedVector3D.h"
    2425#include "ps/Overlay.h" // CColor  (TODO: that file has nothing to do with overlays, it should be renamed)
    2526
    2627class CTerrain;
    struct SOverlayLine  
    3738    std::vector<float> m_Coords; // (x, y, z) vertex coordinate triples; shape is not automatically closed
    3839    u8 m_Thickness; // pixels
    3940
    40     /// Utility function; pushes three vertex coordinates at once onto the coordinates array
    4141    void PushCoords(const float x, const float y, const float z) { m_Coords.push_back(x); m_Coords.push_back(y); m_Coords.push_back(z); }
    42     /// Utility function; pushes a vertex location onto the coordinates array
    4342    void PushCoords(const CVector3D& v) { PushCoords(v.X, v.Y, v.Z); }
     43    void PushCoords(const CFixedVector3D& v) { PushCoords(v.X.ToFloat(), v.Y.ToFloat(), v.Z.ToFloat()); }
    4444};
    4545
    4646/**
  • source/gui/scripting/ScriptFunctions.cpp

    diff --git a/source/gui/scripting/ScriptFunctions.cpp b/source/gui/scripting/ScriptFunctions.cpp
    index 4fad858..a097b17 100644
    a b std::vector<entity_id_t> PickFriendlyEntitiesOnScreen(void* cbdata, int player)  
    141141    return PickFriendlyEntitiesInRect(cbdata, 0, 0, g_xres, g_yres, player);
    142142}
    143143
    144 std::vector<entity_id_t> PickSimilarFriendlyEntities(void* UNUSED(cbdata), std::string templateName, bool includeOffScreen, bool matchRank)
     144std::vector<entity_id_t> PickSimilarFriendlyEntities(void* UNUSED(cbdata), std::string templateName, bool includeOffScreen, bool matchRank, bool allowFoundations)
    145145{
    146     return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank, false);
     146    return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank, false, allowFoundations);
    147147}
    148148
    149149CFixedVector3D GetTerrainAtPoint(void* UNUSED(cbdata), int x, int y)
    void GuiScriptingInit(ScriptInterface& scriptInterface)  
    574574    scriptInterface.RegisterFunction<std::vector<entity_id_t>, int, int, &PickEntitiesAtPoint>("PickEntitiesAtPoint");
    575575    scriptInterface.RegisterFunction<std::vector<entity_id_t>, int, int, int, int, int, &PickFriendlyEntitiesInRect>("PickFriendlyEntitiesInRect");
    576576    scriptInterface.RegisterFunction<std::vector<entity_id_t>, int, &PickFriendlyEntitiesOnScreen>("PickFriendlyEntitiesOnScreen");
    577     scriptInterface.RegisterFunction<std::vector<entity_id_t>, std::string, bool, bool, &PickSimilarFriendlyEntities>("PickSimilarFriendlyEntities");
     577    scriptInterface.RegisterFunction<std::vector<entity_id_t>, std::string, bool, bool, bool, &PickSimilarFriendlyEntities>("PickSimilarFriendlyEntities");
    578578    scriptInterface.RegisterFunction<CFixedVector3D, int, int, &GetTerrainAtPoint>("GetTerrainAtPoint");
    579579
    580580    // Network / game setup functions
  • source/lib/self_test.h

    diff --git a/source/lib/self_test.h b/source/lib/self_test.h
    index 889616a..27e3870 100644
    a b std::vector<T> ts_make_vector(T* start, size_t size_bytes)  
    279279    return std::vector<T>(start, start+(size_bytes/sizeof(T)));
    280280}
    281281#define TS_ASSERT_VECTOR_EQUALS_ARRAY(vec1, array) TS_ASSERT_EQUALS(vec1, ts_make_vector((array), sizeof(array)))
     282#define TS_ASSERT_VECTOR_CONTAINS(vec1, element) TS_ASSERT(std::find((vec1).begin(), (vec1).end(), element) != (vec1).end());
    282283
    283284class ScriptInterface;
    284285// Script-based testing setup (defined in test_setup.cpp). Defines TS_* functions.
  • source/simulation2/components/CCmpObstruction.cpp

    diff --git a/source/simulation2/components/CCmpObstruction.cpp b/source/simulation2/components/CCmpObstruction.cpp
    index 92325ba..cd23aab 100644
    a b  
    2020#include "simulation2/system/Component.h"
    2121#include "ICmpObstruction.h"
    2222
     23#include "ps/CLogger.h"
     24#include "simulation2/MessageTypes.h"
    2325#include "simulation2/components/ICmpObstructionManager.h"
    2426#include "simulation2/components/ICmpPosition.h"
    2527
    26 #include "simulation2/MessageTypes.h"
    27 
    2828/**
    2929 * Obstruction implementation. This keeps the ICmpPathfinder's model of the world updated when the
    3030 * entities move and die, with shapes derived from ICmpFootprint.
    public:  
    4949        STATIC,
    5050        UNIT
    5151    } m_Type;
     52
    5253    entity_pos_t m_Size0; // radius or width
    5354    entity_pos_t m_Size1; // radius or depth
    5455    flags_t m_TemplateFlags;
    5556
    5657    // Dynamic state:
    5758
    58     bool m_Active; // whether the obstruction is obstructing or just an inactive placeholder
     59    /// Whether the obstruction is actively obstructing or just an inactive placeholder
     60    bool m_Active;
    5961    bool m_Moving;
     62
     63    /**
     64     * Unique identifier for grouping obstruction shapes, typically to have member shapes ignore
     65     * each other during obstruction tests. Defaults to the entity ID.
     66     *
     67     * TODO: if needed, perhaps add a mask to specify with respect to which flags members of the
     68     * group should ignore each other.
     69     */
    6070    entity_id_t m_ControlGroup;
     71    entity_id_t m_ControlGroup2;
     72
     73    /// Identifier of this entity's obstruction shape. Contains structure, but should be treated
     74    /// as opaque here.
    6175    tag_t m_Tag;
     76    /// Set of flags affecting the behaviour of this entity's obstruction shape.
    6277    flags_t m_Flags;
    6378
    6479    static std::string GetSchema()
    public:  
    139154        m_Tag = tag_t();
    140155        m_Moving = false;
    141156        m_ControlGroup = GetEntityId();
     157        m_ControlGroup2 = INVALID_ENTITY;
    142158    }
    143159
    144160    virtual void Deinit()
    public:  
    194210                // Need to create a new pathfinder shape:
    195211                if (m_Type == STATIC)
    196212                    m_Tag = cmpObstructionManager->AddStaticShape(GetEntityId(),
    197                         data.x, data.z, data.a, m_Size0, m_Size1, m_Flags);
     213                        data.x, data.z, data.a, m_Size0, m_Size1, m_Flags, m_ControlGroup, m_ControlGroup2);
    198214                else
    199215                    m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(),
    200216                        data.x, data.z, m_Size0, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup);
    public:  
    241257            if (!cmpPosition->IsInWorld())
    242258                return; // don't need an obstruction
    243259
     260            // TODO: code duplication from message handlers
    244261            CFixedVector2D pos = cmpPosition->GetPosition2D();
    245262            if (m_Type == STATIC)
    246263                m_Tag = cmpObstructionManager->AddStaticShape(GetEntityId(),
    247                     pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, m_Flags);
     264                    pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, m_Flags, m_ControlGroup, m_ControlGroup2);
    248265            else
    249266                m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(),
    250267                    pos.X, pos.Y, m_Size0, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup);
    public:  
    255272
    256273            // Delete the obstruction shape
    257274
     275            // TODO: code duplication from message handlers
    258276            if (m_Tag.valid())
    259277            {
    260278                CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
    public:  
    346364        if (!cmpPathfinder)
    347365            return false; // error
    348366
     367        // required precondition to use SkipControlGroupsRequireFlagObstructionFilter
     368        if (m_ControlGroup == INVALID_ENTITY)
     369        {
     370            LOGERROR(L"[CmpObstruction] Cannot test for foundation obstructions; primary control group must be valid");
     371            return false;
     372        }
     373
    349374        // Get passability class
    350375        ICmpPathfinder::pass_class_t passClass = cmpPathfinder->GetPassabilityClass(className);
    351376
    352         // Ignore collisions with self, or with non-foundation-blocking obstructions
    353         SkipTagFlagsObstructionFilter filter(m_Tag, ICmpObstructionManager::FLAG_BLOCK_FOUNDATION);
     377        // Ignore collisions within the same control group, or with other non-foundation-blocking shapes.
     378        // Note that, since the control group for each entity defaults to the entity's ID, this is typically
     379        // equivalent to only ignoring the entity's own shape and other non-foundation-blocking shapes.
     380        SkipControlGroupsRequireFlagObstructionFilter filter(m_ControlGroup, m_ControlGroup2,
     381            ICmpObstructionManager::FLAG_BLOCK_FOUNDATION);
    354382
    355383        if (m_Type == STATIC)
    356384            return cmpPathfinder->CheckBuildingPlacement(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, GetEntityId(), passClass);
    public:  
    375403        if (!cmpObstructionManager)
    376404            return ret; // error
    377405
    378         // Ignore collisions with self, or with non-construction-blocking obstructions
    379         SkipTagFlagsObstructionFilter filter(m_Tag, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION);
     406        // required precondition to use SkipControlGroupsRequireFlagObstructionFilter
     407        if (m_ControlGroup == INVALID_ENTITY)
     408        {
     409            LOGERROR(L"[CmpObstruction] Cannot test for construction obstructions; primary control group must be valid");
     410            return ret;
     411        }
     412
     413        // Ignore collisions within the same control group, or with other non-construction-blocking shapes.
     414        // Note that, since the control group for each entity defaults to the entity's ID, this is typically
     415        // equivalent to only ignoring the entity's own shape and other non-construction-blocking shapes.
     416        SkipControlGroupsRequireFlagObstructionFilter filter(m_ControlGroup, m_ControlGroup2,
     417            ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION);
    380418
    381419        if (m_Type == STATIC)
    382420            cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, &ret);
    public:  
    401439    virtual void SetControlGroup(entity_id_t group)
    402440    {
    403441        m_ControlGroup = group;
     442        UpdateControlGroups();
     443    }
    404444
    405         if (m_Tag.valid() && m_Type == UNIT)
     445    virtual void SetControlGroup2(entity_id_t group2)
     446    {
     447        m_ControlGroup2 = group2;
     448        UpdateControlGroups();
     449    }
     450
     451    virtual entity_id_t GetControlGroup()
     452    {
     453        return m_ControlGroup;
     454    }
     455
     456    virtual entity_id_t GetControlGroup2()
     457    {
     458        return m_ControlGroup2;
     459    }
     460
     461    void UpdateControlGroups()
     462    {
     463        if (m_Tag.valid())
    406464        {
    407465            CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
    408466            if (cmpObstructionManager)
    409                 cmpObstructionManager->SetUnitControlGroup(m_Tag, m_ControlGroup);
     467            {
     468                if (m_Type == UNIT)
     469                {
     470                    cmpObstructionManager->SetUnitControlGroup(m_Tag, m_ControlGroup);
     471                }
     472                else if (m_Type == STATIC)
     473                {
     474                    cmpObstructionManager->SetStaticControlGroup(m_Tag, m_ControlGroup, m_ControlGroup2);
     475                }
     476            }
    410477        }
    411478    }
    412479
  • source/simulation2/components/CCmpObstructionManager.cpp

    diff --git a/source/simulation2/components/CCmpObstructionManager.cpp b/source/simulation2/components/CCmpObstructionManager.cpp
    index e1a8d6a..4836eab 100644
    a b  
    1 /* Copyright (C) 2011 Wildfire Games.
     1/* Copyright (C) 2012 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
    44 * 0 A.D. is free software: you can redistribute it and/or modify
     
    3232#include "ps/Overlay.h"
    3333#include "ps/Profile.h"
    3434#include "renderer/Scene.h"
     35#include "ps/CLogger.h"
    3536
    3637// Externally, tags are opaque non-zero positive integers.
    3738// Internally, they are tagged (by shape) indexes into shape lists.
    struct StaticShape  
    6566    CFixedVector2D u, v; // orthogonal unit vectors - axes of local coordinate space
    6667    entity_pos_t hw, hh; // half width/height in local coordinate space
    6768    ICmpObstructionManager::flags_t flags;
     69    entity_id_t group;
     70    entity_id_t group2;
    6871};
    6972
    7073/**
    struct SerializeStaticShape  
    102105        serialize.NumberFixed_Unbounded("hw", value.hw);
    103106        serialize.NumberFixed_Unbounded("hh", value.hh);
    104107        serialize.NumberU8_Unbounded("flags", value.flags);
     108        serialize.NumberU32_Unbounded("group", value.group);
    105109    }
    106110};
    107111
    public:  
    257261        return UNIT_INDEX_TO_TAG(id);
    258262    }
    259263
    260     virtual tag_t AddStaticShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h, flags_t flags)
     264    virtual tag_t AddStaticShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h, flags_t flags, entity_id_t group, entity_id_t group2 /* = INVALID_ENTITY */)
    261265    {
    262266        fixed s, c;
    263267        sincos_approx(a, s, c);
    264268        CFixedVector2D u(c, -s);
    265269        CFixedVector2D v(s, c);
    266270
    267         StaticShape shape = { ent, x, z, u, v, w/2, h/2, flags };
     271        StaticShape shape = { ent, x, z, u, v, w/2, h/2, flags, group, group2 };
    268272        u32 id = m_StaticShapeNext++;
    269273        m_StaticShapes[id] = shape;
    270274        MakeDirtyStatic(flags);
    public:  
    367371        }
    368372    }
    369373
     374    virtual void SetStaticControlGroup(tag_t tag, entity_id_t group, entity_id_t group2)
     375    {
     376        ENSURE(TAG_IS_VALID(tag) && TAG_IS_STATIC(tag));
     377
     378        if (TAG_IS_STATIC(tag))
     379        {
     380            StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)];
     381            shape.group = group;
     382            shape.group2 = group2;
     383        }
     384    }
     385
    370386    virtual void RemoveShape(tag_t tag)
    371387    {
    372388        ENSURE(TAG_IS_VALID(tag));
    bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, enti  
    533549        std::map<u32, UnitShape>::iterator it = m_UnitShapes.find(unitShapes[i]);
    534550        ENSURE(it != m_UnitShapes.end());
    535551
    536         if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
     552        if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
    537553            continue;
    538554
    539555        CFixedVector2D center(it->second.x, it->second.z);
    bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, enti  
    548564        std::map<u32, StaticShape>::iterator it = m_StaticShapes.find(staticShapes[i]);
    549565        ENSURE(it != m_StaticShapes.end());
    550566
    551         if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
     567        if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
    552568            continue;
    553569
    554570        CFixedVector2D center(it->second.x, it->second.z);
    bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filte  
    592608
    593609    for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
    594610    {
    595         if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
     611        if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
    596612            continue;
    597613
    598614        CFixedVector2D center1(it->second.x, it->second.z);
    bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filte  
    608624
    609625    for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
    610626    {
    611         if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
     627        if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
    612628            continue;
    613629
    614630        CFixedVector2D center1(it->second.x, it->second.z);
    bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter,  
    649665
    650666    for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
    651667    {
    652         if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
     668        if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
    653669            continue;
    654670
    655671        entity_pos_t r1 = it->second.r;
    bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter,  
    665681
    666682    for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
    667683    {
    668         if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
     684        if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
    669685            continue;
    670686
    671687        CFixedVector2D center1(it->second.x, it->second.z);
    void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter  
    869885        std::map<u32, UnitShape>::iterator it = m_UnitShapes.find(unitShapes[i]);
    870886        ENSURE(it != m_UnitShapes.end());
    871887
    872         if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
     888        if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
    873889            continue;
    874890
    875891        entity_pos_t r = it->second.r;
    void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter  
    890906        std::map<u32, StaticShape>::iterator it = m_StaticShapes.find(staticShapes[i]);
    891907        ENSURE(it != m_StaticShapes.end());
    892908
    893         if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
     909        if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
    894910            continue;
    895911
    896912        entity_pos_t r = it->second.hw + it->second.hh; // overestimate the max dist of an edge from the center
  • source/simulation2/components/CCmpRallyPointRenderer.cpp

    diff --git a/source/simulation2/components/CCmpRallyPointRenderer.cpp b/source/simulation2/components/CCmpRallyPointRenderer.cpp
    index dc16628..5e43033 100644
    a b void CCmpRallyPointRenderer::ReduceSegmentsByVisibility(std::vector<CVector2D>&  
    870870    // process from there on until the entire line is checked. The output is the array of base nodes.
    871871
    872872    std::vector<CVector2D> newCoords;
    873     StationaryObstructionFilter obstructionFilter;
     873    StationaryOnlyObstructionFilter obstructionFilter;
    874874    entity_pos_t lineRadius = fixed::FromFloat(m_LineThickness);
    875875    ICmpPathfinder::pass_class_t passabilityClass = cmpPathFinder->GetPassabilityClass(m_LinePassabilityClass);
    876876
  • source/simulation2/components/ICmpObstruction.cpp

    diff --git a/source/simulation2/components/ICmpObstruction.cpp b/source/simulation2/components/ICmpObstruction.cpp
    index ab8324e..15509e9 100644
    a b  
    1 /* Copyright (C) 2011 Wildfire Games.
     1/* Copyright (C) 2012 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
    44 * 0 A.D. is free software: you can redistribute it and/or modify
    DEFINE_INTERFACE_METHOD_1("SetActive", void, ICmpObstruction, SetActive, bool)  
    2929DEFINE_INTERFACE_METHOD_1("SetDisableBlockMovementPathfinding", void, ICmpObstruction, SetDisableBlockMovementPathfinding, bool)
    3030DEFINE_INTERFACE_METHOD_0("GetBlockMovementFlag", bool, ICmpObstruction, GetBlockMovementFlag)
    3131DEFINE_INTERFACE_METHOD_1("SetControlGroup", void, ICmpObstruction, SetControlGroup, entity_id_t)
     32DEFINE_INTERFACE_METHOD_0("GetControlGroup", entity_id_t, ICmpObstruction, GetControlGroup)
     33DEFINE_INTERFACE_METHOD_1("SetControlGroup2", void, ICmpObstruction, SetControlGroup2, entity_id_t)
     34DEFINE_INTERFACE_METHOD_0("GetControlGroup2", entity_id_t, ICmpObstruction, GetControlGroup2)
    3235END_INTERFACE_WRAPPER(Obstruction)
  • source/simulation2/components/ICmpObstruction.h

    diff --git a/source/simulation2/components/ICmpObstruction.h b/source/simulation2/components/ICmpObstruction.h
    index f8693e7..4964b65 100644
    a b  
    1 /* Copyright (C) 2011 Wildfire Games.
     1/* Copyright (C) 2012 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
    44 * 0 A.D. is free software: you can redistribute it and/or modify
    public:  
    4444    /**
    4545     * Test whether this entity is colliding with any obstruction that are set to
    4646     * block the creation of foundations.
     47     * @param ignoredEntities List of entities to ignore during the test.
    4748     * @return true if foundation is valid (not obstructed)
    4849     */
    4950    virtual bool CheckFoundation(std::string className) = 0;
    public:  
    7071     */
    7172    virtual void SetControlGroup(entity_id_t group) = 0;
    7273
     74    /// See SetControlGroup.
     75    virtual entity_id_t GetControlGroup() = 0;
     76
     77    virtual void SetControlGroup2(entity_id_t group2) = 0;
     78    virtual entity_id_t GetControlGroup2() = 0;
     79
    7380    DECLARE_INTERFACE_TYPE(Obstruction)
    7481};
    7582
  • source/simulation2/components/ICmpObstructionManager.h

    diff --git a/source/simulation2/components/ICmpObstructionManager.h b/source/simulation2/components/ICmpObstructionManager.h
    index b81a6d1..9a6507c 100644
    a b  
    1 /* Copyright (C) 2011 Wildfire Games.
     1/* Copyright (C) 2012 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
    44 * 0 A.D. is free software: you can redistribute it and/or modify
    public:  
    9393
    9494    /**
    9595     * Register a static shape.
     96     *
    9697     * @param ent entity ID associated with this shape (or INVALID_ENTITY if none)
    9798     * @param x,z coordinates of center, in world space
    9899     * @param a angle of rotation (clockwise from +Z direction)
    99100     * @param w width (size along X axis)
    100101     * @param h height (size along Z axis)
    101102     * @param flags a set of EFlags values
     103     * @param group primary control group of the shape. Must be a valid control group ID.
     104     * @param group2 Optional; secondary control group of the shape. Defaults to INVALID_ENTITY.
    102105     * @return a valid tag for manipulating the shape
    103106     * @see StaticShape
    104107     */
    105     virtual tag_t AddStaticShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t a, entity_pos_t w, entity_pos_t h, flags_t flags) = 0;
     108    virtual tag_t AddStaticShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t a,
     109        entity_pos_t w, entity_pos_t h, flags_t flags, entity_id_t group, entity_id_t group2 = INVALID_ENTITY) = 0;
    106110
    107111    /**
    108112     * Register a unit shape.
     113     *
    109114     * @param ent entity ID associated with this shape (or INVALID_ENTITY if none)
    110115     * @param x,z coordinates of center, in world space
    111116     * @param r radius of circle or half the unit's width/height
    public:  
    115120     * @return a valid tag for manipulating the shape
    116121     * @see UnitShape
    117122     */
    118     virtual tag_t AddUnitShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t r, flags_t flags, entity_id_t group) = 0;
     123    virtual tag_t AddUnitShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t r, flags_t flags,
     124        entity_id_t group) = 0;
    119125
    120126    /**
    121127     * Adjust the position and angle of an existing shape.
    public:  
    141147    virtual void SetUnitControlGroup(tag_t tag, entity_id_t group) = 0;
    142148
    143149    /**
     150     * Sets the control group of a static shape.
     151     * @param tag Tag of the shape to set the control group for. Must be a valid and static shape tag.
     152     * @param group Control group entity ID.
     153     */
     154    virtual void SetStaticControlGroup(tag_t tag, entity_id_t group, entity_id_t group2) = 0;
     155
     156    /**
    144157     * Remove an existing shape. The tag will be made invalid and must not be used after this.
    145158     * @param tag tag of shape (must be valid)
    146159     */
    public:  
    162175
    163176    /**
    164177     * Collision test a static square shape against the current set of shapes.
    165      * @param filter filter to restrict the shapes that are counted
     178     * @param filter filter to restrict the shapes that are being tested against
    166179     * @param x X coordinate of center
    167180     * @param z Z coordinate of center
    168181     * @param a angle of rotation (clockwise from +Z direction)
    public:  
    176189        std::vector<entity_id_t>* out) = 0;
    177190
    178191    /**
    179      * Collision test a unit shape against the current set of shapes.
    180      * @param filter filter to restrict the shapes that are counted
    181      * @param x X coordinate of center
    182      * @param z Z coordinate of center
    183      * @param r radius (half the unit's width/height)
     192     * Collision test a unit shape against the current set of registered shapes, and optionally writes a list of the colliding
     193     * shapes' entities to an output list.
     194     *
     195     * @param filter filter to restrict the shapes that are being tested against
     196     * @param x X coordinate of shape's center
     197     * @param z Z coordinate of shape's center
     198     * @param r radius of the shape (half the unit's width/height)
    184199     * @param out if non-NULL, all colliding shapes' entities will be added to this list
     200     *
    185201     * @return true if there is a collision
    186202     */
    187203    virtual bool TestUnitShape(const IObstructionTestFilter& filter,
    public:  
    274290    virtual ~IObstructionTestFilter() {}
    275291
    276292    /**
    277      * Return true if the shape should be counted for collisions.
     293     * Return true if the shape with the specified parameters should be tested for collisions.
    278294     * This is called for all shapes that would collide, and also for some that wouldn't.
     295     *
    279296     * @param tag tag of shape being tested
    280297     * @param flags set of EFlags for the shape
    281      * @param group the control group (typically the shape's unit, or the unit's formation controller, or 0)
     298     * @param group the control group of the shape (typically the shape's unit, or the unit's formation controller, or 0)
     299     * @param group2 an optional secondary control group of the shape, or INVALID_ENTITY if none specified. Currently
     300     *               exists only for static shapes.
    282301     */
    283     virtual bool Allowed(tag_t tag, flags_t flags, entity_id_t group) const = 0;
     302    virtual bool TestShape(tag_t tag, flags_t flags, entity_id_t group, entity_id_t group2) const = 0;
    284303};
    285304
    286305/**
    287  * Obstruction test filter that accepts all shapes.
     306 * Obstruction test filter that will test against all shapes.
    288307 */
    289308class NullObstructionFilter : public IObstructionTestFilter
    290309{
    291310public:
    292     virtual bool Allowed(tag_t UNUSED(tag), flags_t UNUSED(flags), entity_id_t UNUSED(group)) const
     311    virtual bool TestShape(tag_t UNUSED(tag), flags_t UNUSED(flags), entity_id_t UNUSED(group), entity_id_t UNUSED(group2)) const
    293312    {
    294313        return true;
    295314    }
    296315};
    297316
    298317/**
    299  * Obstruction test filter that accepts all non-moving shapes.
     318 * Obstruction test filter that will test only against stationary (i.e. non-moving) shapes.
    300319 */
    301 class StationaryObstructionFilter : public IObstructionTestFilter
     320class StationaryOnlyObstructionFilter : public IObstructionTestFilter
    302321{
    303322public:
    304     virtual bool Allowed(tag_t UNUSED(tag), flags_t flags, entity_id_t UNUSED(group)) const
     323    virtual bool TestShape(tag_t UNUSED(tag), flags_t flags, entity_id_t UNUSED(group), entity_id_t UNUSED(group2)) const
    305324    {
    306325        return !(flags & ICmpObstructionManager::FLAG_MOVING);
    307326    }
    public:  
    309328
    310329/**
    311330 * Obstruction test filter that reject shapes in a given control group,
    312  * and optionally rejects moving shapes,
    313  * and rejects shapes that don't block unit movement.
     331 * and rejects shapes that don't block unit movement, and optionally rejects moving shapes.
    314332 */
    315333class ControlGroupMovementObstructionFilter : public IObstructionTestFilter
    316334{
    317335    bool m_AvoidMoving;
    318336    entity_id_t m_Group;
     337
    319338public:
    320339    ControlGroupMovementObstructionFilter(bool avoidMoving, entity_id_t group) :
    321340        m_AvoidMoving(avoidMoving), m_Group(group)
    322     {
    323     }
     341    {}
    324342
    325     virtual bool Allowed(tag_t UNUSED(tag), flags_t flags, entity_id_t group) const
     343    virtual bool TestShape(tag_t UNUSED(tag), flags_t flags, entity_id_t group, entity_id_t group2) const
    326344    {
    327         if (group == m_Group)
     345        if (group == m_Group || (group2 != INVALID_ENTITY && group2 == m_Group))
    328346            return false;
    329347        if (!(flags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT))
    330348            return false;
    public:  
    335353};
    336354
    337355/**
    338  * Obstruction test filter that rejects a specific shape.
     356 * Obstruction test filter that will test only against shapes that:
     357 *     - are part of neither one of the specified control groups
     358 *     - AND have at least one of the specified flags set.
     359 *
     360 * The first (primary) control group to reject shapes from must be specified and valid. Set the
     361 * secondary control group to INVALID_ENTITY to use only the first.
     362 *
     363 * This filter is useful to e.g. allow foundations within the same control group to be placed and
     364 * constructed arbitrarily close together (e.g. for wall pieces that need to link up tightly).
     365 */
     366class SkipControlGroupsRequireFlagObstructionFilter : public IObstructionTestFilter
     367{
     368    entity_id_t m_Group;
     369    entity_id_t m_Group2;
     370    flags_t m_Mask;
     371
     372public:
     373    SkipControlGroupsRequireFlagObstructionFilter(entity_id_t group1, entity_id_t group2, flags_t mask) :
     374        m_Group(group1), m_Group2(group2), m_Mask(mask)
     375    {
     376        // the primary control group to filter out must be valid
     377        ENSURE(m_Group != INVALID_ENTITY);
     378
     379        // for simplicity, if m_Group2 is INVALID_ENTITY (i.e. not used), then set it equal to m_Group
     380        // so that we have fewer special cases to consider in TestShape().
     381        if (m_Group2 == INVALID_ENTITY)
     382            m_Group2 = m_Group;
     383    }
     384
     385    virtual bool TestShape(tag_t UNUSED(tag), flags_t flags, entity_id_t group, entity_id_t group2) const
     386    {
     387        // to be included in the testing, a shape must have at least one of the flags in m_Mask set, and its
     388        // primary control group must be valid and must equal neither our primary nor secondary control group.
     389        bool includeInTesting = ((flags & m_Mask) != 0 && group != m_Group && group != m_Group2);
     390
     391        // if the shape being tested has a valid secondary control group, exclude it from testing if it
     392        // matches either our primary or secondary control group.
     393        if (group2 != INVALID_ENTITY)
     394            includeInTesting = (includeInTesting && group2 != m_Group && group2 != m_Group2);
     395
     396        return includeInTesting;
     397    }
     398};
     399
     400/**
     401 * Obstruction test filter that will test only against shapes that do not have the specified tag set.
    339402 */
    340403class SkipTagObstructionFilter : public IObstructionTestFilter
    341404{
    public:  
    345408    {
    346409    }
    347410
    348     virtual bool Allowed(tag_t tag, flags_t UNUSED(flags), entity_id_t UNUSED(group)) const
     411    virtual bool TestShape(tag_t tag, flags_t UNUSED(flags), entity_id_t UNUSED(group), entity_id_t UNUSED(group2)) const
    349412    {
    350413        return tag.n != m_Tag.n;
    351414    }
    352415};
    353416
    354417/**
    355  * Obstruction test filter that rejects a specific shape, and requires the given flags.
     418 * Obstruction test filter that will test only against shapes that:
     419 *    - do not have the specified tag
     420 *    - AND have at least one of the specified flags set.
    356421 */
    357 class SkipTagFlagsObstructionFilter : public IObstructionTestFilter
     422class SkipTagRequireFlagsObstructionFilter : public IObstructionTestFilter
    358423{
    359424    tag_t m_Tag;
    360425    flags_t m_Mask;
    361426public:
    362     SkipTagFlagsObstructionFilter(tag_t tag, flags_t mask) : m_Tag(tag), m_Mask(mask)
     427    SkipTagRequireFlagsObstructionFilter(tag_t tag, flags_t mask) : m_Tag(tag), m_Mask(mask)
    363428    {
    364429    }
    365430
    366     virtual bool Allowed(tag_t tag, flags_t flags, entity_id_t UNUSED(group)) const
     431    virtual bool TestShape(tag_t tag, flags_t flags, entity_id_t UNUSED(group), entity_id_t UNUSED(group2)) const
    367432    {
    368433        return (tag.n != m_Tag.n && (flags & m_Mask) != 0);
    369434    }
  • new file source/simulation2/components/tests/test_ObstructionManager.h

    diff --git a/source/simulation2/components/tests/test_ObstructionManager.h b/source/simulation2/components/tests/test_ObstructionManager.h
    new file mode 100644
    index 0000000..f327e1a
    - +  
     1/* Copyright (C) 2012 Wildfire Games.
     2 * This file is part of 0 A.D.
     3 *
     4 * 0 A.D. is free software: you can redistribute it and/or modify
     5 * it under the terms of the GNU General Public License as published by
     6 * the Free Software Foundation, either version 2 of the License, or
     7 * (at your option) any later version.
     8 *
     9 * 0 A.D. is distributed in the hope that it will be useful,
     10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
     11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12 * GNU General Public License for more details.
     13 *
     14 * You should have received a copy of the GNU General Public License
     15 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
     16 */
     17
     18#include "simulation2/system/ComponentTest.h"
     19
     20#include "simulation2/components/ICmpObstructionManager.h"
     21
     22class TestCmpObstructionManager : public CxxTest::TestSuite
     23{
     24    typedef ICmpObstructionManager::tag_t tag_t;
     25    typedef ICmpObstructionManager::ObstructionSquare ObstructionSquare;
     26
     27    // some variables for setting up a scene with 3 shapes
     28    entity_id_t ent1, ent2, ent3; // entity IDs
     29    entity_angle_t ent1a, ent2r, ent3r; // angles/radiuses
     30    entity_pos_t ent1x, ent1z, ent1w, ent1h, // positions/dimensions
     31                 ent2x, ent2z,
     32                 ent3x, ent3z;
     33    entity_id_t ent1g1, ent1g2, ent2g, ent3g; // control groups
     34
     35    tag_t shape1, shape2, shape3;
     36   
     37    ICmpObstructionManager* cmp;
     38    ComponentTestHelper* testHelper;
     39
     40public:
     41    void setUp()
     42    {
     43        CXeromyces::Startup();
     44        CxxTest::setAbortTestOnFail(true);
     45
     46        // set up a simple scene with some predefined obstruction shapes
     47        // (we can't position shapes on the origin because the world bounds must range
     48        // from 0 to X, so instead we'll offset things by, say, 10).
     49
     50        ent1 = 1;
     51        ent1a = fixed::Zero();
     52        ent1w = fixed::FromFloat(4);
     53        ent1h = fixed::FromFloat(2);
     54        ent1x = fixed::FromInt(10);
     55        ent1z = fixed::FromInt(10);
     56        ent1g1 = ent1;
     57        ent1g2 = INVALID_ENTITY;
     58
     59        ent2 = 2;
     60        ent2r = fixed::FromFloat(1);
     61        ent2x = ent1x;
     62        ent2z = ent1z;
     63        ent2g = ent1g1;
     64
     65        ent3 = 3;
     66        ent3r = fixed::FromFloat(3);
     67        ent3x = ent2x;
     68        ent3z = ent2z + ent2r + ent3r; // ensure it just touches the border of ent2
     69        ent3g = ent3;
     70
     71        testHelper = new ComponentTestHelper;
     72        cmp = testHelper->Add<ICmpObstructionManager>(CID_ObstructionManager, "");
     73        cmp->SetBounds(fixed::FromInt(0), fixed::FromInt(0), fixed::FromInt(100), fixed::FromInt(100));
     74
     75        shape1 = cmp->AddStaticShape(ent1, ent1x, ent1z, ent1a, ent1w, ent1h,
     76            ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION |
     77            ICmpObstructionManager::FLAG_BLOCK_MOVEMENT |
     78            ICmpObstructionManager::FLAG_MOVING, ent1g1, ent1g2);
     79
     80        shape2 = cmp->AddUnitShape(ent2, ent2x, ent2z, ent2r,
     81            ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION |
     82            ICmpObstructionManager::FLAG_BLOCK_FOUNDATION, ent2g);
     83
     84        shape3 = cmp->AddUnitShape(ent3, ent3x, ent3z, ent3r,
     85            ICmpObstructionManager::FLAG_BLOCK_MOVEMENT |
     86            ICmpObstructionManager::FLAG_BLOCK_FOUNDATION, ent3g);
     87    }
     88
     89    void tearDown()
     90    {
     91        delete testHelper;
     92        cmp = NULL; // not our responsibility to deallocate
     93
     94        CXeromyces::Terminate();
     95    }
     96
     97    /**
     98     * Verifies the collision testing procedure. Collision-tests some simple shapes against the shapes registered in
     99     * the scene, and verifies the result of the test against the expected value.
     100     */
     101    void test_collision_simple()
     102    {
     103        std::vector<entity_id_t> out;
     104        NullObstructionFilter nullFilter;
     105       
     106        // Collision-test a simple shape nested inside shape3 against all shapes in the scene. Since the tested shape
     107        // overlaps only with shape 3, we should find only shape 3 in the result.
     108       
     109        cmp->TestUnitShape(nullFilter, ent3x, ent3z, fixed::FromInt(1), &out);
     110        TS_ASSERT_EQUALS(1, out.size());
     111        TS_ASSERT_EQUALS(ent3, out[0]);
     112        out.clear();
     113
     114        cmp->TestStaticShape(nullFilter, ent3x, ent3z, fixed::Zero(), fixed::FromInt(1), fixed::FromInt(1), &out);
     115        TS_ASSERT_EQUALS(1, out.size());
     116        TS_ASSERT_EQUALS(ent3, out[0]);
     117        out.clear();
     118
     119        // Similarly, collision-test a simple shape nested inside both shape1 and shape2. Since the tested shape overlaps
     120        // only with shapes 1 and 2, those are the only ones we should find in the result.
     121       
     122        cmp->TestUnitShape(nullFilter, ent2x, ent2z, ent2r/2, &out);
     123        TS_ASSERT_EQUALS(2, out.size());
     124        TS_ASSERT_VECTOR_CONTAINS(out, ent1);
     125        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     126        out.clear();
     127
     128        cmp->TestStaticShape(nullFilter, ent2x, ent2z, fixed::Zero(), ent2r, ent2r, &out);
     129        TS_ASSERT_EQUALS(2, out.size());
     130        TS_ASSERT_VECTOR_CONTAINS(out, ent1);
     131        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     132        out.clear();
     133    }
     134
     135    /**
     136     * Verifies the behaviour of the null obstruction filter. Tests with this filter will be performed against all
     137     * registered shapes.
     138     */
     139    void test_collision_filter_null()
     140    {
     141        std::vector<entity_id_t> out;
     142
     143        // Collision test a scene-covering shape against all shapes in the scene. We should find all registered shapes
     144        // in the result.
     145
     146        NullObstructionFilter nullFilter;
     147
     148        cmp->TestUnitShape(nullFilter, ent1x, ent1z, fixed::FromInt(10), &out);
     149        TS_ASSERT_EQUALS(3, out.size());
     150        TS_ASSERT_VECTOR_CONTAINS(out, ent1);
     151        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     152        TS_ASSERT_VECTOR_CONTAINS(out, ent3);
     153        out.clear();
     154
     155        cmp->TestStaticShape(nullFilter, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
     156        TS_ASSERT_EQUALS(3, out.size());
     157        TS_ASSERT_VECTOR_CONTAINS(out, ent1);
     158        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     159        TS_ASSERT_VECTOR_CONTAINS(out, ent3);
     160        out.clear();
     161    }
     162
     163    /**
     164     * Verifies the behaviour of the StationaryOnlyObstructionFilter. Tests with this filter will be performed only
     165     * against non-moving (stationary) shapes.
     166     */
     167    void test_collision_filter_stationary_only()
     168    {
     169        std::vector<entity_id_t> out;
     170
     171        // Collision test a scene-covering shape against all shapes in the scene, but skipping shapes that are moving,
     172        // i.e. shapes that have the MOVING flag. Since only shape 1 is flagged as moving, we should find
     173        // shapes 2 and 3 in each case.
     174
     175        StationaryOnlyObstructionFilter ignoreMoving;
     176
     177        cmp->TestUnitShape(ignoreMoving, ent1x, ent1z, fixed::FromInt(10), &out);
     178        TS_ASSERT_EQUALS(2, out.size());
     179        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     180        TS_ASSERT_VECTOR_CONTAINS(out, ent3);
     181        out.clear();
     182
     183        cmp->TestStaticShape(ignoreMoving, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
     184        TS_ASSERT_EQUALS(2, out.size());
     185        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     186        TS_ASSERT_VECTOR_CONTAINS(out, ent3);
     187        out.clear();
     188    }
     189
     190    /**
     191     * Verifies the behaviour of the SkipTagObstructionFilter. Tests with this filter will be performed against
     192     * all registered shapes that do not have the specified tag set.
     193     */
     194    void test_collision_filter_skip_tag()
     195    {
     196        std::vector<entity_id_t> out;
     197
     198        // Collision-test shape 2's obstruction shape against all shapes in the scene, but skipping tests against
     199        // shape 2. Since shape 2 overlaps only with shape 1, we should find only shape 1's entity ID in the result.
     200
     201        SkipTagObstructionFilter ignoreShape2(shape2);
     202
     203        cmp->TestUnitShape(ignoreShape2, ent2x, ent2z, ent2r/2, &out);
     204        TS_ASSERT_EQUALS(1, out.size());
     205        TS_ASSERT_EQUALS(ent1, out[0]);
     206        out.clear();
     207
     208        cmp->TestStaticShape(ignoreShape2, ent2x, ent2z, fixed::Zero(), ent2r, ent2r, &out);
     209        TS_ASSERT_EQUALS(1, out.size());
     210        TS_ASSERT_EQUALS(ent1, out[0]);
     211        out.clear();
     212    }
     213
     214    /**
     215     * Verifies the behaviour of the SkipTagFlagsObstructionFilter. Tests with this filter will be performed against
     216     * all registered shapes that do not have the specified tag set, and that have at least one of required flags set.
     217     */
     218    void test_collision_filter_skip_tag_require_flag()
     219    {
     220        std::vector<entity_id_t> out;
     221
     222        // Collision-test a scene-covering shape against all shapes in the scene, but skipping tests against shape 1
     223        // and requiring the BLOCK_MOVEMENT flag. Since shape 1 is being ignored and shape 2 does not have the required
     224        // flag, we should find only shape 3 in the results.
     225
     226        SkipTagRequireFlagsObstructionFilter skipShape1RequireBlockMovement(shape1, ICmpObstructionManager::FLAG_BLOCK_MOVEMENT);
     227
     228        cmp->TestUnitShape(skipShape1RequireBlockMovement, ent1x, ent1z, fixed::FromInt(10), &out);
     229        TS_ASSERT_EQUALS(1, out.size());
     230        TS_ASSERT_EQUALS(ent3, out[0]);
     231        out.clear();
     232
     233        cmp->TestStaticShape(skipShape1RequireBlockMovement, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
     234        TS_ASSERT_EQUALS(1, out.size());
     235        TS_ASSERT_EQUALS(ent3, out[0]);
     236        out.clear();
     237
     238        // If we now do the same test, but require at least one of the entire set of available filters, we should find
     239        // all shapes that are not shape 1 and that have at least one flag set. Since all shapes in our testing scene
     240        // have at least one flag set, we should find shape 2 and shape 3 in the results.
     241
     242        SkipTagRequireFlagsObstructionFilter skipShape1RequireAnyFlag(shape1, (ICmpObstructionManager::flags_t) -1);
     243
     244        cmp->TestUnitShape(skipShape1RequireAnyFlag, ent1x, ent1z, fixed::FromInt(10), &out);
     245        TS_ASSERT_EQUALS(2, out.size());
     246        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     247        TS_ASSERT_VECTOR_CONTAINS(out, ent3);
     248        out.clear();
     249
     250        cmp->TestStaticShape(skipShape1RequireAnyFlag, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
     251        TS_ASSERT_EQUALS(2, out.size());
     252        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     253        TS_ASSERT_VECTOR_CONTAINS(out, ent3);
     254        out.clear();
     255
     256        // And if we now do the same test yet again, but specify an empty set of flags, then it becomes impossible for
     257        // any shape to have at least one of the required flags, and we should hence find no shapes in the result.
     258       
     259        SkipTagRequireFlagsObstructionFilter skipShape1RejectAll(shape1, 0U);
     260       
     261        cmp->TestUnitShape(skipShape1RejectAll, ent1x, ent1z, fixed::FromInt(10), &out);
     262        TS_ASSERT_EQUALS(0, out.size());
     263        out.clear();
     264
     265        cmp->TestStaticShape(skipShape1RejectAll, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
     266        TS_ASSERT_EQUALS(0, out.size());
     267        out.clear();
     268    }
     269
     270    /**
     271     * Verifies the behaviour of SkipControlGroupsRequireFlagObstructionFilter. Tests with this filter will be performed
     272     * against all registered shapes that are members of neither specified control groups, and that have at least one of
     273     * the specified flags set.
     274     */
     275    void test_collision_filter_skip_controlgroups_require_flag()
     276    {
     277        std::vector<entity_id_t> out;
     278
     279        // Collision-test a shape that overlaps the entire scene, but ignoring shapes from shape1's control group
     280        // (which also includes shape 2), and requiring that either the BLOCK_FOUNDATION or the
     281        // BLOCK_CONSTRUCTION flag is set, or both. Since shape 1 and shape 2 both belong to shape 1's control
     282        // group, and shape 3 has the BLOCK_FOUNDATION flag (but not BLOCK_CONSTRUCTION), we should find only
     283        // shape 3 in the result.
     284
     285        SkipControlGroupsRequireFlagObstructionFilter skipGroup1ReqFoundConstr(ent1g1, INVALID_ENTITY,
     286            ICmpObstructionManager::FLAG_BLOCK_FOUNDATION | ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION);
     287
     288        cmp->TestUnitShape(skipGroup1ReqFoundConstr, ent1x, ent1z, fixed::FromInt(10), &out);
     289        TS_ASSERT_EQUALS(1, out.size());
     290        TS_ASSERT_EQUALS(ent3, out[0]);
     291        out.clear();
     292
     293        cmp->TestStaticShape(skipGroup1ReqFoundConstr, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
     294        TS_ASSERT_EQUALS(1, out.size());
     295        TS_ASSERT_EQUALS(ent3, out[0]);
     296        out.clear();
     297
     298        // Perform the same test, but now also exclude shape 3's control group (in addition to shape 1's control
     299        // group). Despite shape 3 having at least one of the required flags set, it should now also be ignored,
     300        // yielding an empty result set.
     301       
     302        SkipControlGroupsRequireFlagObstructionFilter skipGroup1And3ReqFoundConstr(ent1g1, ent3g,
     303            ICmpObstructionManager::FLAG_BLOCK_FOUNDATION | ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION);
     304
     305        cmp->TestUnitShape(skipGroup1And3ReqFoundConstr, ent1x, ent1z, fixed::FromInt(10), &out);
     306        TS_ASSERT_EQUALS(0, out.size());
     307        out.clear();
     308
     309        cmp->TestStaticShape(skipGroup1And3ReqFoundConstr, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
     310        TS_ASSERT_EQUALS(0, out.size());
     311        out.clear();
     312
     313        // Same test, but this time excluding only shape 3's control group, and requiring any of the available flags
     314        // to be set. Since both shape 1 and shape 2 have at least one flag set and are both in a different control
     315        // group, we should find them in the result.
     316       
     317        SkipControlGroupsRequireFlagObstructionFilter skipGroup3RequireAnyFlag(ent3g, INVALID_ENTITY,
     318            (ICmpObstructionManager::flags_t) -1);
     319
     320        cmp->TestUnitShape(skipGroup3RequireAnyFlag, ent1x, ent1z, fixed::FromInt(10), &out);
     321        TS_ASSERT_EQUALS(2, out.size());
     322        TS_ASSERT_VECTOR_CONTAINS(out, ent1);
     323        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     324        out.clear();
     325
     326        cmp->TestStaticShape(skipGroup3RequireAnyFlag, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
     327        TS_ASSERT_EQUALS(2, out.size());
     328        TS_ASSERT_VECTOR_CONTAINS(out, ent1);
     329        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     330        out.clear();
     331
     332        // Finally, the same test as the one directly above, now with an empty set of required flags. Since it now becomes
     333        // impossible for shape 1 and shape 2 to have at least one of the required flags set, and shape 3 is excluded by
     334        // virtue of the control group filtering, we should find an empty result.
     335
     336        SkipControlGroupsRequireFlagObstructionFilter skipGroup3RequireNoFlags(ent3g, INVALID_ENTITY, 0U);
     337
     338        cmp->TestUnitShape(skipGroup3RequireNoFlags, ent1x, ent1z, fixed::FromInt(10), &out);
     339        TS_ASSERT_EQUALS(0, out.size());
     340        out.clear();
     341
     342        cmp->TestStaticShape(skipGroup3RequireNoFlags, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
     343        TS_ASSERT_EQUALS(0, out.size());
     344        out.clear();
     345
     346        // ------------------------------------------------------------------------------------
     347
     348        // In the tests up until this point, the shapes have all been filtered out based on their primary control group.
     349        // Now, to verify that shapes are also filtered out based on their secondary control groups, add a fourth shape
     350        // with arbitrarily-chosen dual control groups, and also change shape 1's secondary control group to another
     351        // arbitrarily-chosen control group. Then, do a scene-covering collision test while filtering out a combination
     352        // of shape 1's secondary control group, and one of shape 4's control groups. We should find neither ent1 nor ent4
     353        // in the result.
     354
     355        entity_id_t ent4 = 4,
     356                    ent4g1 = 17,
     357                    ent4g2 = 19,
     358                    ent1g2_new = 18; // new secondary control group for entity 1
     359        entity_pos_t ent4x = fixed::FromInt(4),
     360                     ent4z = fixed::Zero(),
     361                     ent4w = fixed::FromInt(1),
     362                     ent4h = fixed::FromInt(1);
     363        entity_angle_t ent4a = fixed::FromDouble(M_PI/3);
     364
     365        cmp->AddStaticShape(ent4, ent4x, ent4z, ent4a, ent4w, ent4h, ICmpObstructionManager::FLAG_BLOCK_PATHFINDING, ent4g1, ent4g2);
     366        cmp->SetStaticControlGroup(shape1, ent1g1, ent1g2_new);
     367
     368        // Exclude shape 1's and shape 4's secondary control groups from testing, and require any available flag to be set.
     369        // Since neither shape 2 nor shape 3 are part of those control groups and both have at least one available flag set,
     370        // the results should only those two shapes' entities.
     371
     372        SkipControlGroupsRequireFlagObstructionFilter skipGroup1SecAnd4SecRequireAny(ent1g2_new, ent4g2,
     373            (ICmpObstructionManager::flags_t) -1);
     374
     375        cmp->TestUnitShape(skipGroup1SecAnd4SecRequireAny, ent1x, ent1z, fixed::FromInt(10), &out);
     376        TS_ASSERT_EQUALS(2, out.size());
     377        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     378        TS_ASSERT_VECTOR_CONTAINS(out, ent3);
     379        out.clear();
     380
     381        cmp->TestStaticShape(skipGroup1SecAnd4SecRequireAny, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
     382        TS_ASSERT_EQUALS(2, out.size());
     383        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     384        TS_ASSERT_VECTOR_CONTAINS(out, ent3);
     385        out.clear();
     386
     387        // Same as the above, but now exclude shape 1's secondary and shape 4's primary control group, while still requiring
     388        // any available flag to be set. (Note that the test above used shape 4's secondary control group). Results should
     389        // remain the same.
     390       
     391        SkipControlGroupsRequireFlagObstructionFilter skipGroup1SecAnd4PrimRequireAny(ent1g2_new, ent4g1,
     392            (ICmpObstructionManager::flags_t) -1);
     393
     394        cmp->TestUnitShape(skipGroup1SecAnd4PrimRequireAny, ent1x, ent1z, fixed::FromInt(10), &out);
     395        TS_ASSERT_EQUALS(2, out.size());
     396        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     397        TS_ASSERT_VECTOR_CONTAINS(out, ent3);
     398        out.clear();
     399
     400        cmp->TestStaticShape(skipGroup1SecAnd4PrimRequireAny, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out);
     401        TS_ASSERT_EQUALS(2, out.size());
     402        TS_ASSERT_VECTOR_CONTAINS(out, ent2);
     403        TS_ASSERT_VECTOR_CONTAINS(out, ent3);
     404        out.clear();
     405
     406        cmp->SetStaticControlGroup(shape1, ent1g1, ent1g2); // restore shape 1's original secondary control group
     407    }
     408
     409    void test_adjacent_shapes()
     410    {
     411        std::vector<entity_id_t> out;
     412        NullObstructionFilter nullFilter;
     413        SkipTagObstructionFilter ignoreShape1(shape1);
     414        SkipTagObstructionFilter ignoreShape2(shape2);
     415        SkipTagObstructionFilter ignoreShape3(shape3);
     416
     417        // Collision-test a shape that is perfectly adjacent to shape3. This should be counted as a hit according to
     418        // the code at the time of writing.
     419       
     420        entity_angle_t ent4a = fixed::FromDouble(M_PI); // rotated 180 degrees, should not affect collision test
     421        entity_pos_t ent4w = fixed::FromInt(2),
     422                     ent4h = fixed::FromInt(1),
     423                     ent4x = ent3x + ent3r + ent4w/2, // make ent4 adjacent to ent3
     424                     ent4z = ent3z;
     425
     426        cmp->TestStaticShape(nullFilter, ent4x, ent4z, ent4a, ent4w, ent4h, &out);
     427        TS_ASSERT_EQUALS(1, out.size());
     428        TS_ASSERT_EQUALS(ent3, out[0]);
     429        out.clear();
     430
     431        cmp->TestUnitShape(nullFilter, ent4x, ent4z, ent4w/2, &out);
     432        TS_ASSERT_EQUALS(1, out.size());
     433        TS_ASSERT_EQUALS(ent3, out[0]);
     434        out.clear();
     435
     436        // now do the same tests, but move the shape a little bit to the right so that it doesn't touch anymore
     437
     438        cmp->TestStaticShape(nullFilter, ent4x + fixed::FromFloat(1e-5f), ent4z, ent4a, ent4w, ent4h, &out);
     439        TS_ASSERT_EQUALS(0, out.size());
     440        out.clear();
     441
     442        cmp->TestUnitShape(nullFilter, ent4x + fixed::FromFloat(1e-5f), ent4z, ent4w/2, &out);
     443        TS_ASSERT_EQUALS(0, out.size());
     444        out.clear();
     445    }
     446
     447    /**
     448     * Verifies that fetching the registered shapes from the obstruction manager yields the correct results.
     449     */
     450    void test_get_obstruction()
     451    {
     452        ObstructionSquare obSquare1 = cmp->GetObstruction(shape1);
     453        ObstructionSquare obSquare2 = cmp->GetObstruction(shape2);
     454        ObstructionSquare obSquare3 = cmp->GetObstruction(shape3);
     455
     456        TS_ASSERT_EQUALS(obSquare1.hh, ent1h/2);
     457        TS_ASSERT_EQUALS(obSquare1.hw, ent1w/2);
     458        TS_ASSERT_EQUALS(obSquare1.x, ent1x);
     459        TS_ASSERT_EQUALS(obSquare1.z, ent1z);
     460        TS_ASSERT_EQUALS(obSquare1.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0)));
     461        TS_ASSERT_EQUALS(obSquare1.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1)));
     462
     463        TS_ASSERT_EQUALS(obSquare2.hh, ent2r);
     464        TS_ASSERT_EQUALS(obSquare2.hw, ent2r);
     465        TS_ASSERT_EQUALS(obSquare2.x, ent2x);
     466        TS_ASSERT_EQUALS(obSquare2.z, ent2z);
     467        TS_ASSERT_EQUALS(obSquare2.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0)));
     468        TS_ASSERT_EQUALS(obSquare2.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1)));
     469
     470        TS_ASSERT_EQUALS(obSquare3.hh, ent3r);
     471        TS_ASSERT_EQUALS(obSquare3.hw, ent3r);
     472        TS_ASSERT_EQUALS(obSquare3.x, ent3x);
     473        TS_ASSERT_EQUALS(obSquare3.z, ent3z);
     474        TS_ASSERT_EQUALS(obSquare3.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0)));
     475        TS_ASSERT_EQUALS(obSquare3.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1)));
     476    }
     477};
     478 No newline at end of file
  • source/simulation2/helpers/Selection.cpp

    diff --git a/source/simulation2/helpers/Selection.cpp b/source/simulation2/helpers/Selection.cpp
    index b33ab0d..7ab3259 100644
    a b  
    2727#include "simulation2/components/ICmpTemplateManager.h"
    2828#include "simulation2/components/ICmpSelectable.h"
    2929#include "simulation2/components/ICmpVisual.h"
     30#include "ps/CLogger.h"
    3031
    3132std::vector<entity_id_t> EntitySelection::PickEntitiesAtPoint(CSimulation2& simulation, const CCamera& camera, int screenX, int screenY, player_id_t player, bool allowEditorSelectables)
    3233{
    std::vector<entity_id_t> EntitySelection::PickEntitiesInRect(CSimulation2& simul  
    161162    return hitEnts;
    162163}
    163164
    164 std::vector<entity_id_t> EntitySelection::PickSimilarEntities(CSimulation2& simulation, const CCamera& camera, const std::string& templateName, player_id_t owner, bool includeOffScreen, bool matchRank, bool allowEditorSelectables)
     165std::vector<entity_id_t> EntitySelection::PickSimilarEntities(CSimulation2& simulation, const CCamera& camera,
     166    const std::string& templateName, player_id_t owner, bool includeOffScreen, bool matchRank,
     167    bool allowEditorSelectables, bool allowFoundations)
    165168{
    166169    CmpPtr<ICmpTemplateManager> cmpTemplateManager(simulation, SYSTEM_ENTITY);
    167170    CmpPtr<ICmpRangeManager> cmpRangeManager(simulation, SYSTEM_ENTITY);
    std::vector<entity_id_t> EntitySelection::PickSimilarEntities(CSimulation2& simu  
    179182
    180183        if (matchRank)
    181184        {
    182             // Exact template name matching
    183             if (cmpTemplateManager->GetCurrentTemplateName(ent) != templateName)
     185            // Exact template name matching, optionally also allowing foundations
     186            std::string curTemplateName = cmpTemplateManager->GetCurrentTemplateName(ent);
     187            bool matches = (curTemplateName == templateName ||
     188                            (allowFoundations && curTemplateName.substr(0, 11) == "foundation|" && curTemplateName.substr(11) == templateName));
     189            if (!matches)
    184190                continue;
    185191        }
    186192
  • source/simulation2/helpers/Selection.h

    diff --git a/source/simulation2/helpers/Selection.h b/source/simulation2/helpers/Selection.h
    index d3a6a8e..484ee08 100644
    a b std::vector<entity_id_t> PickEntitiesInRect(CSimulation2& simulation, const CCam  
    7676 * @param matchRank if true, only entities that exactly match templateName will be selected,
    7777 *  else entities with matching SelectionGroupName will be selected.
    7878 * @param allowEditorSelectables if true, all entities with the ICmpSelectable interface
    79  *  will be selected (including decorative actors), else only those selectable ingame.
     79 *  will be selected (including decorative actors), else only those selectable in-game.
     80 * @param allowFoundations if true, foundations are also included in the results. Only takes
     81 *  effect when matchRank = true.
    8082 *
    8183 * @return unordered list of selected entities.
    8284 * @see ICmpIdentity
    8385 */
    84 std::vector<entity_id_t> PickSimilarEntities(CSimulation2& simulation, const CCamera& camera, const std::string& templateName, player_id_t owner, bool includeOffScreen, bool matchRank, bool allowEditorSelectables);
     86std::vector<entity_id_t> PickSimilarEntities(CSimulation2& simulation, const CCamera& camera, const std::string& templateName,
     87    player_id_t owner, bool includeOffScreen, bool matchRank, bool allowEditorSelectables, bool allowFoundations);
    8588
    8689} // namespace
    8790
  • source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp

    diff --git a/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp b/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp
    index 4567564..d36b2c9 100644
    a b QUERYHANDLER(PickSimilarObjects)  
    488488    if (cmpOwnership)
    489489        owner = cmpOwnership->GetOwner();
    490490
    491     msg->ids = EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, owner, false, true, true);
     491    msg->ids = EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, owner, false, true, true, false);
    492492}
    493493
    494494