Files
EVEngine/test/climbing_anchor_execution.cpp

419 lines
20 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "climbing/Climbing.h"
#include "physics/Body3D.h"
#include "physics/Physics.h"
#include "physics/Shape3D.h"
#include "physics/World3D.h"
#include <cmath>
#include <memory>
#include <string>
namespace {
eve::Duration seconds(double value) {
auto duration = eve::Duration::fromSeconds(value);
REQUIRE(duration.ok());
return std::move(duration).takeValue();
}
eve::climbing::ClimbingActionDefinition action(std::string id, eve::climbing::ClimbingActionKind kind) {
eve::climbing::ClimbingActionDefinition result;
result.id = std::move(id);
result.kind = kind;
result.minHeight = 0.f;
result.maxHeight = 3.f;
result.duration = seconds(0.1);
result.apexHeight = kind == eve::climbing::ClimbingActionKind::LedgeJump ? 0.35f : 0.f;
return result;
}
eve::climbing::ClimbingAnchorNodeDefinition node(std::string id, eve::climbing::Vec3 position,
eve::climbing::ClimbingAnchorKind kind) {
eve::climbing::ClimbingAnchorNodeDefinition result;
result.id = std::move(id);
result.kind = kind;
result.localPosition = position;
result.localNormal = {0.f, 0.f, -1.f};
result.localTangent = {1.f, 0.f, 0.f};
result.leftHandSocket = {position.x - 0.2f, position.y, position.z};
result.rightHandSocket = {position.x + 0.2f, position.y, position.z};
result.feetSocket = {position.x, position.y - 1.f, position.z - 0.2f};
result.tags = {"route"};
return result;
}
eve::climbing::ClimbingAnchorEdgeDefinition edge(std::string from, std::string to,
eve::climbing::ClimbingAnchorEdgeKind kind) {
eve::climbing::ClimbingAnchorEdgeDefinition result;
result.from = std::move(from);
result.to = std::move(to);
result.kind = kind;
return result;
}
eve::climbing::ClimbingAnchorGraphDefinition ledgeGraph() {
eve::climbing::ClimbingAnchorGraphDefinition graph;
graph.id = "test:ledge-route";
graph.sourceGeometryContentId = "test:geometry-v1";
graph.buildSettingsHash = "test:settings-v1";
graph.nodes = {
node("ledge:left", {-0.5f, 1.f, -0.25f}, eve::climbing::ClimbingAnchorKind::Ledge),
node("ledge:right", {0.5f, 1.f, -0.25f}, eve::climbing::ClimbingAnchorKind::Ledge),
node("corner:outer", {1.f, 1.f, 0.25f}, eve::climbing::ClimbingAnchorKind::CornerOuter),
};
graph.edges = {
edge("ledge:left", "ledge:right", eve::climbing::ClimbingAnchorEdgeKind::Shimmy),
edge("ledge:right", "corner:outer", eve::climbing::ClimbingAnchorEdgeKind::Corner),
};
return graph;
}
eve::climbing::ClimbingAnchorGraphDefinition ladderGraph() {
eve::climbing::ClimbingAnchorGraphDefinition graph;
graph.id = "test:ladder-route";
graph.sourceGeometryContentId = "test:geometry-v1";
graph.buildSettingsHash = "test:settings-v1";
graph.nodes = {
node("ladder:bottom", {0.f, 0.f, -0.25f}, eve::climbing::ClimbingAnchorKind::LadderRung),
node("ladder:top", {0.f, 1.25f, -0.25f}, eve::climbing::ClimbingAnchorKind::LadderRung),
node("ladder:exit", {0.6f, 0.2f, -0.25f}, eve::climbing::ClimbingAnchorKind::Ledge),
};
graph.edges = {
edge("ladder:bottom", "ladder:top", eve::climbing::ClimbingAnchorEdgeKind::Climb),
edge("ladder:top", "ladder:exit", eve::climbing::ClimbingAnchorEdgeKind::Dismount),
};
return graph;
}
eve::climbing::ClimbingAnchorGraphDefinition parkourGraph() {
eve::climbing::ClimbingAnchorGraphDefinition graph;
graph.id = "test:parkour-route";
graph.sourceGeometryContentId = "test:parkour-geometry-v1";
graph.buildSettingsHash = "test:parkour-settings-v1";
graph.nodes = {
node("beam:a", {-0.5f, 2.f, -0.25f}, eve::climbing::ClimbingAnchorKind::Beam),
node("beam:b", {0.5f, 2.f, -0.25f}, eve::climbing::ClimbingAnchorKind::Beam),
node("pole:a", {-0.5f, 1.f, -0.25f}, eve::climbing::ClimbingAnchorKind::Pole),
node("bar:a", {0.5f, 1.f, -0.25f}, eve::climbing::ClimbingAnchorKind::Bar),
};
graph.edges = {
edge("beam:a", "beam:b", eve::climbing::ClimbingAnchorEdgeKind::Balance),
edge("pole:a", "bar:a", eve::climbing::ClimbingAnchorEdgeKind::Swing),
};
return graph;
}
struct AnchorWorld {
AnchorWorld() {
world.reset(eve::physics::Physics::create()->newWorld3D(0.f, 0.f, 0.f, false));
body = world->newBody("kinematic", 0.f, 1.f, 1.f);
shape = body->newBoxShape(4.f, 4.f, 0.5f);
}
eve::climbing::ClimbingPose poseFor(const eve::climbing::ClimbingActionDefinition& start,
eve::climbing::Vec3 localNode) const {
const eve::climbing::Vec3 worldNode{localNode.x, localNode.y + 1.f, localNode.z + 1.f};
return {
{worldNode.x, worldNode.y - start.hangFeetBelowLedge, worldNode.z - (0.3f + 0.03f + start.hangBodyOffset)},
{0.f, 0.f, 1.f},
0.f,
body->getId(),
0.f,
false};
}
std::unique_ptr<eve::physics::World3D> world;
eve::physics::Body3D* body = nullptr;
eve::physics::Shape3D* shape = nullptr;
};
struct GraphLease {
eve::climbing::ClimbingAnchorGraphHandleRef handle;
~GraphLease() {
if (handle.isValid()) {
auto released = eve::climbing::Climbing::releaseAnchorGraph(handle);
released.ignore("test graph lease cleanup");
}
}
};
eve::climbing::ClimbingAnchorNodeRef nodeRef(eve::climbing::ClimbingAnchorGraphHandleRef handle, std::string_view id) {
auto graph = eve::climbing::Climbing::resolveAnchorGraph(handle);
REQUIRE(graph.isBound());
auto reference = graph->nodeRef(id);
REQUIRE(reference.ok());
return std::move(reference).takeValue();
}
void advanceToEnd(eve::climbing::ClimbingRuntime& runtime, eve::physics::World3D& world, std::uint64_t tick) {
auto advanced = runtime.advance(world, {eve::SimulationTick(tick), seconds(0.1)});
REQUIRE(advanced.ok());
}
} // namespace
TEST_CASE("climbing.anchorExecution.shimmyAndCornerKeepOneAtomicReservation") {
AnchorWorld fixture;
auto graphResult =
eve::climbing::Climbing::newAnchorGraph(ledgeGraph(), *fixture.world, fixture.body->runtimeHandle());
REQUIRE(graphResult.ok());
GraphLease lease{std::move(graphResult).takeValue()};
auto graph = eve::climbing::Climbing::resolveAnchorGraph(lease.handle);
REQUIRE(graph.isBound());
auto grab = action("climbing:ledge_grab", eve::climbing::ClimbingActionKind::LedgeGrab);
auto shimmy = action("climbing:shimmy", eve::climbing::ClimbingActionKind::Shimmy);
auto corner = action("climbing:corner_outer", eve::climbing::ClimbingActionKind::CornerOuter);
eve::climbing::ClimbingProfile profile;
profile.maxObstacleHeight = 3.f;
profile.pathValidationSegments = 4;
profile.actions = {grab, shimmy, corner};
eve::climbing::ClimbingRuntime runtime;
REQUIRE(runtime.setProfile(profile).ok());
const auto left = nodeRef(lease.handle, "ledge:left");
REQUIRE(runtime
.tryBeginAnchor(*fixture.world, lease.handle, left, eve::climbing::ClimbingAnchorAgentId(1001), grab.id,
fixture.poseFor(grab, {-0.5f, 1.f, -0.25f}), eve::SimulationTick(1))
.ok());
CHECK_EQ(graph->reservationCount(), std::size_t(1));
advanceToEnd(runtime, *fixture.world, 2);
CHECK_EQ(static_cast<int>(runtime.phase()), static_cast<int>(eve::climbing::ClimbingPhase::Hanging));
const auto right = nodeRef(lease.handle, "ledge:right");
REQUIRE(runtime
.transitionAnchor(*fixture.world, right, eve::climbing::ClimbingAnchorEdgeKind::Shimmy, shimmy.id,
eve::SimulationTick(3))
.ok());
CHECK_EQ(graph->reservationCount(), std::size_t(1));
advanceToEnd(runtime, *fixture.world, 4);
auto current = runtime.currentAnchor();
REQUIRE(current.ok());
CHECK_EQ(current.value().nodeId, std::string("ledge:right"));
const auto cornerNode = nodeRef(lease.handle, "corner:outer");
REQUIRE(runtime
.transitionAnchor(*fixture.world, cornerNode, eve::climbing::ClimbingAnchorEdgeKind::Corner, corner.id,
eve::SimulationTick(5))
.ok());
CHECK_EQ(graph->reservationCount(), std::size_t(1));
advanceToEnd(runtime, *fixture.world, 6);
current = runtime.currentAnchor();
REQUIRE(current.ok());
CHECK_EQ(current.value().nodeId, std::string("corner:outer"));
CHECK_EQ(graph->reservationCount(), std::size_t(1));
REQUIRE(runtime.cancel(eve::climbing::ClimbingCancelReason::PlayerRequest, eve::SimulationTick(6)).ok());
CHECK_EQ(graph->reservationCount(), std::size_t(0));
auto events = runtime.drainEvents();
REQUIRE(events.ok());
REQUIRE_EQ(events.value().size(), std::size_t(7));
CHECK_EQ(static_cast<int>(events.value()[0].kind), static_cast<int>(eve::climbing::ClimbingEventKind::Started));
CHECK_EQ(static_cast<int>(events.value()[1].kind), static_cast<int>(eve::climbing::ClimbingEventKind::Hanging));
CHECK_EQ(static_cast<int>(events.value()[2].kind),
static_cast<int>(eve::climbing::ClimbingEventKind::AnchorTransitionStarted));
CHECK_EQ(static_cast<int>(events.value()[3].kind),
static_cast<int>(eve::climbing::ClimbingEventKind::AnchorReached));
CHECK_EQ(static_cast<int>(events.value()[6].kind), static_cast<int>(eve::climbing::ClimbingEventKind::Cancelled));
}
TEST_CASE("climbing.anchorExecution.beamPoleAndBarUseExplicitStablePhases") {
AnchorWorld fixture;
auto graphResult =
eve::climbing::Climbing::newAnchorGraph(parkourGraph(), *fixture.world, fixture.body->runtimeHandle());
REQUIRE(graphResult.ok());
GraphLease lease{std::move(graphResult).takeValue()};
auto beam = action("parkour:beam_balance", eve::climbing::ClimbingActionKind::BeamBalance);
auto pole = action("parkour:pole_swing", eve::climbing::ClimbingActionKind::PoleSwing);
auto bar = action("parkour:bar_swing", eve::climbing::ClimbingActionKind::BarSwing);
eve::climbing::ClimbingProfile profile;
profile.maxObstacleHeight = 4.f;
profile.pathValidationSegments = 4;
profile.actions = {beam, pole, bar};
eve::climbing::ClimbingRuntime beamRuntime;
REQUIRE(beamRuntime.setProfile(profile).ok());
auto beamStart = nodeRef(lease.handle, "beam:a");
eve::climbing::ClimbingPose beamPose{{-0.5f, 3.f, 0.75f}, {1.f, 0.f, 0.f}, 0.f, fixture.body->getId(), 0.f, true};
REQUIRE(beamRuntime
.tryBeginAnchor(*fixture.world, lease.handle, beamStart, eve::climbing::ClimbingAnchorAgentId(2001),
beam.id, beamPose, eve::SimulationTick(1))
.ok());
advanceToEnd(beamRuntime, *fixture.world, 2);
CHECK_EQ(static_cast<int>(beamRuntime.phase()), static_cast<int>(eve::climbing::ClimbingPhase::Balanced));
auto beamTarget = nodeRef(lease.handle, "beam:b");
REQUIRE(beamRuntime
.transitionAnchor(*fixture.world, beamTarget, eve::climbing::ClimbingAnchorEdgeKind::Balance, beam.id,
eve::SimulationTick(3))
.ok());
advanceToEnd(beamRuntime, *fixture.world, 4);
CHECK_EQ(static_cast<int>(beamRuntime.phase()), static_cast<int>(eve::climbing::ClimbingPhase::Balanced));
REQUIRE(beamRuntime.cancel(eve::climbing::ClimbingCancelReason::PlayerRequest, eve::SimulationTick(5)).ok());
eve::climbing::ClimbingRuntime swingRuntime;
REQUIRE(swingRuntime.setProfile(profile).ok());
auto poleStart = nodeRef(lease.handle, "pole:a");
REQUIRE(swingRuntime
.tryBeginAnchor(*fixture.world, lease.handle, poleStart, eve::climbing::ClimbingAnchorAgentId(2002),
pole.id, fixture.poseFor(pole, {-0.5f, 1.f, -0.25f}), eve::SimulationTick(10))
.ok());
advanceToEnd(swingRuntime, *fixture.world, 11);
CHECK_EQ(static_cast<int>(swingRuntime.phase()), static_cast<int>(eve::climbing::ClimbingPhase::Swinging));
auto barTarget = nodeRef(lease.handle, "bar:a");
REQUIRE(swingRuntime
.transitionAnchor(*fixture.world, barTarget, eve::climbing::ClimbingAnchorEdgeKind::Swing, bar.id,
eve::SimulationTick(12))
.ok());
advanceToEnd(swingRuntime, *fixture.world, 13);
CHECK_EQ(static_cast<int>(swingRuntime.phase()), static_cast<int>(eve::climbing::ClimbingPhase::Swinging));
}
TEST_CASE("climbing.anchorExecution.ladderMountClimbAndDismountShareOneExecution") {
AnchorWorld fixture;
auto graphResult =
eve::climbing::Climbing::newAnchorGraph(ladderGraph(), *fixture.world, fixture.body->runtimeHandle());
REQUIRE(graphResult.ok());
GraphLease lease{std::move(graphResult).takeValue()};
auto graph = eve::climbing::Climbing::resolveAnchorGraph(lease.handle);
REQUIRE(graph.isBound());
auto mount = action("climbing:ladder_mount", eve::climbing::ClimbingActionKind::LadderMount);
auto climb = action("climbing:ladder_climb", eve::climbing::ClimbingActionKind::LadderClimb);
auto dismount = action("climbing:ladder_dismount", eve::climbing::ClimbingActionKind::LadderDismount);
eve::climbing::ClimbingProfile profile;
profile.maxObstacleHeight = 3.f;
profile.pathValidationSegments = 4;
profile.actions = {mount, climb, dismount};
eve::climbing::ClimbingRuntime runtime;
REQUIRE(runtime.setProfile(profile).ok());
const auto bottom = nodeRef(lease.handle, "ladder:bottom");
REQUIRE(runtime
.tryBeginAnchor(*fixture.world, lease.handle, bottom, eve::climbing::ClimbingAnchorAgentId(2001),
mount.id, fixture.poseFor(mount, {0.f, 0.f, -0.25f}), eve::SimulationTick(10))
.ok());
const auto executionId = runtime.executionId();
advanceToEnd(runtime, *fixture.world, 11);
const auto top = nodeRef(lease.handle, "ladder:top");
REQUIRE(runtime
.transitionAnchor(*fixture.world, top, eve::climbing::ClimbingAnchorEdgeKind::Climb, climb.id,
eve::SimulationTick(12))
.ok());
advanceToEnd(runtime, *fixture.world, 13);
CHECK(runtime.executionId() == executionId);
CHECK_EQ(graph->reservationCount(), std::size_t(1));
const auto exit = nodeRef(lease.handle, "ladder:exit");
REQUIRE(runtime
.transitionAnchor(*fixture.world, exit, eve::climbing::ClimbingAnchorEdgeKind::Dismount, dismount.id,
eve::SimulationTick(14))
.ok());
advanceToEnd(runtime, *fixture.world, 15);
CHECK_EQ(static_cast<int>(runtime.phase()), static_cast<int>(eve::climbing::ClimbingPhase::Completed));
CHECK(runtime.executionId().isZero());
CHECK_EQ(graph->reservationCount(), std::size_t(0));
}
TEST_CASE("climbing.anchorExecution.reloadAndDestructionCannotLeakOccupancy") {
AnchorWorld fixture;
auto graphResult =
eve::climbing::Climbing::newAnchorGraph(ledgeGraph(), *fixture.world, fixture.body->runtimeHandle());
REQUIRE(graphResult.ok());
GraphLease lease{std::move(graphResult).takeValue()};
auto graph = eve::climbing::Climbing::resolveAnchorGraph(lease.handle);
REQUIRE(graph.isBound());
auto grab = action("climbing:ledge_grab", eve::climbing::ClimbingActionKind::LedgeGrab);
const auto left = nodeRef(lease.handle, "ledge:left");
{
eve::climbing::ClimbingRuntime runtime;
REQUIRE(runtime.upsertAction(grab).ok());
REQUIRE(runtime
.tryBeginAnchor(*fixture.world, lease.handle, left, eve::climbing::ClimbingAnchorAgentId(3001),
grab.id, fixture.poseFor(grab, {-0.5f, 1.f, -0.25f}), eve::SimulationTick(20))
.ok());
CHECK_EQ(graph->reservationCount(), std::size_t(1));
}
CHECK_EQ(graph->reservationCount(), std::size_t(0));
eve::climbing::ClimbingRuntime runtime;
REQUIRE(runtime.upsertAction(grab).ok());
REQUIRE(runtime
.tryBeginAnchor(*fixture.world, lease.handle, left, eve::climbing::ClimbingAnchorAgentId(3002), grab.id,
fixture.poseFor(grab, {-0.5f, 1.f, -0.25f}), eve::SimulationTick(30))
.ok());
auto reload = graph->reload(ledgeGraph());
REQUIRE(reload.ok());
REQUIRE_EQ(reload.value().invalidatedOccupants.size(), std::size_t(1));
CHECK_EQ(reload.value().invalidatedOccupants.front().agentId.value(), std::uint64_t(3002));
auto stale = runtime.advance(*fixture.world, {eve::SimulationTick(31), seconds(0.1)});
CHECK(!stale.ok());
CHECK_EQ(static_cast<int>(runtime.phase()), static_cast<int>(eve::climbing::ClimbingPhase::Cancelled));
CHECK_EQ(runtime.inspect().terminalCode, std::string("climbing.anchor.stale"));
CHECK_EQ(graph->reservationCount(), std::size_t(0));
}
TEST_CASE("climbing.anchorExecution.snapshotTransfersOrReacquiresOneExclusiveClaim") {
AnchorWorld fixture;
auto graphResult =
eve::climbing::Climbing::newAnchorGraph(ledgeGraph(), *fixture.world, fixture.body->runtimeHandle());
REQUIRE(graphResult.ok());
GraphLease lease{std::move(graphResult).takeValue()};
auto graph = eve::climbing::Climbing::resolveAnchorGraph(lease.handle);
REQUIRE(graph.isBound());
auto grab = action("climbing:ledge_grab", eve::climbing::ClimbingActionKind::LedgeGrab);
const auto left = nodeRef(lease.handle, "ledge:left");
eve::climbing::ClimbingRuntime source;
REQUIRE(source.upsertAction(grab).ok());
REQUIRE(source
.tryBeginAnchor(*fixture.world, lease.handle, left, eve::climbing::ClimbingAnchorAgentId(4001), grab.id,
fixture.poseFor(grab, {-0.5f, 1.f, -0.25f}), eve::SimulationTick(40))
.ok());
advanceToEnd(source, *fixture.world, 41);
auto snapshot = source.snapshot();
REQUIRE(snapshot.ok());
eve::climbing::ClimbingRuntime restored;
REQUIRE(restored.restore(snapshot.value(), *fixture.world).ok());
CHECK_EQ(graph->reservationCount(), std::size_t(1));
CHECK_EQ(static_cast<int>(restored.phase()), static_cast<int>(eve::climbing::ClimbingPhase::Hanging));
auto sourceStale = source.advance(*fixture.world, {eve::SimulationTick(42), seconds(0.1)});
CHECK(!sourceStale.ok());
CHECK_EQ(static_cast<int>(source.phase()), static_cast<int>(eve::climbing::ClimbingPhase::Cancelled));
CHECK_EQ(graph->reservationCount(), std::size_t(1));
eve::climbing::ClimbingRuntime replay;
auto duplicate = replay.restore(snapshot.value(), *fixture.world);
CHECK(!duplicate.ok());
CHECK_EQ(static_cast<int>(duplicate.error()->code()), static_cast<int>(eve::DiagnosticCode::Conflict));
CHECK_EQ(graph->reservationCount(), std::size_t(1));
REQUIRE(restored.cancel(eve::climbing::ClimbingCancelReason::PlayerRequest, eve::SimulationTick(42)).ok());
CHECK_EQ(graph->reservationCount(), std::size_t(0));
eve::Value detachedSnapshot;
{
eve::climbing::ClimbingRuntime detachedSource;
REQUIRE(detachedSource.upsertAction(grab).ok());
REQUIRE(detachedSource
.tryBeginAnchor(*fixture.world, lease.handle, left, eve::climbing::ClimbingAnchorAgentId(4002),
grab.id, fixture.poseFor(grab, {-0.5f, 1.f, -0.25f}), eve::SimulationTick(50))
.ok());
auto captured = detachedSource.snapshot();
REQUIRE(captured.ok());
detachedSnapshot = std::move(captured).takeValue();
CHECK_EQ(graph->reservationCount(), std::size_t(1));
}
CHECK_EQ(graph->reservationCount(), std::size_t(0));
eve::climbing::ClimbingRuntime reacquired;
REQUIRE(reacquired.restore(detachedSnapshot, *fixture.world).ok());
CHECK_EQ(graph->reservationCount(), std::size_t(1));
REQUIRE(reacquired.cancel(eve::climbing::ClimbingCancelReason::PlayerRequest, eve::SimulationTick(50)).ok());
CHECK_EQ(graph->reservationCount(), std::size_t(0));
}