#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "card/CardDefinitionRuntime.h" #include "common/definitions/DefinitionRuntime.h" #include "definitions/Definitions.h" #include "effects/EffectDefinitionRuntime.h" #include #include #include #include namespace { struct TypedState { int value = 0; std::string label; }; eve::PersistentId testId() { const auto parsed = eve::PersistentId::parse("11111111-1111-7111-8111-111111111111"); return *parsed; } eve::DefinitionRef testReference(const char* text) { auto parsed = eve::DefinitionRef::parse(text); if (!parsed.ok()) return {}; return std::move(parsed).takeValue(); } eve::SnapshotHashProvider testHash() { return [](std::string_view input) -> eve::Result { eve::ContentId::Bytes bytes{}; std::uint8_t value = 29; for (std::size_t index = 0; index < bytes.size(); ++index) { for (const unsigned char byte : input) value = static_cast(value * 33u + byte); bytes[index] = static_cast(value + index); } return eve::Result::success(eve::ContentId(bytes)); }; } } // namespace TEST_CASE("definitionRuntime.policiesAndGenerationQualifiedStaleChecks") { const auto reference = testReference("test:unit"); auto created = eve::definition::RuntimeInstance::create(testId(), reference, eve::Generation(1), TypedState{7, "original"}); REQUIRE(created.ok()); auto runtime = std::move(created).takeValue(); auto wrongGeneration = runtime.checkDefinition(eve::definition::DefinitionHandle{reference, eve::Generation(2)}); CHECK(!wrongGeneration.ok()); CHECK(static_cast(wrongGeneration.error()->code()) == static_cast(eve::DiagnosticCode::StaleHandle)); auto kept = runtime.reload(eve::definition::DefinitionHandle{reference, eve::Generation(2)}, eve::definition::ReloadPolicy::KeepInstanceValues, TypedState{99, "new defaults"}); REQUIRE(kept.ok()); CHECK(static_cast(kept.value().disposition) == static_cast(eve::definition::ReloadDisposition::Kept)); CHECK_EQ(runtime.state().value, 7); CHECK_EQ(runtime.identity().definitionGeneration.value(), uint64_t{2}); auto reapplied = runtime.reload(eve::definition::DefinitionHandle{reference, eve::Generation(3)}, eve::definition::ReloadPolicy::ReapplyDefaults, TypedState{42, "defaults"}); REQUIRE(reapplied.ok()); CHECK(static_cast(reapplied.value().disposition) == static_cast(eve::definition::ReloadDisposition::DefaultsReapplied)); CHECK_EQ(runtime.state().value, 42); CHECK_EQ(runtime.state().label, std::string("defaults")); auto rebuilt = runtime.reload(eve::definition::DefinitionHandle{reference, eve::Generation(4)}, eve::definition::ReloadPolicy::RebuildInstance, TypedState{100, "ignored"}, [](const TypedState& oldState, const eve::definition::InstanceIdentity& identity, const eve::definition::DefinitionHandle& handle) -> eve::Result { if (!identity.isValid() || !handle.isValid()) return eve::Result::failure(eve::Diagnostic::error( eve::DiagnosticCode::InvalidArgument, "test rebuild received invalid identity")); return eve::Result::success(TypedState{oldState.value + 1, "rebuilt"}); }); REQUIRE(rebuilt.ok()); CHECK(static_cast(rebuilt.value().disposition) == static_cast(eve::definition::ReloadDisposition::Rebuilt)); CHECK_EQ(runtime.state().value, 43); auto failedRebuild = runtime.reload(eve::definition::DefinitionHandle{reference, eve::Generation(5)}, eve::definition::ReloadPolicy::RebuildInstance, TypedState{0, "must not apply"}, [](const TypedState&, const eve::definition::InstanceIdentity&, const eve::definition::DefinitionHandle&) -> eve::Result { return eve::Result::failure(eve::Diagnostic::error( eve::DiagnosticCode::Failed, "injected rebuild failure")); }); CHECK(!failedRebuild.ok()); CHECK_EQ(runtime.identity().definitionGeneration.value(), uint64_t{4}); CHECK_EQ(runtime.state().value, 43); auto rejected = runtime.reload(eve::definition::DefinitionHandle{reference, eve::Generation(5)}, eve::definition::ReloadPolicy::RejectWhileActive, TypedState{0, "must not apply"}); CHECK(!rejected.ok()); CHECK(static_cast(rejected.error()->code()) == static_cast(eve::DiagnosticCode::Conflict)); CHECK_EQ(runtime.identity().definitionGeneration.value(), uint64_t{4}); CHECK_EQ(runtime.state().value, 43); runtime.setActive(false); auto inactiveReject = runtime.reload(eve::definition::DefinitionHandle{reference, eve::Generation(5)}, eve::definition::ReloadPolicy::RejectWhileActive, TypedState{0, "still kept"}); REQUIRE(inactiveReject.ok()); CHECK(static_cast(inactiveReject.value().disposition) == static_cast(eve::definition::ReloadDisposition::Kept)); CHECK_EQ(runtime.identity().definitionGeneration.value(), uint64_t{5}); CHECK_EQ(runtime.state().value, 43); } TEST_CASE("definitionRuntime.cardAdapterProjectsTypedIdentityAndReloads") { eve::definitions::DefinitionRegistry registry; auto inserted = registry.insert( "card", "scout", 1, R"({"name":"Scout","kind":"creature","cost":2,"attack":3,"health":5,"tags":["unit"]})"); REQUIRE(inserted.ok()); auto runtimeResult = eve::card::CardDefinitionRuntime::create(registry, testReference("card:scout"), testId(), eve::definition::ReloadPolicy::RebuildInstance); REQUIRE(runtimeResult.ok()); auto runtime = std::move(runtimeResult).takeValue(); CHECK_EQ(runtime.state().attack, 3); CHECK_EQ(runtime.identity().definitionGeneration.value(), uint64_t{1}); runtime.state().currentHealth = 2; auto snapshot = runtime.snapshot(eve::Revision(1), eve::SimulationTick(2), testHash()); REQUIRE(snapshot.ok()); runtime.state().currentHealth = 1; auto restored = runtime.restore(snapshot.value(), testHash()); REQUIRE(restored.ok()); CHECK_EQ(runtime.state().currentHealth, 2); auto card = eve::card::CardData::createCard(); REQUIRE(card != nullptr); auto projected = runtime.applyTo(card); REQUIRE(projected.ok()); CHECK_EQ(card->stats()->health, 2); CHECK_EQ(card->definitionBinding()->identity.instanceId, testId()); CHECK_EQ(card->definitionBinding()->identity.definitionGeneration.value(), uint64_t{1}); card->release(); auto replaced = registry.replace("card", "scout", 2, R"({"name":"Veteran Scout","kind":"creature","cost":3,"attack":6,"health":4})"); REQUIRE(replaced.ok()); auto staleSnapshot = runtime.restore(snapshot.value(), testHash()); CHECK(!staleSnapshot.ok()); CHECK_EQ(runtime.state().currentHealth, 2); auto reloaded = runtime.reload(eve::definition::ReloadPolicy::RebuildInstance); REQUIRE(reloaded.ok()); CHECK(static_cast(reloaded.value().disposition) == static_cast(eve::definition::ReloadDisposition::Rebuilt)); CHECK_EQ(runtime.state().attack, 6); CHECK_EQ(runtime.state().currentHealth, 2); CHECK_EQ(runtime.identity().definitionGeneration.value(), uint64_t{2}); } TEST_CASE("definitionRuntime.effectAdapterKeepsTypedPolicyAndRejectsActiveReload") { eve::definitions::DefinitionRegistry registry; auto inserted = registry.insert( "effect", "burn", 1, R"({"stackKey":"burn","priority":4,"duration":8.0,"magnitude":3.5,"stackCount":1,"maxStacks":3,"stackMode":"reuse","stackCountPolicy":"increment","payload":{"damageType":"fire"}})"); REQUIRE(inserted.ok()); auto runtimeResult = eve::effects::EffectDefinitionRuntime::create( registry, testReference("effect:burn"), "actor:target", "spell:ember", testId(), eve::definition::ReloadPolicy::RejectWhileActive); REQUIRE(runtimeResult.ok()); auto runtime = std::move(runtimeResult).takeValue(); CHECK(static_cast(runtime.state().policy.stackMode) == static_cast(eve::effects::StackMode::Reuse)); CHECK(static_cast(runtime.state().policy.stackCount) == static_cast(eve::effects::StackCountPolicy::Increment)); eve::effects::EffectInstance instance; auto projected = runtime.applyTo(&instance); REQUIRE(projected.ok()); CHECK(static_cast(instance.definitionIdentity == runtime.identity())); CHECK(static_cast(instance.policy.stackMode) == static_cast(eve::effects::StackMode::Reuse)); CHECK_EQ(instance.payload.getJson("damageType"), std::string("\"fire\"")); auto replaced = registry.replace( "effect", "burn", 2, R"({"stackKey":"burn","priority":5,"duration":10.0,"magnitude":4.0,"stackCount":1,"maxStacks":3})"); REQUIRE(replaced.ok()); auto rejected = runtime.reload(eve::definition::ReloadPolicy::RejectWhileActive); CHECK(!rejected.ok()); CHECK(static_cast(rejected.error()->code()) == static_cast(eve::DiagnosticCode::Conflict)); CHECK_EQ(runtime.identity().definitionGeneration.value(), uint64_t{1}); }