mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
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
317 lines
15 KiB
C++
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));
|
|
}
|
|
};
|