Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js @@ -339,12 +339,34 @@ for (let target of this.targetUnits) addTarget(target); + // The obstruction manager performs approximate range checks + // so we need to verify them here. + // TODO: perhaps an optional 'precise' mode to range queries would be more performant. + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + let range = cmpAttack.GetRange(attackType); + + let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!thisCmpPosition.IsInWorld()) + return; + let s = thisCmpPosition.GetPosition(); + for (let i = 0; i < arrowsToFire; ++i) { let selectedIndex = targets.randomIndex(); let selectedTarget = targets.itemAt(selectedIndex); - if (selectedTarget && this.CheckTargetVisible(selectedTarget)) + // Copied from UnitAI's MoveToTargetAttackRange. + let targetCmpPosition = Engine.QueryInterface(selectedTarget, IID_Position); + if (!targetCmpPosition.IsInWorld()) + continue; + + let t = targetCmpPosition.GetPosition(); + // h is positive when I'm higher than the target + let h = s.y - t.y + range.elevationBonus; + let parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h); + if (selectedTarget && this.CheckTargetVisible(selectedTarget) && + h > -range.max / 2 && cmpObstructionManager.IsInTargetRange( + this.entity, selectedTarget, range.min, parabolicMaxRange, false)) { cmpAttack.PerformAttack(attackType, selectedTarget); PlaySound("attack_" + attackType.toLowerCase(), this.entity); Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml @@ -7,7 +7,7 @@ Monument Monument - 150 + 135 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml @@ -7,7 +7,7 @@ Pillar Pillar - 75 + 70 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml @@ -4,7 +4,7 @@ own neutral MercenaryCamp - 100 + 70 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_gate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_gate.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_gate.xml @@ -11,7 +11,7 @@ 7.0 - 20 + 2 0.5 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml @@ -4,7 +4,7 @@ own neutral MercenaryCamp - 100 + 70 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml @@ -38,7 +38,7 @@ ArmyCamp ArmyCamp - 80 + 45 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml @@ -44,7 +44,7 @@ CivilCentre CivilCentre - 200 + 160 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml @@ -8,7 +8,7 @@ Colony CivilCentre - 120 + 80 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml @@ -7,7 +7,7 @@ own neutral Outpost - 50 + 45 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml @@ -35,7 +35,7 @@ Tower Tower - 60 + 55 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_gate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_gate.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_gate.xml @@ -12,7 +12,7 @@ - 20 + 2 2500 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml @@ -37,7 +37,7 @@ Fortress Fortress - 80 + 55 Index: ps/trunk/source/simulation2/components/CCmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpRangeManager.cpp +++ ps/trunk/source/simulation2/components/CCmpRangeManager.cpp @@ -142,22 +142,24 @@ */ struct Query { - bool enabled; - bool parabolic; + std::vector lastMatch; CEntityHandle source; // TODO: this could crash if an entity is destroyed while a Query is still referencing it entity_pos_t minRange; entity_pos_t maxRange; - entity_pos_t elevationBonus; + entity_pos_t elevationBonus; // Used for parabolas only. u32 ownersMask; i32 interface; - std::vector lastMatch; u8 flagsMask; + bool enabled; + bool parabolic; }; /** * Checks whether v is in a parabolic range of (0,0,0) * The highest point of the paraboloid is (0,range/2,0) * and the circle of distance 'range' around (0,0,0) on height y=0 is part of the paraboloid + * This equates to computing f(x, z) = y = -(xx + zz)/(2*range) + range/2 > 0, + * or alternatively √(xx+zz) <= √(range^2 - 2range*y). * * Avoids sqrting and overflowing. */ @@ -1206,19 +1208,20 @@ continue; CFixedVector3D secondPosition = cmpSecondPosition->GetPosition(); - // Restrict based on precise distance - if (!InParabolicRange( - CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) - - pos3d, - q.maxRange)) + // Doing an exact check for parabolas with obstruction sizes is not really possible. + // However, we can prove that InParabolicRange(d, range + size) > InParabolicRange(d, range) + // in the sense that it always returns true when the latter would, which is enough. + // To do so, compute the derivative with respect to distance, and notice that + // they have an intersection after which the former grows slower, and then use that to prove the above. + // Note that this is only true because we do not account for vertical size here, + // if we did, we would also need to artificially 'raise' the source over the target. + if (!InParabolicRange(CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) - pos3d, + q.maxRange + fixed::FromInt(it->second.size))) continue; if (!q.minRange.IsZero()) - { - int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange); - if (distVsMin < 0) + if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange) < 0) continue; - } r.push_back(it->first); } @@ -1239,17 +1242,13 @@ if (!TestEntityQuery(q, it->first, it->second)) continue; - // Restrict based on precise distance - int distVsMax = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange); - if (distVsMax > 0) + // Restrict based on approximate circle-circle distance. + if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange + fixed::FromInt(it->second.size)) > 0) continue; if (!q.minRange.IsZero()) - { - int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange); - if (distVsMin < 0) + if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange) < 0) continue; - } r.push_back(it->first); } @@ -1377,6 +1376,16 @@ q.maxRange = maxRange; q.elevationBonus = entity_pos_t::Zero(); + if (q.source.GetId() != INVALID_ENTITY && q.maxRange != entity_pos_t::FromInt(-1)) + { + EntityMap::const_iterator it = m_EntityData.find(q.source.GetId()); + ENSURE(it != m_EntityData.end()); + // Adjust the range query based on the querier's obstruction radius. + // The smallest side of the obstruction isn't known here, so we can't safely adjust the min-range, only the max. + // 'size' is the diagonal size rounded up so this will cover all possible rotations of the querier. + q.maxRange += fixed::FromInt(it->second.size); + } + q.ownersMask = 0; for (size_t i = 0; i < owners.size(); ++i) q.ownersMask |= CalcOwnerMask(owners[i]); Index: ps/trunk/source/simulation2/components/ICmpRangeManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpRangeManager.h +++ ps/trunk/source/simulation2/components/ICmpRangeManager.h @@ -73,7 +73,7 @@ * * In most cases the users are event-based and want notifications when something * has entered or left the range, and the query can be set up once and rarely changed. - * These queries have to be fast. It's fine to approximate an entity as a point. + * These queries have to be fast. Entities are approximated as circles. * * Current design: * Index: ps/trunk/source/simulation2/components/tests/test_RangeManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_RangeManager.h +++ ps/trunk/source/simulation2/components/tests/test_RangeManager.h @@ -17,13 +17,14 @@ #include "simulation2/system/ComponentTest.h" #include "simulation2/components/ICmpRangeManager.h" +#include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpVision.h" #include #include -class MockVision : public ICmpVision +class MockVisionRgm : public ICmpVision { public: DEFAULT_MOCK_COMPONENT() @@ -32,7 +33,7 @@ virtual bool GetRevealShore() const { return false; } }; -class MockPosition : public ICmpPosition +class MockPositionRgm : public ICmpPosition { public: DEFAULT_MOCK_COMPONENT() @@ -56,8 +57,8 @@ virtual void SetFloating(bool UNUSED(flag)) { } virtual void SetActorFloating(bool UNUSED(flag)) { } virtual void SetConstructionProgress(fixed UNUSED(progress)) { } - virtual CFixedVector3D GetPosition() const { return CFixedVector3D(); } - virtual CFixedVector2D GetPosition2D() const { return CFixedVector2D(); } + virtual CFixedVector3D GetPosition() const { return m_Pos; } + virtual CFixedVector2D GetPosition2D() const { return CFixedVector2D(m_Pos.X, m_Pos.Z); } virtual CFixedVector3D GetPreviousPosition() const { return CFixedVector3D(); } virtual CFixedVector2D GetPreviousPosition2D() const { return CFixedVector2D(); } virtual void TurnTo(entity_angle_t UNUSED(y)) { } @@ -67,6 +68,45 @@ virtual fixed GetDistanceTravelled() const { return fixed::Zero(); } virtual void GetInterpolatedPosition2D(float UNUSED(frameOffset), float& x, float& z, float& rotY) const { x = z = rotY = 0; } virtual CMatrix3D GetInterpolatedTransform(float UNUSED(frameOffset)) const { return CMatrix3D(); } + + CFixedVector3D m_Pos; +}; + +class MockObstructionRgm : public ICmpObstruction +{ +public: + DEFAULT_MOCK_COMPONENT(); + + MockObstructionRgm(entity_pos_t s) : m_Size(s) {}; + + virtual ICmpObstructionManager::tag_t GetObstruction() const { return {}; }; + virtual bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const { return false; }; + virtual bool GetPreviousObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const { return false; }; + virtual entity_pos_t GetSize() const { return m_Size; }; + virtual CFixedVector2D GetStaticSize() const { return {}; }; + virtual EObstructionType GetObstructionType() const { return {}; }; + virtual void SetUnitClearance(const entity_pos_t&) {}; + virtual bool IsControlPersistent() const { return {}; }; + virtual bool CheckShorePlacement() const { return {}; }; + virtual EFoundationCheck CheckFoundation(const std::string&) const { return {}; }; + virtual EFoundationCheck CheckFoundation(const std::string& , bool) const { return {}; }; + virtual std::string CheckFoundation_wrapper(const std::string&, bool) const { return {}; }; + virtual bool CheckDuplicateFoundation() const { return {}; }; + virtual std::vector GetEntitiesByFlags(ICmpObstructionManager::flags_t) const { return {}; }; + virtual std::vector GetEntitiesBlockingMovement() const { return {}; }; + virtual std::vector GetEntitiesBlockingConstruction() const { return {}; }; + virtual std::vector GetEntitiesDeletedUponConstruction() const { return {}; }; + virtual void ResolveFoundationCollisions() const {}; + virtual void SetActive(bool) {}; + virtual void SetMovingFlag(bool) {}; + virtual void SetDisableBlockMovementPathfinding(bool, bool, int32_t) {}; + virtual bool GetBlockMovementFlag() const { return {}; }; + virtual void SetControlGroup(entity_id_t) {}; + virtual entity_id_t GetControlGroup() const { return {}; }; + virtual void SetControlGroup2(entity_id_t) {}; + virtual entity_id_t GetControlGroup2() const { return {}; }; +private: + entity_pos_t m_Size; }; class TestCmpRangeManager : public CxxTest::TestSuite @@ -91,10 +131,10 @@ ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); - MockVision vision; + MockVisionRgm vision; test.AddMock(100, IID_Vision, vision); - MockPosition position; + MockPositionRgm position; test.AddMock(100, IID_Position, position); // This tests that the incremental computation produces the correct result @@ -153,4 +193,76 @@ } } } + + void test_queries() + { + ComponentTestHelper test(g_ScriptContext); + + ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); + + MockVisionRgm vision, vision2; + MockPositionRgm position, position2; + MockObstructionRgm obs(fixed::FromInt(2)), obs2(fixed::Zero()); + test.AddMock(100, IID_Vision, vision); + test.AddMock(100, IID_Position, position); + test.AddMock(100, IID_Obstruction, obs); + + test.AddMock(101, IID_Vision, vision2); + test.AddMock(101, IID_Position, position2); + test.AddMock(101, IID_Obstruction, obs2); + + cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512), 512/TERRAIN_TILE_SIZE + 1); + cmp->Verify(); + { CMessageCreate msg(100); cmp->HandleMessage(msg, false); } + { CMessageCreate msg(101); cmp->HandleMessage(msg, false); } + + { CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); } + { CMessageOwnershipChanged msg(101, -1, 1); cmp->HandleMessage(msg, false); } + + auto move = [&cmp](entity_id_t ent, MockPositionRgm& pos, fixed x, fixed z) { + pos.m_Pos = CFixedVector3D(x, fixed::Zero(), z); + { CMessagePositionChanged msg(ent, true, x, z, entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } + }; + + move(100, position, fixed::FromInt(10), fixed::FromInt(10)); + move(101, position2, fixed::FromInt(10), fixed::FromInt(20)); + + std::vector nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{}); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + + move(101, position2, fixed::FromInt(10), fixed::FromInt(10)); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{}); + + move(101, position2, fixed::FromInt(10), fixed::FromInt(13)); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{}); + + move(101, position2, fixed::FromInt(10), fixed::FromInt(15)); + // In range thanks to self obstruction size. + nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + // In range thanks to target obstruction size. + nearby = cmp->ExecuteQuery(101, fixed::FromInt(0), fixed::FromInt(4), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{100}); + + // Trickier: min-range is closest-to-closest, but rotation may change the real distance. + nearby = cmp->ExecuteQuery(100, fixed::FromInt(2), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(5), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(6), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{}); + nearby = cmp->ExecuteQuery(101, fixed::FromInt(5), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{100}); + nearby = cmp->ExecuteQuery(101, fixed::FromInt(6), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{}); + + } };