- Replace bank payment flow with a checkout mutation using explicit source context - Return backend errors to players instead of silently falling back to local state - Queue hot state persistence for actors, garages, lockers, orgs, and owned assets
124 lines
4.1 KiB
Rust
124 lines
4.1 KiB
Rust
//! Entry point and runtime bootstrap for the Forge Arma server extension.
|
|
//!
|
|
//! Initializes a global async runtime, the Redis connection pool, and registers
|
|
//! all command groups. Provides status/version commands and maintains a shared
|
|
//! Arma `Context` for engine interop.
|
|
//!
|
|
#![allow(future_incompatible)] // Future-incompatible lint is triggered by arma_rs
|
|
|
|
use arma_rs::{Context, Extension, Group, arma};
|
|
use std::sync::{LazyLock, OnceLock, RwLock as StdRwLock};
|
|
use tokio::runtime::{Builder, Runtime};
|
|
use tokio::sync::RwLock as TokioRwLock;
|
|
|
|
pub mod actor;
|
|
pub mod adapters;
|
|
pub mod bank;
|
|
pub mod cad;
|
|
pub mod garage;
|
|
pub mod helpers;
|
|
pub mod icom;
|
|
pub mod locker;
|
|
mod log;
|
|
pub mod org;
|
|
pub mod redis;
|
|
pub mod terrain;
|
|
pub mod transport;
|
|
pub mod v_garage;
|
|
pub mod v_locker;
|
|
|
|
/// Global Arma `Context` captured at initialization and made available to
|
|
/// commands that need engine interop. Stored inside an async `RwLock` to
|
|
/// allow mutation by the startup task and later reads.
|
|
static CONTEXT: LazyLock<TokioRwLock<Option<Context>>> = LazyLock::new(|| TokioRwLock::new(None));
|
|
/// Global Redis connection pool, created once and shared by all commands.
|
|
/// Initialized asynchronously after `init()` returns so the extension starts
|
|
/// quickly without blocking the main thread.
|
|
static REDIS_POOL: OnceLock<redis::client::RedisClient> = OnceLock::new();
|
|
/// Global multi-threaded Tokio runtime used to execute async operations from
|
|
/// command handlers and startup tasks.
|
|
pub(crate) static RUNTIME: LazyLock<Runtime> = LazyLock::new(|| {
|
|
Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()
|
|
.expect("Failed to create tokio runtime")
|
|
});
|
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
/// Connection state for the Redis pool so SQF can gate behavior on readiness.
|
|
enum ConnectionState {
|
|
Initializing,
|
|
Connected,
|
|
Failed,
|
|
}
|
|
static CONNECTION_STATE: LazyLock<StdRwLock<ConnectionState>> =
|
|
LazyLock::new(|| StdRwLock::new(ConnectionState::Initializing));
|
|
|
|
pub(crate) fn enqueue_persistence_task<F>(module: &'static str, job: F)
|
|
where
|
|
F: FnOnce() -> Result<(), String> + Send + 'static,
|
|
{
|
|
RUNTIME.spawn_blocking(move || {
|
|
if let Err(error) = job() {
|
|
crate::log::log(
|
|
module,
|
|
"ERROR",
|
|
&format!("Async persistence failed: {}", error),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
#[arma]
|
|
/// Initializes the extension, registers commands/groups, and asynchronously
|
|
/// creates the Redis connection pool on the global runtime.
|
|
fn init() -> Extension {
|
|
let config = redis::config::load();
|
|
let ext = Extension::build()
|
|
.command("version", get_version)
|
|
.command("status", get_status)
|
|
.group("redis", redis::group())
|
|
.group("actor", actor::group())
|
|
.group("bank", bank::group())
|
|
.group("cad", cad::group())
|
|
.group("garage", garage::group())
|
|
.group("icom", icom::group())
|
|
.group("locker", locker::group())
|
|
.group("org", org::group())
|
|
.group("terrain", terrain::group())
|
|
.group("transport", transport::group())
|
|
.group(
|
|
"owned",
|
|
Group::new()
|
|
.group("garage", v_garage::group())
|
|
.group("locker", v_locker::group()),
|
|
)
|
|
.finish();
|
|
|
|
// Spawn initialization tasks for Redis and ICOM
|
|
// These run asynchronously and don't block extension startup
|
|
// Redis initialization will set the global CONTEXT
|
|
RUNTIME.spawn(async move {
|
|
redis::initialize(config.redis).await;
|
|
});
|
|
|
|
ext
|
|
}
|
|
|
|
/// Returns current Redis connection state as a string: `initializing`,
|
|
/// `connected`, or `failed`. Intended for SQF polling before issuing
|
|
/// operations that require Redis.
|
|
fn get_status() -> String {
|
|
let state = *CONNECTION_STATE.read().unwrap();
|
|
match state {
|
|
ConnectionState::Initializing => "initializing".into(),
|
|
ConnectionState::Connected => "connected".into(),
|
|
ConnectionState::Failed => "failed".into(),
|
|
}
|
|
}
|
|
|
|
/// Returns the extension version string for diagnostics and tooling.
|
|
pub fn get_version() -> String {
|
|
format!("forge-server v{}", env!("CARGO_PKG_VERSION"))
|
|
}
|