0ad/source/simulation2/components/tests/test_RangeManager.h
Vantha eb2f59eeaa Allow actors to override the entity's anchor type
Optionally let actor templates define their own anchor types.
These override the default value defined in the template.
Useful for actors without their own entity template.

Reviewed-By: Itms
Reviewed-On: https://gitea.wildfiregames.com/0ad/0ad/pulls/7542
2025-06-10 23:01:09 +02:00

317 lines
15 KiB
C++

/* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#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 <boost/random/mersenne_twister.hpp>
#include <boost/random/uniform_real_distribution.hpp>
#include <optional>
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<entity_id_t>* 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 SetActorAnchor(const CStr& UNUSED(anchor)) 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<entity_id_t> GetEntitiesByFlags(ICmpObstructionManager::flags_t) const override { return {}; };
std::vector<entity_id_t> GetEntitiesBlockingMovement() const override { return {}; };
std::vector<entity_id_t> GetEntitiesBlockingConstruction() const override { return {}; };
std::vector<entity_id_t> 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
{
std::optional<CXeromycesEngine> xeromycesEngine;
public:
void setUp()
{
xeromycesEngine.emplace();
}
void tearDown()
{
xeromycesEngine.reset();
}
// 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<ICmpRangeManager>(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<double>(0.0, 512.0)(rng);
double z = boost::random::uniform_real_distribution<double>(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<ICmpRangeManager>(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<entity_id_t> nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{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<entity_id_t>{101});
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
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<entity_id_t>{101});
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
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<entity_id_t>{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<entity_id_t>{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<entity_id_t>{101});
nearby = cmp->ExecuteQuery(100, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
nearby = cmp->ExecuteQuery(100, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
nearby = cmp->ExecuteQuery(101, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{100});
nearby = cmp->ExecuteQuery(101, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
}
void test_IsInTargetParabolicRange()
{
ComponentTestHelper test(*g_ScriptContext);
ICmpRangeManager* cmp = test.Add<ICmpRangeManager>(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));
}
};