Ticket #786: 786_walls_04may12.patch

File 786_walls_04may12.patch, 223.2 KB (added by vts, 12 years ago)
  • 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 cf50970..c0d71a2 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()  
    8383        Engine.SetCursor("arrow-default");
    8484    if (!tooltipSet)
    8585        informationTooltip.hidden = true;
     86   
     87    var wallDragTooltip = getGUIObjectByName("wallDragTooltip");
     88    if (placementSupport.wallDragTooltip)
     89    {
     90        wallDragTooltip.caption = placementSupport.wallDragTooltip;
     91        wallDragTooltip.hidden = false;
     92    }
     93    else
     94    {
     95        wallDragTooltip.caption = "";
     96        wallDragTooltip.hidden = true;
     97    }
    8698}
    8799
    88100function updateBuildingPlacementPreview()
    89101{
    90     // The preview should be recomputed every turn, so that it responds
    91     // to obstructions/fog/etc moving underneath it
     102    // The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or
     103    // in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to.
     104    // See onSimulationUpdate in session.js.
    92105
    93     if (placementEntity && placementPosition)
     106    if (placementSupport.mode === "building")
    94107    {
    95         return Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
    96             "template": placementEntity,
    97             "x": placementPosition.x,
    98             "z": placementPosition.z,
    99             "angle": placementAngle
    100         });
     108        if (placementSupport.template && placementSupport.position)
     109        {
     110            return Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
     111                "template": placementSupport.template,
     112                "x": placementSupport.position.x,
     113                "z": placementSupport.position.z,
     114                "angle": placementSupport.angle,
     115            });
     116        }
     117    }
     118    else if (placementSupport.mode === "wall")
     119    {
     120        if (placementSupport.wallSet && placementSupport.position)
     121        {
     122            // Fetch an updated list of snapping candidate entities
     123            placementSupport.wallSnapEntities = Engine.PickSimilarFriendlyEntities(
     124                placementSupport.wallSet.templates.tower,
     125                placementSupport.wallSnapEntitiesIncludeOffscreen,
     126                true, // require exact template match
     127                true  // include foundations
     128            );
     129           
     130            return Engine.GuiInterfaceCall("SetWallPlacementPreview", {
     131                "wallSet": placementSupport.wallSet,
     132                "start": placementSupport.position,
     133                "end": placementSupport.wallEndPosition,
     134                "snapEntities": placementSupport.wallSnapEntities,  // snapping entities (towers) for starting a wall segment
     135            });
     136        }
    101137    }
    102138
    103139    return false;
    104140}
    105141
    106 function resetPlacementEntity()
    107 {
    108     Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
    109     placementEntity = undefined;
    110     placementPosition = undefined;
    111     placementAngle = undefined;
    112 }
    113 
    114142function findGatherType(gatherer, supply)
    115143{
    116144    if (!gatherer || !supply)
    var dragStart; // used for remembering mouse coordinates at start of drag operat  
    459487
    460488function tryPlaceBuilding(queued)
    461489{
     490    if (placementSupport.mode !== "building")
     491    {
     492        error("[tryPlaceBuilding] Called while in '"+placementSupport.mode+"' placement mode instead of 'building'");
     493        return false;
     494    }
     495   
    462496    var selection = g_Selection.toList();
    463497
    464498    // Use the preview to check it's a valid build location
    function tryPlaceBuilding(queued)  
    472506    // Start the construction
    473507    Engine.PostNetworkCommand({
    474508        "type": "construct",
    475         "template": placementEntity,
    476         "x": placementPosition.x,
    477         "z": placementPosition.z,
    478         "angle": placementAngle,
     509        "template": placementSupport.template,
     510        "x": placementSupport.position.x,
     511        "z": placementSupport.position.z,
     512        "angle": placementSupport.angle,
    479513        "entities": selection,
    480514        "autorepair": true,
    481515        "autocontinue": true,
    function tryPlaceBuilding(queued)  
    484518    Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
    485519
    486520    if (!queued)
    487         resetPlacementEntity();
     521        placementSupport.Reset();
     522
     523    return true;
     524}
     525
     526function tryPlaceWall()
     527{
     528    if (placementSupport.mode !== "wall")
     529    {
     530        error("[tryPlaceWall] Called while in '" + placementSupport.mode + "' placement mode; expected 'wall' mode");
     531        return false;
     532    }
     533   
     534    var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...)
     535    if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object"))
     536    {
     537        error("[tryPlaceWall] Unexpected return value from updateBuildingPlacementPreview: '" + uneval(placementInfo) + "'; expected either 'false' or 'object'");
     538        return false;
     539    }
     540   
     541    if (!wallPlacementInfo)
     542        return false;
     543   
     544    var selection = g_Selection.toList();
     545    var cmd = {
     546        "type": "construct-wall",
     547        "autorepair": true,
     548        "autocontinue": true,
     549        "queued": true,
     550        "entities": selection,
     551        "wallSet": placementSupport.wallSet,
     552        "pieces": wallPlacementInfo.pieces,
     553        "startSnappedEntity": wallPlacementInfo.startSnappedEnt,
     554        "endSnappedEntity": wallPlacementInfo.endSnappedEnt,
     555    };
     556   
     557    //warn("-------------");
     558    //for (var k in cmd)
     559    //  warn("  " + k + ": " + uneval(cmd[k]));
     560    //warn("-------------");
     561   
     562    // make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end
     563    // point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed
     564    // (this is somewhat non-ideal and hardcode-ish)
     565    var hasWallSegment = false;
     566    for (var k in cmd.pieces)
     567    {
     568        if (cmd.pieces[k].template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :(
     569            hasWallSegment = true;
     570    }
     571   
     572    if (hasWallSegment)
     573    {
     574        Engine.PostNetworkCommand(cmd);
     575        Engine.GuiInterfaceCall("PlaySound", {"name": "order_repair", "entity": selection[0] });
     576    }
    488577
    489578    return true;
    490579}
    function handleInputBeforeGui(ev, hoveredObject)  
    684773            if (ev.button == SDL_BUTTON_RIGHT)
    685774            {
    686775                // Cancel building
    687                 resetPlacementEntity();
     776                placementSupport.Reset();
     777                inputState = INPUT_NORMAL;
     778                return true;
     779            }
     780            break;
     781        }
     782        break;
     783       
     784    case INPUT_BUILDING_WALL_CLICK:
     785        // User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point
     786        // by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode.
     787        switch (ev.type)
     788        {
     789        case "mousebuttonup":
     790            if (ev.button === SDL_BUTTON_LEFT)
     791            {
     792                inputState = INPUT_BUILDING_WALL_PATHING;
     793                return true;
     794            }
     795            break;
     796           
     797        case "mousebuttondown":
     798            if (ev.button == SDL_BUTTON_RIGHT)
     799            {
     800                // Cancel building
     801                placementSupport.Reset();
     802                updateBuildingPlacementPreview();
     803               
    688804                inputState = INPUT_NORMAL;
    689805                return true;
    690806            }
    function handleInputBeforeGui(ev, hoveredObject)  
    692808        }
    693809        break;
    694810
     811    case INPUT_BUILDING_WALL_PATHING:
     812        // User has chosen a starting point for constructing the wall, and is now looking to set the endpoint.
     813        // Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to
     814        // normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the
     815        // user to continue building walls.
     816        switch (ev.type)
     817        {
     818            case "mousemotion":
     819                placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
     820               
     821                // Update the building placement preview, and by extension, the list of snapping candidate entities for both (!)
     822                // the ending point and the starting point to snap to.
     823                //
     824                // TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case
     825                // where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a
     826                // foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on
     827                // the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers
     828                // in them. Might be useful to query only for entities within a certain range around the starting point and ending
     829                // points.
     830               
     831                placementSupport.wallSnapEntitiesIncludeOffscreen = true;
     832                var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
     833               
     834                if (result && result.cost)
     835                {
     836                    placementSupport.wallDragTooltip = "";
     837                    for (var resource in result.cost)
     838                    {
     839                        if (result.cost[resource] > 0)
     840                            placementSupport.wallDragTooltip += getCostComponentDisplayName(resource) + ": " + result.cost[resource] + "\n";
     841                    }
     842                }
     843               
     844                break;
     845               
     846            case "mousebuttondown":
     847                if (ev.button == SDL_BUTTON_LEFT)
     848                {
     849                    if (tryPlaceWall())
     850                    {
     851                        if (Engine.HotkeyIsPressed("session.queue"))
     852                        {
     853                            // continue building, just set a new starting position where we left off
     854                            placementSupport.position = placementSupport.wallEndPosition;
     855                            placementSupport.wallEndPosition = undefined;
     856                           
     857                            inputState = INPUT_BUILDING_WALL_CLICK;
     858                        }
     859                        else
     860                        {
     861                            placementSupport.Reset();
     862                            inputState = INPUT_NORMAL;
     863                        }
     864                    }
     865                    else
     866                    {
     867                        placementSupport.wallDragTooltip = "Cannot build wall here!";
     868                    }
     869                   
     870                    updateBuildingPlacementPreview();
     871                    return true;
     872                }
     873                else if (ev.button == SDL_BUTTON_RIGHT)
     874                {
     875                    // reset to normal input mode
     876                    placementSupport.Reset();
     877                    updateBuildingPlacementPreview();
     878                   
     879                    inputState = INPUT_NORMAL;
     880                    return true;
     881                }
     882                break;
     883        }
     884        break;
     885       
    695886    case INPUT_BUILDING_DRAG:
    696887        switch (ev.type)
    697888        {
    function handleInputBeforeGui(ev, hoveredObject)  
    702893            if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
    703894            {
    704895                // Rotate in the direction of the mouse
    705                 var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
    706                 placementAngle = Math.atan2(target.x - placementPosition.x, target.z - placementPosition.z);
     896                var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
     897                placementSupport.angle = Math.atan2(target.x - placementSupport.position.x, target.z - placementSupport.position.z);
    707898            }
    708899            else
    709900            {
    710901                // If the mouse is near the center, snap back to the default orientation
    711                 placementAngle = defaultPlacementAngle;
     902                placementSupport.SetDefaultAngle();
    712903            }
    713904
    714905            var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
    715                 "template": placementEntity,
    716                 "x": placementPosition.x,
    717                 "z": placementPosition.z
     906                "template": placementSupport.template,
     907                "x": placementSupport.position.x,
     908                "z": placementSupport.position.z
    718909            });
    719910            if (snapData)
    720911            {
    721                 placementAngle = snapData.angle;
    722                 placementPosition.x = snapData.x;
    723                 placementPosition.z = snapData.z;
     912                placementSupport.angle = snapData.angle;
     913                placementSupport.position.x = snapData.x;
     914                placementSupport.position.z = snapData.z;
    724915            }
    725916
    726917            updateBuildingPlacementPreview();
    function handleInputBeforeGui(ev, hoveredObject)  
    750941            if (ev.button == SDL_BUTTON_RIGHT)
    751942            {
    752943                // Cancel building
    753                 resetPlacementEntity();
     944                placementSupport.Reset();
    754945                inputState = INPUT_NORMAL;
    755946                return true;
    756947            }
    function handleInputAfterGui(ev)  
    8441035                break;
    8451036        }
    8461037        break;
     1038       
    8471039    case INPUT_PRESELECTEDACTION:
    8481040        switch (ev.type)
    8491041        {
    function handleInputAfterGui(ev)  
    8741066            }
    8751067        }
    8761068        break;
     1069       
    8771070    case INPUT_SELECTING:
    8781071        switch (ev.type)
    8791072        {
    function handleInputAfterGui(ev)  
    9471140                    }
    9481141
    9491142                    // TODO: Should we handle "control all units" here as well?
    950                     ents = Engine.PickSimilarFriendlyEntities(templateToMatch, showOffscreen, matchRank);
     1143                    ents = Engine.PickSimilarFriendlyEntities(templateToMatch, showOffscreen, matchRank, false);
    9511144                }
    9521145                else
    9531146                {
    function handleInputAfterGui(ev)  
    9861179        switch (ev.type)
    9871180        {
    9881181        case "mousemotion":
    989             placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y);
    990             var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
    991                 "template": placementEntity,
    992                 "x": placementPosition.x,
    993                 "z": placementPosition.z
    994             });
    995             if (snapData)
     1182           
     1183            placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
     1184           
     1185            if (placementSupport.mode === "wall")
    9961186            {
    997                 placementAngle = snapData.angle;
    998                 placementPosition.x = snapData.x;
    999                 placementPosition.z = snapData.z;
     1187                // Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is
     1188                // still selecting a starting point (which must necessarily be on-screen). (The update itself happens in the
     1189                // call to updateBuildingPlacementPreview below).
     1190                placementSupport.wallSnapEntitiesIncludeOffscreen = false;
     1191            }
     1192            else
     1193            {
     1194                var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
     1195                    "template": placementSupport.template,
     1196                    "x": placementSupport.position.x,
     1197                    "z": placementSupport.position.z,
     1198                });
     1199                if (snapData)
     1200                {
     1201                    placementSupport.angle = snapData.angle;
     1202                    placementSupport.position.x = snapData.x;
     1203                    placementSupport.position.z = snapData.z;
     1204                }
    10001205            }
    10011206
    1002             updateBuildingPlacementPreview();
    1003 
     1207            updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
    10041208            return false; // continue processing mouse motion
    10051209
    10061210        case "mousebuttondown":
    10071211            if (ev.button == SDL_BUTTON_LEFT)
    10081212            {
    1009                 placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y);
    1010                 dragStart = [ ev.x, ev.y ];
    1011                 inputState = INPUT_BUILDING_CLICK;
     1213                if (placementSupport.mode === "wall")
     1214                {
     1215                    var validPlacement = updateBuildingPlacementPreview();
     1216                    if (validPlacement !== false)
     1217                    {
     1218                        inputState = INPUT_BUILDING_WALL_CLICK;
     1219                    }
     1220                }
     1221                else
     1222                {
     1223                    placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
     1224                    dragStart = [ ev.x, ev.y ];
     1225                    inputState = INPUT_BUILDING_CLICK;
     1226                }
    10121227                return true;
    10131228            }
    10141229            else if (ev.button == SDL_BUTTON_RIGHT)
    10151230            {
    10161231                // Cancel building
    1017                 resetPlacementEntity();
     1232                placementSupport.Reset();
    10181233                inputState = INPUT_NORMAL;
    10191234                return true;
    10201235            }
    function handleInputAfterGui(ev)  
    10271242            switch (ev.hotkey)
    10281243            {
    10291244            case "session.rotate.cw":
    1030                 placementAngle += rotation_step;
     1245                placementSupport.angle += rotation_step;
    10311246                updateBuildingPlacementPreview();
    10321247                break;
    10331248            case "session.rotate.ccw":
    1034                 placementAngle -= rotation_step;
     1249                placementSupport.angle -= rotation_step;
    10351250                updateBuildingPlacementPreview();
    10361251                break;
    10371252            }
    function doAction(action, ev)  
    10541269    switch (action.type)
    10551270    {
    10561271    case "move":
    1057         var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
     1272        var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
    10581273        Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued});
    10591274        Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] });
    10601275        return true;
    function doAction(action, ev)  
    11051320        }
    11061321        else
    11071322        {
    1108             pos = Engine.GetTerrainAtPoint(ev.x, ev.y);
     1323            pos = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
    11091324        }
    11101325        Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": pos.x, "z": pos.z, "data": action.data});
    11111326        // Display rally point at the new coordinates, to avoid display lag
    function doAction(action, ev)  
    11171332        return true;
    11181333
    11191334    case "unset-rallypoint":
    1120         var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
     1335        var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
    11211336        Engine.PostNetworkCommand({"type": "unset-rallypoint", "entities": selection});
    11221337        // Remove displayed rally point
    11231338        Engine.GuiInterfaceCall("DisplayRallyPoint", {
    function handleMinimapEvent(target)  
    11751390}
    11761391
    11771392// Called by GUI when user clicks construction button
    1178 function startBuildingPlacement(buildEntType)
     1393// @param buildTemplate Template name of the entity the user wants to build
     1394function startBuildingPlacement(buildTemplate)
    11791395{
    1180     placementEntity = buildEntType;
    1181     placementAngle = defaultPlacementAngle;
    1182     inputState = INPUT_BUILDING_PLACEMENT;
     1396    // TODO: we should clear any highlight selection rings here. If the mouse was over an entity before going onto the GUI
     1397    // to start building a structure, then the highlight selection rings are kept during the construction of the building.
     1398    // Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing.
     1399   
     1400    placementSupport.SetDefaultAngle();
     1401   
     1402    // find out if we're building a wall, and change the entity appropriately if so
     1403    var templateData = GetTemplateData(buildTemplate);
     1404    if (templateData.wallSet)
     1405    {
     1406        placementSupport.mode = "wall";
     1407        placementSupport.wallSet = templateData.wallSet;
     1408        inputState = INPUT_BUILDING_PLACEMENT;
     1409    }
     1410    else
     1411    {
     1412        placementSupport.mode = "building";
     1413        placementSupport.template = buildTemplate;
     1414        inputState = INPUT_BUILDING_PLACEMENT;
     1415    }
    11831416}
    11841417
    11851418// 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..eb31161
    - +  
     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    this.wallDragTooltip = null;     // tooltip text while the user is draggin the wall. Used to indicate the current cost to build the wall.
     21   
     22    this.SetDefaultAngle();
     23   
     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 4155773..e334319 100644
    a b function getSavedGameData()  
    187187}
    188188
    189189var lastTickTime = new Date;
     190
     191/**
     192 * Called every frame.
     193 */
    190194function onTick()
    191195{
    192196    var now = new Date;
    function onTick()  
    205209
    206210    updateCursorAndTooltip();
    207211
    208     // If the selection changed, we need to regenerate the sim display
     212    // If the selection changed, we need to regenerate the sim display (the display depends on both the
     213    // simulation state and the current selection).
    209214    if (g_Selection.dirty)
    210215    {
    211216        onSimulationUpdate();
    function checkPlayerState()  
    292297    }
    293298}
    294299
     300/**
     301 * Recomputes GUI state that depends on simulation state or selection state. Called directly every simulation
     302 * update (see session.xml), or from onTick when the selection has changed.
     303 */
    295304function onSimulationUpdate()
    296305{
    297306    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 a623c86..cd8b22d 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"/>
     
    465466        </object>
    466467
    467468        <!-- ================================  ================================ -->
    468         <!-- Information tooltip -->
     469        <!-- Information tooltip
     470             Follows the mouse around if 'independent' is set to 'true'. -->
    469471        <!-- ================================  ================================ -->
    470472        <object name="informationTooltip" type="tooltip" independent="true" style="informationTooltip"/>
    471473
    472474        <!-- ================================  ================================ -->
     475        <!-- Wall-dragging tooltip. Shows the total cost of building a wall while the player is dragging it. -->
     476        <!-- ================================  ================================ -->
     477        <object name="wallDragTooltip" type="tooltip" independent="true" style="informationTooltip"/>
     478
     479        <!-- ================================  ================================ -->
    473480        <!-- START of BOTTOM PANEL -->
    474481        <!-- ================================  ================================ -->
    475482
  • 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 efb7151..b7e2cd0 100644
    a b function setOverlay(object, value)  
    133133    object.hidden = !value;
    134134}
    135135
    136 // Sets up "unit panels" - the panels with rows of icons (Helper function for updateUnitDisplay)
     136/**
     137 * Helper function for updateUnitCommands; sets up "unit panels" (i.e. panels with rows of icons) for the currently selected
     138 * unit.
     139 *
     140 * @param guiName Short identifier string of this panel; see constants defined at the top of this file.
     141 * @param usedPanels Output object; usedPanels[guiName] will be set to 1 to indicate that this panel was used during this
     142 *                     run of updateUnitCommands and should not be hidden. TODO: why is this done this way instead of having
     143 *                     updateUnitCommands keep track of this?
     144 * @param unitEntState Entity state of the (first) selected unit.
     145 * @param items Panel-specific data to construct the icons with.
     146 * @param callback Callback function to argument to execute when an item's icon gets clicked. Takes a single 'item' argument.
     147 */
    137148function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
    138149{
    139150    usedPanels[guiName] = 1;
    function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)  
    320331                var [batchSize, batchIncrement] = getTrainingBatchStatus(unitEntState.id, entType);
    321332                var trainNum = batchSize ? batchSize+batchIncrement : batchIncrement;
    322333
    323                 tooltip += "\n" + getEntityCost(template);
     334                tooltip += "\n" + getEntityCostTooltip(template);
    324335
    325336                if (template.health)
    326337                    tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + template.health;
    function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)  
    340351                if (template.tooltip)
    341352                    tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]";
    342353
    343                 tooltip += "\n" + getEntityCost(template);
     354                tooltip += "\n" + getEntityCostTooltip(template);
    344355
    345356                if (item.pair)
    346357                {
    function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)  
    348359                    if (template1.tooltip)
    349360                        tooltip1 += "\n[font=\"serif-13\"]" + template1.tooltip + "[/font]";
    350361
    351                     tooltip1 += "\n" + getEntityCost(template1);
     362                    tooltip1 += "\n" + getEntityCostTooltip(template1);
    352363                }
    353364                break;
    354365
    function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)  
    357368                if (template.tooltip)
    358369                    tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]";
    359370
    360                 tooltip += "\n" + getEntityCost(template);
     371                tooltip += "\n" + getEntityCostTooltip(template); // see utility_functions.js
     372                tooltip += getPopulationBonusTooltip(template); // see utility_functions.js
    361373
    362                 tooltip += getPopulationBonus(template);
    363374                if (template.health)
    364375                    tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + template.health;
    365376
    function setupUnitBarterPanel(unitEntState)  
    685696    }
    686697}
    687698
    688 // Updates right Unit Commands Panel - runs in the main session loop via updateSelectionDetails()
     699/**
     700 * Updates the right hand side "Unit Commands" panel. Runs in the main session loop via updateSelectionDetails().
     701 * Delegates to setupUnitPanel to set up individual subpanels, appropriately activated depending on the selected
     702 * unit's state.
     703 *
     704 * @param entState Entity state of the (first) selected unit.
     705 * @param supplementalDetailsPanel Reference to the "supplementalSelectionDetails" GUI Object
     706 * @param selection Array of currently selected entity IDs.
     707 */
    689708function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection)
    690709{
    691710    // 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..f6400d1 100644
    a b const FLORA = "flora";  
    33const FAUNA = "fauna";
    44const SPECIAL = "special";
    55
     6const COST_DISPLAY_NAMES = {
     7    "food": "Food",
     8    "wood": "Wood",
     9    "stone": "Stone",
     10    "metal": "Metal",
     11    "population": "Population",
     12};
     13
    614//-------------------------------- -------------------------------- --------------------------------
    715// Utility functions
    816//-------------------------------- -------------------------------- --------------------------------
    function getEntityCommandsList(entState)  
    193201    return commands;
    194202}
    195203
    196 function getEntityCost(template)
     204/**
     205 * Translates a cost component identifier as they are used internally (e.g. "population", "food", etc.) to proper
     206 * display names.
     207 */
     208function getCostComponentDisplayName(costComponentName)
     209{
     210    return COST_DISPLAY_NAMES[costComponentName];
     211}
     212
     213/**
     214 * Helper function for getEntityCostTooltip.
     215 */
     216function getEntityCostComponentsTooltipString(template)
     217{
     218    var costs = [];
     219    if (template.cost.food) costs.push(template.cost.food + " [font=\"serif-12\"]" + getCostComponentDisplayName("food") + "[/font]");
     220    if (template.cost.wood) costs.push(template.cost.wood + " [font=\"serif-12\"]" + getCostComponentDisplayName("wood") + "[/font]");
     221    if (template.cost.metal) costs.push(template.cost.metal + " [font=\"serif-12\"]" + getCostComponentDisplayName("metal") + "[/font]");
     222    if (template.cost.stone) costs.push(template.cost.stone + " [font=\"serif-12\"]" + getCostComponentDisplayName("stone") + "[/font]");
     223    if (template.cost.population) costs.push(template.cost.population + " [font=\"serif-12\"]" + getCostComponentDisplayName("population") + "[/font]");
     224    return costs;
     225}
     226
     227/**
     228 * Returns the cost information to display in the specified entity's construction button tooltip.
     229 */
     230function getEntityCostTooltip(template)
    197231{
    198     var cost = "";
    199     if (template.cost)
     232    var cost = "[font=\"serif-bold-13\"]Costs:[/font] ";
     233   
     234    // Entities with a wallset component are proxies for initiating wall placement and as such do not have a cost of
     235    // their own; the individual wall pieces within it do.
     236    if (template.wallSet)
     237    {
     238        var templateLong = GetTemplateData(template.wallSet.templates.long);
     239        var templateMedium = GetTemplateData(template.wallSet.templates.medium);
     240        var templateShort = GetTemplateData(template.wallSet.templates.short);
     241        var templateTower = GetTemplateData(template.wallSet.templates.tower);
     242       
     243        // TODO: the costs of the wall segments should be the same, and for now we will assume they are (ideally we
     244        // should take the average here or something).
     245        var wallCosts = getEntityCostComponentsTooltipString(templateLong);
     246        var towerCosts = getEntityCostComponentsTooltipString(templateTower);
     247       
     248        cost += "\n";
     249        cost += " Walls:  " + wallCosts.join(", ") + "\n";
     250        cost += " Towers: " + towerCosts.join(", ");
     251    }
     252    else if (template.cost)
    200253    {
    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(", ");
     254        var costs = getEntityCostComponentsTooltipString(template);
     255        cost += costs.join(", ");
    209256    }
     257    else
     258    {
     259        cost = ""; // cleaner than duplicating the serif-bold-13 stuff
     260    }
     261   
    210262    return cost;
    211263}
    212264
    213 function getPopulationBonus(template)
     265/**
     266 * Returns the population bonus information to display in the specified entity's construction button tooltip.
     267 */
     268function getPopulationBonusTooltip(template)
    214269{
    215270    var popBonus = "";
    216     if (template.cost.populationBonus)
     271    if (template.cost && template.cost.populationBonus)
    217272        popBonus = "\n[font=\"serif-bold-13\"]Population Bonus:[/font] " + template.cost.populationBonus;
    218273    return popBonus;
    219274}
  • 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..6ee915f
    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..976a455
    - +  
     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="270.873" y="91.2117" z="198.563"/>
     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="64">
     130            <Template>units/cart_infantry_swordsman_a</Template>
     131            <Player>1</Player>
     132            <Position x="297.3943" z="245.43734"/>
     133            <Orientation y="2.35621"/>
     134        </Entity>
     135        <Entity uid="65">
     136            <Template>units/celt_infantry_spearman_a</Template>
     137            <Player>1</Player>
     138            <Position x="300.65796" z="247.85009"/>
     139            <Orientation y="2.35621"/>
     140        </Entity>
     141        <Entity uid="66">
     142            <Template>units/hele_infantry_spearman_a</Template>
     143            <Player>1</Player>
     144            <Position x="304.05463" z="250.32634"/>
     145            <Orientation y="2.35621"/>
     146        </Entity>
     147        <Entity uid="67">
     148            <Template>units/iber_infantry_swordsman_a</Template>
     149            <Player>1</Player>
     150            <Position x="307.60114" z="254.54291"/>
     151            <Orientation y="2.35621"/>
     152        </Entity>
     153        <Entity uid="68">
     154            <Template>units/pers_infantry_spearman_a</Template>
     155            <Player>1</Player>
     156            <Position x="311.45011" z="258.70716"/>
     157            <Orientation y="2.35621"/>
     158        </Entity>
     159        <Entity uid="69">
     160            <Template>units/rome_infantry_swordsman_a</Template>
     161            <Player>1</Player>
     162            <Position x="315.29044" z="262.83661"/>
     163            <Orientation y="2.35621"/>
     164        </Entity>
     165        <Entity uid="70">
     166            <Template>structures/cart_house</Template>
     167            <Player>1</Player>
     168            <Position x="252.36847" z="276.04014"/>
     169            <Orientation y="2.35621"/>
     170        </Entity>
     171        <Entity uid="71">
     172            <Template>structures/cart_farmstead</Template>
     173            <Player>1</Player>
     174            <Position x="266.76517" z="291.61655"/>
     175            <Orientation y="2.35621"/>
     176        </Entity>
     177        <Entity uid="72">
     178            <Template>structures/cart_mill</Template>
     179            <Player>1</Player>
     180            <Position x="285.44394" z="308.2942"/>
     181            <Orientation y="2.35621"/>
     182        </Entity>
     183    </Entities>
     184    <Paths/>
     185</Scenario>
     186 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 c29ded5..b2a2fd3 100644
    a b Foundation.prototype.Build = function(builderEnt, work)  
    189189        cmpBuildingPosition.SetXZRotation(rot.x, rot.z);
    190190        // TODO: should add a ICmpPosition::CopyFrom() instead of all this
    191191
     192        // ----------------------------------------------------------------------
     193       
    192194        var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    193195        var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership);
    194196        cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner());
    195197       
     198        // ----------------------------------------------------------------------
     199       
     200        // Copy over the obstruction control group IDs from the foundation entities. This is needed to ensure that when a foundation
     201        // is completed and replaced by a new entity, it remains in the same control group(s) as any other foundation entities that
     202        // may surround it. This is the mechanism that is used to e.g. enable wall pieces to be built closely together, ignoring their
     203        // mutual obstruction shapes (since they would otherwise be prevented from being built so closely together). If the control
     204        // groups are not copied over, the new entity will default to a new control group containing only itself, and will hence block
     205        // construction of any surrounding foundations that it was previously in the same control group with.
     206       
     207        // Note that this will result in the completed building entities having control group IDs that equal entity IDs of old (and soon
     208        // to be deleted) foundation entities. This should not have any consequences, however, since the control group IDs are only meant
     209        // to be unique identifiers, which is still true when reusing the old ones.
     210       
     211        var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
     212        var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction);
     213        cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
     214        cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
     215       
     216        // ----------------------------------------------------------------------
     217       
    196218        var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
    197219        cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter();
    198220
    199221        var cmpIdentity = Engine.QueryInterface(building, IID_Identity);
    200         if (cmpIdentity.GetClassesList().indexOf("CivCentre") != -1) cmpPlayerStatisticsTracker.IncreaseBuiltCivCentresCounter();
     222        if (cmpIdentity.GetClassesList().indexOf("CivCentre") != -1)
     223            cmpPlayerStatisticsTracker.IncreaseBuiltCivCentresCounter();
    201224       
    202225        var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
    203226        var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health);
    Foundation.prototype.Build = function(builderEnt, work)  
    207230
    208231        Engine.PostMessage(this.entity, MT_ConstructionFinished,
    209232            { "entity": this.entity, "newentity": building });
    210 
    211233        Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: building });
    212234
    213235        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 2292e94..f1b9bea 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)  
    141143    var ret = {
    142144        "id": ent,
    143145        "template": template
    144     }
     146    };
    145147
    146148    var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
    147149    if (cmpIdentity)
    GuiInterface.prototype.GetEntityState = function(player, ent)  
    214216        };
    215217    }
    216218
     219    var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
     220    if (cmpObstruction)
     221    {
     222        ret.obstruction = {
     223            "controlGroup": cmpObstruction.GetControlGroup(),
     224            "controlGroup2": cmpObstruction.GetControlGroup2(),
     225        };
     226    }
     227
    217228    var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
    218229    if (cmpOwnership)
    219230    {
    GuiInterface.prototype.GetTemplateData = function(player, name)  
    331342        }
    332343    }
    333344   
     345    if (template.BuildRestrictions)
     346    {
     347        // required properties
     348        ret.buildRestrictions = {
     349            "placementType": template.BuildRestrictions.PlacementType,
     350            "territory": template.BuildRestrictions.Territory,
     351            "category": template.BuildRestrictions.Category,
     352        };
     353       
     354        // optional properties
     355        if (template.BuildRestrictions.Distance)
     356        {
     357            ret.buildRestrictions.distance = {
     358                "fromCategory": template.BuildRestrictions.Distance.FromCategory,
     359            };
     360            if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = +template.BuildRestrictions.Distance.MinDistance;
     361            if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = +template.BuildRestrictions.Distance.MaxDistance;
     362        }
     363    }
     364   
    334365    if (template.Cost)
    335366    {
    336367        ret.cost = {};
    GuiInterface.prototype.GetTemplateData = function(player, name)  
    342373        if (template.Cost.PopulationBonus) ret.cost.populationBonus = +template.Cost.PopulationBonus;
    343374    }
    344375   
     376    if (template.Footprint)
     377    {
     378        ret.footprint = {"height": template.Footprint.Height};
     379       
     380        if (template.Footprint.Square)
     381            ret.footprint.square = {"width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"]};
     382        else if (template.Footprint.Circle)
     383            ret.footprint.circle = {"radius": +template.Footprint.Circle["@radius"]};
     384        else
     385            warn("[GetTemplateData] Unrecognized Footprint type");
     386    }
     387   
     388    if (template.Obstruction)
     389    {
     390        ret.obstruction = {
     391            "active": ("" + template.Obstruction.Active == "true"),
     392            "blockMovement": ("" + template.Obstruction.BlockMovement == "true"),
     393            "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"),
     394            "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"),
     395            "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"),
     396            "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"),
     397            "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"),
     398            "shape": {}
     399        };
     400       
     401        if (template.Obstruction.Static)
     402        {
     403            ret.obstruction.shape.type = "static";
     404            ret.obstruction.shape.width = +template.Obstruction.Static["@width"];
     405            ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"];
     406        }
     407        else
     408        {
     409            ret.obstruction.shape.type = "unit";
     410            ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"];
     411        }
     412    }
     413   
    345414    if (template.Health)
    346415    {
    347416        ret.health = +template.Health.Max;
    GuiInterface.prototype.GetTemplateData = function(player, name)  
    367436        if (template.UnitMotion.Run) ret.speed.run = +template.UnitMotion.Run.Speed;
    368437    }
    369438
     439    if (template.WallSet)
     440    {
     441        ret.wallSet = {
     442            "templates": {
     443                "tower": template.WallSet.Templates.Tower,
     444                "gate": template.WallSet.Templates.Gate,
     445                "long": template.WallSet.Templates.WallLong,
     446                "medium": template.WallSet.Templates.WallMedium,
     447                "short": template.WallSet.Templates.WallShort,
     448            },
     449            "maxTowerOverlap": +template.WallSet.MaxTowerOverlap,
     450            "minTowerOverlap": +template.WallSet.MinTowerOverlap,
     451        };
     452    }
     453   
     454    if (template.WallPiece)
     455    {
     456        ret.wallPiece = {"length": +template.WallPiece.Length};
     457    }
     458
    370459    return ret;
    371460};
    372461
    GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)  
    598687 * Display the building placement preview.
    599688 * cmd.template is the name of the entity template, or "" to disable the preview.
    600689 * cmd.x, cmd.z, cmd.angle give the location.
     690 *
    601691 * Returns true if the placement is okay (everything is valid and the entity is not obstructed by others).
    602692 */
    603693GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
    GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)  
    666756    return false;
    667757};
    668758
     759/**
     760 * 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
     761 * specified. Returns an object with information about the list of entities that need to be newly constructed to complete
     762 * 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
     763 * them can be validly constructed.
     764 *
     765 * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
     766 * another depending on things like snapping and whether some of the entities inside them can be validly positioned.
     767 * We have:
     768 *    - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
     769 *      entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
     770 *      to preview the completed tower on top of its foundation.
     771 *     
     772 *    - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
     773 *      any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
     774 *      towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
     775 *      snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
     776 *      constructed.
     777 *     
     778 *    - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
     779 *      as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
     780 *      e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
     781 *      constructed but come after said first invalid entity are also truncated away.
     782 *
     783 * 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
     784 * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
     785 * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
     786 * argument (see below). Otherwise, it will return an object with the following information:
     787 *
     788 * result: {
     789 *   'startSnappedEnt':   ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
     790 *   'endSnappedEnt':     ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
     791 *                        can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
     792 *                        but the wall construction was truncated before we could reach it, it won't be set here. Currently only
     793 *                        supports towers.
     794 *   'pieces':            Array with the following data for each of the entities in the third list:
     795 *    [{
     796 *       'template':      Template name of the entity.
     797 *       'x':             X coordinate of the entity's position.
     798 *       'z':             Z coordinate of the entity's position.
     799 *       'angle':         Rotation around the Y axis of the entity (in radians).
     800 *     },
     801 *     ...]
     802 *   'cost': {            The total cost required for constructing all the pieces as listed above.
     803 *     'food': ...,
     804 *     'wood': ...,
     805 *     'stone': ...,
     806 *     'metal': ...,
     807 *     'population': ...,
     808 *     'populationBonus': ...,
     809 *   }
     810 * }
     811 *
     812 * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
     813 * @param cmd.start Starting point of the wall segment being created.
     814 * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
     815 *                 the starting point of the wall is available at this time (e.g. while the player is still in the process
     816 *                 of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
     817 *                 previewed.
     818 * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
     819 */
     820GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
     821{
     822    var wallSet = cmd.wallSet;
     823   
     824    var start = {
     825        "pos": cmd.start,
     826        "angle": 0,
     827        "snapped": false,                       // did the start position snap to anything?
     828        "snappedEnt": INVALID_ENTITY,           // if we snapped, was it to an entity? if yes, holds that entity's ID
     829    };
     830   
     831    var end = {
     832        "pos": cmd.end,
     833        "angle": 0,
     834        "snapped": false,                       // did the start position snap to anything?
     835        "snappedEnt": INVALID_ENTITY,           // if we snapped, was it to an entity? if yes, holds that entity's ID
     836    };
     837   
     838    // --------------------------------------------------------------------------------
     839    // do some entity cache management and check for snapping
     840   
     841    if (!this.placementWallEntities)
     842        this.placementWallEntities = {};
     843   
     844    if (!wallSet)
     845    {
     846        // we're clearing the preview, clear the entity cache and bail
     847        var numCleared = 0;
     848        for (var tpl in this.placementWallEntities)
     849        {
     850            for each (var ent in this.placementWallEntities[tpl].entities)
     851                Engine.DestroyEntity(ent);
     852           
     853            this.placementWallEntities[tpl].numUsed = 0;
     854            this.placementWallEntities[tpl].entities = [];
     855            // keep template data around
     856        }
     857       
     858        return false;
     859    }
     860    else
     861    {
     862        // Move all existing cached entities outside of the world and reset their use count
     863        for (var tpl in this.placementWallEntities)
     864        {
     865            for each (var ent in this.placementWallEntities[tpl].entities)
     866            {
     867                var pos = Engine.QueryInterface(ent, IID_Position);
     868                if (pos)
     869                    pos.MoveOutOfWorld();
     870            }
     871           
     872            this.placementWallEntities[tpl].numUsed = 0;
     873        }
     874       
     875        // Create cache entries for templates we haven't seen before
     876        for each (var tpl in wallSet.templates)
     877        {
     878            if (!(tpl in this.placementWallEntities))
     879            {
     880                this.placementWallEntities[tpl] = {
     881                    "numUsed": 0,
     882                    "entities": [],
     883                    "templateData": this.GetTemplateData(player, tpl),
     884                };
     885               
     886                // ensure that the loaded template data contains a wallPiece component
     887                if (!this.placementWallEntities[tpl].templateData.wallPiece)
     888                {
     889                    error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
     890                    return false;
     891                }
     892            }
     893        }
     894    }
     895   
     896    // prevent division by zero errors further on if the start and end positions are the same
     897    if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
     898        end.pos = undefined;
     899   
     900    // 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
     901    // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
     902    // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
     903    if (cmd.snapEntities)
     904    {
     905        var snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
     906        var startSnapData = this.GetFoundationSnapData(player, {
     907            "x": start.pos.x,
     908            "z": start.pos.z,
     909            "template": wallSet.templates.tower,
     910            "snapEntities": cmd.snapEntities,
     911            "snapRadius": snapRadius,
     912        });
     913       
     914        if (startSnapData)
     915        {
     916            start.pos.x = startSnapData.x;
     917            start.pos.z = startSnapData.z;
     918            start.angle = startSnapData.angle;
     919            start.snapped = true;
     920           
     921            if (startSnapData.ent)
     922                start.snappedEnt = startSnapData.ent;
     923        }
     924       
     925        if (end.pos)
     926        {
     927            var endSnapData = this.GetFoundationSnapData(player, {
     928                "x": end.pos.x,
     929                "z": end.pos.z,
     930                "template": wallSet.templates.tower,
     931                "snapEntities": cmd.snapEntities,
     932                "snapRadius": snapRadius,
     933            });
     934           
     935            if (endSnapData)
     936            {
     937                end.pos.x = endSnapData.x;
     938                end.pos.z = endSnapData.z;
     939                end.angle = endSnapData.angle;
     940                end.snapped = true;
     941               
     942                if (endSnapData.ent)
     943                    end.snappedEnt = endSnapData.ent;
     944            }
     945        }
     946    }
     947   
     948    // clear the single-building preview entity (we'll be rolling our own)
     949    this.SetBuildingPlacementPreview(player, {"template": ""});
     950   
     951    // --------------------------------------------------------------------------------
     952    // calculate wall placement and position preview entities
     953   
     954    var result = {
     955        "pieces": [],
     956        "cost": {"food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, "populationBonus": 0},
     957    };
     958   
     959    var previewEntities = [];
     960    if (end.pos)
     961        previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
     962   
     963    // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
     964    // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
     965    // an issue, because all preview entities have their obstruction components deactivated, meaning that their
     966    // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
     967    // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
     968   
     969    // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
     970    // flag set), which is what we want. The only exception to this is when snapping to existing towers (or
     971    // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
     972    // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
     973    // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
     974    // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
     975    // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
     976   
     977    // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
     978    // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
     979    // by the foundation it snaps to.
     980   
     981    if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
     982    {
     983        var startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
     984        if (previewEntities.length > 0 && startEntObstruction)
     985            previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
     986       
     987        // if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
     988        var startEntState = this.GetEntityState(player, start.snappedEnt);
     989        if (startEntState.foundation)
     990        {
     991            var cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
     992            if (cmpPosition)
     993            {
     994                previewEntities.unshift({
     995                    "template": wallSet.templates.tower,
     996                    "pos": start.pos,
     997                    "angle": cmpPosition.GetRotation().y,
     998                    "controlGroups": [(startEntObstruction ? startEntObstruction.GetControlGroup() : undefined)],
     999                    "excludeFromResult": true, // preview only, must not appear in the result
     1000                });
     1001            }
     1002        }
     1003    }
     1004    else
     1005    {
     1006        // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
     1007        // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
     1008        // wall piece.
     1009       
     1010        // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
     1011        // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
     1012        // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
     1013        // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
     1014        // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
     1015        // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
     1016        // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
     1017        // the foundation's angle.
     1018       
     1019        // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
     1020        // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
     1021        previewEntities.unshift({
     1022            "template": wallSet.templates.tower,
     1023            "pos": start.pos,
     1024            "angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle)
     1025        });
     1026    }
     1027   
     1028    if (end.pos)
     1029    {
     1030        // Analogous to the starting side case above
     1031        if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
     1032        {
     1033            var endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
     1034           
     1035            // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
     1036            // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
     1037            // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
     1038            // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
     1039            // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
     1040            if (previewEntities.length > 0 && endEntObstruction)
     1041            {
     1042                previewEntities[previewEntities.length-1].controlGroups = (previewEntities[previewEntities.length-1].controlGroups || []);
     1043                previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup());
     1044            }
     1045           
     1046            // if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
     1047            var endEntState = this.GetEntityState(player, end.snappedEnt);
     1048            if (endEntState.foundation)
     1049            {
     1050                var cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
     1051                if (cmpPosition)
     1052                {
     1053                    previewEntities.push({
     1054                        "template": wallSet.templates.tower,
     1055                        "pos": end.pos,
     1056                        "angle": cmpPosition.GetRotation().y,
     1057                        "controlGroups": [(endEntObstruction ? endEntObstruction.GetControlGroup() : undefined)],
     1058                        "excludeFromResult": true
     1059                    });
     1060                }
     1061            }
     1062        }
     1063        else
     1064        {
     1065            previewEntities.push({
     1066                "template": wallSet.templates.tower,
     1067                "pos": end.pos,
     1068                "angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle)
     1069            });
     1070        }
     1071    }
     1072   
     1073    var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
     1074    if (!cmpTerrain)
     1075    {
     1076        error("[SetWallPlacementPreview] System Terrain component not found");
     1077        return false;
     1078    }
     1079   
     1080    var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     1081    if (!cmpRangeManager)
     1082    {
     1083        error("[SetWallPlacementPreview] System RangeManager component not found");
     1084        return false;
     1085    }
     1086   
     1087    // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
     1088    // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
     1089    // but cannot validly be, constructed). See method-level documentation for more details.
     1090   
     1091    var allPiecesValid = true;
     1092    var numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
     1093   
     1094    for (var i = 0; i < previewEntities.length; ++i)
     1095    {
     1096        var entInfo = previewEntities[i];
     1097       
     1098        var ent = null;
     1099        var tpl = entInfo.template;
     1100        var tplData = this.placementWallEntities[tpl].templateData;
     1101        var entPool = this.placementWallEntities[tpl];
     1102       
     1103        if (entPool.numUsed >= entPool.entities.length)
     1104        {
     1105            // allocate new entity
     1106            ent = Engine.AddLocalEntity("preview|" + tpl);
     1107            entPool.entities.push(ent);
     1108        }
     1109        else
     1110        {
     1111             // reuse an existing one
     1112            ent = entPool.entities[entPool.numUsed];
     1113        }
     1114       
     1115        if (!ent)
     1116        {
     1117            error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
     1118            continue;
     1119        }
     1120       
     1121        // move piece to right location
     1122        // TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities
     1123        var cmpPosition = Engine.QueryInterface(ent, IID_Position);
     1124        if (cmpPosition)
     1125        {
     1126            cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
     1127            cmpPosition.SetYRotation(entInfo.angle);
     1128           
     1129            // if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces
     1130            if (tpl === wallSet.templates.tower)
     1131            {
     1132                var terrainGroundPrev = null;
     1133                var terrainGroundNext = null;
     1134               
     1135                if (i > 0)
     1136                    terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z);
     1137                if (i < previewEntities.length - 1)
     1138                    terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z);
     1139               
     1140                if (terrainGroundPrev != null || terrainGroundNext != null)
     1141                {
     1142                    var targetY = Math.max(terrainGroundPrev, terrainGroundNext);
     1143                    cmpPosition.SetHeightFixed(targetY);
     1144                }
     1145            }
     1146        }
     1147       
     1148        var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
     1149        if (!cmpObstruction)
     1150        {
     1151            error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
     1152            continue;
     1153        }
     1154       
     1155        // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
     1156        // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
     1157        // first-come first-served basis; the first value in the array is always assigned as the primary control group, and
     1158        // any second value as the secondary control group.
     1159       
     1160        // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
     1161        // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
     1162        // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
     1163        // once snapped to.
     1164       
     1165        var primaryControlGroup = ent;
     1166        var secondaryControlGroup = INVALID_ENTITY;
     1167       
     1168        if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
     1169        {
     1170            if (entInfo.controlGroups.length > 2)
     1171            {
     1172                error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
     1173                break;
     1174            }
     1175           
     1176            primaryControlGroup = entInfo.controlGroups[0];
     1177            if (entInfo.controlGroups.length > 1)
     1178                secondaryControlGroup = entInfo.controlGroups[1];
     1179        }
     1180       
     1181        cmpObstruction.SetControlGroup(primaryControlGroup);
     1182        cmpObstruction.SetControlGroup2(secondaryControlGroup);
     1183       
     1184        // check whether this wall piece can be validly positioned here
     1185        var validPlacement = false;
     1186       
     1187        // Check whether it's in a visible or fogged region
     1188        //  tell GetLosVisibility to force RetainInFog because preview entities set this to false,
     1189        //  which would show them as hidden instead of fogged
     1190        // TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
     1191        var visible = (cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
     1192        if (visible)
     1193        {
     1194            var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
     1195            if (!cmpBuildRestrictions)
     1196            {
     1197                error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
     1198                continue;
     1199            }
     1200           
     1201            validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement(player));
     1202        }
     1203       
     1204        allPiecesValid = allPiecesValid && validPlacement;
     1205       
     1206        var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
     1207        if (cmpVisual)
     1208        {
     1209            if (!allPiecesValid)
     1210                cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1);
     1211            else
     1212                cmpVisual.SetShadingColour(1, 1, 1, 1);
     1213        }
     1214       
     1215        // The requirement below that all pieces so far have to have valid positions, rather than only this single one,
     1216        // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
     1217        // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
     1218        // through and past an existing building).
     1219       
     1220        // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
     1221        // on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
     1222       
     1223        if (!entInfo.excludeFromResult)
     1224            numRequiredPieces++;
     1225       
     1226        if (allPiecesValid && !entInfo.excludeFromResult)
     1227        {
     1228            result.pieces.push({
     1229                "template": tpl,
     1230                "x": entInfo.pos.x,
     1231                "z": entInfo.pos.z,
     1232                "angle": entInfo.angle,
     1233            });
     1234            this.placementWallLastAngle = entInfo.angle;
     1235           
     1236            // grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
     1237            // copied over, so we need to fetch it from the template instead).
     1238            // TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
     1239            // boilerplate that's probably duplicated in tons of places.
     1240            result.cost.food += tplData.cost.food;
     1241            result.cost.wood += tplData.cost.wood;
     1242            result.cost.stone += tplData.cost.stone;
     1243            result.cost.metal += tplData.cost.metal;
     1244            result.cost.population += tplData.cost.population;
     1245            result.cost.populationBonus += tplData.cost.populationBonus;
     1246        }
     1247       
     1248        entPool.numUsed++;
     1249    }
     1250   
     1251    // If any were entities required to build the wall, but none of them could be validly positioned, return failure
     1252    // (see method-level documentation).
     1253    if (numRequiredPieces > 0 && result.pieces.length == 0)
     1254        return false;
     1255   
     1256    if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
     1257        result.startSnappedEnt = start.snappedEnt;
     1258   
     1259    // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
     1260    // i.e. are included in result.pieces (see docs for the result object).
     1261    if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
     1262        result.endSnappedEnt = end.snappedEnt;
     1263   
     1264    return result;
     1265};
     1266
     1267/**
     1268 * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
     1269 * it to (if necessary/useful).
     1270 *
     1271 * @param data.x            The X position of the foundation to snap.
     1272 * @param data.z            The Z position of the foundation to snap.
     1273 * @param data.template     The template to get the foundation snapping data for.
     1274 * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
     1275 *                            around the entity. Only takes effect when used in conjunction with data.snapRadius.
     1276 *                          When this option is used and the foundation is found to snap to one of the entities passed in this list
     1277 *                            (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
     1278 *                            holding the ID of the entity that was snapped to.
     1279 * @param data.snapRadius   Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
     1280 *                            {data.x, data.z} must be located within to have it snap to that entity.
     1281 */
    6691282GuiInterface.prototype.GetFoundationSnapData = function(player, data)
    6701283{
    6711284    var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
    6721285    var template = cmpTemplateMgr.GetTemplate(data.template);
    6731286
     1287    if (!template)
     1288    {
     1289        warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
     1290        return false;
     1291    }
     1292   
     1293    if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
     1294    {
     1295        // 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
     1296        // (TODO: break unlikely ties by choosing the lowest entity ID)
     1297       
     1298        var minDist2 = -1;
     1299        var minDistEntitySnapData = null;
     1300        var radius2 = data.snapRadius * data.snapRadius;
     1301       
     1302        for each (ent in data.snapEntities)
     1303        {
     1304            var cmpPosition = Engine.QueryInterface(ent, IID_Position);
     1305            if (!cmpPosition || !cmpPosition.IsInWorld())
     1306                continue;
     1307           
     1308            var pos = cmpPosition.GetPosition();
     1309            var dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
     1310            if (dist2 > radius2)
     1311                continue;
     1312           
     1313            if (minDist2 < 0 || dist2 < minDist2)
     1314            {
     1315                minDist2 = dist2;
     1316                minDistEntitySnapData = {"x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent};
     1317            }
     1318        }
     1319       
     1320        if (minDistEntitySnapData != null)
     1321            return minDistEntitySnapData;
     1322    }
     1323   
    6741324    if (template.BuildRestrictions.Category == "Dock")
    6751325    {
    6761326        var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
    var exposedFunctions = {  
    9061556    "SetStatusBars": 1,
    9071557    "DisplayRallyPoint": 1,
    9081558    "SetBuildingPlacementPreview": 1,
     1559    "SetWallPlacementPreview": 1,
    9091560    "GetFoundationSnapData": 1,
    9101561    "PlaySound": 1,
    9111562    "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..84cacb6
    - +  
     1function WallSet() {}
     2
     3WallSet.prototype.Schema =
     4    "<a:help></a:help>" +
     5    "<a:example>" +
     6    "</a:example>" +
     7    "<element name='Templates'>" +
     8        "<interleave>" +
     9            "<element name='Tower' a:help='Template name of the tower piece'>" +
     10                "<text/>" +
     11            "</element>" +
     12            "<element name='Gate' a:help='Template name of the gate piece'>" +
     13                "<text/>" +
     14            "</element>" +
     15            "<element name='WallLong' a:help='Template name of the long wall segment'>" +
     16                "<text/>" +
     17            "</element>" +
     18            "<element name='WallMedium' a:help='Template name of the medium-size wall segment'>" +
     19                "<text/>" +
     20            "</element>" +
     21            "<element name='WallShort' a:help='Template name of the short wall segment'>" +
     22                "<text/>" +
     23            "</element>" +
     24        "</interleave>" +
     25    "</element>" +
     26    "<element name='MinTowerOverlap' a:help='Maximum fraction that wall segments are allowed to overlap towers, where 0 signifies no overlap and 1 full overlap'>" +
     27        "<data type='decimal'><param name='minInclusive'>0.0</param><param name='maxInclusive'>1.0</param></data>" +
     28    "</element>" +
     29    "<element name='MaxTowerOverlap' a:help='Minimum fraction that wall segments are required to overlap towers, where 0 signifies no overlap and 1 full overlap'>" +
     30        "<data type='decimal'><param name='minInclusive'>0.0</param><param name='maxInclusive'>1.0</param></data>" +
     31    "</element>";
     32
     33
     34WallSet.prototype.Init = function()
     35{
     36};
     37
     38WallSet.prototype.Serialize = null;
     39
     40Engine.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 4205c9b..e242529 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)  
    203206        break;
    204207
    205208    case "construct":
    206         // Message structure:
    207         // {
    208         //   "type": "construct",
    209         //   "entities": [...],
    210         //   "template": "...",
    211         //   "x": ...,
    212         //   "z": ...,
    213         //   "angle": ...,
    214         //   "autorepair": true, // whether to automatically start constructing/repairing the new foundation
    215         //   "autocontinue": true, // whether to automatically gather/build/etc after finishing this
    216         //   "queued": true,
    217         // }
    218 
    219         /*
    220          * Construction process:
    221          *  . Take resources away immediately.
    222          *  . Create a foundation entity with 1hp, 0% build progress.
    223          *  . Increase hp and build progress up to 100% when people work on it.
    224          *  . If it's destroyed, an appropriate fraction of the resource cost is refunded.
    225          *  . If it's completed, it gets replaced with the real building.
    226          */
    227          
    228         // Check that we can control these units
    229         var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
    230         if (!entities.length)
    231             break;
    232 
    233         // Tentatively create the foundation (we might find later that it's a invalid build command)
    234         var ent = Engine.AddEntity("foundation|" + cmd.template);
    235         if (ent == INVALID_ENTITY)
    236         {
    237             // Error (e.g. invalid template names)
    238             error("Error creating foundation entity for '" + cmd.template + "'");
    239             break;
    240         }
    241 
    242         // Move the foundation to the right place
    243         var cmpPosition = Engine.QueryInterface(ent, IID_Position);
    244         cmpPosition.JumpTo(cmd.x, cmd.z);
    245         cmpPosition.SetYRotation(cmd.angle);
    246 
    247         // Check whether it's obstructed by other entities or invalid terrain
    248         var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
    249         if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player))
    250         {
    251             if (g_DebugCommands)
    252             {
    253                 warn("Invalid command: build restrictions check failed for player "+player+": "+uneval(cmd));
    254             }
    255 
    256             var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
    257             cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" });
    258 
    259             // Remove the foundation because the construction was aborted
    260             Engine.DestroyEntity(ent);
    261             break;
    262         }
    263 
    264         // Check build limits
    265         var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits);
    266         if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
    267         {
    268             if (g_DebugCommands)
    269             {
    270                 warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
    271             }
    272 
    273             // TODO: The UI should tell the user they can't build this (but we still need this check)
    274 
    275             // Remove the foundation because the construction was aborted
    276             Engine.DestroyEntity(ent);
    277             break;
    278         }
    279        
    280         var cmpTechMan = QueryPlayerIDInterface(player, IID_TechnologyManager);
    281         // TODO: Enable this check once the AI gets technology support
    282         if (!cmpTechMan.CanProduce(cmd.template) && false)
    283         {
    284             if (g_DebugCommands)
    285             {
    286                 warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd));
    287             }
    288 
    289             var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
    290             cmpGuiInterface.PushNotification({ "player": player, "message": "Building's technology requirements are not met." });
    291 
    292             // Remove the foundation because the construction was aborted
    293             Engine.DestroyEntity(ent);
    294         }
    295 
    296         // TODO: AI has no visibility info
    297         if (!cmpPlayer.IsAI())
    298         {
    299             // Check whether it's in a visible or fogged region
    300             //  tell GetLosVisibility to force RetainInFog because preview entities set this to false,
    301             //  which would show them as hidden instead of fogged
    302             var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
    303             var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
    304             if (!visible)
    305             {
    306                 if (g_DebugCommands)
    307                 {
    308                     warn("Invalid command: foundation visibility check failed for player "+player+": "+uneval(cmd));
    309                 }
    310 
    311                 var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
    312                 cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was not visible" });
    313 
    314                 Engine.DestroyEntity(ent);
    315                 break;
    316             }
    317         }
    318 
    319         var cmpCost = Engine.QueryInterface(ent, IID_Cost);
    320         if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
    321         {
    322             if (g_DebugCommands)
    323             {
    324                 warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
    325             }
    326 
    327             Engine.DestroyEntity(ent);
    328             break;
    329         }
    330 
    331         // Make it owned by the current player
    332         var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
    333         cmpOwnership.SetOwner(player);
    334 
    335         // Initialise the foundation
    336         var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
    337         cmpFoundation.InitialiseConstruction(player, cmd.template);
    338 
    339         // Tell the units to start building this new entity
    340         if (cmd.autorepair)
    341         {
    342             ProcessCommand(player, {
    343                 "type": "repair",
    344                 "entities": entities,
    345                 "target": ent,
    346                 "autocontinue": cmd.autocontinue,
    347                 "queued": cmd.queued
    348             });
    349         }
     209        TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd);
     210        break;
    350211
     212    case "construct-wall":
     213        TryConstructWall(player, cmpPlayer, controlAllUnits, cmd);
    351214        break;
    352215
    353216    case "delete-entities":
    function ExtractFormations(ents)  
    550413}
    551414
    552415/**
     416 * Attempts to construct a building using the specified parameters.
     417 * Returns true on success, false on failure.
     418 */
     419function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
     420{
     421    // Message structure:
     422    // {
     423    //   "type": "construct",
     424    //   "entities": [...],                 // entities that will be ordered to construct the building (if applicable)
     425    //   "template": "...",                 // template name of the entity being constructed
     426    //   "x": ...,
     427    //   "z": ...,
     428    //   "angle": ...,
     429    //   "autorepair": true,                // whether to automatically start constructing/repairing the new foundation
     430    //   "autocontinue": true,              // whether to automatically gather/build/etc after finishing this
     431    //   "queued": true,                    // whether to add the construction/repairing of this foundation to entities' queue (if applicable)
     432    //   "obstructionControlGroup": ...,    // Optional; the obstruction control group ID that should be set for this building prior to obstruction
     433    //                                      // testing to determine placement validity. If specified, must be a valid control group ID (> 0).
     434    //   "obstructionControlGroup2": ...,   // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction
     435    //                                      // testing to determine placement validity. May be INVALID_ENTITY.
     436    // }
     437   
     438    /*
     439     * Construction process:
     440     *  . Take resources away immediately.
     441     *  . Create a foundation entity with 1hp, 0% build progress.
     442     *  . Increase hp and build progress up to 100% when people work on it.
     443     *  . If it's destroyed, an appropriate fraction of the resource cost is refunded.
     444     *  . If it's completed, it gets replaced with the real building.
     445     */
     446   
     447    // Check whether we can control these units
     448    var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
     449    if (!entities.length)
     450        return false;
     451   
     452    // Tentatively create the foundation (we might find later that it's a invalid build command)
     453    var ent = Engine.AddEntity("foundation|" + cmd.template);
     454    if (ent == INVALID_ENTITY)
     455    {
     456        // Error (e.g. invalid template names)
     457        error("Error creating foundation entity for '" + cmd.template + "'");
     458        return false;
     459    }
     460   
     461    // Move the foundation to the right place
     462    var cmpPosition = Engine.QueryInterface(ent, IID_Position);
     463    cmpPosition.JumpTo(cmd.x, cmd.z);
     464    cmpPosition.SetYRotation(cmd.angle);
     465   
     466    // Set the obstruction control group if needed
     467    if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2)
     468    {
     469        var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
     470       
     471        // primary control group must always be valid
     472        if (cmd.obstructionControlGroup)
     473        {
     474            if (cmd.obstructionControlGroup <= 0)
     475                warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0");
     476           
     477            cmpObstruction.SetControlGroup(cmd.obstructionControlGroup);
     478        }
     479       
     480        if (cmd.obstructionControlGroup2)
     481            cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2);
     482    }
     483   
     484    // Check whether it's obstructed by other entities or invalid terrain
     485    var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
     486    if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player))
     487    {
     488        if (g_DebugCommands)
     489        {
     490            warn("Invalid command: build restrictions check failed for player "+player+": "+uneval(cmd));
     491        }
     492       
     493        var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     494        cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" });
     495       
     496        // Remove the foundation because the construction was aborted
     497        Engine.DestroyEntity(ent);
     498        return false;
     499    }
     500   
     501    // Check build limits
     502    var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits);
     503    if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
     504    {
     505        if (g_DebugCommands)
     506        {
     507            warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
     508        }
     509       
     510        // TODO: The UI should tell the user they can't build this (but we still need this check)
     511       
     512        // Remove the foundation because the construction was aborted
     513        Engine.DestroyEntity(ent);
     514        return false;
     515    }
     516   
     517    var cmpTechMan = QueryPlayerIDInterface(player, IID_TechnologyManager);
     518    // TODO: Enable this check once the AI gets technology support
     519    if (!cmpTechMan.CanProduce(cmd.template) && false)
     520    {
     521        if (g_DebugCommands)
     522        {
     523            warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd));
     524        }
     525       
     526        var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     527        cmpGuiInterface.PushNotification({ "player": player, "message": "Building's technology requirements are not met." });
     528       
     529        // Remove the foundation because the construction was aborted
     530        Engine.DestroyEntity(ent);
     531    }
     532   
     533    // TODO: AI has no visibility info
     534    if (!cmpPlayer.IsAI())
     535    {
     536        // Check whether it's in a visible or fogged region
     537        //  tell GetLosVisibility to force RetainInFog because preview entities set this to false,
     538        //  which would show them as hidden instead of fogged
     539        var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     540        var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
     541        if (!visible)
     542        {
     543            if (g_DebugCommands)
     544            {
     545                warn("Invalid command: foundation visibility check failed for player "+player+": "+uneval(cmd));
     546            }
     547           
     548            var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
     549            cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was not visible" });
     550           
     551            Engine.DestroyEntity(ent);
     552            return false;
     553        }
     554    }
     555   
     556    var cmpCost = Engine.QueryInterface(ent, IID_Cost);
     557    if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
     558    {
     559        if (g_DebugCommands)
     560        {
     561            warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
     562        }
     563       
     564        Engine.DestroyEntity(ent);
     565        return false;
     566    }
     567   
     568    // Make it owned by the current player
     569    var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
     570    cmpOwnership.SetOwner(player);
     571   
     572    // Initialise the foundation
     573    var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
     574    cmpFoundation.InitialiseConstruction(player, cmd.template);
     575   
     576    // Tell the units to start building this new entity
     577    if (cmd.autorepair)
     578    {
     579        ProcessCommand(player, {
     580            "type": "repair",
     581            "entities": entities,
     582            "target": ent,
     583            "autocontinue": cmd.autocontinue,
     584            "queued": cmd.queued
     585        });
     586    }
     587   
     588    return ent;
     589}
     590
     591function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd)
     592{
     593    // 'cmd' message structure:
     594    // {
     595    //   "type": "construct-wall",
     596    //   "entities": [...],           // entities that will be ordered to construct the wall (if applicable)
     597    //   "pieces": [                  // ordered list of information about the pieces making up the wall (towers, wall segments, ...)
     598    //      {
     599    //         "template": "...",     // one of the templates from the wallset
     600    //         "x": ...,
     601    //         "z": ...,
     602    //         "angle": ...,
     603    //      },
     604    //      ...
     605    //   ],
     606    //   "wallSet": {
     607    //      "templates": {
     608    //        "tower":                // tower template name
     609    //        "long":                 // long wall segment template name
     610    //        ...                     // etc.
     611    //      },
     612    //      "maxTowerOverlap": ...,
     613    //      "minTowerOverlap": ...,
     614    //   },
     615    //   "startSnappedEntity":        // optional; entity ID of tower being snapped to at the starting side of the wall
     616    //   "endSnappedEntity":          // optional; entity ID of tower being snapped to at the ending side of the wall
     617    //   "autorepair": true,          // whether to automatically start constructing/repairing the new foundation
     618    //   "autocontinue": true,        // whether to automatically gather/build/etc after finishing this
     619    //   "queued": true,              // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable)
     620    // }
     621   
     622    if (cmd.pieces.length <= 0)
     623        return;
     624   
     625    if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower)
     626    {
     627        error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side");
     628        return;
     629    }
     630   
     631    if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower)
     632    {
     633        error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side");
     634        return;
     635    }
     636   
     637    // Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement
     638    // and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control
     639    // groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing
     640    // towers in the case of snapping). The towers themselves all keep their default unique control groups.
     641   
     642    // To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour
     643    // it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the
     644    // first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of
     645    // the first tower encountered towards the ending side of the wall (if any).
     646   
     647    // We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the
     648    // wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes:
     649    //
     650    //   FIRST PASS:
     651    //    - Go from start to end and construct wall piece foundations as far as we can without running into a piece that
     652    //        cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID
     653    //        as the primary control group, thus allowing it to be built overlapping the previous piece.
     654    //    - If we encounter a new tower along the way (which will gain its own control group), do the following:
     655    //        o First build it using temporarily the same control group of the previous (non-tower) piece
     656    //        o Set the previous piece's secondary control group to the tower's entity ID
     657    //        o Restore the primary control group of the constructed tower back its original (unique) value.
     658    //      The temporary control group is necessary to allow the newer tower with its unique control group ID to be able
     659    //        to be placed while overlapping the previous piece.
     660    //   
     661    //   SECOND PASS:   
     662    //    - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this
     663    //      time registering the right neighbouring tower in each non-tower piece.
     664   
     665    // first pass; L -> R
     666   
     667    var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces
     668    var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
     669   
     670    // If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that
     671    // the first wall piece can be built while overlapping it.
     672    if (cmd.startSnappedEntity)
     673    {
     674        var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction);
     675        if (!cmpSnappedStartObstruction)
     676        {
     677            error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component");
     678            return;
     679        }
     680       
     681        lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup();
     682        //warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup);
     683    }
     684   
     685    var i = 0;
     686    for (; i < cmd.pieces.length; ++i)
     687    {
     688        var piece = cmd.pieces[i];
     689       
     690        // 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do
     691        // start position snapping (implying that the first entity we build must be a tower)
     692        if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
     693        {
     694            if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity))
     695            {
     696                error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")");
     697                break;
     698            }
     699        }
     700       
     701        var constructPieceCmd = {
     702            "type": "construct",
     703            "entities": cmd.entities,
     704            "template": piece.template,
     705            "x": piece.x,
     706            "z": piece.z,
     707            "angle": piece.angle,
     708            "autorepair": cmd.autorepair,
     709            "autocontinue": cmd.autocontinue,
     710            "queued": cmd.queued,
     711            // Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed
     712            // using the control group of the last tower (see comments above).
     713            "obstructionControlGroup": lastTowerControlGroup,
     714        };
     715       
     716        // If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's
     717        // control group directly at construction time (instead of setting it in the second pass) to allow it to be built
     718        // while overlapping the snapped entity.
     719        if (i == cmd.pieces.length - 1 && cmd.endSnappedEntity)
     720        {
     721            var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
     722            if (cmpEndSnappedObstruction)
     723                constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup();
     724        }
     725       
     726        var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd);
     727        if (pieceEntityId)
     728        {
     729            // wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later
     730            piece.ent = pieceEntityId;
     731           
     732            // if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex
     733            if (piece.template == cmd.wallSet.templates.tower)
     734            {
     735                var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction);
     736                var newTowerControlGroup = pieceEntityId;
     737               
     738                if (i > 0)
     739                {
     740                    //warn("   updating previous wall piece's secondary control group to " + newTowerControlGroup);
     741                    var cmpPreviousObstruction = Engine.QueryInterface(cmd.pieces[i-1].ent, IID_Obstruction);
     742                    // TODO: ensure that cmpPreviousObstruction exists
     743                    // TODO: ensure that the previous obstruction does not yet have a secondary control group set
     744                    cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup);
     745                }
     746               
     747                // TODO: ensure that cmpTowerObstruction exists
     748                cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group
     749               
     750                lastTowerIndex = i;
     751                lastTowerControlGroup = newTowerControlGroup;
     752            }
     753        }
     754        else
     755        {
     756            // failed to build wall piece, abort
     757            i = j + 1; // compensate for the -1 subtracted by lastBuiltPieceIndex below
     758            break;
     759        }
     760    }
     761   
     762    var lastBuiltPieceIndex = i - 1;
     763    var wallComplete = (lastBuiltPieceIndex == cmd.pieces.length - 1);
     764   
     765    // At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower).
     766    // Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any)
     767    // as their secondary control groups.
     768   
     769    lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
     770   
     771    // only start off with the ending side's snapped tower's control group if we were able to build the entire wall
     772    if (cmd.endSnappedEntity && wallComplete)
     773    {
     774        var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
     775        if (!cmpSnappedEndObstruction)
     776        {
     777            error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component");
     778            return;
     779        }
     780       
     781        lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup();
     782    }
     783   
     784    for (var j = lastBuiltPieceIndex; j >= 0; --j)
     785    {
     786        var piece = cmd.pieces[j];
     787       
     788        if (!piece.ent)
     789        {
     790            error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'");
     791            continue;
     792        }
     793       
     794        var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction);
     795        if (!cmpPieceObstruction)
     796        {
     797            error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component");
     798            continue;
     799        }
     800       
     801        if (piece.template == cmd.wallSet.templates.tower)
     802        {
     803            // encountered a tower entity, update the last tower control group
     804            lastTowerControlGroup = cmpPieceObstruction.GetControlGroup();
     805        }
     806        else
     807        {
     808            // Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'.
     809            // Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group
     810            // dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'.
     811           
     812            var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2();
     813            if (existingSecondaryControlGroup == INVALID_ENTITY)
     814            {
     815                if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY)
     816                {
     817                    cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup);
     818                }
     819            }
     820            else if (existingSecondaryControlGroup != lastTowerControlGroup)
     821            {
     822                error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")");
     823                break;
     824            }
     825        }
     826    }
     827}
     828
     829/**
    553830 * Remove the given list of entities from their current formations.
    554831 */
    555832function 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..c6e3b8b
    - +  
     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 primarily holds the template names for the supported wall pieces in this set (under the
     14 *                  'templates' key), as well as the min and max allowed overlap factors (see GetWallSegmentsRec). Expected
     15 *                  to contain template names for keys "long" (long wall segment), "medium" (medium wall segment), "short"
     16 *                  (short wall segment), "tower" (intermediate tower between wall segments), "gate" (replacement for long
     17 *                  walls).
     18 * @param start Object holding the starting position of the wall. Must contain keys 'x' and 'z'.
     19 * @param end   Object holding the ending position of the wall. Must contains keys 'x' and 'z'.
     20 */
     21function GetWallPlacement(placementData, wallSet, start, end)
     22{
     23    var result = [];
     24   
     25    var candidateSegments = [
     26        {"template": wallSet.templates.long,   "len": placementData[wallSet.templates.long].templateData.wallPiece.length},
     27        {"template": wallSet.templates.medium, "len": placementData[wallSet.templates.medium].templateData.wallPiece.length},
     28        {"template": wallSet.templates.short,  "len": placementData[wallSet.templates.short].templateData.wallPiece.length},
     29    ];
     30   
     31    var towerWidth = placementData[wallSet.templates.tower].templateData.wallPiece.length;
     32   
     33    var dir = {"x": end.pos.x - start.pos.x, "z": end.pos.z - start.pos.z};
     34    var len = Math.sqrt(dir.x * dir.x + dir.z * dir.z);
     35   
     36    // we'll need room for at least our starting and ending towers to fit next to eachother
     37    if (len <= towerWidth)
     38        return result;
     39   
     40    var placement = GetWallSegmentsRec(len, candidateSegments, wallSet.minTowerOverlap, wallSet.maxTowerOverlap, towerWidth, 0, []);
     41   
     42    // TODO: make sure intermediate towers are spaced out far enough for their obstructions to not overlap, implying that
     43    // tower's wallpiece lengths should be > their obstruction width, which is undesirable because it prevents towers with
     44    // wide bases
     45    if (placement)
     46    {
     47        var placedEntities = placement.segments; // list of chosen candidate segments
     48        var r = placement.r; // remaining distance to target without towers (must be <= (N-1) * towerWidth)
     49        var s = r / (2 * placedEntities.length); // spacing
     50       
     51        var dirNormalized = {"x": dir.x / len, "z": dir.z / len};
     52        var angle = -Math.atan2(dir.z, dir.x);    // angle of this wall segment (relative to world-space X/Z axes)
     53       
     54        var progress = 0;
     55        for (var i = 0; i < placedEntities.length; i++)
     56        {
     57            var placedEntity = placedEntities[i];
     58            var targetX = start.pos.x + (progress + s + placedEntity.len/2) * dirNormalized.x;
     59            var targetZ = start.pos.z + (progress + s + placedEntity.len/2) * dirNormalized.z;
     60           
     61            result.push({
     62                "template": placedEntity.template,
     63                "pos": {"x": targetX, "z": targetZ},
     64                "angle": angle,
     65            });
     66           
     67            if (i < placedEntities.length - 1)
     68            {
     69                var towerX = start.pos.x + (progress + placedEntity.len + 2*s) * dirNormalized.x;
     70                var towerZ = start.pos.z + (progress + placedEntity.len + 2*s) * dirNormalized.z;
     71               
     72                result.push({
     73                    "template": wallSet.templates.tower,
     74                    "pos": {"x": towerX, "z": towerZ},
     75                    "angle": angle,
     76                });
     77            }
     78           
     79            progress += placedEntity.len + 2*s;
     80        }
     81    }
     82    else
     83    {
     84        error("No placement possible for distance=" + Math.round(len*1000)/1000.0 + ", minOverlap=" + wallSet.minTowerOverlap + ", maxOverlap=" + wallSet.maxTowerOverlap);
     85    }
     86   
     87    return result;
     88}
     89
     90/**
     91 * Helper function for GetWallPlacement. Finds a list of wall segments and the corresponding remaining spacing/overlap
     92 * distance "r" that will suffice to construct a wall of the given distance. It is understood that two extra towers will
     93 * be placed centered at the starting and ending points of the wall.
     94 *
     95 * @param d Total distance between starting and ending points (constant throughout calls).
     96 * @param candidateSegments List of candidate segments (constant throughout calls). Should be ordered longer-to-shorter
     97 *                            for better execution speed.
     98 * @param minOverlap Minimum overlap factor (constant throughout calls). Must have a value between 0 (meaning walls are
     99 *                     not allowed to overlap towers) and 1 (meaning they're allowed to overlap towers entirely).
     100 *                     Must be <= maxOverlap.
     101 * @param maxOverlap Maximum overlap factor (constant throughout calls). Must have a value between 0 (meaning walls are
     102 *                     not allowed to overlap towers) and 1 (meaning they're allowed to overlap towers entirely).
     103 *                     Must be >= minOverlap.
     104 * @param t Length of a single tower (constant throughout calls). Acts as buffer space for wall segments (see comments).
     105 * @param distSoFar Sum of all the wall segments' lengths in 'segments'.
     106 * @param segments Current list of wall segments placed.
     107 */
     108function GetWallSegmentsRec(d, candidateSegments, minOverlap, maxOverlap, t, distSoFar, segments)
     109{
     110    // The idea is to find a number N of wall segments (excluding towers) so that the sum of their lengths adds up to a
     111    // value that is within certain bounds of the distance 'd' between the starting and ending points of the wall. This
     112    // creates either a positive or negative 'buffer' of space, that can be compensated for by spacing the wall segments
     113    // out away from each other, or inwards, overlapping each other. The spaces or overlaps can then be covered up by
     114    // placing towers on top of them. In this way, the same set of wall segments can be used to span a wider range of
     115    // target distances.
     116    //
     117    // In this function, it is understood that two extra towers will be placed centered at the starting and ending points.
     118    // They are allowed to contribute to the buffer space.
     119    //
     120    // The buffer space equals the difference between d and the sum of the lengths of all the wall segments, and is denoted
     121    // 'r' for 'remaining space'. Positive values of r mean that the walls will need to be spaced out, negative values of r
     122    // mean that they will need to overlap. Clearly, there are limits to how far wall segments can be spaced out or
     123    // overlapped, depending on how much 'buffer space' each tower provides, and how far 'into' towers the wall segments are
     124    // allowed to overlap.
     125    //
     126    // Let 't' signify the width of a tower. When there are N wall segments, then the maximum distance that can be covered
     127    // using only these walls (plus the towers covering up any gaps) is achieved when the walls and towers touch outer-border-
     128    // to-outer-border. Therefore, the maximum value of r is then given by:
     129    //
     130    //   rMax = t/2 + (N-1)*t + t/2
     131    //        = N*t
     132    //
     133    // where the two half-tower widths are buffer space contributed by the implied towers on the starting and ending points.
     134    // 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
     135    // wall segment lengths add up to exactly d, meaning that each one starts and ends right in the center of a tower.
     136    //
     137    // Thus, we establish:
     138    //   -Nt <= r <= Nt
     139    //
     140    // We can further generalize this by adding in parameters to control the depth to within which wall segments are allowed to
     141    // overlap with a tower. The bounds above assume that a wall segment is allowed to overlap across the entire range of 0
     142    // (not overlapping at all, as in the upper boundary) to 1 (overlapping maximally, as in the lower boundary).
     143    //
     144    // By requiring that walls overlap towers to a degree of at least 0 < minOverlap <= 1, it is clear that this lowers the
     145    // distance that can be maximally reached by the same set of wall segments, compared to the value of minOverlap = 0 that
     146    // we assumed to initially find Nt.
     147    //
     148    // Consider a value of minOverlap = 0.5, meaning that any wall segment must protrude at least halfway into towers; in this
     149    // situation, wall segments must at least touch boundaries or overlap mutually, implying that the sum of their lengths
     150    // must equal or exceed 'd', establishing an upper bound of 0 for r.
     151    // Similarly, consider a value of minOverlap = 1, meaning that any wall segment must overlap towers maximally; this situation
     152    // is equivalent to the one for finding the lower bound -Nt on r.
     153    //
     154    // With the implicit value minOverlap = 0 that yielded the upper bound Nt above, simple interpolation and a similar exercise
     155    // for maxOverlap, we find:
     156    //   (1-2*maxOverlap) * Nt <= r <= (1-2*minOverlap) * Nt
     157    //
     158    // To find N segments that satisfy this requirement, we try placing L, M and S wall segments in turn and continue recursively
     159    // as long as the value of r is not within the bounds. If continuing recursively returns an impossible configuration, we
     160    // backtrack and try a wall segment of the next length instead. Note that we should prefer to use the long segments first since
     161    // they can be replaced by gates.
     162   
     163    for each (var candSegment in candidateSegments)
     164    {
     165        segments.push(candSegment);
     166       
     167        var newDistSoFar = distSoFar + candSegment.len;
     168        var r = d - newDistSoFar;
     169       
     170        var rLowerBound = (1 - 2 * maxOverlap) * segments.length * t;
     171        var rUpperBound = (1 - 2 * minOverlap) * segments.length * t;
     172       
     173        if (r < rLowerBound)
     174        {
     175            // we've allocated too much wall length, pop the last segment and try the next
     176            //warn("Distance so far exceeds target, trying next level");
     177            segments.pop();
     178            continue;
     179        }
     180        else if (r > rUpperBound)
     181        {
     182            var recursiveResult = GetWallSegmentsRec(d, candidateSegments, minOverlap, maxOverlap, t, newDistSoFar, segments);
     183            if (!recursiveResult)
     184            {
     185                // recursive search with this piece yielded no results, pop it and try the next one
     186                segments.pop();
     187                continue;
     188            }
     189            else
     190                return recursiveResult;
     191        }
     192        else
     193        {
     194            // found a placement
     195            return {"segments": segments, "r": r};
     196        }
     197    }
     198   
     199    // no placement possible :(
     200    return false;
     201}
     202
     203Engine.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>
  • new file inaries/data/mods/public/simulation/templates/other/wallset_palisade.xml

    diff --git a/binaries/data/mods/public/simulation/templates/other/wallset_palisade.xml b/binaries/data/mods/public/simulation/templates/other/wallset_palisade.xml
    new file mode 100644
    index 0000000..2c506e1
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<Entity parent="template_structure_defense_wallset">
     3  <Identity>
     4    <Civ>gaia</Civ>
     5    <SpecificName>Palisade</SpecificName>
     6    <History>A cheap, quick defensive structure constructed with sharpened tree trunks</History>
     7    <Icon>gaia/special_palisade.png</Icon>
     8  </Identity>
     9  <WallSet>
     10    <Templates>
     11      <Tower>other/palisades_rocks_tower</Tower>
     12      <Gate>other/palisades_rocks_gate</Gate>
     13      <WallLong>other/palisades_rocks_long</WallLong>
     14      <WallMedium>other/palisades_rocks_medium</WallMedium>
     15      <WallShort>other/palisades_rocks_short</WallShort>
     16    </Templates>
     17  </WallSet>
     18</Entity>
  • binaries/data/mods/public/simulation/templates/structures/cart_wall_gate.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/cart_wall_gate.xml b/binaries/data/mods/public/simulation/templates/structures/cart_wall_gate.xml
    index f09c78e..a78841b 100644
    a b  
    1717  <VisualActor>
    1818    <Actor>structures/carthaginians/wall_gate.xml</Actor>
    1919  </VisualActor>
     20  <WallPiece>
     21    <Length>3.0</Length>
     22  </WallPiece>
    2023</Entity>
  • binaries/data/mods/public/simulation/templates/structures/cart_wall_long.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/cart_wall_long.xml b/binaries/data/mods/public/simulation/templates/structures/cart_wall_long.xml
    index f23303b..49e0c85 100644
    a b  
    2424  <VisualActor>
    2525    <Actor>structures/carthaginians/wall_long.xml</Actor>
    2626  </VisualActor>
     27  <WallPiece>
     28    <Length>46.0</Length>
     29  </WallPiece>
    2730</Entity>
     31 No newline at end of file
  • binaries/data/mods/public/simulation/templates/structures/cart_wall_medium.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/cart_wall_medium.xml b/binaries/data/mods/public/simulation/templates/structures/cart_wall_medium.xml
    index 434c78a..0422dfa 100644
    a b  
    2424  <VisualActor>
    2525    <Actor>structures/carthaginians/wall_medium.xml</Actor>
    2626  </VisualActor>
     27  <WallPiece>
     28    <Length>30.0</Length>
     29  </WallPiece>
    2730</Entity>
     31 No newline at end of file
  • binaries/data/mods/public/simulation/templates/structures/cart_wall_short.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/cart_wall_short.xml b/binaries/data/mods/public/simulation/templates/structures/cart_wall_short.xml
    index 7c4656a..df913e6 100644
    a b  
    2424  <VisualActor>
    2525    <Actor>structures/carthaginians/wall_short.xml</Actor>
    2626  </VisualActor>
     27  <WallPiece>
     28    <Length>16.0</Length>
     29  </WallPiece>
    2730</Entity>
     31 No newline at end of file
  • binaries/data/mods/public/simulation/templates/structures/cart_wall_tower.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/cart_wall_tower.xml b/binaries/data/mods/public/simulation/templates/structures/cart_wall_tower.xml
    index f209260..a15014f 100644
    a b  
    2525    <Actor>structures/carthaginians/wall_tower.xml</Actor>
    2626    <FoundationActor>structures/fndn_3x3.xml</FoundationActor>
    2727  </VisualActor>
     28  <WallPiece>
     29    <Length>11.0</Length>
     30  </WallPiece>
    2831</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/cart_wallset_stone.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/cart_wallset_stone.xml b/binaries/data/mods/public/simulation/templates/structures/cart_wallset_stone.xml
    new file mode 100644
    index 0000000..47beb5f
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<Entity parent="template_structure_defense_wallset">
     3  <Identity>
     4    <Civ>cart</Civ>
     5    <SpecificName>Jdar</SpecificName>
     6    <History>The Carthaginians built what are referred to as "triple walls" to fortify some of their cities; as triple walls aren't a practical construct for 0 A.D, the construction of the inner wall is to be used. This wall served not only as a defensive structure but had barracks and stables integrated right into it, and raised towers at intervals. Fodder for elephants and horses, and arms were stored onsite. The ground level consisted of housing for elephants, the second level for horses, and the third level as barracks for the troops. In Carthage alone, 200 elephants, a thousand horses and 15,000~30,000 troops could be housed within the city walls. As shown in the reference drawing, there was also a ditch at the base in front of the wall. These walls were typically built of large blocks of sandstone hewn from deposits nearby, and were never breached by invaders.</History>
     7  </Identity>
     8  <WallSet>
     9    <Templates>
     10      <Tower>structures/cart_wall_tower</Tower>
     11      <Gate>structures/cart_wall_gate</Gate>
     12      <WallLong>structures/cart_wall_long</WallLong>
     13      <WallMedium>structures/cart_wall_medium</WallMedium>
     14      <WallShort>structures/cart_wall_short</WallShort>
     15    </Templates>
     16  </WallSet>
     17</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..5c466ba 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/celts/wall_gate.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>25.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 7b333ee..72f1590 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/celts/wall_long.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>37.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 eace896..18cb741 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/celts/wall_medium.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>25.0</Length>
     20  </WallPiece>
    1821</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 b7a2f24..5a19b08 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/celts/wall_short.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>13.0</Length>
     20  </WallPiece>
    1821</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..4c854fc 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/celts/wall_tower.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>9.0</Length>
     20  </WallPiece>
    1821</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/celt_wallset_stone.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/celt_wallset_stone.xml b/binaries/data/mods/public/simulation/templates/structures/celt_wallset_stone.xml
    new file mode 100644
    index 0000000..dec4349
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<Entity parent="template_structure_defense_wallset">
     3  <Identity>
     4    <Civ>celt</Civ>
     5    <SpecificName>Gwarchglawdd</SpecificName>
     6    <History>The Romans called this wall 'Murus Gallicus'. Translated, it means 'Gaulish wall'. It was extremely resistant to assault by battering ram. Julius Caesar described a type of wood and stone wall, known as a Murus Gallicus, in his account of the Gallic Wars. These walls were made of a stone wall filled with rubble, with wooden logs inside for stability. Caesar noted how the flexibility of the wood added to the strength of the fort in case of battering ram attack.</History>
     7  </Identity>
     8  <WallSet>
     9    <Templates>
     10      <Tower>structures/celt_wall_tower</Tower>
     11      <Gate>structures/celt_wall_gate</Gate>
     12      <WallLong>structures/celt_wall_long</WallLong>
     13      <WallMedium>structures/celt_wall_medium</WallMedium>
     14      <WallShort>structures/celt_wall_short</WallShort>
     15    </Templates>
     16    <MaxTowerOverlap>0.80</MaxTowerOverlap>
     17    <MinTowerOverlap>0.05</MinTowerOverlap>
     18  </WallSet>
     19</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..99da09e 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_gate.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>38.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..8bdce62 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_long.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>37.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..a1e06dc 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_medium.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>24.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..4154533 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_medium.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>24.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..fc3a336 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_short.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>13.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..5e7017b 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/hellenes/wall_tower.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>7.5</Length>
     26  </WallPiece>
    2427</Entity>
     28 No newline at end of file
  • new file inaries/data/mods/public/simulation/templates/structures/hele_wallset_stone.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/hele_wallset_stone.xml b/binaries/data/mods/public/simulation/templates/structures/hele_wallset_stone.xml
    new file mode 100644
    index 0000000..cbff5e2
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<Entity parent="template_structure_defense_wallset">
     3  <Identity>
     4    <Civ>hele</Civ>
     5    <SpecificName>Teîkhos</SpecificName>
     6    <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>
     7  </Identity>
     8  <WallSet>
     9    <Templates>
     10      <Tower>structures/hele_wall_tower</Tower>
     11      <Gate>structures/hele_wall_gate</Gate>
     12      <WallLong>structures/hele_wall_long</WallLong>
     13      <WallMedium>structures/hele_wall_med</WallMedium>
     14      <WallShort>structures/hele_wall_short</WallShort>
     15    </Templates>
     16    <MaxTowerOverlap>0.90</MaxTowerOverlap>
     17    <MinTowerOverlap>0.05</MinTowerOverlap>
     18  </WallSet>
     19</Entity>
  • 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 4f93a9e..3f5c865 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/iberians/wall_gate.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>36.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 8ea2372..acdc61b 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/iberians/wall_long.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>36.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 3bc17bb..6d1b894 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/iberians/wall_medium.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>24.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 78aa48d..0adbe5b 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/iberians/wall_short.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>12.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 5c567e2..8ab95e2 100644
    a b  
    1010    <History>Sturdy battlements for city walls.</History>
    1111  </Identity>
    1212  <Obstruction>
    13     <Static width="11" depth="11"/>
     13    <Static width="10" depth="10"/>
    1414  </Obstruction>
    1515  <VisualActor>
    1616    <Actor>structures/iberians/wall_tower.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>10</Length>
     20  </WallPiece>
    1821</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/iber_wallset_stone.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/iber_wallset_stone.xml b/binaries/data/mods/public/simulation/templates/structures/iber_wallset_stone.xml
    new file mode 100644
    index 0000000..c5000db
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<Entity parent="template_structure_defense_wallset">
     3  <Identity>
     4    <Civ>iber</Civ>
     5    <SpecificName>Muro Ancho</SpecificName>
     6    <History>High and strongly built defensive stone walls were a common structure of the Iberian Peninsula during the period, and for long thereafter.</History>
     7  </Identity>
     8  <WallSet>
     9    <Templates>
     10      <Tower>structures/iber_wall_tower</Tower>
     11      <Gate>structures/iber_wall_gate</Gate>
     12      <WallLong>structures/iber_wall_long</WallLong>
     13      <WallMedium>structures/iber_wall_medium</WallMedium>
     14      <WallShort>structures/iber_wall_short</WallShort>
     15    </Templates>
     16    <MaxTowerOverlap>0.80</MaxTowerOverlap>
     17    <MinTowerOverlap>0.20</MinTowerOverlap>
     18  </WallSet>
     19</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..26f64c8 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/persians/wall_gate.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>37.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..1920cad 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/persians/wall_long.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>37.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..759b3d6 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/persians/wall_medium.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>24.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..317987a 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/persians/wall_short.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>13.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..50f4672 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/persians/wall_tower.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>8.5</Length>
     26  </WallPiece>
    2427</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/pers_wallset_stone.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/pers_wallset_stone.xml b/binaries/data/mods/public/simulation/templates/structures/pers_wallset_stone.xml
    new file mode 100644
    index 0000000..3c1c7c9
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<Entity parent="template_structure_defense_wallset">
     3  <Identity>
     4    <Civ>pers</Civ>
     5    <SpecificName>Dida</SpecificName>
     6    <History>These were the massive walls that Nebuchadnezzar built to protect the city. It is said that two four-horse chariots could easily pass by each other. Babylon, although not an official royal residence (there were 4 of them all together), was a preferred place for holidays.</History>
     7  </Identity>
     8  <WallSet>
     9    <Templates>
     10      <Tower>structures/pers_wall_tower</Tower>
     11      <Gate>structures/pers_wall_gate</Gate>
     12      <WallLong>structures/pers_wall_long</WallLong>
     13      <WallMedium>structures/pers_wall_medium</WallMedium>
     14      <WallShort>structures/pers_wall_short</WallShort>
     15    </Templates>
     16  </WallSet>
     17</Entity>
  • binaries/data/mods/public/simulation/templates/structures/rome_siege_wall.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall.xml b/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall.xml
    index 3607383..5b83a30 100644
    a b  
    2323  </Health>
    2424  <Identity>
    2525    <Civ>rome</Civ>
    26     <GenericName>Siege Wall</GenericName>
     26    <GenericName>Siege Wall</GenericName>
    2727    <SpecificName>Murus Latericius</SpecificName>
    2828    <Icon>structures/palisade.png</Icon>
    29     <Tooltip>A wooden and turf palisade buildable in enemy and neutral territories.</Tooltip>
     29    <Tooltip>A wooden and turf palisade buildable in enemy and neutral territories.</Tooltip>
    3030    <History>Quick building, but expensive wooden and earthen walls used to surround and siege an enemy town or fortified position. The most famous examples are the Roman sieges of the Iberian stronghold of Numantia and the Gallic stronghold of Alesia.</History>
    3131  </Identity>
    3232  <Obstruction>
  • binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_gate.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_gate.xml b/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_gate.xml
    index 70b961a..c0b3d8f 100644
    a b  
    3737    <Actor>structures/romans/siege_wall_gate.xml</Actor>
    3838    <FoundationActor>structures/fndn_wall.xml</FoundationActor>
    3939  </VisualActor>
     40  <WallPiece>
     41    <Length>36.0</Length>
     42  </WallPiece>
    4043</Entity>
  • binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml b/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml
    index 5d7f105..2eb11f2 100644
    a b  
    4040    <Actor>structures/romans/siege_wall_long.xml</Actor>
    4141    <FoundationActor>structures/fndn_wall.xml</FoundationActor>
    4242  </VisualActor>
     43  <WallPiece>
     44    <Length>36.0</Length>
     45  </WallPiece>
    4346</Entity>
  • binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml b/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml
    index 633b2f0..531bc52 100644
    a b  
    4040    <Actor>structures/romans/siege_wall_medium.xml</Actor>
    4141    <FoundationActor>structures/fndn_wall_short.xml</FoundationActor>
    4242  </VisualActor>
     43  <WallPiece>
     44    <Length>24.0</Length>
     45  </WallPiece>
    4346</Entity>
  • binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_short.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_short.xml b/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_short.xml
    index 388c17c..c531c90 100644
    a b  
    4040    <Actor>structures/romans/siege_wall_short.xml</Actor>
    4141    <FoundationActor>structures/fndn_1x1.xml</FoundationActor>
    4242  </VisualActor>
     43  <WallPiece>
     44    <Length>12.0</Length>
     45  </WallPiece>
    4346</Entity>
  • binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_tower.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_tower.xml b/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_tower.xml
    index b0f1252..d8158ea 100644
    a b  
    3737    <Actor>structures/romans/siege_wall_tower.xml</Actor>
    3838    <FoundationActor>structures/fndn_1x1.xml</FoundationActor>
    3939  </VisualActor>
     40  <WallPiece>
     41    <Length>6.0</Length>
     42  </WallPiece>
    4043</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..4e1ca29 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/romans/wall_gate.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>37.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..42aa3a0 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/romans/wall_long.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>37.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..3d721cb 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/romans/wall_medium.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>25.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..8febba2 100644
    a b  
    2121  <VisualActor>
    2222    <Actor>structures/romans/wall_short.xml</Actor>
    2323  </VisualActor>
     24  <WallPiece>
     25    <Length>13.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..8041f3e 100644
    a b  
    1515  <VisualActor>
    1616    <Actor>structures/romans/wall_tower.xml</Actor>
    1717  </VisualActor>
     18  <WallPiece>
     19    <Length>9.5</Length>
     20  </WallPiece>
    1821</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/rome_wallset_siege.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_wallset_siege.xml b/binaries/data/mods/public/simulation/templates/structures/rome_wallset_siege.xml
    new file mode 100644
    index 0000000..c56ae0f
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<Entity parent="template_structure_defense_wallset">
     3  <Identity>
     4    <Civ>rome</Civ>
     5    <GenericName>Siege Wall</GenericName>
     6    <SpecificName>Murus Latericius</SpecificName>
     7    <Icon>structures/palisade.png</Icon>
     8    <Tooltip>A wooden and turf palisade buildable in enemy and neutral territories.</Tooltip>
     9    <History>Quick building, but expensive wooden and earthen walls used to surround and siege an enemy town or fortified position. The most famous examples are the Roman sieges of the Iberian stronghold of Numantia and the Gallic stronghold of Alesia.</History>
     10  </Identity>
     11  <WallSet>
     12    <Templates>
     13      <Tower>structures/rome_siege_wall_tower</Tower>
     14      <Gate>structures/rome_siege_wall_gate</Gate>
     15      <WallLong>structures/rome_siege_wall_long</WallLong>
     16      <WallMedium>structures/rome_siege_wall_medium</WallMedium>
     17      <WallShort>structures/rome_siege_wall_short</WallShort>
     18    </Templates>
     19    <MaxTowerOverlap>1.00</MaxTowerOverlap>
     20    <MinTowerOverlap>0.05</MinTowerOverlap>
     21  </WallSet>
     22</Entity>
  • new file inaries/data/mods/public/simulation/templates/structures/rome_wallset_stone.xml

    diff --git a/binaries/data/mods/public/simulation/templates/structures/rome_wallset_stone.xml b/binaries/data/mods/public/simulation/templates/structures/rome_wallset_stone.xml
    new file mode 100644
    index 0000000..3991b42
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<Entity parent="template_structure_defense_wallset">
     3  <Identity>
     4    <Civ>rome</Civ>
     5    <SpecificName>Moenia</SpecificName>
     6    <History>Roman city walls used a number of innovations to thwart besiegers.</History>
     7  </Identity>
     8  <WallSet>
     9    <Templates>
     10      <Tower>structures/rome_wall_tower</Tower>
     11      <Gate>structures/rome_wall_gate</Gate>
     12      <WallLong>structures/rome_wall_long</WallLong>
     13      <WallMedium>structures/rome_wall_medium</WallMedium>
     14      <WallShort>structures/rome_wall_short</WallShort>
     15    </Templates>
     16    <MaxTowerOverlap>0.80</MaxTowerOverlap>
     17    <MinTowerOverlap>0.10</MinTowerOverlap>
     18  </WallSet>
     19</Entity>
  • new file inaries/data/mods/public/simulation/templates/template_structure_defense_wallset.xml

    diff --git a/binaries/data/mods/public/simulation/templates/template_structure_defense_wallset.xml b/binaries/data/mods/public/simulation/templates/template_structure_defense_wallset.xml
    new file mode 100644
    index 0000000..ab9dfcc
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<!-- Abstract entity to serve as a bare-minimum dummy constructable entity to initiate wall placement.
     3     Defines the set of actual entities that are part of the same wall construction system (i.e., towers,
     4     gates, wall segments of various length, etc.) using the WallSet component (to be overridden by child
     5     templates). -->
     6<Entity>
     7  <Identity>
     8    <Icon>structures/wall.png</Icon>
     9    <Classes datatype="tokens">Town Wall</Classes>
     10    <GenericName>City Wall</GenericName>
     11    <Tooltip>Wall off your town for a stout defense.</Tooltip>
     12    <Icon>structures/wall.png</Icon>
     13    <RequiredTechnology>phase_town</RequiredTechnology>
     14  </Identity>
     15  <WallSet>
     16    <MaxTowerOverlap>0.85</MaxTowerOverlap>
     17    <MinTowerOverlap>0.05</MinTowerOverlap>
     18  </WallSet>
     19</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 b4f530d..827c191 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      structures/{civ}_wallset_stone
    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 f85a53b..25831ce 100644
    a b  
    1919      structures/{civ}_barracks
    2020      structures/{civ}_dock
    2121      structures/{civ}_defense_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/spart_champion_infantry_sword.xml

    diff --git a/binaries/data/mods/public/simulation/templates/units/spart_champion_infantry_sword.xml b/binaries/data/mods/public/simulation/templates/units/spart_champion_infantry_sword.xml
    index 96cb330..d5f8530 100644
    a b  
    4848  </Attack>
    4949  <Builder>
    5050    <Entities datatype="tokens">
    51       -structures/{civ}_wall
    52       -structures/{civ}_wall_gate
    53       -structures/{civ}_wall_tower
     51      -structures/{civ}_wallset_stone
    5452      structures/spart_syssiton
    5553    </Entities>
    5654  </Builder>
  • binaries/data/mods/public/simulation/templates/units/spart_infantry_javelinist_b.xml

    diff --git a/binaries/data/mods/public/simulation/templates/units/spart_infantry_javelinist_b.xml b/binaries/data/mods/public/simulation/templates/units/spart_infantry_javelinist_b.xml
    index ca5172d..cc1d87a 100644
    a b  
    22<Entity parent="template_unit_infantry_ranged_javelinist">
    33  <Builder>
    44    <Entities datatype="tokens">
    5       -structures/{civ}_wall
    6       -structures/{civ}_wall_gate
    7       -structures/{civ}_wall_tower
     5      -structures/{civ}_wallset_stone
    86      structures/spart_syssiton
    97    </Entities>
    108  </Builder>
  • binaries/data/mods/public/simulation/templates/units/spart_infantry_spearman_b.xml

    diff --git a/binaries/data/mods/public/simulation/templates/units/spart_infantry_spearman_b.xml b/binaries/data/mods/public/simulation/templates/units/spart_infantry_spearman_b.xml
    index 6969fe3..e1372ca 100644
    a b  
    1313  </Attack>
    1414  <Builder>
    1515    <Entities datatype="tokens">
    16       -structures/{civ}_wall
    17       -structures/{civ}_wall_gate
    18       -structures/{civ}_wall_tower
     16      -structures/{civ}_wallset_stone
    1917      structures/spart_syssiton
    2018    </Entities>
    2119  </Builder>
  • 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 604665e..a197bb4 100644
    a b  
    2222#include "graphics/Texture.h"
    2323#include "maths/Vector2D.h"
    2424#include "maths/Vector3D.h"
     25#include "maths/FixedVector3D.h"
    2526#include "ps/Overlay.h" // CColor  (TODO: that file has nothing to do with overlays, it should be renamed)
    2627
    2728class CTerrain;
  • source/gui/CTooltip.cpp

    diff --git a/source/gui/CTooltip.cpp b/source/gui/CTooltip.cpp
    index 4d12296..dfacb4e 100644
    a b void CTooltip::SetupText()  
    8787    CPos mousepos, offset;
    8888    EVAlign anchor;
    8989    bool independent;
     90   
    9091    GUI<bool>::GetSetting(this, "independent", independent);
    9192    if (independent)
    9293        mousepos = GetMousePos();
    9394    else
    9495        GUI<CPos>::GetSetting(this, "_mousepos", mousepos);
     96
    9597    GUI<CPos>::GetSetting(this, "offset", offset);
    9698    GUI<EVAlign>::GetSetting(this, "anchor", anchor);
    9799
  • source/gui/scripting/ScriptFunctions.cpp

    diff --git a/source/gui/scripting/ScriptFunctions.cpp b/source/gui/scripting/ScriptFunctions.cpp
    index 711cd41..d2be90a 100644
    a b std::vector<entity_id_t> PickFriendlyEntitiesOnScreen(void* cbdata, int player)  
    142142    return PickFriendlyEntitiesInRect(cbdata, 0, 0, g_xres, g_yres, player);
    143143}
    144144
    145 std::vector<entity_id_t> PickSimilarFriendlyEntities(void* UNUSED(cbdata), std::string templateName, bool includeOffScreen, bool matchRank)
     145std::vector<entity_id_t> PickSimilarFriendlyEntities(void* UNUSED(cbdata), std::string templateName, bool includeOffScreen, bool matchRank, bool allowFoundations)
    146146{
    147     return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank, false);
     147    return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank, false, allowFoundations);
    148148}
    149149
    150 CFixedVector3D GetTerrainAtPoint(void* UNUSED(cbdata), int x, int y)
     150CFixedVector3D GetTerrainAtScreenPoint(void* UNUSED(cbdata), int x, int y)
    151151{
    152152    CVector3D pos = g_Game->GetView()->GetCamera()->GetWorldCoordinates(x, y, true);
    153153    return CFixedVector3D(fixed::FromFloat(pos.X), fixed::FromFloat(pos.Y), fixed::FromFloat(pos.Z));
    void GuiScriptingInit(ScriptInterface& scriptInterface)  
    589589    scriptInterface.RegisterFunction<std::vector<entity_id_t>, int, int, &PickEntitiesAtPoint>("PickEntitiesAtPoint");
    590590    scriptInterface.RegisterFunction<std::vector<entity_id_t>, int, int, int, int, int, &PickFriendlyEntitiesInRect>("PickFriendlyEntitiesInRect");
    591591    scriptInterface.RegisterFunction<std::vector<entity_id_t>, int, &PickFriendlyEntitiesOnScreen>("PickFriendlyEntitiesOnScreen");
    592     scriptInterface.RegisterFunction<std::vector<entity_id_t>, std::string, bool, bool, &PickSimilarFriendlyEntities>("PickSimilarFriendlyEntities");
    593     scriptInterface.RegisterFunction<CFixedVector3D, int, int, &GetTerrainAtPoint>("GetTerrainAtPoint");
     592    scriptInterface.RegisterFunction<std::vector<entity_id_t>, std::string, bool, bool, bool, &PickSimilarFriendlyEntities>("PickSimilarFriendlyEntities");
     593    scriptInterface.RegisterFunction<CFixedVector3D, int, int, &GetTerrainAtScreenPoint>("GetTerrainAtScreenPoint");
    594594
    595595    // Network / game setup functions
    596596    scriptInterface.RegisterFunction<void, &StartNetworkGame>("StartNetworkGame");
  • 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..1a244ce 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:  
    151167        serialize.Bool("active", m_Active);
    152168        serialize.Bool("moving", m_Moving);
    153169        serialize.NumberU32_Unbounded("control group", m_ControlGroup);
     170        serialize.NumberU32_Unbounded("control group 2", m_ControlGroup2);
    154171        serialize.NumberU32_Unbounded("tag", m_Tag.n);
    155172        serialize.NumberU8_Unbounded("flags", m_Flags);
    156173    }
    public:  
    194211                // Need to create a new pathfinder shape:
    195212                if (m_Type == STATIC)
    196213                    m_Tag = cmpObstructionManager->AddStaticShape(GetEntityId(),
    197                         data.x, data.z, data.a, m_Size0, m_Size1, m_Flags);
     214                        data.x, data.z, data.a, m_Size0, m_Size1, m_Flags, m_ControlGroup, m_ControlGroup2);
    198215                else
    199216                    m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(),
    200217                        data.x, data.z, m_Size0, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup);
    public:  
    241258            if (!cmpPosition->IsInWorld())
    242259                return; // don't need an obstruction
    243260
     261            // TODO: code duplication from message handlers
    244262            CFixedVector2D pos = cmpPosition->GetPosition2D();
    245263            if (m_Type == STATIC)
    246264                m_Tag = cmpObstructionManager->AddStaticShape(GetEntityId(),
    247                     pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, m_Flags);
     265                    pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, m_Flags, m_ControlGroup, m_ControlGroup2);
    248266            else
    249267                m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(),
    250268                    pos.X, pos.Y, m_Size0, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup);
    public:  
    255273
    256274            // Delete the obstruction shape
    257275
     276            // TODO: code duplication from message handlers
    258277            if (m_Tag.valid())
    259278            {
    260279                CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
    public:  
    346365        if (!cmpPathfinder)
    347366            return false; // error
    348367
     368        // required precondition to use SkipControlGroupsRequireFlagObstructionFilter
     369        if (m_ControlGroup == INVALID_ENTITY)
     370        {
     371            LOGERROR(L"[CmpObstruction] Cannot test for foundation obstructions; primary control group must be valid");
     372            return false;
     373        }
     374
    349375        // Get passability class
    350376        ICmpPathfinder::pass_class_t passClass = cmpPathfinder->GetPassabilityClass(className);
    351377
    352         // Ignore collisions with self, or with non-foundation-blocking obstructions
    353         SkipTagFlagsObstructionFilter filter(m_Tag, ICmpObstructionManager::FLAG_BLOCK_FOUNDATION);
     378        // Ignore collisions within the same control group, or with other non-foundation-blocking shapes.
     379        // Note that, since the control group for each entity defaults to the entity's ID, this is typically
     380        // equivalent to only ignoring the entity's own shape and other non-foundation-blocking shapes.
     381        SkipControlGroupsRequireFlagObstructionFilter filter(m_ControlGroup, m_ControlGroup2,
     382            ICmpObstructionManager::FLAG_BLOCK_FOUNDATION);
    354383
    355384        if (m_Type == STATIC)
    356385            return cmpPathfinder->CheckBuildingPlacement(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, GetEntityId(), passClass);
    public:  
    375404        if (!cmpObstructionManager)
    376405            return ret; // error
    377406
    378         // Ignore collisions with self, or with non-construction-blocking obstructions
    379         SkipTagFlagsObstructionFilter filter(m_Tag, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION);
     407        // required precondition to use SkipControlGroupsRequireFlagObstructionFilter
     408        if (m_ControlGroup == INVALID_ENTITY)
     409        {
     410            LOGERROR(L"[CmpObstruction] Cannot test for construction obstructions; primary control group must be valid");
     411            return ret;
     412        }
     413
     414        // Ignore collisions within the same control group, or with other non-construction-blocking shapes.
     415        // Note that, since the control group for each entity defaults to the entity's ID, this is typically
     416        // equivalent to only ignoring the entity's own shape and other non-construction-blocking shapes.
     417        SkipControlGroupsRequireFlagObstructionFilter filter(m_ControlGroup, m_ControlGroup2,
     418            ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION);
    380419
    381420        if (m_Type == STATIC)
    382421            cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, &ret);
    public:  
    401440    virtual void SetControlGroup(entity_id_t group)
    402441    {
    403442        m_ControlGroup = group;
     443        UpdateControlGroups();
     444    }
    404445
    405         if (m_Tag.valid() && m_Type == UNIT)
     446    virtual void SetControlGroup2(entity_id_t group2)
     447    {
     448        m_ControlGroup2 = group2;
     449        UpdateControlGroups();
     450    }
     451
     452    virtual entity_id_t GetControlGroup()
     453    {
     454        return m_ControlGroup;
     455    }
     456
     457    virtual entity_id_t GetControlGroup2()
     458    {
     459        return m_ControlGroup2;
     460    }
     461
     462    void UpdateControlGroups()
     463    {
     464        if (m_Tag.valid())
    406465        {
    407466            CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
    408467            if (cmpObstructionManager)
    409                 cmpObstructionManager->SetUnitControlGroup(m_Tag, m_ControlGroup);
     468            {
     469                if (m_Type == UNIT)
     470                {
     471                    cmpObstructionManager->SetUnitControlGroup(m_Tag, m_ControlGroup);
     472                }
     473                else if (m_Type == STATIC)
     474                {
     475                    cmpObstructionManager->SetStaticControlGroup(m_Tag, m_ControlGroup, m_ControlGroup2);
     476                }
     477            }
    410478        }
    411479    }
    412480
  • source/simulation2/components/CCmpObstructionManager.cpp

    diff --git a/source/simulation2/components/CCmpObstructionManager.cpp b/source/simulation2/components/CCmpObstructionManager.cpp
    index e1a8d6a..68089e8 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);
     109        serialize.NumberU32_Unbounded("group2", value.group2);
    105110    }
    106111};
    107112
    public:  
    257262        return UNIT_INDEX_TO_TAG(id);
    258263    }
    259264
    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)
     265    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 */)
    261266    {
    262267        fixed s, c;
    263268        sincos_approx(a, s, c);
    264269        CFixedVector2D u(c, -s);
    265270        CFixedVector2D v(s, c);
    266271
    267         StaticShape shape = { ent, x, z, u, v, w/2, h/2, flags };
     272        StaticShape shape = { ent, x, z, u, v, w/2, h/2, flags, group, group2 };
    268273        u32 id = m_StaticShapeNext++;
    269274        m_StaticShapes[id] = shape;
    270275        MakeDirtyStatic(flags);
    public:  
    367372        }
    368373    }
    369374
     375    virtual void SetStaticControlGroup(tag_t tag, entity_id_t group, entity_id_t group2)
     376    {
     377        ENSURE(TAG_IS_VALID(tag) && TAG_IS_STATIC(tag));
     378
     379        if (TAG_IS_STATIC(tag))
     380        {
     381            StaticShape& shape = m_StaticShapes[TAG_TO_INDEX(tag)];
     382            shape.group = group;
     383            shape.group2 = group2;
     384        }
     385    }
     386
    370387    virtual void RemoveShape(tag_t tag)
    371388    {
    372389        ENSURE(TAG_IS_VALID(tag));
    bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, enti  
    533550        std::map<u32, UnitShape>::iterator it = m_UnitShapes.find(unitShapes[i]);
    534551        ENSURE(it != m_UnitShapes.end());
    535552
    536         if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
     553        if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
    537554            continue;
    538555
    539556        CFixedVector2D center(it->second.x, it->second.z);
    bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, enti  
    548565        std::map<u32, StaticShape>::iterator it = m_StaticShapes.find(staticShapes[i]);
    549566        ENSURE(it != m_StaticShapes.end());
    550567
    551         if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
     568        if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
    552569            continue;
    553570
    554571        CFixedVector2D center(it->second.x, it->second.z);
    bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filte  
    592609
    593610    for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
    594611    {
    595         if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
     612        if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
    596613            continue;
    597614
    598615        CFixedVector2D center1(it->second.x, it->second.z);
    bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filte  
    608625
    609626    for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
    610627    {
    611         if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
     628        if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
    612629            continue;
    613630
    614631        CFixedVector2D center1(it->second.x, it->second.z);
    bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter,  
    649666
    650667    for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
    651668    {
    652         if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
     669        if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
    653670            continue;
    654671
    655672        entity_pos_t r1 = it->second.r;
    bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter,  
    665682
    666683    for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
    667684    {
    668         if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
     685        if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
    669686            continue;
    670687
    671688        CFixedVector2D center1(it->second.x, it->second.z);
    void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter  
    869886        std::map<u32, UnitShape>::iterator it = m_UnitShapes.find(unitShapes[i]);
    870887        ENSURE(it != m_UnitShapes.end());
    871888
    872         if (!filter.Allowed(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group))
     889        if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
    873890            continue;
    874891
    875892        entity_pos_t r = it->second.r;
    void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter  
    890907        std::map<u32, StaticShape>::iterator it = m_StaticShapes.find(staticShapes[i]);
    891908        ENSURE(it != m_StaticShapes.end());
    892909
    893         if (!filter.Allowed(STATIC_INDEX_TO_TAG(it->first), it->second.flags, INVALID_ENTITY))
     910        if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
    894911            continue;
    895912
    896913        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 0a52b6f..467ff1c 100644
    a b void CCmpRallyPointRenderer::ReduceSegmentsByVisibility(std::vector<CVector2D>&  
    869869    // process from there on until the entire line is checked. The output is the array of base nodes.
    870870
    871871    std::vector<CVector2D> newCoords;
    872     StationaryObstructionFilter obstructionFilter;
     872    StationaryOnlyObstructionFilter obstructionFilter;
    873873    entity_pos_t lineRadius = fixed::FromFloat(m_LineThickness);
    874874    ICmpPathfinder::pass_class_t passabilityClass = cmpPathFinder->GetPassabilityClass(m_LinePassabilityClass);
    875875
  • source/simulation2/components/CCmpTemplateManager.cpp

    diff --git a/source/simulation2/components/CCmpTemplateManager.cpp b/source/simulation2/components/CCmpTemplateManager.cpp
    index 7b24af7..d69cc36 100644
    a b bool CCmpTemplateManager::LoadTemplateFile(const std::string& templateName, int  
    357357    }
    358358
    359359    // Load the new file into the template data (overriding parent values)
    360     CParamNode::LoadXML(m_TemplateFileData[templateName], xero);
     360    CParamNode::LoadXML(m_TemplateFileData[templateName], xero, wstring_from_utf8(templateName).c_str());
    361361
    362362    return true;
    363363}
    void CCmpTemplateManager::ConstructTemplateActor(const std::string& actorName, C  
    376376    out = m_TemplateFileData[templateName];
    377377
    378378    // Initialise the actor's name and make it an Atlas selectable entity.
    379     std::string name = utf8_from_wstring(CParamNode::EscapeXMLString(wstring_from_utf8(actorName)));
     379    std::wstring actorNameW = wstring_from_utf8(actorName);
     380    std::string name = utf8_from_wstring(CParamNode::EscapeXMLString(actorNameW));
    380381    std::string xml = "<Entity>"
    381382                          "<VisualActor><Actor>" + name + "</Actor></VisualActor>"
    382383                          "<Selectable>"
    void CCmpTemplateManager::ConstructTemplateActor(const std::string& actorName, C  
    385386                          "</Selectable>"
    386387                      "</Entity>";
    387388
    388     CParamNode::LoadXMLString(out, xml.c_str());
     389    CParamNode::LoadXMLString(out, xml.c_str(), actorNameW.c_str());
    389390}
    390391
    391392static Status AddToTemplates(const VfsPath& pathname, const FileInfo& UNUSED(fileInfo), const uintptr_t cbData)
  • 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..d2a3089 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. The secondary
     361 * control group to reject entities from may be set to INVALID_ENTITY to not use it.
     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..8210017
    - +  
     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(1000), fixed::FromInt(1000));
     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_simple_collisions()
     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_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_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_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_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_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/simulation2/system/ParamNode.cpp

    diff --git a/source/simulation2/system/ParamNode.cpp b/source/simulation2/system/ParamNode.cpp
    index 7cbf1bd..94fd500 100644
    a b CParamNode::CParamNode(bool isOk) :  
    4444{
    4545}
    4646
    47 void CParamNode::LoadXML(CParamNode& ret, const XMBFile& xmb)
     47void CParamNode::LoadXML(CParamNode& ret, const XMBFile& xmb, const wchar_t* sourceIdentifier /*= NULL*/)
    4848{
    49     ret.ApplyLayer(xmb, xmb.GetRoot());
     49    ret.ApplyLayer(xmb, xmb.GetRoot(), sourceIdentifier);
    5050}
    5151
    5252void CParamNode::LoadXML(CParamNode& ret, const VfsPath& path)
    void CParamNode::LoadXML(CParamNode& ret, const VfsPath& path)  
    5656    if (ok != PSRETURN_OK)
    5757        return; // (Xeromyces already logged an error)
    5858
    59     LoadXML(ret, xero);
     59    LoadXML(ret, xero, path.string().c_str());
    6060}
    6161
    62 PSRETURN CParamNode::LoadXMLString(CParamNode& ret, const char* xml)
     62PSRETURN CParamNode::LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier /*=NULL*/)
    6363{
    6464    CXeromyces xero;
    6565    PSRETURN ok = xero.LoadString(xml);
    PSRETURN CParamNode::LoadXMLString(CParamNode& ret, const char* xml)  
    7171    return PSRETURN_OK;
    7272}
    7373
    74 void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element)
     74void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element, const wchar_t* sourceIdentifier /*= NULL*/)
    7575{
    7676    ResetScriptVal();
    7777
    void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element)  
    128128                        if (tokenIt != tokens.end())
    129129                            tokens.erase(tokenIt);
    130130                        else
    131                             LOGWARNING(L"[ParamNode] Could not remove token '%ls' from node '%hs'; not present in list nor inherited (possible typo?)",
    132                                 newTokens[i].substr(1).c_str(), name.c_str());
     131                            LOGWARNING(L"[ParamNode] Could not remove token '%ls' from node '%hs'%ls; not present in list nor inherited (possible typo?)",
     132                                newTokens[i].substr(1).c_str(), name.c_str(), sourceIdentifier ? (L" in '" + std::wstring(sourceIdentifier) + L"'").c_str() : L"");
    133133                    }
    134134                    else
    135135                    {
    void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element)  
    153153    // Recurse through the element's children
    154154    XERO_ITER_EL(element, child)
    155155    {
    156         node.ApplyLayer(xmb, child);
     156        node.ApplyLayer(xmb, child, sourceIdentifier);
    157157    }
    158158
    159159    // Add the element's attributes, prefixing names with "@"
  • source/simulation2/system/ParamNode.h

    diff --git a/source/simulation2/system/ParamNode.h b/source/simulation2/system/ParamNode.h
    index 7caffbd..8f0d7d5 100644
    a b public:  
    123123     * Loads the XML data specified by @a file into the node @a ret.
    124124     * Any existing data in @a ret will be overwritten or else kept, so this
    125125     * can be called multiple times to build up a node from multiple inputs.
     126     *
     127     * @param sourceIdentifier Optional; string you can pass along to indicate the source of
     128     *        the data getting loaded. Used for output to log messages if an error occurs.
    126129     */
    127     static void LoadXML(CParamNode& ret, const XMBFile& file);
     130    static void LoadXML(CParamNode& ret, const XMBFile& file, const wchar_t* sourceIdentifier = NULL);
    128131
    129132    /**
    130133     * Loads the XML data specified by @a path into the node @a ret.
    public:  
    136139    /**
    137140     * See LoadXML, but parses the XML string @a xml.
    138141     * @return error code if parsing failed, else @c PSRETURN_OK
     142     *
     143     * @param sourceIdentifier Optional; string you can pass along to indicate the source of
     144     *        the data getting loaded. Used for output to log messages if an error occurs.
    139145     */
    140     static PSRETURN LoadXMLString(CParamNode& ret, const char* xml);
     146    static PSRETURN LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier = NULL);
    141147
    142148    /**
    143149     * Finds the childs named @a name from @a src and from @a this, and copies the source child's children
    public:  
    228234    static std::wstring EscapeXMLString(const std::wstring& str);
    229235
    230236private:
    231     void ApplyLayer(const XMBFile& xmb, const XMBElement& element);
     237   
     238    /**
     239     * Overlays the specified data onto this node. See class documentation for the concept and examples.
     240     *
     241     * @param xmb Representation of the XMB file containing an element with the data to apply.
     242     * @param element Element inside the specified @p xmb file containing the data to apply.
     243     * @param sourceIdentifier Optional; string you can pass along to indicate the source of
     244     *        the data getting applied. Used for output to log messages if an error occurs.
     245     */
     246    void ApplyLayer(const XMBFile& xmb, const XMBElement& element, const wchar_t* sourceIdentifier = NULL);
    232247
    233248    void ResetScriptVal();
    234249
  • 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 414a95b..46cf352 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
  • new file wall_placement_1.html

    diff --git a/wall_placement_1.html b/wall_placement_1.html
    new file mode 100644
    index 0000000..bd16f42
    - +  
     1<doctype html>
     2<html>
     3    <head>
     4        <title>Wall Placement Demo</title>
     5    </head>
     6    <body>
     7        <canvas id="canvas" width="1600" height="1000" style="border:1px solid black;"></canvas>
     8        <script type="text/javascript">
     9       
     10        var canvas = document.getElementById("canvas");
     11        var ctx = canvas.getContext("2d");
     12        ctx.font = "1.5pt Arial";
     13       
     14        var start = null;
     15        var end = null;
     16        var endFixed = false;
     17       
     18        var scale = 5;
     19        var pointMarkerRadius = 0.5;
     20        var pointMarkerColor = "#000";
     21       
     22        var candidateSegments = [
     23            {"template": "L",  "len": 34.0},
     24            {"template": "M",  "len": 23.0},
     25            {"template": "S",  "len": 10.0},
     26        ];
     27        var len = 31.3;
     28        var towerLen = 8.5;
     29        var wallThickness = 2; // for drawing purposes only
     30       
     31        function fillCircle(x, y, r, s)
     32        {
     33            ctx.beginPath();
     34            ctx.arc(x, y, r, 0, 2 * Math.PI, false);
     35            ctx.fillStyle = s;
     36            ctx.fill();
     37        }
     38       
     39        function drawRect(x, y, w, h, a)
     40        {
     41            ctx.save();
     42            ctx.translate(x, y);
     43            ctx.rotate(a);
     44            ctx.beginPath();
     45            ctx.rect(-w/2, -h/2, w, h);
     46            ctx.stroke();
     47            ctx.restore();
     48        }
     49       
     50        function drawText(x, y, a, txt)
     51        {
     52            ctx.save();
     53            ctx.translate(x, y);
     54            ctx.rotate(a);
     55            ctx.textAlign = "center";
     56            ctx.fillText(txt, 0, 0.7);
     57            ctx.restore();
     58        }
     59       
     60        function drawTower(x, y, a, ss)
     61        {
     62            ctx.strokeStyle = 'gray';
     63            if (ss)
     64                ctx.strokeStyle = ss;
     65           
     66            drawRect(x, y, towerLen, towerLen, a);
     67        }
     68       
     69        function drawWall(x, y, l, a, type, ss)
     70        {
     71            ctx.strokeStyle = 'black';
     72            if (ss)
     73                ctx.strokeStyle = ss;
     74           
     75            drawRect(x, y, l, wallThickness, a);
     76            drawText(x, y, a, type);
     77        }
     78       
     79        function drawStats(stats)
     80        {
     81            ctx.save();
     82            ctx.font = "3pt Courier New";
     83            ctx.textAlign = "left";
     84            //ctx.fillText("r = " + stats.r, 10, 10);
     85           
     86            var num = 1;
     87            for (var each in stats)
     88                ctx.fillText(each + ":  " + stats[each], 5, 2+5*(num++));
     89           
     90            ctx.restore();
     91        }
     92       
     93        function draw()
     94        {
     95            ctx.save();
     96            ctx.translate(0.5, 0.5); // finer lines
     97            ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
     98            ctx.scale(scale, scale);
     99            ctx.lineWidth = 1/scale;
     100           
     101            var pointMarkerStyle = "#000";
     102            var placement = null;
     103           
     104            if (start && end)
     105            {
     106                var dir = {"x": end.x - start.x, "z": end.z - start.z};
     107                var len = Math.sqrt(dir.x * dir.x + dir.z * dir.z);
     108                var angle = Math.atan2(dir.z, dir.x);    // angle of this wall segment (relative to world-space X/Z axes)
     109               
     110                placement = WallPlaceSegmentRec(len, 0, [], candidateSegments, towerLen, 0);
     111                if (placement)
     112                {
     113                    var placedEntities = placement.segments; // list of chosen candidate segments
     114                    var r = placement.r; // remaining distance to target without towers (must be <= (N-1) * towerWidth)
     115                    var s = r / (placedEntities.length - 1); // spacing
     116                   
     117                    var dirNormalized = {"x": dir.x / len, "z": dir.z / len};
     118                   
     119                    var progress = 0;
     120                    for (var i = 0; i < placedEntities.length; i++)
     121                    {
     122                        var placedEntity = placedEntities[i];
     123                        var targetX = start.x + (progress + placedEntity.len/2) * dirNormalized.x;
     124                        var targetZ = start.z + (progress + placedEntity.len/2) * dirNormalized.z;
     125                       
     126                        /*result.push({
     127                            "template": placedEntity.template,
     128                            "pos": {"x": targetX, "z": targetZ},
     129                            "angle": angle,
     130                        });*/
     131                        drawWall(targetX, targetZ, placedEntity.len, angle, placedEntity.template);
     132                       
     133                        if (i < placedEntities.length - 1)
     134                        {
     135                            var towerX = start.x + (progress + placedEntity.len + s/2) * dirNormalized.x;
     136                            var towerZ = start.z + (progress + placedEntity.len + s/2) * dirNormalized.z;
     137                           
     138                            /*result.push({
     139                                "template": wallSet.tower,
     140                                "pos": {"x": towerX, "z": towerZ},
     141                                "angle": angle,
     142                            });*/
     143                            drawTower(towerX, towerZ, angle);
     144                        }
     145                       
     146                        progress += placedEntity.len + s;
     147                    }
     148                }
     149                else
     150                {
     151                    pointMarkerStyle = "#c00";
     152                }
     153            }
     154           
     155            if (start)
     156            {
     157                fillCircle(start.x, start.z, pointMarkerRadius, placement ? "#000" : "#c00");
     158                drawTower(start.x, start.z, angle, placement ? "gray" : "#c00");
     159            }
     160            if (end)
     161            {
     162                fillCircle(end.x, end.z, pointMarkerRadius, placement ? "#000" : "#c00");
     163                drawTower(end.x, end.z, angle, placement ? "gray" : "#c00");
     164            }
     165           
     166            var stats = {"r ": "", "N ": "", "Nt": ""};
     167            if (placement)
     168            {
     169                stats["r "] = Math.round(placement.r*100)/100;
     170                stats["N "] = placement.segments.length;
     171                stats["Nt"] = stats["N "] * towerLen;
     172            }
     173            drawStats(stats);
     174           
     175            ctx.restore();
     176        }
     177       
     178        function onCanvasMouseDown(ev)
     179        {
     180            if (start === null)
     181            {
     182                start = {"x": (ev.clientX - canvas.offsetLeft)/scale, "z": (ev.clientY - canvas.offsetTop)/scale};
     183            }
     184            else if (!endFixed)
     185            {
     186                endFixed = true;
     187                end = {"x": (ev.clientX - canvas.offsetLeft)/scale, "z": (ev.clientY - canvas.offsetTop)/scale};
     188            }
     189            else
     190            {
     191                endFixed = false;
     192                start = null;
     193                end = null;
     194            }
     195            draw();
     196        }
     197       
     198        function onCanvasMouseMove(ev)
     199        {
     200            if (start !== null && !endFixed)
     201                end = {"x": (ev.clientX - canvas.offsetLeft)/scale, "z": (ev.clientY - canvas.offsetTop)/scale};
     202            draw();
     203        }
     204       
     205        canvas.addEventListener("mousedown", onCanvasMouseDown, false);
     206        canvas.addEventListener("mousemove", onCanvasMouseMove, false);
     207       
     208        function WallPlaceSegmentRec(totalDist, distSoFar, segments, candidateSegments, bufferLen, depth)
     209        {
     210            //var prefix = "[" + depth + "] ";
     211            //for (var d = 0; d < depth; d++)
     212                //prefix += "   ";
     213           
     214            //console.log("--------------------------");
     215            //console.log("%sdistSoFar = %f/%f", prefix, distSoFar, totalDist);
     216            //console.log("%ssegments (%d) = %s", prefix, segments.length, uneval(segments));
     217           
     218            // in turn, try placing L, M and S wall segments; if continuing recursively returns an impossible configuration, try the next
     219            for each (var candSegment in candidateSegments)
     220            {
     221                //console.log("%sTrying to place segment %s of length %d", prefix, candSegment.template, candSegment.len);
     222                segments.push(candSegment);
     223               
     224                var newDistSoFar = distSoFar + candSegment.len;
     225                var r = totalDist - newDistSoFar; // remaining distance
     226               
     227                if (newDistSoFar > totalDist)
     228                {
     229                    // won't fit, try next one
     230                    //console.log("%sDistance so far (%d) exceeds target (%d), trying next piece", prefix, newDistSoFar, totalDist);
     231                    segments.pop();
     232                    continue;
     233                }
     234                else if (r > (segments.length - 1) * bufferLen)
     235                {
     236                    // current placement doesn't exceed the total distance, but we still have some distance to go before we hit
     237                    // (d - newDistSoFar) <= (segments.len - 1 ) * bufferLen), continue building recursively
     238                    // if none of the recursively evaluated placements succeed, then try the next one
     239                    //console.log("%sr > (#segments - 1) * bufferLen", prefix);
     240                    //console.log("%s%f > %d * %f", prefix, r, segments.length - 1, bufferLen);
     241                    //console.log("%sContinue checking recursively", prefix);
     242                   
     243                    var recursiveResult = WallPlaceSegmentRec(totalDist, newDistSoFar, segments, candidateSegments, bufferLen, depth + 1);
     244                    if (!recursiveResult)
     245                    {
     246                        //console.log("%sNo solution found in recursive tests, trying next piece (segments = %s)", prefix, uneval(segments));
     247                        segments.pop();
     248                        continue;
     249                    }
     250                    else
     251                    {
     252                        //console.log("%sFound solution in recursive search, returning", prefix);
     253                        return recursiveResult;
     254                    }
     255                }
     256                else
     257                {
     258                    // found a placement
     259                    //console.log("%sFound a placement!", prefix);
     260                    return {"segments": segments, "r": r};
     261                }
     262            }
     263           
     264            // no placement possible :(
     265            return false;
     266        }
     267       
     268        //var placement = WallPlaceSegmentRec(len, 0, [], candidateSegments, towerLen, 0);
     269        //console.log(placement);
     270        </script>
     271    </body>
     272</html>
     273 No newline at end of file
  • new file wall_placement_2.html

    diff --git a/wall_placement_2.html b/wall_placement_2.html
    new file mode 100644
    index 0000000..8c86e0b
    - +  
     1<doctype html>
     2<html>
     3    <head>
     4        <title>Wall Placement Demo</title>
     5    </head>
     6    <body>
     7        <canvas id="canvas" width="1600" height="1000" style="border:1px solid black;"></canvas>
     8        <script type="text/javascript">
     9       
     10        var canvas = document.getElementById("canvas");
     11        var ctx = canvas.getContext("2d");
     12        ctx.font = "1.5pt Arial";
     13       
     14        var start = null;
     15        var end = null;
     16        var endFixed = false;
     17       
     18        var scale = 5;
     19        var pointMarkerRadius = 0.5;
     20        var pointMarkerColor = "#000";
     21       
     22        var candidateSegments = [
     23            {"template": "L",  "len": 34.0},
     24            {"template": "M",  "len": 23.0},
     25            {"template": "S",  "len": 10.0},
     26        ];
     27        var len = 31.3;
     28        var towerLen = 8.5;
     29        var wallThickness = 2; // for drawing purposes only
     30       
     31        function fillCircle(x, y, r, s)
     32        {
     33            ctx.beginPath();
     34            ctx.arc(x, y, r, 0, 2 * Math.PI, false);
     35            ctx.fillStyle = s;
     36            ctx.fill();
     37        }
     38       
     39        function drawRect(x, y, w, h, a)
     40        {
     41            ctx.save();
     42            ctx.translate(x, y);
     43            ctx.rotate(a);
     44            ctx.beginPath();
     45            ctx.rect(-w/2, -h/2, w, h);
     46            ctx.stroke();
     47            ctx.restore();
     48        }
     49       
     50        function drawText(x, y, a, txt)
     51        {
     52            ctx.save();
     53            ctx.translate(x, y);
     54            ctx.rotate(a);
     55            ctx.textAlign = "center";
     56            ctx.fillText(txt, 0, 0.7);
     57            ctx.restore();
     58        }
     59       
     60        function drawTower(x, y, a, ss)
     61        {
     62            ctx.strokeStyle = 'gray';
     63            if (ss)
     64                ctx.strokeStyle = ss;
     65           
     66            drawRect(x, y, towerLen, towerLen, a);
     67        }
     68       
     69        function drawWall(x, y, l, a, type, ss)
     70        {
     71            ctx.strokeStyle = 'black';
     72            if (ss)
     73                ctx.strokeStyle = ss;
     74           
     75            drawRect(x, y, l, wallThickness, a);
     76            drawText(x, y, a, type);
     77        }
     78       
     79        function drawStats(stats)
     80        {
     81            ctx.save();
     82            ctx.font = "3pt Courier New";
     83            ctx.textAlign = "left";
     84            //ctx.fillText("r = " + stats.r, 10, 10);
     85           
     86            var num = 1;
     87            for (var each in stats)
     88                ctx.fillText(each + ":  " + stats[each], 5, 2+5*(num++));
     89           
     90            ctx.restore();
     91        }
     92       
     93        function draw()
     94        {
     95            ctx.save();
     96            ctx.translate(0.5, 0.5); // finer lines
     97            ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
     98            ctx.scale(scale, scale);
     99            ctx.lineWidth = 1/scale;
     100           
     101            var pointMarkerStyle = "#000";
     102            var placement = null;
     103           
     104            if (start && end)
     105            {
     106                var dir = {"x": end.x - start.x, "z": end.z - start.z};
     107                var len = Math.sqrt(dir.x * dir.x + dir.z * dir.z);
     108                var dirNormalized = {"x": dir.x / len, "z": dir.z / len};
     109               
     110                // require a minimal distance of 'towerLen'
     111                if(len < towerLen)
     112                {
     113                    end = {"x": start.x + towerLen * dirNormalized.x, "z": start.z + towerLen * dirNormalized.z};
     114                    len = towerLen;
     115                }
     116               
     117                var angle = Math.atan2(dir.z, dir.x);    // angle of this wall segment (relative to world-space X/Z axes)
     118               
     119               
     120                placement = WallPlaceSegmentRec(len, 0, 0.9, 0.1, [], candidateSegments, towerLen, 0);
     121                if (placement)
     122                {
     123                    var placedEntities = placement.segments; // list of chosen candidate segments
     124                    var r = placement.r; // remaining distance to target without towers (must be <= (N) * towerWidth)
     125                    //var s = r / (placedEntities.length - 1); // spacing
     126                    var s = r / (2 * placedEntities.length);
     127                   
     128                    var progress = 0;
     129                    var remainingR = r;
     130                    for (var i = 0; i < placedEntities.length; i++)
     131                    {
     132                        var placedEntity = placedEntities[i];
     133                        var targetX = start.x + (progress + s + placedEntity.len/2) * dirNormalized.x;
     134                        var targetZ = start.z + (progress + s + placedEntity.len/2) * dirNormalized.z;
     135                       
     136                        /*result.push({
     137                            "template": placedEntity.template,
     138                            "pos": {"x": targetX, "z": targetZ},
     139                            "angle": angle,
     140                        });*/
     141                        drawWall(targetX, targetZ, placedEntity.len, angle, placedEntity.template);
     142                       
     143                        if (i < placedEntities.length - 1)
     144                        {
     145                            var towerX = start.x + (progress + s + placedEntity.len + s) * dirNormalized.x;
     146                            var towerZ = start.z + (progress + s + placedEntity.len + s) * dirNormalized.z;
     147                           
     148                            /*result.push({
     149                                "template": wallSet.tower,
     150                                "pos": {"x": towerX, "z": towerZ},
     151                                "angle": angle,
     152                            });*/
     153                            drawTower(towerX, towerZ, angle);
     154                        }
     155                       
     156                        progress += placedEntity.len + 2*s;
     157                    }
     158                }
     159                else
     160                {
     161                    pointMarkerStyle = "#c00";
     162                }
     163            }
     164           
     165            if (start)
     166            {
     167                fillCircle(start.x, start.z, pointMarkerRadius, placement ? "#000" : "#c00");
     168                drawTower(start.x, start.z, angle, placement ? "gray" : "#c00");
     169            }
     170            if (end)
     171            {
     172                fillCircle(end.x, end.z, pointMarkerRadius, placement ? "#000" : "#c00");
     173                drawTower(end.x, end.z, angle, placement ? "gray" : "#c00");
     174            }
     175           
     176            var stats = {"r ": "", "N ": "", "Nt": ""};
     177            if (placement)
     178            {
     179                stats["r "] = Math.round(placement.r*100)/100;
     180                stats["N "] = placement.segments.length;
     181                stats["Nt"] = stats["N "] * towerLen;
     182            }
     183            drawStats(stats);
     184           
     185            ctx.restore();
     186        }
     187       
     188        function onCanvasMouseDown(ev)
     189        {
     190            if (start === null)
     191            {
     192                start = {"x": (ev.clientX - canvas.offsetLeft)/scale, "z": (ev.clientY - canvas.offsetTop)/scale};
     193            }
     194            else if (!endFixed)
     195            {
     196                endFixed = true;
     197                end = {"x": (ev.clientX - canvas.offsetLeft)/scale, "z": (ev.clientY - canvas.offsetTop)/scale};
     198            }
     199            else
     200            {
     201                endFixed = false;
     202                start = null;
     203                end = null;
     204            }
     205            draw();
     206        }
     207       
     208        function onCanvasMouseMove(ev)
     209        {
     210            if (start !== null && !endFixed)
     211                end = {"x": (ev.clientX - canvas.offsetLeft)/scale, "z": (ev.clientY - canvas.offsetTop)/scale};
     212           
     213            draw();
     214        }
     215       
     216        canvas.addEventListener("mousedown", onCanvasMouseDown, false);
     217        canvas.addEventListener("mousemove", onCanvasMouseMove, false);
     218       
     219        function WallPlaceSegmentRec(totalDist, distSoFar, maxOverlap, minOverlap, segments, candidateSegments, bufferLen, depth)
     220        {
     221            //var prefix = "[" + depth + "] ";
     222            //for (var d = 0; d < depth; d++)
     223                //prefix += "   ";
     224           
     225            //console.log("--------------------------");
     226            //console.log("%sdistSoFar = %f/%f", prefix, distSoFar, totalDist);
     227            //console.log("%ssegments (%d) = %s", prefix, segments.length, uneval(segments));
     228           
     229            // in turn, try placing L, M and S wall segments; if continuing recursively returns an impossible configuration, try the next
     230            for (var k in candidateSegments)
     231            {
     232                var candSegment = candidateSegments[k];
     233                //console.log("%sTrying to place segment %s of length %d", prefix, candSegment.template, candSegment.len);
     234                segments.push(candSegment);
     235               
     236                var newDistSoFar = distSoFar + candSegment.len;
     237                var r = totalDist - newDistSoFar; // remaining distance
     238               
     239                if (r < (1 - 2*maxOverlap)*segments.length*bufferLen)
     240                {
     241                    // we placed 'too much' wall, pop it back out and try the next
     242                    //console.log("%sDistance so far (%d) exceeds target (%d), trying next piece", prefix, newDistSoFar, totalDist);
     243                    segments.pop();
     244                    continue;
     245                }
     246                else if (r > (1-2*minOverlap)*segments.length*bufferLen)
     247                {
     248                    // current placement doesn't exceed the total distance, but we still have some distance to go before we hit
     249                    // (d - newDistSoFar) <= (segments.len - 1 ) * bufferLen), continue building recursively
     250                    // if none of the recursively evaluated placements succeed, then try the next one
     251                    //console.log("%sr > (#segments - 1) * bufferLen", prefix);
     252                    //console.log("%s%f > %d * %f", prefix, r, segments.length - 1, bufferLen);
     253                    //console.log("%sContinue checking recursively", prefix);
     254                   
     255                    var recursiveResult = WallPlaceSegmentRec(totalDist, newDistSoFar, maxOverlap, minOverlap, segments, candidateSegments, bufferLen, depth + 1);
     256                    if (!recursiveResult)
     257                    {
     258                        //console.log("%sNo solution found in recursive tests, trying next piece (segments = %s)", prefix, uneval(segments));
     259                        segments.pop();
     260                        continue;
     261                    }
     262                    else
     263                    {
     264                        //console.log("%sFound solution in recursive search, returning", prefix);
     265                        return recursiveResult;
     266                    }
     267                }
     268                else
     269                {
     270                    // found a placement
     271                    //console.log("%sFound a placement!", prefix);
     272                    return {"segments": segments, "r": r};
     273                }
     274            }
     275           
     276            // no placement possible :(
     277            return false;
     278        }
     279       
     280        //var placement = WallPlaceSegmentRec(len, 0, [], candidateSegments, towerLen, 0);
     281        //console.log(placement);
     282        </script>
     283    </body>
     284</html>
     285 No newline at end of file