242 lines
9.8 KiB
C++
242 lines
9.8 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "common/Capability.h"
|
|
#include "physics/ArtifactProvider.h"
|
|
#include "physics/World3D.h"
|
|
#include "procgen/ArtifactPublish.h"
|
|
#include "procgen/GeneratedArtifact.h"
|
|
#include "procgen/Params.h"
|
|
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <span>
|
|
#include <string>
|
|
|
|
namespace {
|
|
|
|
eve::PersistentId id(const char* text) {
|
|
const auto parsed = eve::PersistentId::parse(text);
|
|
REQUIRE(parsed.has_value());
|
|
return *parsed;
|
|
}
|
|
|
|
struct ColliderPublication {
|
|
std::array<float, 12> vertices{-1.f, 0.f, -1.f, 1.f, 0.f, -1.f, 1.f, 0.f, 1.f, -1.f, 0.f, 1.f};
|
|
std::array<std::uint32_t, 6> indices{0, 2, 1, 0, 3, 2};
|
|
eve::artifact::PartView part;
|
|
eve::artifact::PublicationView publication;
|
|
|
|
explicit ColliderPublication(eve::PersistentId artifactId) {
|
|
part.id = artifactId;
|
|
part.role = "collider";
|
|
part.kind = eve::artifact::PartKind::Collider;
|
|
part.schemaVersion = 1;
|
|
part.buildKey = "test.box3d.collider.v1";
|
|
part.bounds = {-1.f, 0.f, -1.f, 1.f, 0.f, 1.f, true};
|
|
part.positions = std::span<const float>(vertices);
|
|
part.indices = std::span<const std::uint32_t>(indices);
|
|
|
|
publication.id = artifactId;
|
|
publication.schemaVersion = 1;
|
|
publication.buildKey = part.buildKey;
|
|
publication.bounds = part.bounds;
|
|
publication.parts = std::span<const eve::artifact::PartView>(&part, 1);
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("physics.artifact.stageUsesRealBox3DAndRayQuery") {
|
|
eve::physics::PhysicsArtifactProvider provider;
|
|
const auto artifactId = id("018f0b7e-6e50-7a10-8c22-2c8f8e3e0201");
|
|
ColliderPublication input(artifactId);
|
|
|
|
auto prepared = provider.prepare(input.publication);
|
|
REQUIRE(prepared.ok());
|
|
auto stage = std::move(prepared).takeValue();
|
|
REQUIRE(static_cast<bool>(stage));
|
|
CHECK_EQ(provider.size(), std::size_t(0));
|
|
stage->commit();
|
|
stage.reset();
|
|
|
|
const auto* collider = provider.find(artifactId);
|
|
REQUIRE(collider != nullptr);
|
|
CHECK_EQ(provider.size(), std::size_t(1));
|
|
CHECK_EQ(provider.backendName(artifactId), std::string("box3d"));
|
|
CHECK(provider.isBox3DBacked(artifactId));
|
|
CHECK(provider.isHandleLive(collider->handle));
|
|
|
|
const auto hit = provider.rayCast(artifactId, 0.f, 2.f, 0.f, 0.f, -4.f, 0.f);
|
|
CHECK(hit.hit);
|
|
CHECK(hit.fraction >= 0.f);
|
|
CHECK(hit.fraction <= 1.f);
|
|
CHECK(std::fabs(hit.y) < 0.001f);
|
|
}
|
|
|
|
TEST_CASE("physics.artifact.rollbackAndValidationLeaveNoCollider") {
|
|
eve::physics::PhysicsArtifactProvider provider;
|
|
eve::physics::World3D sharedWorld(0.f, 0.f, 0.f, true);
|
|
REQUIRE(provider.bindWorld(sharedWorld).ok());
|
|
const auto artifactId = id("018f0b7e-6e50-7a10-8c22-2c8f8e3e0202");
|
|
ColliderPublication input(artifactId);
|
|
|
|
auto prepared = provider.prepare(input.publication);
|
|
REQUIRE(prepared.ok());
|
|
auto stage = std::move(prepared).takeValue();
|
|
CHECK_EQ(sharedWorld.getBodyCount(), 1);
|
|
sharedWorld.rayCast(0.f, 2.f, 0.f, 0.f, -2.f, 0.f);
|
|
CHECK(!sharedWorld.hasRayHit());
|
|
stage->rollback();
|
|
stage.reset();
|
|
CHECK(provider.emptyState());
|
|
CHECK_EQ(sharedWorld.getBodyCount(), 0);
|
|
|
|
provider.setPrepareFailure(true);
|
|
auto injected = provider.prepare(input.publication);
|
|
CHECK(!injected.ok());
|
|
CHECK(provider.emptyState());
|
|
|
|
input.indices[0] = 99;
|
|
provider.setPrepareFailure(false);
|
|
auto invalid = provider.prepare(input.publication);
|
|
CHECK(!invalid.ok());
|
|
CHECK(provider.emptyState());
|
|
}
|
|
|
|
TEST_CASE("physics.artifact.restoreRebuildsBox3DAndInvalidatesOldHandle") {
|
|
eve::physics::PhysicsArtifactProvider provider;
|
|
const auto artifactId = id("018f0b7e-6e50-7a10-8c22-2c8f8e3e0203");
|
|
ColliderPublication input(artifactId);
|
|
|
|
auto prepared = provider.prepare(input.publication);
|
|
REQUIRE(prepared.ok());
|
|
auto stage = std::move(prepared).takeValue();
|
|
stage->commit();
|
|
stage.reset();
|
|
const auto oldHandle = provider.find(artifactId)->handle;
|
|
auto snapshot = provider.snapshotState();
|
|
REQUIRE(snapshot.ok());
|
|
auto encoded = std::move(snapshot).takeValue();
|
|
|
|
// A failed candidate build must leave the live Box3D world untouched.
|
|
auto malformed = encoded;
|
|
auto* malformedObject = malformed.getIf<eve::Value::Object>();
|
|
REQUIRE(malformedObject != nullptr);
|
|
auto* encodedColliders = malformedObject->at("colliders").getIf<eve::Value::Array>();
|
|
REQUIRE(encodedColliders != nullptr);
|
|
auto* malformedCollider = encodedColliders->front().getIf<eve::Value::Object>();
|
|
REQUIRE(malformedCollider != nullptr);
|
|
auto* malformedIndices = malformedCollider->at("indices").getIf<eve::Value::Array>();
|
|
REQUIRE(malformedIndices != nullptr);
|
|
malformedIndices->front() = eve::Value(std::int64_t(99));
|
|
auto rejected = provider.restoreState(malformed);
|
|
CHECK(!rejected.ok());
|
|
CHECK_EQ(provider.size(), std::size_t(1));
|
|
CHECK(provider.isHandleLive(oldHandle));
|
|
CHECK(provider.rayCast(artifactId, 0.f, 2.f, 0.f, 0.f, -4.f, 0.f).hit);
|
|
|
|
provider.clear();
|
|
CHECK(!provider.isHandleLive(oldHandle));
|
|
auto restored = provider.restoreState(encoded);
|
|
REQUIRE(restored.ok());
|
|
CHECK_EQ(provider.size(), std::size_t(1));
|
|
const auto* rebuilt = provider.find(artifactId);
|
|
REQUIRE(rebuilt != nullptr);
|
|
CHECK(rebuilt->handle != oldHandle);
|
|
CHECK(!provider.isHandleLive(oldHandle));
|
|
CHECK(provider.isHandleLive(rebuilt->handle));
|
|
CHECK(provider.isBox3DBacked(artifactId));
|
|
CHECK(provider.rayCast(artifactId, 0.f, 2.f, 0.f, 0.f, -4.f, 0.f).hit);
|
|
}
|
|
|
|
TEST_CASE("physics.artifact.compositeColliderUsesPublicProcgenPublication") {
|
|
eve::cap::detail::clearAllRaw();
|
|
auto& provider = eve::physics::physicsArtifactProvider();
|
|
provider.clear();
|
|
eve::physics::World3D sharedWorld(0.f, 0.f, 0.f, true);
|
|
REQUIRE(provider.bindWorld(sharedWorld).ok());
|
|
eve::physics::registerPhysicsArtifactProvider();
|
|
|
|
eve::procgen::Params params;
|
|
params.setSeed(424242u);
|
|
params.setInt("width", 8);
|
|
params.setInt("height", 7);
|
|
params.setFloat("radius", 1.f);
|
|
const auto artifactId = eve::procgen::ArtifactId::parse("018f0b7e-6e50-7a10-8c22-2c8f8e3e0204");
|
|
REQUIRE(artifactId.has_value());
|
|
|
|
auto generated = eve::procgen::generateHexTerrainArtifact(params, *artifactId);
|
|
REQUIRE(generated.ok());
|
|
eve::procgen::ArtifactStore store;
|
|
eve::procgen::ArtifactPublisher publisher(store);
|
|
eve::procgen::ArtifactPublishOptions options;
|
|
options.physics = true;
|
|
auto published = publisher.publish(std::move(generated).takeValue(), options);
|
|
REQUIRE(published.ok());
|
|
|
|
const auto persistentId = eve::PersistentId::fromUuid(*artifactId);
|
|
CHECK_EQ(provider.size(), std::size_t(1));
|
|
CHECK(provider.find(persistentId) != nullptr);
|
|
CHECK(provider.isBox3DBacked(persistentId));
|
|
CHECK(provider.isSharedWorldBacked(persistentId));
|
|
CHECK_EQ(provider.boundWorld(), sharedWorld.runtimeHandle());
|
|
CHECK_EQ(sharedWorld.getBodyCount(), 1);
|
|
const auto* collider = provider.find(persistentId);
|
|
REQUIRE(collider != nullptr);
|
|
REQUIRE_GE(collider->indices.size(), std::size_t(3));
|
|
const auto vertex = [collider](std::size_t corner, std::size_t axis) {
|
|
return collider->vertices[std::size_t(collider->indices[corner]) * 3u + axis];
|
|
};
|
|
const float rayX = (vertex(0, 0) + vertex(1, 0) + vertex(2, 0)) / 3.f;
|
|
const float rayZ = (vertex(0, 2) + vertex(1, 2) + vertex(2, 2)) / 3.f;
|
|
const auto hit = provider.rayCast(persistentId, rayX, collider->bounds.maxY + 10.f, rayZ, 0.f,
|
|
-((collider->bounds.maxY - collider->bounds.minY) + 20.f), 0.f);
|
|
CHECK(hit.hit);
|
|
|
|
sharedWorld.rayCast(rayX, collider->bounds.maxY + 10.f, rayZ, rayX,
|
|
collider->bounds.minY - 10.f, rayZ);
|
|
CHECK(sharedWorld.hasRayHit());
|
|
provider.clear();
|
|
CHECK_EQ(sharedWorld.getBodyCount(), 0);
|
|
REQUIRE(provider.unbindWorld(sharedWorld.runtimeHandle()).ok());
|
|
}
|
|
|
|
TEST_CASE("physics.artifact.destroyedSharedWorldMakesPublicationStale") {
|
|
eve::physics::PhysicsArtifactProvider provider;
|
|
const auto artifactId = id("018f0b7e-6e50-7a10-8c22-2c8f8e3e0205");
|
|
ColliderPublication input(artifactId);
|
|
auto world = std::make_unique<eve::physics::World3D>(0.f, 0.f, 0.f, true);
|
|
REQUIRE(provider.bindWorld(*world).ok());
|
|
auto prepared = provider.prepare(input.publication);
|
|
REQUIRE(prepared.ok());
|
|
auto stage = std::move(prepared).takeValue();
|
|
stage->commit();
|
|
const auto colliderHandle = provider.find(artifactId)->handle;
|
|
|
|
world.reset();
|
|
CHECK(provider.boundWorld().isInvalid());
|
|
CHECK(!provider.isBox3DBacked(artifactId));
|
|
CHECK(!provider.isSharedWorldBacked(artifactId));
|
|
CHECK(!provider.isHandleLive(colliderHandle));
|
|
CHECK(!provider.rayCast(artifactId, 0.f, 2.f, 0.f, 0.f, -4.f, 0.f).hit);
|
|
provider.clear();
|
|
}
|
|
|
|
TEST_CASE("physics.artifact.preparedStageOutlivesProviderWithoutOwnerCallback") {
|
|
eve::physics::World3D sharedWorld(0.f, 0.f, 0.f, true);
|
|
std::unique_ptr<eve::artifact::PreparedPublication> stage;
|
|
{
|
|
auto provider = std::make_unique<eve::physics::PhysicsArtifactProvider>();
|
|
REQUIRE(provider->bindWorld(sharedWorld).ok());
|
|
ColliderPublication input(id("018f0b7e-6e50-7a10-8c22-2c8f8e3e0206"));
|
|
auto prepared = provider->prepare(input.publication);
|
|
REQUIRE(prepared.ok());
|
|
stage = std::move(prepared).takeValue();
|
|
CHECK_EQ(sharedWorld.getBodyCount(), 1);
|
|
}
|
|
stage.reset();
|
|
CHECK_EQ(sharedWorld.getBodyCount(), 0);
|
|
}
|