#include "weapon/WeaponDefinitionRuntime.h" #include "common/Value.h" #include "weapon/Weapon.h" #include #include #include #include #include #include #include #include namespace eve::weapon { namespace { template eve::Result invalid(std::string message, std::string path = {}) { return eve::Result::failure(eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, std::move(message), std::move(path), {}, "weapon.definition_runtime")); } const eve::Value* field(const eve::Value::Object& object, std::string_view name) { const auto it = object.find(std::string(name)); return it == object.end() ? nullptr : &it->second; } bool readText(const eve::Value::Object& object, std::string_view name, std::string& output) { const auto* value = field(object, name); if (value == nullptr) return true; const auto* text = value->getIf(); if (text == nullptr) return false; output = *text; return true; } bool readNumber(const eve::Value::Object& object, std::string_view name, float& output) { const auto* value = field(object, name); if (value == nullptr) return true; if (const auto* number = value->getIf()) { if (!std::isfinite(*number)) return false; output = static_cast(*number); return std::isfinite(output); } if (const auto* integer = value->getIf()) { output = static_cast(*integer); return std::isfinite(output); } return false; } bool readInteger(const eve::Value::Object& object, std::string_view name, int& output) { const auto* value = field(object, name); if (value == nullptr) return true; const auto* integer = value->getIf(); if (integer == nullptr || *integer < std::numeric_limits::min() || *integer > std::numeric_limits::max()) return false; output = static_cast(*integer); return true; } bool readBoolean(const eve::Value::Object& object, std::string_view name, bool& output) { const auto* value = field(object, name); if (value == nullptr) return true; const auto* boolean = value->getIf(); if (boolean == nullptr) return false; output = *boolean; return true; } bool readStringArray(const eve::Value::Object& object, std::string_view name, std::vector& output) { const auto* value = field(object, name); if (value == nullptr) return true; const auto* array = value->getIf(); if (array == nullptr) return false; std::vector candidate; candidate.reserve(array->size()); for (const auto& entry : *array) { const auto* text = entry.getIf(); if (text == nullptr || text->empty()) return false; candidate.push_back(*text); } std::sort(candidate.begin(), candidate.end()); if (std::adjacent_find(candidate.begin(), candidate.end()) != candidate.end()) return false; output = std::move(candidate); return true; } const char* resourceKindName(ResourceKind kind) noexcept { switch (kind) { case ResourceKind::None: return "none"; case ResourceKind::Ammo: return "ammo"; case ResourceKind::Mana: return "mana"; case ResourceKind::Charges: return "charges"; case ResourceKind::Stamina: return "stamina"; } return "none"; } ResourceKind resourceKindFromName(std::string_view name) noexcept { if (name == "ammo") return ResourceKind::Ammo; if (name == "mana") return ResourceKind::Mana; if (name == "charges") return ResourceKind::Charges; if (name == "stamina") return ResourceKind::Stamina; return ResourceKind::None; } AttackStage attackStageFromName(std::string_view name) noexcept { if (name == "windup") return AttackStage::Windup; if (name == "active") return AttackStage::Active; if (name == "recover") return AttackStage::Recover; return AttackStage::Idle; } eve::Result parseDefinition(const eve::definitions::Definition& source, const eve::DefinitionRef& reference) { auto parsed = eve::Value::fromJson(source.json); if (!parsed) return eve::Result::failure(parsed.status()); auto value = std::move(parsed).takeValue(); const auto* object = value.getIf(); if (object == nullptr) return invalid("weapon definition must be an object", "json"); WeaponDefinition result; result.id = std::string(reference.id().name()); std::string id = result.id; if (!readText(*object, "id", id) || id != result.id) return invalid("weapon definition id does not match its logical reference", "id"); std::string kind = weaponKindName(result.kind); std::string logic = result.logic; if (!readText(*object, "kind", kind) || !readText(*object, "logic", logic)) return invalid("weapon kind and logic must be strings", "definition"); result.kind = weaponKindFromName(kind); result.logic = std::move(logic); if (!readNumber(*object, "damage", result.damage) || !readNumber(*object, "penetration", result.penetration) || !readNumber(*object, "range", result.range) || !readNumber(*object, "spread", result.spread) || !readNumber(*object, "falloffStart", result.falloffStart) || !readNumber(*object, "minimumDamageFactor", result.minimumDamageFactor) || !readNumber(*object, "splashMinimumDamageFactor", result.splashMinimumDamageFactor) || !readNumber(*object, "preferredTargetBonus", result.preferredTargetBonus) || !readNumber(*object, "accuracy", result.accuracy) || !readNumber(*object, "scatterRadius", result.scatterRadius) || !readNumber(*object, "spreadMin", result.spreadMin) || !readNumber(*object, "spreadMax", result.spreadMax) || !readNumber(*object, "spreadPerShot", result.spreadPerShot) || !readNumber(*object, "spreadRecover", result.spreadRecover) || !readNumber(*object, "recoilPitch", result.recoilPitch) || !readNumber(*object, "recoilYaw", result.recoilYaw) || !readNumber(*object, "recoilRecover", result.recoilRecover) || !readNumber(*object, "zoomFov", result.zoomFov) || !readNumber(*object, "cooldown", result.cooldown) || !readNumber(*object, "arc", result.arc)) return invalid("weapon numeric fields must be finite numbers", "definition"); if (result.falloffStart < 0.0f || result.falloffStart > result.range || result.minimumDamageFactor < 0.0f || result.minimumDamageFactor > 1.0f || result.splashMinimumDamageFactor < 0.0f || result.splashMinimumDamageFactor > 1.0f || result.accuracy < 0.0f || result.accuracy > 1.0f || result.scatterRadius < 0.0f || result.preferredTargetBonus < 1.0f) return invalid("weapon damage falloff fields are outside valid ranges", "definition"); if (!readText(*object, "damageType", result.damageType) || !readText(*object, "element", result.element) || !readBoolean(*object, "targetsGround", result.targetsGround) || !readBoolean(*object, "targetsAir", result.targetsAir) || !readBoolean(*object, "friendlyFire", result.friendlyFire) || !readBoolean(*object, "blockedByObstacles", result.blockedByObstacles) || !readStringArray(*object, "requiredTargetTags", result.requiredTargetTags) || !readStringArray(*object, "excludedTargetTags", result.excludedTargetTags) || !readStringArray(*object, "preferredTargetTags", result.preferredTargetTags)) return invalid("weapon target policy fields are invalid", "definition"); if (!result.targetsGround && !result.targetsAir) return invalid("weapon must target ground, air, or both", "definition"); if (const auto* modes = field(*object, "fireModes")) { const auto* array = modes->getIf(); if (array == nullptr) return invalid("fireModes must be an array", "fireModes"); for (const auto& entry : *array) { const auto* text = entry.getIf(); if (text == nullptr) return invalid("fire mode must be a string", "fireModes"); result.selectableModes.push_back(fireModeFromName(*text)); } } std::string fireMode = fireModeName(result.fireMode); if (!readText(*object, "fireMode", fireMode)) return invalid("fireMode must be a string", "fireMode"); result.fireMode = fireModeFromName(fireMode); if (const auto* resource = field(*object, "resource")) { const auto* nested = resource->getIf(); if (nested == nullptr) return invalid("resource must be an object", "resource"); std::string resourceName = resourceKindName(result.resource.kind); if (!readText(*nested, "kind", resourceName) || !readNumber(*nested, "max", result.resource.max) || !readNumber(*nested, "regen", result.resource.regen) || !readNumber(*nested, "cost", result.resource.cost) || !readBoolean(*nested, "infinite", result.resource.infinite)) return invalid("weapon resource fields are invalid", "resource"); result.resource.kind = resourceKindFromName(resourceName); } if (const auto* stages = field(*object, "stages")) { const auto* nested = stages->getIf(); if (nested == nullptr || !readNumber(*nested, "windup", result.stages.windupTime) || !readNumber(*nested, "active", result.stages.activeTime) || !readNumber(*nested, "recover", result.stages.recoverTime)) return invalid("stages fields are invalid", "stages"); } if (const auto* burst = field(*object, "burst")) { const auto* nested = burst->getIf(); if (nested == nullptr || !readInteger(*nested, "size", result.burstSize) || !readNumber(*nested, "interval", result.burstInterval)) return invalid("burst fields are invalid", "burst"); } if (const auto* ammo = field(*object, "ammo")) { const auto* nested = ammo->getIf(); if (nested == nullptr || !readInteger(*nested, "mag", result.magSize) || !readInteger(*nested, "reserve", result.reserveSize) || !readNumber(*nested, "reload", result.reloadTime)) return invalid("ammo fields are invalid", "ammo"); } if (const auto* projectile = field(*object, "projectile")) { const auto* nested = projectile->getIf(); if (nested == nullptr || !readText(*nested, "type", result.projectile.type) || !readNumber(*nested, "speed", result.projectile.speed) || !readNumber(*nested, "gravity", result.projectile.gravity) || !readNumber(*nested, "aoe", result.projectile.aoe) || !readInteger(*nested, "pelletCount", result.projectile.pelletCount) || !readNumber(*nested, "pelletSpread", result.projectile.pelletSpread)) return invalid("projectile fields are invalid", "projectile"); } if (const auto* effects = field(*object, "effects")) { const auto* nested = effects->getIf(); if (nested == nullptr || !readText(*nested, "muzzle", result.effectMuzzle) || !readText(*nested, "sound", result.effectSound)) return invalid("effects fields are invalid", "effects"); } if (result.resource.kind == ResourceKind::None) result.resource.infinite = true; return eve::Result::success(std::move(result)); } eve::Result currentHandle(eve::definitions::DefinitionRegistry& registry, const eve::DefinitionRef& reference) { const auto& logical = reference.id(); return registry.handle(std::string(logical.namespaceName()), std::string(logical.name())); } eve::Result definitionFor(eve::definitions::DefinitionRegistry& registry, const eve::definition::DefinitionHandle& handle, const eve::DefinitionRef& reference) { auto resolved = registry.resolveHandle(handle); if (!resolved) return eve::Result::failure(resolved.status()); return parseDefinition(resolved.value().get(), reference); } eve::LogicalId runtimeSchema() { static const eve::LogicalId value = [] { auto parsed = eve::LogicalId::parse("weapon:runtime"); if (!parsed) throw std::logic_error("invalid built-in weapon snapshot schema"); return *parsed; }(); return value; } WeaponRuntimeState initialState(const WeaponDefinition& definition) { WeaponRuntimeState result; result.selector = definition.fireMode; result.currentSpread = definition.spreadMin; result.resource.kind = definition.resource.kind; result.resource.max = definition.resource.max; result.resource.regen = definition.resource.regen; result.resource.cost = definition.resource.cost; result.resource.infinite = definition.resource.infinite; if (definition.kind == WeaponKind::Ranged) { result.resource.kind = ResourceKind::Ammo; result.resource.max = static_cast(definition.magSize); result.resource.value = static_cast(definition.magSize); result.resource.cost = 1.f; result.resource.reserve = definition.reserveSize < 0 ? 0 : definition.reserveSize; result.resource.infinite = definition.reserveSize < 0; } else { result.resource.value = result.resource.infinite ? 0.f : result.resource.max; } return result; } eve::Value encodeState(const WeaponRuntimeState& state) { const Resource& resource = state.resource; eve::Value::Object resourceObject{ {"cost", eve::Value(resource.cost)}, {"infinite", eve::Value(resource.infinite)}, {"kind", eve::Value(resourceKindName(resource.kind))}, {"max", eve::Value(resource.max)}, {"regen", eve::Value(resource.regen)}, {"reloadProgress", eve::Value(resource.reloadProgress)}, {"reloading", eve::Value(resource.reloading)}, {"reserve", eve::Value(resource.reserve)}, {"value", eve::Value(resource.value)}, }; return eve::Value(eve::Value::Object{ {"aiming", eve::Value(state.aiming)}, {"burstRemaining", eve::Value(state.burstRemaining)}, {"burstTimer", eve::Value(state.burstTimer)}, {"cooldown", eve::Value(state.cooldown)}, {"currentSpread", eve::Value(state.currentSpread)}, {"jammed", eve::Value(state.jammed)}, {"recoilPitch", eve::Value(state.recoilPitch)}, {"recoilYaw", eve::Value(state.recoilYaw)}, {"resource", eve::Value(std::move(resourceObject))}, {"selector", eve::Value(fireModeName(state.selector))}, {"stage", eve::Value(attackStageName(state.stage))}, {"stageTimer", eve::Value(state.stageTimer)}, }); } bool readStateNumber(const eve::Value::Object& object, const char* name, float& output) { return readNumber(object, name, output); } eve::Result decodeState(const eve::Value& value) { const auto* object = value.getIf(); if (object == nullptr) return invalid("weapon runtime state must be an object", "state"); static const std::set fields = {"aiming", "burstRemaining", "burstTimer", "cooldown", "currentSpread", "jammed", "recoilPitch", "recoilYaw", "resource", "selector", "stage", "stageTimer"}; for (const auto& [name, unused] : *object) { (void)unused; if (!fields.contains(name)) return invalid("unknown weapon runtime state field", "state." + name); } for (const auto& name : fields) if (!object->contains(name)) return invalid("weapon runtime state is missing a field", "state." + name); WeaponRuntimeState result; std::string selector; std::string stage; if (!readBoolean(*object, "aiming", result.aiming) || !readInteger(*object, "burstRemaining", result.burstRemaining) || !readStateNumber(*object, "burstTimer", result.burstTimer) || !readStateNumber(*object, "cooldown", result.cooldown) || !readStateNumber(*object, "currentSpread", result.currentSpread) || !readBoolean(*object, "jammed", result.jammed) || !readStateNumber(*object, "recoilPitch", result.recoilPitch) || !readStateNumber(*object, "recoilYaw", result.recoilYaw) || !readText(*object, "selector", selector) || !readText(*object, "stage", stage) || !readStateNumber(*object, "stageTimer", result.stageTimer)) return invalid("weapon runtime scalar field is invalid", "state"); result.selector = fireModeFromName(selector); result.stage = attackStageFromName(stage); const auto* resourceValue = object->at("resource").getIf(); if (resourceValue == nullptr) return invalid("weapon runtime resource is invalid", "state.resource"); std::string kind; if (!readText(*resourceValue, "kind", kind) || !readStateNumber(*resourceValue, "value", result.resource.value) || !readStateNumber(*resourceValue, "max", result.resource.max) || !readStateNumber(*resourceValue, "regen", result.resource.regen) || !readStateNumber(*resourceValue, "cost", result.resource.cost) || !readBoolean(*resourceValue, "infinite", result.resource.infinite) || !readInteger(*resourceValue, "reserve", result.resource.reserve) || !readBoolean(*resourceValue, "reloading", result.resource.reloading) || !readStateNumber(*resourceValue, "reloadProgress", result.resource.reloadProgress)) return invalid("weapon runtime resource fields are invalid", "state.resource"); result.resource.kind = resourceKindFromName(kind); return eve::Result::success(std::move(result)); } } // namespace eve::Result parseWeaponDefinition(const eve::definitions::Definition& source) { auto logical = eve::LogicalId::fromParts("weapon", source.id); if (!logical) return invalid("weapon definition id is invalid", "id"); auto reference = eve::DefinitionRef::fromId(*logical); if (!reference) return eve::Result::failure(reference.status()); if (source.type != "weapon") return invalid("definition type must be weapon", "type"); return parseDefinition(source, reference.value()); } eve::Result WeaponDefinitionRuntime::create(eve::definitions::DefinitionRegistry& registry, eve::DefinitionRef definition, eve::PersistentId instanceId, eve::definition::ReloadPolicy policy) { if (!definition.id().isValid() || definition.id().namespaceName() != "weapon") return invalid("weapon definition reference must use weapon namespace", "definition"); auto handle = currentHandle(registry, definition); if (!handle) return eve::Result::failure(handle.status()); auto typed = definitionFor(registry, handle.value(), definition); if (!typed) return eve::Result::failure(typed.status()); auto runtime = eve::definition::RuntimeInstance::create( instanceId, std::move(definition), handle.value().generation, initialState(typed.value())); if (!runtime) return eve::Result::failure(runtime.status()); return eve::Result::success( WeaponDefinitionRuntime(registry, std::move(runtime).takeValue(), policy)); } eve::Result WeaponDefinitionRuntime::create(eve::definitions::DefinitionRegistry& registry, std::string_view definitionId, eve::PersistentId instanceId, eve::definition::ReloadPolicy policy) { auto logical = eve::LogicalId::fromParts("weapon", definitionId); if (!logical) return invalid("weapon id is not a valid logical name", "definitionId"); auto reference = eve::DefinitionRef::fromId(*logical); if (!reference) return eve::Result::failure(reference.status()); return create(registry, std::move(reference).takeValue(), instanceId, policy); } eve::Result WeaponDefinitionRuntime::create(Weapon& module, std::string_view definitionId, eve::PersistentId instanceId, eve::definition::ReloadPolicy policy) { return create(module.definitionRegistry(), definitionId, instanceId, policy); } const eve::definition::InstanceIdentity& WeaponDefinitionRuntime::identity() const noexcept { return runtime_.identity(); } const WeaponRuntimeState& WeaponDefinitionRuntime::state() const noexcept { return runtime_.state(); } WeaponRuntimeState& WeaponDefinitionRuntime::state() noexcept { return runtime_.state(); } eve::definition::DefinitionHandle WeaponDefinitionRuntime::definitionHandle() const noexcept { return runtime_.identity().definitionHandle(); } eve::Result WeaponDefinitionRuntime::definition() const { if (registry_ == nullptr) return invalid("weapon definition registry is not bound", "registry"); return definitionFor(*registry_, definitionHandle(), identity().definition); } void WeaponDefinitionRuntime::setActive(bool active) noexcept { runtime_.setActive(active); } bool WeaponDefinitionRuntime::isActive() const noexcept { return runtime_.isActive(); } eve::Result WeaponDefinitionRuntime::applyTo(WeaponEntity* entity) const { if (entity == nullptr) return eve::Result::failure(eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "weapon runtime cannot project to a null WeaponEntity", "entity", {}, "weapon.definition_runtime")); auto typed = definition(); if (!typed) return eve::Result::failure(typed.status()); try { auto definitionOwner = std::make_shared(typed.value()); auto definitionComponent = *entity->definition(); auto binding = *entity->definitionBinding(); auto targetState = *entity->state(); definitionComponent.owned = std::move(definitionOwner); definitionComponent.def = definitionComponent.owned.get(); binding.identity = identity(); binding.reloadPolicy = policy_; binding.active = isActive(); const auto& source = state(); targetState.resource = source.resource; targetState.cooldown = source.cooldown; targetState.burstRemaining = source.burstRemaining; targetState.burstTimer = source.burstTimer; targetState.jammed = source.jammed; targetState.stage = source.stage; targetState.stageTimer = source.stageTimer; targetState.stages = &definitionComponent.def->stages; targetState.currentSpread = source.currentSpread; targetState.recoilPitch = source.recoilPitch; targetState.recoilYaw = source.recoilYaw; targetState.selector = source.selector; targetState.aiming = source.aiming; using std::swap; swap(*entity->definition(), definitionComponent); swap(*entity->definitionBinding(), binding); swap(*entity->state(), targetState); return eve::Result::success(); } catch (const std::exception&) { return eve::Result::failure(eve::Diagnostic::error(eve::DiagnosticCode::Failed, "weapon runtime projection allocation failed", "entity", {}, "weapon.definition_runtime")); } } eve::Result WeaponDefinitionRuntime::reload(eve::definition::ReloadPolicy policy) { if (registry_ == nullptr) return invalid("weapon definition registry is not bound", "registry"); auto next = currentHandle(*registry_, identity().definition); if (!next) return eve::Result::failure(next.status()); auto typed = definitionFor(*registry_, next.value(), identity().definition); if (!typed) return eve::Result::failure(typed.status()); const WeaponRuntimeState defaults = initialState(typed.value()); const float maxSpread = std::max(defaults.currentSpread, typed.value().spreadMax); auto rebuild = [defaults, maxSpread](const WeaponRuntimeState& oldState, const eve::definition::InstanceIdentity&, const eve::definition::DefinitionHandle&) { WeaponRuntimeState rebuilt = oldState; if (oldState.resource.kind != defaults.resource.kind) { rebuilt.resource = defaults.resource; } else { rebuilt.resource.kind = defaults.resource.kind; rebuilt.resource.max = defaults.resource.max; rebuilt.resource.regen = defaults.resource.regen; rebuilt.resource.cost = defaults.resource.cost; rebuilt.resource.infinite = defaults.resource.infinite; if (rebuilt.resource.infinite) { rebuilt.resource.value = 0.f; } else { rebuilt.resource.value = std::clamp(rebuilt.resource.value, 0.f, rebuilt.resource.max); } rebuilt.resource.reserve = defaults.resource.reserve < 0 ? 0 : rebuilt.resource.reserve; } rebuilt.selector = defaults.selector; rebuilt.currentSpread = std::clamp(oldState.currentSpread, 0.f, maxSpread); return eve::Result::success(std::move(rebuilt)); }; auto result = runtime_.reload(next.value(), policy, defaults, std::move(rebuild)); if (result.ok()) policy_ = policy; return result; } eve::Result WeaponDefinitionRuntime::snapshot( eve::Revision revision, eve::SimulationTick tick, const eve::SnapshotHashProvider& hashProvider) const { const eve::definition::RuntimeStateEncoder encoder = [](const WeaponRuntimeState& value) { return eve::Result::success(encodeState(value)); }; return eve::definition::snapshotRuntimeInstance(runtime_, "weapon.runtime", runtimeSchema(), eve::SchemaVersion(1), revision, tick, hashProvider, encoder); } eve::Result WeaponDefinitionRuntime::snapshotJson(eve::Revision revision, eve::SimulationTick tick, const eve::SnapshotHashProvider& hashProvider) const { auto result = snapshot(revision, tick, hashProvider); if (!result) return eve::Result::failure(result.status()); return std::move(result).andThen( [](eve::SnapshotEnvelope&& value) { return eve::serializeSnapshotEnvelope(value); }); } eve::Result WeaponDefinitionRuntime::restore(const eve::SnapshotEnvelope& snapshotValue, const eve::SnapshotHashProvider& hashProvider) { if (registry_ == nullptr) return invalid("weapon definition registry is not bound", "registry"); auto current = currentHandle(*registry_, identity().definition); if (!current) return eve::Result::failure(current.status()); if (current.value() != definitionHandle()) return eve::Result::failure(eve::Diagnostic::error( eve::DiagnosticCode::StaleHandle, "weapon snapshot cannot restore against a replaced definition", "definitionGeneration", {}, "weapon.definition_runtime")); const eve::definition::RuntimeStateDecoder decoder = decodeState; return eve::definition::restoreRuntimeInstance(runtime_, snapshotValue, "weapon.runtime", runtimeSchema(), eve::SchemaVersion(1), hashProvider, decoder); } } // namespace eve::weapon