- add the imported server task addon to the current framework with task ownership, task catalog, mission-manager attack generation, org-owned reward routing, participant notifications, and reputation syncing - restructure org persistence so core org data, assets, fleet, and members are handled through the current Redis/extension model with matching Rust repository and service updates - wire the client CAD addon into the framework, actor device action, shared web UI bridge pattern, and task listing/acceptance flow - add a source-driven CAD web UI layout with ui.config.mjs and extend the shared web UI builder to support custom HTML template pages for multi-surface UIs
Forge Arma 3 Server Extension
This extension provides the core server-side functionality for the Forge framework, handling persistent data storage, actor management, and game state synchronization through a high-performance Rust backend.
Architecture
The extension follows a layered architecture designed for reliability, performance, and maintainability:
- Extension Layer: Handles the raw Arma 3
callExtensioninterface, parameter parsing, and command routing. - Service Layer: Implements business logic, validation, and orchestration of operations (e.g.,
ActorService). - Repository Layer: Manages data persistence and retrieval using Redis (e.g.,
RedisActorRepository). - Model Layer: Defines strict data structures and validation rules (e.g.,
Actormodel).
This separation ensures that game logic is decoupled from data storage and that all data entering the system is validated before persistence.
Module Documentation
For detailed information about specific modules, see:
- Redis Operations: Comprehensive guide to Redis commands (hash, list, set, common operations)
- Adapters: Adapter pattern implementation bridging repositories with Redis
Organization Management
The Organization module handles guild/clan management, allowing players to form groups, manage members, and persist organizational data. It supports role management, automatic UID resolution, and robust error handling.
Available Commands
| Command | Description |
|---|---|
org:get |
Retrieve organization data by key or ID. |
org:create |
Create a new organization with provided JSON data. |
org:update |
Update an existing organization with partial JSON data. |
org:delete |
Permanently remove an organization and its data. |
org:exists |
Check if an organization exists. |
org:get_members |
Retrieve a list of organization members. |
org:add_member |
Add a member to an organization. |
org:remove_member |
Remove a member from an organization. |
SQF Examples
Retrieving an Organization
// Get organization by ID
private _result = "forge_server" callExtension ["org:get", ["elite_squad"]];
private _orgData = fromJSON (_result select 0);
// Access data
private _name = _orgData get "name";
private _leader = _orgData get "leader";
Creating an Organization
// Prepare data using HashMap
private _data = createHashMapFromArray [
["name", "Elite Squad"],
["description", "Best players"],
["leader", getPlayerUID player],
["max_members", 50],
["type", "military"]
];
// Create the organization
private _result = "forge_server" callExtension ["org:create", ["elite_squad", toJSON _data]];
if ((_result select 0) find "Error:" == 0) then {
diag_log format ["Failed to create org: %1", _result select 0];
} else {
private _createdOrg = fromJSON (_result select 0);
systemChat format ["Created organization: %1", _createdOrg get "name"];
};
Updating an Organization
// Prepare partial update
private _update = createHashMapFromArray [
["description", "Updated description"],
["max_members", 100]
];
// Apply update
private _result = "forge_server" callExtension ["org:update", ["elite_squad", toJSON _update]];
Managing Members
// Get members
private _result = "forge_server" callExtension ["org:get_members", ["elite_squad"]];
private _members = fromJSON (_result select 0);
// Add a member
private _addResult = "forge_server" callExtension ["org:add_member", ["elite_squad", "76561198123456789"]];
// Remove a member
private _removeResult = "forge_server" callExtension ["org:remove_member", ["elite_squad", "76561198123456789"]];
Checking Existence
private _exists = "forge_server" callExtension ["org:exists", ["elite_squad"]];
if ((_exists select 0) == "true") then {
systemChat "Organization exists.";
};
Deleting an Organization
// Permanently delete organization
private _result = "forge_server" callExtension ["org:delete", ["elite_squad"]];
if ((_result select 0) == "OK") then {
systemChat "Organization deleted.";
};
Actor Management
The Actor module handles all player-related operations, including data retrieval, creation, updates, and existence checks. It features automatic Steam UID resolution and robust error handling.
Available Commands
| Command | Description |
|---|---|
actor:get |
Retrieve actor data by key or UID. |
actor:create |
Create a new actor with provided JSON data. |
actor:update |
Update an existing actor with partial JSON data. |
actor:exists |
Check if an actor exists in the database. |
actor:delete |
Permanently remove an actor and their data. |
SQF Examples
The extension is designed to work seamlessly with modern Arma 3 SQF features like HashMap and toJSON/fromJSON.
Retrieving an Actor
// Get actor by Steam UID
private _result = "forge_server" callExtension ["actor:get", ["76561198123456789"]];
private _actorData = fromJSON (_result select 0);
// Access data
private _name = _actorData get "name";
private _bank = _actorData get "bank";
Creating an Actor
// Prepare data using HashMap
private _data = createHashMapFromArray [
["name", "John Doe"],
["bank", 1000],
["cash", 100],
["level", 1],
["class", "civilian"]
];
// Create the actor
private _result = "forge_server" callExtension ["actor:create", ["player123", toJSON _data]];
if ((_result select 0) find "Error:" == 0) then {
diag_log format ["Failed to create actor: %1", _result select 0];
} else {
private _createdActor = fromJSON (_result select 0);
systemChat format ["Welcome, %1!", _createdActor get "name"];
};
Updating an Actor
// Prepare partial update
private _update = createHashMapFromArray [
["bank", 1500],
["level", 2]
];
// Apply update
private _result = "forge_server" callExtension ["actor:update", ["player123", toJSON _update]];
Checking Existence
private _exists = "forge_server" callExtension ["actor:exists", ["player123"]];
if ((_exists select 0) == "true") then {
systemChat "Player profile found.";
} else {
systemChat "Player profile not found.";
};
Deleting an Actor
// Permanently delete actor data
private _result = "forge_server" callExtension ["actor:delete", ["player123"]];
if ((_result select 0) == "OK") then {
systemChat "Actor deleted successfully.";
};
Error Handling
The extension uses a consistent error reporting format. If an operation fails, the returned string will start with Error: followed by a descriptive message.
- Consistent Responses: All commands return JSON on success or an error message on failure.
- No Fallbacks:
actor:getandorg:getwill return error messages if the requested entity cannot be found, rather than fallback objects with dummy data. - Validation: All input data is validated against the strict schema defined in the models. Invalid data will result in an error message.
Example Error Handling
private _result = "forge_server" callExtension ["actor:get", ["76561198123456789"]];
private _response = _result select 0;
if (_response find "Error:" == 0) then {
diag_log format ["Failed to get actor: %1", _response];
} else {
private _actorData = fromJSON _response;
systemChat format ["Welcome, %1!", _actorData get "name"];
};
Performance
- Asynchronous Core: Built on
tokio, the extension performs heavy I/O operations (like database writes) without blocking the Arma 3 simulation thread. - Connection Pooling: Uses a Redis connection pool to efficiently manage database connections.
- Lazy Initialization: Services are initialized only when first needed, reducing startup time.
- Minimal Serialization: Only necessary data is serialized and transferred between Rust and SQF to minimize overhead.
Contributing
We welcome contributions to the Forge Extension! This guide will help you understand how to add new commands and maintain the existing codebase.
Adding a Command to an Existing Module
To add a new command to an existing module (e.g., actor:set_position), follow these steps:
-
Register the Command: In the module file (e.g.,
src/actor.rs), add the command to thegroup()function.pub fn group() -> Group { Group::new() .command("get", get_actor) .command("exists", exists_actor) .command("create", create_actor) .command("update", update_actor) .command("delete", delete_actor) .command("set_position", set_actor_position) // New command } -
Implement the Handler Function: Create the function that handles the command logic.
use crate::log::log; /// Sets the position of an actor. pub fn set_actor_position(call_context: CallContext, key: String, position: String) -> String { log("actor", "DEBUG", &format!("Setting position for key: {}", key)); // 1. Resolve UID let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => uid, None => { let error_msg = format!("Error: Failed to resolve UID for key: {}", key); log("actor", "ERROR", &error_msg); return error_msg; } }; // 2. Parse and validate input let position_data: Vec<f64> = match serde_json::from_str(&position) { Ok(data) => data, Err(e) => { let error_msg = format!("Error: Invalid position JSON: {}", e); log("actor", "ERROR", &error_msg); return error_msg; } }; // 3. Get the actor, update position, and save match ACTOR_SERVICE.get_actor(resolved_uid.clone()) { Ok(mut actor) => { actor.set_position(position_data); match ACTOR_SERVICE.update_actor(actor.clone()) { Ok(_) => { log("actor", "INFO", &format!("Updated position for: {}", resolved_uid)); match serde_json::to_string(&actor) { Ok(json) => json, Err(e) => format!("Error: Failed to serialize actor: {}", e), } } Err(e) => format!("Error: {}", e), } } Err(e) => format!("Error: {}", e), } }
Creating a New Module
To create a new module (e.g., vehicle), follow these steps:
-
Create the Module File: Add
src/vehicle.rs. -
Create the Global Service Instance: Define a lazily initialized singleton service.
use std::sync::LazyLock; use forge_services::VehicleService; use forge_repositories::RedisVehicleRepository; use crate::adapters::ExtensionRedisClient; static VEHICLE_SERVICE: LazyLock<VehicleService<RedisVehicleRepository<ExtensionRedisClient>>> = LazyLock::new(|| { let redis_client = ExtensionRedisClient::new(); let repository = RedisVehicleRepository::new(redis_client); VehicleService::new(repository) }); -
Register the Command: In the module file, register the command in the
group()function.pub fn group() -> Group { Group::new() .command("get", get_vehicle) .command("create", create_vehicle) // ... other commands } -
Use Logging: Import and use the generic
logfunction in your handler functions.use crate::log::log; pub fn get_vehicle(key: String) -> String { log("vehicle", "DEBUG", &format!("Getting vehicle for key: {}", key)); // Call service layer match VEHICLE_SERVICE.get_vehicle(key.clone()) { Ok(vehicle) => { log("vehicle", "INFO", &format!("Successfully retrieved vehicle: {}", key)); match serde_json::to_string(&vehicle) { Ok(json) => { log("vehicle", "DEBUG", &format!("Serialized vehicle to JSON: {}", json)); json } Err(e) => { let error_msg = format!("Error: Failed to serialize vehicle: {}", e); log("vehicle", "ERROR", &error_msg); error_msg } } } Err(e) => { let error_msg = format!("Error: {}", e); log("vehicle", "ERROR", &format!("Failed to get vehicle '{}': {}", key, e)); error_msg } } }The
logfunction takes three parameters:category: The log category (e.g., "vehicle", "actor", "org")level: The log level ("INFO", "DEBUG", "WARN", "ERROR")message: The message to log
Log files are created automatically in
@forge_server/logs/{category}.log. -
Register the Module (if new): If you created a new module, add it to
src/lib.rs.pub mod vehicle; // In the extension function, register the group extension.group("vehicle", vehicle::group());
Testing
- In-Game Testing: Test your commands in Arma 3 to ensure they work correctly with SQF.
- Error Cases: Test error scenarios (invalid input, missing entities, etc.) to ensure proper error messages.
Best Practices
- Return Types: Always return
String(JSON on success, error message on failure). - Error Messages: Prefix all error messages with
"Error: "for consistency. - Logging: Use the
log(category, level, message)function to track operations. - Service Layer: Delegate business logic to the service layer. The extension layer should only handle parameter parsing and response formatting.
- Validation: Validate inputs before calling the service layer to provide clear error messages.