/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "maths/Matrix3D.h" #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 MockVisionRgm : public ICmpVision { public: DEFAULT_MOCK_COMPONENT() entity_pos_t GetRange() const override { return entity_pos_t::FromInt(66); } bool GetRevealShore() const override { return false; } }; class MockPositionRgm : public ICmpPosition { public: DEFAULT_MOCK_COMPONENT() void SetTurretParent(entity_id_t UNUSED(id), const CFixedVector3D& UNUSED(pos)) override {} entity_id_t GetTurretParent() const override {return INVALID_ENTITY;} void UpdateTurretPosition() override {} std::set* GetTurrets() override { return nullptr; } bool IsInWorld() const override { return true; } void MoveOutOfWorld() override { } void MoveTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) override { } void MoveAndTurnTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z), entity_angle_t UNUSED(a)) override { } void JumpTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) override { } void SetHeightOffset(entity_pos_t UNUSED(dy)) override { } entity_pos_t GetHeightOffset() const override { return entity_pos_t::Zero(); } void SetHeightFixed(entity_pos_t UNUSED(y)) override { } entity_pos_t GetHeightFixed() const override { return entity_pos_t::Zero(); } entity_pos_t GetHeightAtFixed(entity_pos_t, entity_pos_t) const override { return entity_pos_t::Zero(); } bool IsHeightRelative() const override { return true; } void SetHeightRelative(bool UNUSED(relative)) override { } bool CanFloat() const override { return false; } void SetFloating(bool UNUSED(flag)) override { } void SetActorFloating(bool UNUSED(flag)) override { } void SetConstructionProgress(fixed UNUSED(progress)) override { } CFixedVector3D GetPosition() const override { return m_Pos; } CFixedVector2D GetPosition2D() const override { return CFixedVector2D(m_Pos.X, m_Pos.Z); } CFixedVector3D GetPreviousPosition() const override { return CFixedVector3D(); } CFixedVector2D GetPreviousPosition2D() const override { return CFixedVector2D(); } fixed GetTurnRate() const override { return fixed::Zero(); } void TurnTo(entity_angle_t UNUSED(y)) override { } void SetYRotation(entity_angle_t UNUSED(y)) override { } void SetXZRotation(entity_angle_t UNUSED(x), entity_angle_t UNUSED(z)) override { } CFixedVector3D GetRotation() const override { return CFixedVector3D(); } fixed GetDistanceTravelled() const override { return fixed::Zero(); } void GetInterpolatedPosition2D(float UNUSED(frameOffset), float& x, float& z, float& rotY) const override { x = z = rotY = 0; } CMatrix3D GetInterpolatedTransform(float UNUSED(frameOffset)) const override { return CMatrix3D(); } CFixedVector3D m_Pos; }; class MockObstructionRgm : public ICmpObstruction { public: DEFAULT_MOCK_COMPONENT(); MockObstructionRgm(entity_pos_t s) : m_Size(s) {}; ICmpObstructionManager::tag_t GetObstruction() const override { return {}; }; bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const override { return false; }; bool GetPreviousObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const override { return false; }; entity_pos_t GetSize() const override { return m_Size; }; CFixedVector2D GetStaticSize() const override { return {}; }; EObstructionType GetObstructionType() const override { return {}; }; void SetUnitClearance(const entity_pos_t&) override {}; bool IsControlPersistent() const override { return {}; }; bool CheckShorePlacement() const override { return {}; }; EFoundationCheck CheckFoundation(const std::string&) const override { return {}; }; EFoundationCheck CheckFoundation(const std::string& , bool) const override { return {}; }; std::string CheckFoundation_wrapper(const std::string&, bool) const override { return {}; }; bool CheckDuplicateFoundation() const override { return {}; }; std::vector GetEntitiesByFlags(ICmpObstructionManager::flags_t) const override { return {}; }; std::vector GetEntitiesBlockingMovement() const override { return {}; }; std::vector GetEntitiesBlockingConstruction() const override { return {}; }; std::vector GetEntitiesDeletedUponConstruction() const override { return {}; }; void ResolveFoundationCollisions() const override {}; void SetActive(bool) override {}; void SetMovingFlag(bool) override {}; void SetDisableBlockMovementPathfinding(bool, bool, int32_t) override {}; bool GetBlockMovementFlag(bool) const override { return {}; }; void SetControlGroup(entity_id_t) override {}; entity_id_t GetControlGroup() const override { return {}; }; void SetControlGroup2(entity_id_t) override {}; entity_id_t GetControlGroup2() const override { return {}; }; private: entity_pos_t m_Size; }; class TestCmpRangeManager : public CxxTest::TestSuite { public: void setUp() { CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); } // TODO It would be nice to call Verify() with the shore revealing system // but that means testing on an actual map, with water and land. void test_basic() { ComponentTestHelper test(*g_ScriptContext); ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); MockVisionRgm vision; test.AddMock(100, IID_Vision, vision); MockPositionRgm position; test.AddMock(100, IID_Position, position); // This tests that the incremental computation produces the correct result // in various edge cases cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512)); cmp->Verify(); { CMessageCreate msg(100); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromDouble(257.95), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromInt(253), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(383), entity_pos_t::FromInt(84), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(348), entity_pos_t::FromInt(83), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); boost::mt19937 rng; for (size_t i = 0; i < 1024; ++i) { double x = boost::random::uniform_real_distribution(0.0, 512.0)(rng); double z = boost::random::uniform_real_distribution(0.0, 512.0)(rng); { CMessagePositionChanged msg(100, true, entity_pos_t::FromDouble(x), entity_pos_t::FromDouble(z), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); } // Test OwnershipChange, GetEntitiesByPlayer, GetNonGaiaEntities { player_id_t previousOwner = -1; for (player_id_t newOwner = 0; newOwner < 8; ++newOwner) { CMessageOwnershipChanged msg(100, previousOwner, newOwner); cmp->HandleMessage(msg, false); for (player_id_t i = 0; i < 8; ++i) TS_ASSERT_EQUALS(cmp->GetEntitiesByPlayer(i).size(), i == newOwner ? 1 : 0); TS_ASSERT_EQUALS(cmp->GetNonGaiaEntities().size(), newOwner > 0 ? 1 : 0); previousOwner = newOwner; } } } 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)); 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, true); TS_ASSERT_EQUALS(nearby, std::vector{}); nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true); 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, true); TS_ASSERT_EQUALS(nearby, std::vector{101}); nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true); 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, true); TS_ASSERT_EQUALS(nearby, std::vector{101}); nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true); 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, true); 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, true); 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, true); TS_ASSERT_EQUALS(nearby, std::vector{101}); nearby = cmp->ExecuteQuery(100, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{101}); nearby = cmp->ExecuteQuery(100, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{}); nearby = cmp->ExecuteQuery(101, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{100}); nearby = cmp->ExecuteQuery(101, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{}); } void test_IsInTargetParabolicRange() { ComponentTestHelper test(*g_ScriptContext); ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); const entity_id_t source = 200; const entity_id_t target = 201; entity_pos_t range = fixed::FromInt(-3); entity_pos_t yOrigin = fixed::FromInt(-20); // Invalid range. TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), range); // No source ICmpPosition. range = fixed::FromInt(10); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE); // No target ICmpPosition. MockPositionRgm cmpSourcePosition; test.AddMock(source, IID_Position, cmpSourcePosition); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE); // Too much height difference. MockPositionRgm cmpTargetPosition; test.AddMock(target, IID_Position, cmpTargetPosition); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE); // If no offset we get the range. range = fixed::FromInt(20); yOrigin = fixed::Zero(); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), range); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, fixed::Zero(), yOrigin), fixed::Zero()); // Normal case. yOrigin = fixed::FromInt(5); range = fixed::FromInt(10); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(14.142136f)); // Big range. range = fixed::FromInt(260); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(264.952820f)); } };