Files
EVEngine/test/projectile_runtime.cpp

170 lines
7.2 KiB
C++

#include "weapon/ProjectileRuntime.h"
#include "common/Diagnostic.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <cmath>
#include <limits>
#include <map>
#include <string_view>
namespace {
eve::LogicalId id(std::string_view value) {
auto parsed = eve::LogicalId::parse(value);
REQUIRE(parsed.has_value());
return std::move(*parsed);
}
eve::weapon::ProjectileDefinition definition(eve::weapon::ProjectileMode mode) {
eve::weapon::ProjectileDefinition result;
result.id = id("combat-projectile:test");
result.mode = mode;
result.speed = 10.0;
result.gravity = 10.0;
result.maxTurnRateDegrees = 90.0;
result.lifetime = eve::Duration::fromNanoseconds(2000000000);
return result;
}
class TargetProvider final : public eve::weapon::IProjectileTargetProvider {
public:
[[nodiscard]] eve::Result<eve::weapon::ProjectilePoint> position(ecs::EntityHandle target) const override {
++queries;
auto found = positions.find(target.id);
if (reject || found == positions.end())
return eve::Result<eve::weapon::ProjectilePoint>::failure(
eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "target unavailable"));
return eve::Result<eve::weapon::ProjectilePoint>::success(found->second);
}
mutable bool reject = false;
mutable int queries = 0;
std::map<std::uint32_t, eve::weapon::ProjectilePoint> positions;
};
} // namespace
TEST_CASE("projectileRuntime.integratesLinearAndBallisticModes") {
eve::weapon::ProjectileRuntime runtime;
REQUIRE(runtime.configurePool(4).ok());
auto linear = runtime.spawn(definition(eve::weapon::ProjectileMode::Linear), {{}, {1.0, 0.0, 0.0}, {}});
REQUIRE(linear.ok());
auto ballistic = runtime.spawn(definition(eve::weapon::ProjectileMode::Ballistic), {{}, {0.0, 1.0, 0.0}, {}});
REQUIRE(ballistic.ok());
auto update = runtime.update(eve::Duration::fromNanoseconds(500000000));
REQUIRE(update.ok());
auto linearState = runtime.find(linear.value());
REQUIRE(linearState.has_value());
CHECK(std::fabs(linearState->position.x - 5.0) < 1e-9);
auto ballisticState = runtime.find(ballistic.value());
REQUIRE(ballisticState.has_value());
CHECK(std::fabs(ballisticState->position.y - 2.5) < 1e-9);
}
TEST_CASE("projectileRuntime.homingFailureRollsBackEveryProjectile") {
eve::weapon::ProjectileRuntime runtime;
REQUIRE(runtime.configurePool(4).ok());
ecs::EntityHandle target;
target.id = 42;
target.generation = 7;
auto linear = runtime.spawn(definition(eve::weapon::ProjectileMode::Linear), {{}, {1.0, 0.0, 0.0}, {}});
REQUIRE(linear.ok());
auto homing = runtime.spawn(definition(eve::weapon::ProjectileMode::Homing), {{}, {1.0, 0.0, 0.0}, target});
REQUIRE(homing.ok());
TargetProvider targets;
targets.reject = true;
auto update = runtime.update(eve::Duration::fromNanoseconds(500000000), &targets);
CHECK(!update.ok());
REQUIRE(runtime.find(linear.value()).has_value());
const auto linearPosition = runtime.find(linear.value())->position;
CHECK(std::fabs(linearPosition.x) < 1e-9);
CHECK(std::fabs(linearPosition.y) < 1e-9);
CHECK(std::fabs(linearPosition.z) < 1e-9);
const auto homingPosition = runtime.find(homing.value())->position;
CHECK(std::fabs(homingPosition.x) < 1e-9);
CHECK(std::fabs(homingPosition.y) < 1e-9);
CHECK(std::fabs(homingPosition.z) < 1e-9);
}
TEST_CASE("projectileRuntime.homingTurnsByInjectedRate") {
eve::weapon::ProjectileRuntime runtime;
ecs::EntityHandle target;
target.id = 7;
target.generation = 1;
auto spawned = runtime.spawn(definition(eve::weapon::ProjectileMode::Homing), {{}, {1.0, 0.0, 0.0}, target});
REQUIRE(spawned.ok());
TargetProvider targets;
targets.positions[target.id] = {0.0, 0.0, 10.0};
auto update = runtime.update(eve::Duration::fromNanoseconds(500000000), &targets);
REQUIRE(update.ok());
auto state = runtime.find(spawned.value());
REQUIRE(state.has_value());
CHECK(state->velocity.x > 0.0);
CHECK(state->velocity.z > 0.0);
CHECK(std::fabs(std::sqrt(state->velocity.x * state->velocity.x + state->velocity.z * state->velocity.z) - 10.0) <
1e-9);
}
TEST_CASE("projectileRuntime.poolReuseInvalidatesOldGeneration") {
eve::weapon::ProjectileRuntime runtime;
REQUIRE(runtime.configurePool(1).ok());
auto first = runtime.spawn(definition(eve::weapon::ProjectileMode::Linear), {{}, {1.0, 0.0, 0.0}, {}});
REQUIRE(first.ok());
auto exhausted = runtime.spawn(definition(eve::weapon::ProjectileMode::Linear), {{}, {1.0, 0.0, 0.0}, {}});
CHECK(!exhausted.ok());
REQUIRE(runtime.release(first.value()).ok());
auto second = runtime.spawn(definition(eve::weapon::ProjectileMode::Linear), {{}, {1.0, 0.0, 0.0}, {}});
REQUIRE(second.ok());
CHECK_EQ(first.value().slot, second.value().slot);
CHECK(first.value().generation != second.value().generation);
CHECK(!runtime.release(first.value()).ok());
}
TEST_CASE("projectileRuntime.expiryReturnsSlotToPool") {
eve::weapon::ProjectileRuntime runtime;
auto shortLived = definition(eve::weapon::ProjectileMode::Linear);
shortLived.lifetime = eve::Duration::fromNanoseconds(100);
auto spawned = runtime.spawn(shortLived, {{}, {1.0, 0.0, 0.0}, {}});
REQUIRE(spawned.ok());
auto update = runtime.update(eve::Duration::fromNanoseconds(100));
REQUIRE(update.ok());
REQUIRE_EQ(update.value().released.size(), 1u);
CHECK_EQ(runtime.activeCount(), 0u);
CHECK(!runtime.find(spawned.value()).has_value());
}
TEST_CASE("projectileRuntime.snapshotRestoresTrajectoryAndEmptySlotGenerationsAtomically") {
eve::weapon::ProjectileRuntime source;
REQUIRE(source.configurePool(2).ok());
auto first = source.spawn(definition(eve::weapon::ProjectileMode::Linear), {{}, {1.0, 0.0, 0.0}, {}});
REQUIRE(first.ok());
REQUIRE(source.update(eve::Duration::fromNanoseconds(250000000)).ok());
const auto snapshot = source.snapshot();
eve::weapon::ProjectileRuntime restored;
REQUIRE(restored.restore(snapshot).ok());
REQUIRE_EQ(restored.capacity(), 2u);
REQUIRE_EQ(restored.activeCount(), 1u);
auto state = restored.find(first.value());
REQUIRE(state.has_value());
CHECK(std::fabs(state->position.x - 2.5) < 1e-9);
REQUIRE(restored.release(first.value()).ok());
auto reused = restored.spawn(definition(eve::weapon::ProjectileMode::Linear), {{}, {1.0, 0.0, 0.0}, {}});
REQUIRE(reused.ok());
CHECK_EQ(reused.value().slot, first.value().slot);
CHECK(reused.value().generation != first.value().generation);
auto invalid = snapshot;
invalid.slots[0].state->position.x = std::numeric_limits<double>::quiet_NaN();
const auto before = restored.snapshot();
CHECK(!restored.restore(invalid).ok());
CHECK_EQ(restored.activeCount(), 1u);
CHECK(restored.find(reused.value()).has_value());
CHECK_EQ(restored.snapshot().slots.size(), before.slots.size());
}