Implemented features: - High-performance Rust extension with Redis persistence - Actor/player management with loadout, position, and state tracking - Banking system with deposit, withdraw, and transfer operations - Physical and virtual garage/locker systems for vehicle and equipment storage - Organization management with member tracking and permissions - Client-side UI with React-like state management - Server-side event-driven architecture with CBA Events - Security: Self-transfer prevention at multiple layers - Logging system with per-module log files - ICOM module for inter-server communication Co-Authored-By: Warp <agent@warp.dev>
354 lines
12 KiB
Rust
354 lines
12 KiB
Rust
//! ICOM (Internal Communication) module for inter-server communication
|
|
//!
|
|
//! This module provides functionality for Arma 3 servers to communicate with each other
|
|
//! through a central ICOM hub server. It enables real-time event passing between multiple
|
|
//! game servers, allowing for synchronized gameplay experiences.
|
|
//!
|
|
//! # Architecture
|
|
//!
|
|
//! - **Extension Module**: This code runs inside the Arma 3 server extension
|
|
//! - **ICOM Client**: Connects to the central ICOM server on initialization
|
|
//! - **Event Listener**: Spawned task that continuously listens for incoming events
|
|
//! - **Event Forwarder**: Receives events and forwards them to Arma via callbacks
|
|
//!
|
|
//! # Event Flow
|
|
//!
|
|
//! 1. **Outgoing**: SQF → Extension command → ICOM client → ICOM server → Target server
|
|
//! 2. **Incoming**: ICOM server → Event listener → Forward helper → Arma callback → SQF
|
|
//!
|
|
//! # Usage from SQF
|
|
//!
|
|
//! ```sqf
|
|
//! // Send event to specific server
|
|
//! "forge_server" callExtension ["icom:send_event", ["server_2", "supply_drop", '{"coords":[1234,5678,0]}']]
|
|
//!
|
|
//! // Broadcast to all servers
|
|
//! "forge_server" callExtension ["icom:broadcast", ["global_alert", '{"message":"Event starting"}']]
|
|
//!
|
|
//! // Handle incoming events
|
|
//! ["forge_icom_event", {
|
|
//! params ["_eventName", "_data"];
|
|
//! // Process event based on _eventName
|
|
//! }] call CBA_fnc_addEventHandler;
|
|
//! ```
|
|
|
|
use arma_rs::{Context, Group};
|
|
use forge_icom::Message;
|
|
use forge_icom::client::IComClient;
|
|
use std::sync::OnceLock;
|
|
|
|
use crate::{CONTEXT, RUNTIME, log};
|
|
|
|
/// Global ICOM client, created once and shared by all commands.
|
|
/// Initialized asynchronously after extension loads.
|
|
pub static ICOM_CLIENT: OnceLock<IComClient> = OnceLock::new();
|
|
|
|
/// Initialize ICOM client connection
|
|
///
|
|
/// Establishes connection to the ICOM server and spawns a background listener task.
|
|
/// This function is called during extension startup (from `lib.rs`) or manually via
|
|
/// the `icom:connect` command.
|
|
///
|
|
/// # Process
|
|
///
|
|
/// 1. Stores Arma context globally for use in event forwarding callbacks
|
|
/// 2. Connects to ICOM server and registers with the provided server ID
|
|
/// 3. Spawns persistent listener task on the runtime's thread pool
|
|
/// 4. Listener automatically forwards incoming events to Arma via CBA
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `ctx` - Arma extension context for triggering CBA callbacks
|
|
/// * `address` - ICOM server address (e.g., "127.0.0.1:9090")
|
|
/// * `server_id` - Unique identifier for this server (e.g., "server_1")
|
|
///
|
|
/// # Behavior
|
|
///
|
|
/// - If connection fails, a warning is logged but the extension continues
|
|
/// - If already connected, logs an error and returns
|
|
/// - Listener runs indefinitely until connection is lost or extension unloads
|
|
pub async fn initialize(ctx: Context, address: String, server_id: String) {
|
|
// Store context in global CONTEXT
|
|
*CONTEXT.write().await = Some(ctx);
|
|
match IComClient::connect(&address, server_id).await {
|
|
Ok(client) => {
|
|
if ICOM_CLIENT.set(client).is_ok() {
|
|
log::log("icom", "INFO", "Connected to ICOM server");
|
|
|
|
// Spawn listener task
|
|
RUNTIME.spawn(async move {
|
|
if let Some(client) = ICOM_CLIENT.get() {
|
|
let result = client
|
|
.listen_for_events(|msg| {
|
|
match msg {
|
|
Message::Event {
|
|
event_name, data, ..
|
|
} => {
|
|
log::log(
|
|
"icom",
|
|
"INFO",
|
|
&format!("Received event '{}': {}", event_name, data),
|
|
);
|
|
|
|
// Forward event to Arma
|
|
forward(&event_name, &data);
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
})
|
|
.await;
|
|
|
|
if let Err(e) = result {
|
|
log::log("icom", "ERROR", &format!("Event listener error: {}", e));
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
log::log("icom", "ERROR", "Failed to set ICOM client (already set)");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
log::log(
|
|
"icom",
|
|
"WARN",
|
|
&format!("Failed to connect to ICOM server: {}", e),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create ICOM command group
|
|
///
|
|
/// Builds the command group exposed to SQF under the `icom:` namespace.
|
|
///
|
|
/// # Available Commands
|
|
///
|
|
/// - `connect` - Connect to the ICOM server manually
|
|
/// - `send_event` - Send event to a specific server
|
|
/// - `broadcast` - Broadcast event to all connected servers
|
|
pub fn group() -> Group {
|
|
Group::new()
|
|
.command("connect", connect)
|
|
.command("broadcast", broadcast)
|
|
.command("send_event", send_event)
|
|
}
|
|
|
|
/// Connect to ICOM server from SQF
|
|
///
|
|
/// Initiates connection to the ICOM server with custom parameters. Useful for:
|
|
/// - Retrying after initial connection failure
|
|
/// - Connecting to non-default ICOM server address
|
|
/// - Setting server ID at runtime instead of compile-time
|
|
///
|
|
/// The connection process runs asynchronously in the background. Check logs
|
|
/// for success/failure status.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `ctx` - Arma extension context (automatically provided by arma-rs)
|
|
/// * `address` - ICOM server address (e.g., "127.0.0.1:9090")
|
|
/// * `server_id` - Unique server identifier (e.g., "server_1", "server_2")
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// - `"Connection initiated"` - Background connection task started successfully
|
|
/// - `"ERROR: Already connected"` - Client is already connected (disconnect first)
|
|
///
|
|
/// # SQF Usage
|
|
///
|
|
/// ```sqf
|
|
/// private _result = "forge_server" callExtension ["icom:connect", ["127.0.0.1:9090", "server_1"]];
|
|
/// systemChat _result; // "Connection initiated"
|
|
/// // Check @forge_server/logs/icom.log for connection status
|
|
/// ```
|
|
fn connect(ctx: Context, address: String, server_id: String) -> String {
|
|
if ICOM_CLIENT.get().is_some() {
|
|
return "ERROR: Already connected".to_string();
|
|
}
|
|
|
|
RUNTIME.spawn(async move {
|
|
log::log(
|
|
"icom",
|
|
"INFO",
|
|
&format!("Connecting to {} as {}", address, server_id),
|
|
);
|
|
|
|
initialize(ctx, address, server_id).await;
|
|
});
|
|
|
|
"Connection initiated".to_string()
|
|
}
|
|
|
|
/// Broadcast an event to all connected servers
|
|
///
|
|
/// Sends an event to all servers currently connected to the ICOM hub,
|
|
/// except the sender itself.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `event_name` - Name of the event (e.g., "global_alert", "server_restart")
|
|
/// * `data` - JSON string containing event data
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// - `"OK"` if the broadcast was sent successfully
|
|
/// - `"ERROR: <reason>"` if broadcast failed
|
|
///
|
|
/// # SQF Usage
|
|
///
|
|
/// ```sqf
|
|
/// private _result = "forge_server" callExtension [
|
|
/// "icom:broadcast",
|
|
/// ["global_alert", '{"message":"Server restart in 5 minutes","severity":"warning"}']
|
|
/// ];
|
|
/// ```
|
|
fn broadcast(event_name: String, data: String) -> String {
|
|
let client = match ICOM_CLIENT.get() {
|
|
Some(c) => c,
|
|
None => {
|
|
log::log("icom", "ERROR", "ICOM client not connected");
|
|
return "ERROR: Not connected to ICOM server".to_string();
|
|
}
|
|
};
|
|
|
|
// Parse JSON data
|
|
let json_data: serde_json::Value = match serde_json::from_str(&data) {
|
|
Ok(d) => d,
|
|
Err(e) => {
|
|
log::log("icom", "ERROR", &format!("Invalid JSON data: {}", e));
|
|
return format!("ERROR: Invalid JSON - {}", e);
|
|
}
|
|
};
|
|
|
|
log::log(
|
|
"icom",
|
|
"INFO",
|
|
&format!("Broadcasting event '{}'", event_name),
|
|
);
|
|
|
|
// Broadcast the event asynchronously
|
|
RUNTIME.spawn(async move {
|
|
if let Err(e) = client.broadcast(&event_name, json_data).await {
|
|
log::log("icom", "ERROR", &format!("Failed to send event: {}", e));
|
|
}
|
|
});
|
|
|
|
"OK".to_string()
|
|
}
|
|
|
|
/// Send an event to a specific server
|
|
///
|
|
/// Sends a custom event with arbitrary JSON data to another server connected to ICOM.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `target_server` - The server ID to send the event to (e.g., "server_2")
|
|
/// * `event_name` - Name of the event (e.g., "supply_drop", "spawn_mission")
|
|
/// * `data` - JSON string containing event data
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// - `"OK"` if the event was sent successfully
|
|
/// - `"ERROR: <reason>"` if sending failed
|
|
///
|
|
/// # SQF Usage
|
|
///
|
|
/// ```sqf
|
|
/// private _result = "forge_server" callExtension [
|
|
/// "icom:send_event",
|
|
/// ["server_2", "supply_drop", '{"coords":[1234,5678,0],"supplies":["ammo","medical"]}']
|
|
/// ];
|
|
/// ```
|
|
fn send_event(target_server: String, event_name: String, data: String) -> String {
|
|
let client = match ICOM_CLIENT.get() {
|
|
Some(c) => c,
|
|
None => {
|
|
log::log("icom", "ERROR", "ICOM client not connected");
|
|
return "ERROR: Not connected to ICOM server".to_string();
|
|
}
|
|
};
|
|
|
|
// Parse JSON data
|
|
let json_data: serde_json::Value = match serde_json::from_str(&data) {
|
|
Ok(d) => d,
|
|
Err(e) => {
|
|
log::log("icom", "ERROR", &format!("Invalid JSON data: {}", e));
|
|
return format!("ERROR: Invalid JSON - {}", e);
|
|
}
|
|
};
|
|
|
|
log::log(
|
|
"icom",
|
|
"INFO",
|
|
&format!("Sending event '{}' to '{}'", event_name, target_server),
|
|
);
|
|
|
|
// Send the event asynchronously
|
|
RUNTIME.spawn(async move {
|
|
if let Err(e) = client
|
|
.send_event(&target_server, &event_name, json_data)
|
|
.await
|
|
{
|
|
log::log("icom", "ERROR", &format!("Failed to send event: {}", e));
|
|
}
|
|
});
|
|
|
|
"OK".to_string()
|
|
}
|
|
|
|
/// Forward an ICOM event to Arma via callback
|
|
///
|
|
/// Internal helper function that takes an event received from ICOM and forwards it
|
|
/// to Arma 3 via the callback mechanism. The event is sent to SQF as a JSON array:
|
|
/// `["event_name", {data}]`
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `event_name` - The name of the event
|
|
/// * `data` - The event data as a JSON value
|
|
///
|
|
/// # Implementation Notes
|
|
///
|
|
/// Uses `try_read()` instead of blocking to avoid deadlocks when called from within
|
|
/// an async task running on the same runtime that manages the context lock.
|
|
///
|
|
/// The callback triggers the "forge_icom_event" CBA event in Arma, which should be
|
|
/// handled by mission code to process incoming inter-server events.
|
|
fn forward(event_name: &str, data: &serde_json::Value) {
|
|
// Use try_read to avoid blocking inside async context
|
|
let context_guard = match CONTEXT.try_read() {
|
|
Ok(guard) => guard,
|
|
Err(_) => {
|
|
log::log("icom", "WARN", "Could not acquire CONTEXT read lock");
|
|
return;
|
|
}
|
|
};
|
|
|
|
if let Some(ctx) = context_guard.as_ref() {
|
|
// Format as JSON array: ["event_name", data]
|
|
let event_data = serde_json::json!([event_name, data]);
|
|
let event_json =
|
|
serde_json::to_string(&event_data).unwrap_or_else(|_| "[\"error\",null]".to_string());
|
|
|
|
match ctx.callback_data("icom", "forge_icom_event", Some(event_json)) {
|
|
Ok(_) => {
|
|
log::log(
|
|
"icom",
|
|
"INFO",
|
|
&format!("Forwarded event '{}' to Arma", event_name),
|
|
);
|
|
}
|
|
Err(e) => {
|
|
log::log(
|
|
"icom",
|
|
"ERROR",
|
|
&format!("Failed to forward event to Arma: {}", e),
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
log::log("icom", "WARN", "Context not available for callback");
|
|
}
|
|
}
|