Jacob Schmidt 5ded3a60e5 Add credit line repayment to bank UI
- Wire bank client and server for credit line repayment requests
- Show credit line balance and repay action in the banking view
- Extend org/bank payloads and models with credit line fields
2026-04-02 16:41:10 -05:00
..
2026-04-02 16:41:10 -05:00

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: 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: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

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.

    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.

    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:

  1. Create the Module File: Add src/vehicle.rs.

  2. 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)
        });
    
  3. 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
    }
    
  4. Use Logging: Import and use the generic log function 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 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.

    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.