400 lines
14 KiB
Markdown
400 lines
14 KiB
Markdown
# 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<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.
|
|
```rust
|
|
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.
|
|
```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.
|