Index: ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js +++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js @@ -251,6 +251,11 @@ if (cmpUnitAI) cmpUnitAI.SetTurretStance(); + // Remove the unit's obstruction to avoid interfering with pathing. + let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction); + if (cmpObstruction) + cmpObstruction.SetActive(false); + isVisiblyGarrisoned = true; } else @@ -368,6 +373,11 @@ break; } + // Reset the obstruction flags to template defaults. + let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction); + if (cmpObstruction) + cmpObstruction.SetActive(true); + if (cmpEntUnitAI) cmpEntUnitAI.Ungarrison(); Index: ps/trunk/binaries/data/mods/public/simulation/components/Gate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Gate.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Gate.js @@ -15,6 +15,7 @@ Gate.prototype.Init = function() { this.allies = []; + this.ignoreList = []; this.opened = false; this.locked = false; }; @@ -32,10 +33,11 @@ Gate.prototype.OnDiplomacyChanged = function(msg) { - var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() == msg.player) { this.allies = []; + this.ignoreList = []; this.SetupRangeQuery(msg.player); } }; @@ -91,11 +93,36 @@ if (msg.added.length > 0) for (let entity of msg.added) + { + // Ignore entities that cannot move as those won't be able to go through the gate. + let unitAI = Engine.QueryInterface(entity, IID_UnitAI); + if (!unitAI || !unitAI.AbleToMove()) + this.ignoreList.push(entity); this.allies.push(entity); + } if (msg.removed.length > 0) for (let entity of msg.removed) + { + let index = this.ignoreList.indexOf(entity); + if (index !== -1) + this.ignoreList.splice(index, 1); this.allies.splice(this.allies.indexOf(entity), 1); + } + + this.OperateGate(); +}; + +Gate.prototype.OnGlobalUnitAbleToMoveChanged = function(msg) +{ + if (this.allies.indexOf(msg.entity) === -1) + return; + + let index = this.ignoreList.indexOf(msg.entity); + if (msg.ableToMove && index !== -1) + this.ignoreList.splice(index, 1); + else if (!msg.ableToMove && index === -1) + this.ignoreList.push(msg.entity); this.OperateGate(); }; @@ -108,6 +135,11 @@ return +this.template.PassRange; }; +Gate.prototype.ShouldOpen = function() +{ + return this.allies.some(ent => this.ignoreList.indexOf(ent) === -1); +}; + /** * Attempt to open or close the gate. * An ally must be in range to open the gate, but an unlocked gate will only close @@ -122,10 +154,9 @@ cmpTimer.CancelTimer(this.timer); this.timer = undefined; } - - if (this.opened && (this.allies.length == 0 || this.locked)) + if (this.opened && (this.locked || !this.ShouldOpen())) this.CloseGate(); - else if (!this.opened && this.allies.length) + else if (!this.opened && this.ShouldOpen()) this.OpenGate(); }; @@ -215,19 +246,24 @@ */ Gate.prototype.CloseGate = function() { - var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); + let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (!cmpObstruction) return; // The gate can't be closed if there are entities colliding with it. - var collisions = cmpObstruction.GetEntitiesBlockingConstruction(); + // NB: because walls are overlapping, they requires special care to not break + // in particular, walls do not block construction, so walls from skirmish maps + // do not appear in this check even if they have different control groups from the gate. + // This no longer works if gates are made to check for entities blocking movement. + // Fixing that would let us change this code, but it sounds decidedly non-trivial. + let collisions = cmpObstruction.GetEntitiesBlockingConstruction(); if (collisions.length) { if (!this.timer) { // Set an "instant" timer which will run on the next simulation turn. - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.timer = cmpTimer.SetTimeout(this.entity, IID_Gate, "OperateGate", 0, {}); + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + this.timer = cmpTimer.SetTimeout(this.entity, IID_Gate, "OperateGate", 0); } return; } @@ -241,7 +277,7 @@ this.opened = false; PlaySound("gate_closing", this.entity); - var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("gate_closing", true, 1.0); }; Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js @@ -202,7 +202,7 @@ // Called when being told to walk as part of a formation "Order.FormationWalk": function(msg) { // Let players move captured domestic animals around - if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) + if (this.IsAnimal() && !this.IsDomestic() || !this.AbleToMove()) { this.FinishOrder(); return; @@ -262,7 +262,7 @@ "Order.Walk": function(msg) { // Let players move captured domestic animals around - if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) + if (this.IsAnimal() && !this.IsDomestic() || !this.AbleToMove()) { this.FinishOrder(); return; @@ -288,7 +288,7 @@ "Order.WalkAndFight": function(msg) { // Let players move captured domestic animals around - if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) + if (this.IsAnimal() && !this.IsDomestic() || !this.AbleToMove()) { this.FinishOrder(); return; @@ -315,7 +315,7 @@ "Order.WalkToTarget": function(msg) { // Let players move captured domestic animals around - if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) + if (this.IsAnimal() && !this.IsDomestic() || !this.AbleToMove()) { this.FinishOrder(); return; @@ -444,7 +444,7 @@ // If we can't reach the target, but are standing ground, then abandon this attack order. // Unless we're hunting, that's a special case where we should continue attacking our target. - if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting || this.IsTurret()) + if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting || !this.AbleToMove()) { this.FinishOrder(); return; @@ -470,7 +470,7 @@ }, "Order.Patrol": function(msg) { - if (this.IsAnimal() || this.IsTurret()) + if (this.IsAnimal() || !this.AbleToMove()) { this.FinishOrder(); return; @@ -623,7 +623,7 @@ }, "Order.Garrison": function(msg) { - if (this.IsTurret()) + if (!this.AbleToMove()) { this.SetNextState("IDLE"); return; @@ -3042,6 +3042,7 @@ if (cmpGarrisonHolder.Garrison(this.entity)) { this.isGarrisoned = true; + this.SetImmobile(true); if (this.formationController) { @@ -3373,6 +3374,7 @@ this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to this.isGarrisoned = false; this.isIdle = false; + this.isImmobile = false; // True if the unit is currently unable to move (garrisoned,...) this.finishedOrder = false; // used to find if all formation members finished the order this.heldPosition = undefined; @@ -3472,6 +3474,30 @@ return !this.orderQueue.length || this.orderQueue[0].type == "Garrison"; }; +UnitAI.prototype.SetImmobile = function(immobile) +{ + this.isImmobile = immobile; + Engine.PostMessage(this.entity, MT_UnitAbleToMoveChanged, { + "entity": this.entity, + "ableToMove": this.AbleToMove() + }); +}; + +/** + * @param cmpUnitMotion - optionally pass unitMotion to avoid querying it here + * @returns true if the entity can move, i.e. has UnitMotion and isn't immobile. + */ +UnitAI.prototype.AbleToMove = function(cmpUnitMotion) +{ + if (this.isImmobile || this.IsTurret()) + return false; + + if (!cmpUnitMotion) + cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + + return !!cmpUnitMotion; +}; + UnitAI.prototype.IsFleeing = function() { var state = this.GetCurrentState().split(".").pop(); @@ -3908,9 +3934,8 @@ // If foundation is not ally of entity, or if entity is unpacked siege, // ignore the order. if (!IsOwnedByAllyOfEntity(this.entity, target) && - !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() || - checkPacking && this.IsPacking() || - this.CanPack() || this.IsTurret()) + !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() || + checkPacking && this.IsPacking() || this.CanPack() || !this.AbleToMove()) return false; // Move a tile outside the building. @@ -4492,13 +4517,13 @@ UnitAI.prototype.MoveToPoint = function(x, z) { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion && cmpUnitMotion.MoveToPointRange(x, z, 0, 0); // For point goals, allow a max range of 0. + return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToPointRange(x, z, 0, 0); // For point goals, allow a max range of 0. }; UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion && cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); + return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); }; UnitAI.prototype.MoveToTarget = function(target) @@ -4507,12 +4532,12 @@ return false; let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, 0, 1); + return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, 0, 1); }; UnitAI.prototype.MoveToTargetRange = function(target, iid, type) { - if (!this.CheckTargetVisible(target) || this.IsTurret()) + if (!this.CheckTargetVisible(target)) return false; let range = this.GetRange(iid, type); @@ -4520,7 +4545,7 @@ return false; let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); + return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; /** @@ -4538,6 +4563,10 @@ return false; } + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + if (!this.AbleToMove(cmpUnitMotion)) + return false; + let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) target = cmpFormation.GetClosestMember(this.entity); @@ -4574,7 +4603,6 @@ // The parabole changes while walking so be cautious: let guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange; - let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange); }; @@ -4584,7 +4612,7 @@ return false; let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, min, max); + return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, min, max); }; /** @@ -4599,7 +4627,7 @@ if (cmpTargetFormation) target = cmpTargetFormation.GetClosestMember(this.entity); - if (!this.CheckTargetVisible(target) || this.IsTurret()) + if (!this.CheckTargetVisible(target)) return false; let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack); @@ -4608,7 +4636,7 @@ let range = cmpFormationAttack.GetRange(target); let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); + return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; UnitAI.prototype.MoveToGarrisonRange = function(target) @@ -4622,7 +4650,7 @@ var range = cmpGarrisonHolder.GetLoadingRange(); let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); + return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; /** @@ -5007,7 +5035,7 @@ */ UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force) { - if (this.IsTurret()) + if (!this.AbleToMove()) return false; if (this.GetStance().respondChase) @@ -5333,11 +5361,11 @@ { // If we're already being told to leave a foundation, then // ignore this new request so we don't end up being too indecisive - // to ever actually move anywhere - // Ignore also the request if we are packing + // to ever actually move anywhere. if (this.order && (this.order.type == "LeaveFoundation" || (this.order.type == "Flee" && this.order.data.target == target))) return; + // Ignore also the request if we are packing. if (this.orderQueue.length && this.orderQueue[0].type == "Unpack" && this.WillMoveFromFoundation(target, false)) { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); @@ -5399,7 +5427,10 @@ UnitAI.prototype.Ungarrison = function() { if (this.IsGarrisoned()) + { + this.SetImmobile(false); this.AddOrder("Ungarrison", null, false); + } }; /** Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/UnitAI.js @@ -7,6 +7,13 @@ Engine.RegisterMessageType("UnitIdleChanged"); /** + * Message of the form { "ableToMove": boolean } + * sent from UnitAI whenever the unit's ability to move changes. + */ +Engine.RegisterMessageType("UnitAbleToMoveChanged"); + + +/** * Message of the form { "to": string } * where "to" value is a UnitAI stance, * sent from UnitAI whenever the unit's stance changes. Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/setup.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/setup.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/setup.js @@ -74,6 +74,7 @@ if (!g_Components[ent]) g_Components[ent] = {}; g_Components[ent][iid] = mock; + return g_Components[ent][iid]; }; global.DeleteMock = function(ent, iid) @@ -110,3 +111,27 @@ return cmp; }; + +/** + * A simple Spy proxy that tracks and forward function calls. + * NB: this immediately replaces obj's func. + */ +global.Spy = function(obj, func) +{ + this._called = 0; + this._callargs = []; + let og_func = obj[func]; + let spy = (...args) => { + ++this._called; + this._callargs.push(args); + return og_func.apply(obj, args); + }; + obj[func] = spy; + + this._reset = () => { + this._called = 0; + this._callargs = []; + }; + + return this; +}; Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Gate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Gate.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Gate.js @@ -0,0 +1,101 @@ +Engine.LoadComponentScript("interfaces/Gate.js"); +Engine.LoadComponentScript("interfaces/UnitAI.js"); +Engine.LoadComponentScript("Gate.js"); + +function testBasicBehaviour() +{ + const gate = 10; + const own = 11; + const passRange = 20; + + Engine.RegisterGlobal("QueryPlayerIDInterface", () => ({ + "GetAllies": () => [1, 2], + })); + Engine.RegisterGlobal("PlaySound", () => {}); + + let cmpRangeMgr = AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "GetEntityFlagMask": () => {}, + "CreateActiveQuery": () => {}, + "EnableActiveQuery": () => {}, + }); + let querySpy = new Spy(cmpRangeMgr, "CreateActiveQuery"); + + let ownUnitAI = AddMock(own, IID_UnitAI, { + "AbleToMove": () => true + }); + + let cmpGate = ConstructComponent(gate, "Gate", { + "PassRange": passRange + }); + let setupSpy = new Spy(cmpGate, "SetupRangeQuery"); + let cmpGateObst = AddMock(gate, IID_Obstruction, { + "SetDisableBlockMovementPathfinding": () => {}, + "GetEntitiesBlockingConstruction": () => [], + "GetBlockMovementFlag": () => false, + }); + AddMock(gate, IID_Ownership, { + "GetOwner": () => 1, + }); + + // Test that gates are closed at startup. + TS_ASSERT_EQUALS(cmpGate.locked, false); + cmpGate.OnOwnershipChanged({ "from": INVALID_PLAYER, "to": 1 }); + TS_ASSERT_EQUALS(setupSpy._called, 1); + TS_ASSERT_EQUALS(querySpy._callargs[0][2], passRange); + TS_ASSERT_UNEVAL_EQUALS(querySpy._callargs[0][3], [1, 2]); + TS_ASSERT_EQUALS(cmpGate.opened, false); + + // Test that they open if units get in range + cmpGate.OnRangeUpdate({ "tag": cmpGate.unitsQuery, "added": [own], "removed": [] }); + TS_ASSERT_EQUALS(cmpGate.opened, true); + TS_ASSERT_UNEVAL_EQUALS(cmpGate.allies, [own]); + TS_ASSERT_UNEVAL_EQUALS(cmpGate.ignoreList, []); + + // Assert that it closes if the unit says it can't move anymore. + cmpGate.OnGlobalUnitAbleToMoveChanged({ "entity": own }); + TS_ASSERT_EQUALS(cmpGate.opened, false); + TS_ASSERT_UNEVAL_EQUALS(cmpGate.ignoreList, [own]); + + // Assert that it is OK if the entity goes away + cmpGate.OnRangeUpdate({ "tag": cmpGate.unitsQuery, "added": [], "removed": [own] }); + TS_ASSERT_EQUALS(cmpGate.opened, false); + TS_ASSERT_UNEVAL_EQUALS(cmpGate.allies, []); + TS_ASSERT_UNEVAL_EQUALS(cmpGate.ignoreList, []); + + // Lock the gates, try again. + cmpGate.LockGate(); + TS_ASSERT(cmpGate.IsLocked()); + cmpGate.OnRangeUpdate({ "tag": cmpGate.unitsQuery, "added": [own], "removed": [] }); + TS_ASSERT_EQUALS(cmpGate.opened, false); + TS_ASSERT_UNEVAL_EQUALS(cmpGate.allies, [own]); + + cmpGate.UnlockGate(); + TS_ASSERT_EQUALS(cmpGate.opened, true); + cmpGate.LockGate(); + TS_ASSERT_EQUALS(cmpGate.opened, false); + + // Finally, trigger some other handlers to see if things remain correct. + setupSpy._reset(); + cmpGate.OnOwnershipChanged({ "from": 1, "to": 2 }); + TS_ASSERT_EQUALS(setupSpy._called, 1); + cmpGate.OnDiplomacyChanged({ "player": 1 }); + TS_ASSERT_EQUALS(setupSpy._called, 2); +} + +function testShouldOpen() +{ + let cmpGate = ConstructComponent(5, "Gate", {}); + cmpGate.allies = [1, 2, 3, 4]; + cmpGate.ignoreList = []; + TS_ASSERT_EQUALS(cmpGate.ShouldOpen(), true); + cmpGate.ignoreList = [2, 3]; + TS_ASSERT_EQUALS(cmpGate.ShouldOpen(), true); + cmpGate.ignoreList = [1, 2, 3, 4]; + TS_ASSERT_EQUALS(cmpGate.ShouldOpen(), false); + cmpGate.allies = []; + cmpGate.ignoreList = []; + TS_ASSERT_EQUALS(cmpGate.ShouldOpen(), false); +} + +testBasicBehaviour(); +testShouldOpen(); Index: ps/trunk/source/simulation2/components/CCmpObstruction.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpObstruction.cpp +++ ps/trunk/source/simulation2/components/CCmpObstruction.cpp @@ -651,6 +651,11 @@ return ret; } + virtual std::vector GetEntitiesBlockingMovement() const + { + return GetEntitiesByFlags(ICmpObstructionManager::FLAG_BLOCK_MOVEMENT); + } + virtual std::vector GetEntitiesBlockingConstruction() const { return GetEntitiesByFlags(ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION); Index: ps/trunk/source/simulation2/components/ICmpObstruction.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpObstruction.h +++ ps/trunk/source/simulation2/components/ICmpObstruction.h @@ -105,6 +105,12 @@ virtual std::vector GetEntitiesByFlags(ICmpObstructionManager::flags_t flags) const = 0; /** + * Returns a list of entities that are blocking movement. + * @return vector of blocking entities + */ + virtual std::vector GetEntitiesBlockingMovement() const = 0; + + /** * Returns a list of entities that are blocking construction of a foundation. * @return vector of blocking entities */ Index: ps/trunk/source/simulation2/components/ICmpObstruction.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpObstruction.cpp +++ ps/trunk/source/simulation2/components/ICmpObstruction.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -50,6 +50,7 @@ DEFINE_INTERFACE_METHOD_CONST_0("CheckShorePlacement", bool, ICmpObstruction, CheckShorePlacement) DEFINE_INTERFACE_METHOD_CONST_2("CheckFoundation", std::string, ICmpObstruction, CheckFoundation_wrapper, std::string, bool) DEFINE_INTERFACE_METHOD_CONST_0("CheckDuplicateFoundation", bool, ICmpObstruction, CheckDuplicateFoundation) +DEFINE_INTERFACE_METHOD_CONST_0("GetEntitiesBlockingMovement", std::vector, ICmpObstruction, GetEntitiesBlockingMovement) DEFINE_INTERFACE_METHOD_CONST_0("GetEntitiesBlockingConstruction", std::vector, ICmpObstruction, GetEntitiesBlockingConstruction) DEFINE_INTERFACE_METHOD_CONST_0("GetEntitiesDeletedUponConstruction", std::vector, ICmpObstruction, GetEntitiesDeletedUponConstruction) DEFINE_INTERFACE_METHOD_1("SetActive", void, ICmpObstruction, SetActive, bool) Index: ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h +++ ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h @@ -41,6 +41,7 @@ virtual std::string CheckFoundation_wrapper(const std::string& UNUSED(className), bool UNUSED(onlyCenterPoint)) const { return std::string(); } virtual bool CheckDuplicateFoundation() const { return true; } virtual std::vector GetEntitiesByFlags(ICmpObstructionManager::flags_t UNUSED(flags)) const { return std::vector(); } + virtual std::vector GetEntitiesBlockingMovement() const { return std::vector(); } virtual std::vector GetEntitiesBlockingConstruction() const { return std::vector(); } virtual std::vector GetEntitiesDeletedUponConstruction() const { return std::vector(); } virtual void ResolveFoundationCollisions() const { }