# 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 `callExtension` interface, 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., `Actor` model). 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](src/redis/README.md)**: Comprehensive guide to Redis commands (hash, list, set, common operations) - **[Adapters](src/adapters/README.md)**: 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 ```sqf // 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 ```sqf // 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 ```sqf // 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 ```sqf // 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 ```sqf private _exists = "forge_server" callExtension ["org:exists", ["elite_squad"]]; if ((_exists select 0) == "true") then { systemChat "Organization exists."; }; ``` #### Deleting an Organization ```sqf // 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 ```sqf // 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 ```sqf // 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 ```sqf // Prepare partial update private _update = createHashMapFromArray [ ["bank", 1500], ["level", 2] ]; // Apply update private _result = "forge_server" callExtension ["actor:update", ["player123", toJSON _update]]; ``` #### Checking Existence ```sqf 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 ```sqf // 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:get` and `org:get` will 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 ```sqf 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: 1. **Register the Command**: In the module file (e.g., `src/actor.rs`), add the command to the `group()` function. ```rust 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 } ``` 2. **Implement the Handler Function**: Create the function that handles the command logic. ```rust 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 = 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: 1. **Create the Module File**: Add `src/vehicle.rs`. 2. **Create the Global Service Instance**: Define a lazily initialized singleton service. ```rust use std::sync::LazyLock; use forge_services::VehicleService; use forge_repositories::RedisVehicleRepository; use crate::adapters::ExtensionRedisClient; static VEHICLE_SERVICE: LazyLock>> = LazyLock::new(|| { let redis_client = ExtensionRedisClient::new(); let repository = RedisVehicleRepository::new(redis_client); VehicleService::new(repository) }); ``` 3. **Register the Command**: In the module file, register the command in the `group()` function. ```rust pub fn group() -> Group { Group::new() .command("get", get_vehicle) .command("create", create_vehicle) // ... other commands } ``` 4. **Use Logging**: Import and use the generic `log` function in your handler functions. ```rust 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 `log` function 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`. 5. **Register the Module** (if new): If you created a new module, add it to `src/lib.rs`. ```rust 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.