#include "weapon/ProjectileRuntime.h" #include "common/Diagnostic.h" #include #include #include #include namespace eve::weapon { namespace { constexpr std::uint32_t kDefaultCapacity = 128; constexpr std::uint32_t kMaximumCapacity = 1024 * 1024; constexpr double kPi = 3.14159265358979323846; Result projectileError(DiagnosticCode code, std::string message, std::string path = {}) { return Result::failure(Diagnostic::error(code, std::move(message), std::move(path))); } template Result projectileValueError(DiagnosticCode code, std::string message, std::string path = {}) { return Result::failure(Diagnostic::error(code, std::move(message), std::move(path))); } bool finite(const ProjectilePoint& value) { return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z); } bool finite(const ProjectileVector& value) { return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z); } double length(const ProjectileVector& value) { return std::sqrt(value.x * value.x + value.y * value.y + value.z * value.z); } ProjectileVector normalized(const ProjectileVector& value) { const double magnitude = length(value); return {value.x / magnitude, value.y / magnitude, value.z / magnitude}; } ProjectileVector directionTo(const ProjectilePoint& from, const ProjectilePoint& to) { return {to.x - from.x, to.y - from.y, to.z - from.z}; } ProjectileVector steer(ProjectileVector velocity, ProjectileVector desired, double maxRadians) { const double speed = length(velocity); if (speed <= 0.0) return velocity; ProjectileVector current = normalized(velocity); desired = normalized(desired); const double dot = std::clamp(current.x * desired.x + current.y * desired.y + current.z * desired.z, -1.0, 1.0); const double angle = std::acos(dot); if (angle <= maxRadians || angle <= 1e-12) return {desired.x * speed, desired.y * speed, desired.z * speed}; const double alpha = maxRadians / angle; ProjectileVector blended{current.x + (desired.x - current.x) * alpha, current.y + (desired.y - current.y) * alpha, current.z + (desired.z - current.z) * alpha}; blended = normalized(blended); return {blended.x * speed, blended.y * speed, blended.z * speed}; } } // namespace Result ProjectileDefinition::validate() const { if (!id.isValid()) return projectileError(DiagnosticCode::InvalidArgument, "Projectile id must not be empty", "id"); if (!std::isfinite(speed) || speed <= 0.0) return projectileError(DiagnosticCode::InvalidArgument, "Projectile speed must be finite and positive", "speed"); if (!std::isfinite(gravity) || gravity < 0.0) return projectileError(DiagnosticCode::InvalidArgument, "Projectile gravity must be finite and non-negative", "gravity"); if (!std::isfinite(maxTurnRateDegrees) || maxTurnRateDegrees < 0.0) return projectileError(DiagnosticCode::InvalidArgument, "Projectile turn rate must be finite and non-negative", "maxTurnRateDegrees"); if (lifetime <= Duration::zero()) return projectileError(DiagnosticCode::InvalidArgument, "Projectile lifetime must be positive", "lifetime"); if (mode == ProjectileMode::Homing && maxTurnRateDegrees <= 0.0) return projectileError(DiagnosticCode::InvalidArgument, "Homing projectile turn rate must be positive", "maxTurnRateDegrees"); return Result::success(); } ProjectileRuntime::ProjectileRuntime() : slots_(kDefaultCapacity) {} Result ProjectileRuntime::configurePool(std::uint32_t capacity) { if (activeCount_ != 0) return projectileError(DiagnosticCode::Conflict, "Projectile pool cannot be resized while active"); if (capacity == 0 || capacity > kMaximumCapacity) return projectileError(DiagnosticCode::InvalidArgument, "Projectile pool capacity must be in 1..1048576", "capacity"); slots_.assign(capacity, Slot{}); return Result::success(); } Result ProjectileRuntime::validateSpawn(const ProjectileDefinition& definition, const ProjectileSpawnRequest& request) { auto valid = definition.validate(); if (!valid) return valid; if (!finite(request.position) || !finite(request.direction)) return projectileError(DiagnosticCode::InvalidArgument, "Projectile transform must contain finite values", "request"); if (length(request.direction) <= 1e-12) return projectileError(DiagnosticCode::InvalidArgument, "Projectile direction must be non-zero", "direction"); if (definition.mode == ProjectileMode::Homing && !request.target) return projectileError(DiagnosticCode::InvalidArgument, "Homing projectile requires a target", "target"); return Result::success(); } Result ProjectileRuntime::spawn(const ProjectileDefinition& definition, const ProjectileSpawnRequest& request) { auto valid = validateSpawn(definition, request); if (!valid) return Result::failure(valid.status()); auto free = std::find_if(slots_.begin(), slots_.end(), [](const Slot& slot) { return !slot.state.has_value(); }); if (free == slots_.end()) return projectileValueError(DiagnosticCode::Conflict, "Projectile pool is exhausted"); const std::size_t index = static_cast(std::distance(slots_.begin(), free)); ++free->generation; if (free->generation == 0) ++free->generation; const ProjectileVector direction = normalized(request.direction); ProjectileState state; state.handle = {static_cast(index), free->generation}; state.definitionId = definition.id; state.mode = definition.mode; state.position = request.position; state.velocity = {direction.x * definition.speed, direction.y * definition.speed, direction.z * definition.speed}; state.gravity = definition.gravity; state.maxTurnRateDegrees = definition.maxTurnRateDegrees; state.lifetime = definition.lifetime; state.target = request.target; free->state = state; ++activeCount_; return Result::success(state.handle); } Result ProjectileRuntime::update(Duration delta, const IProjectileTargetProvider* targets) { if (delta < Duration::zero()) return projectileValueError(DiagnosticCode::InvalidArgument, "Projectile delta must be non-negative", "delta"); if (delta.isZero()) return Result::success({}, Status::success(StatusCode::NoOp)); std::vector staged = slots_; ProjectileUpdate update; const double seconds = delta.seconds(); for (Slot& slot : staged) { if (!slot.state) continue; ProjectileState& state = *slot.state; auto nextAge = state.age.tryAdd(delta); if (!nextAge) return Result::failure(nextAge.status()); state.age = std::move(nextAge).takeValue(); if (state.age >= state.lifetime) { update.released.push_back(state.handle); slot.state.reset(); continue; } if (state.mode == ProjectileMode::Homing) { if (!targets) return projectileValueError(DiagnosticCode::Unsupported, "Homing projectile target provider is unavailable"); auto target = targets->position(*state.target); if (!target) return Result::failure(target.status()); ProjectileVector desired = directionTo(state.position, target.value()); if (length(desired) <= 1e-12) return projectileValueError(DiagnosticCode::Conflict, "Homing projectile overlaps its target"); state.velocity = steer(state.velocity, desired, state.maxTurnRateDegrees * kPi / 180.0 * seconds); } if (state.mode == ProjectileMode::Ballistic) state.velocity.y -= state.gravity * seconds; state.position.x += state.velocity.x * seconds; state.position.y += state.velocity.y * seconds; state.position.z += state.velocity.z * seconds; update.advanced.push_back(state.handle); } slots_ = std::move(staged); activeCount_ -= update.released.size(); return Result::success(std::move(update)); } Result ProjectileRuntime::release(ProjectileHandle handle) { if (handle.slot >= slots_.size() || !slots_[handle.slot].state || slots_[handle.slot].generation != handle.generation) return projectileError(DiagnosticCode::StaleHandle, "Projectile handle is stale"); slots_[handle.slot].state.reset(); --activeCount_; return Result::success(); } std::optional ProjectileRuntime::find(ProjectileHandle handle) const { if (handle.slot >= slots_.size() || !slots_[handle.slot].state || slots_[handle.slot].generation != handle.generation) return std::nullopt; return slots_[handle.slot].state; } std::vector ProjectileRuntime::states() const { std::vector result; result.reserve(activeCount_); for (const Slot& slot : slots_) if (slot.state) result.push_back(*slot.state); return result; } ProjectileRuntimeSnapshot ProjectileRuntime::snapshot() const { ProjectileRuntimeSnapshot result; result.slots.reserve(slots_.size()); for (const Slot& slot : slots_) result.slots.push_back({slot.generation, slot.state}); return result; } Result ProjectileRuntime::restore(const ProjectileRuntimeSnapshot& snapshot) { if (snapshot.slots.empty() || snapshot.slots.size() > 1048576) return projectileError(DiagnosticCode::InvalidArgument, "Projectile snapshot pool capacity is outside 1..1048576"); std::vector staged(snapshot.slots.size()); std::size_t active = 0; for (std::size_t index = 0; index < snapshot.slots.size(); ++index) { const auto& input = snapshot.slots[index]; staged[index].generation = input.generation; if (!input.state) continue; const auto& state = *input.state; const auto finitePoint = [](const ProjectilePoint& point) { return std::isfinite(point.x) && std::isfinite(point.y) && std::isfinite(point.z); }; const auto finiteVector = [](const ProjectileVector& value) { return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z); }; if (state.handle.slot != index || state.handle.generation != input.generation || !state.definitionId.isValid() || !finitePoint(state.position) || !finiteVector(state.velocity) || !std::isfinite(state.gravity) || state.gravity < 0.0 || !std::isfinite(state.maxTurnRateDegrees) || state.maxTurnRateDegrees < 0.0 || state.age.nanoseconds() < 0 || state.lifetime.nanoseconds() <= 0 || state.age >= state.lifetime || (state.mode == ProjectileMode::Homing && !state.target)) return projectileError(DiagnosticCode::InvalidArgument, "Projectile snapshot contains an invalid live slot"); staged[index].state = state; ++active; } slots_ = std::move(staged); activeCount_ = active; return Result::success(); } } // namespace eve::weapon