Jacob Schmidt d178e39164 Refactor client UI stores and normalize docs formatting
- Rework org and store UI state modules (rename/move store/getter files, add runtime and bridge wiring)
- Update store UI components and page structure (navbar/cart split, new StoreView flow)
- Apply broad markdown/YAML/HTML/CSS/JS formatting cleanup across docs, templates, and workflows
2026-03-10 19:13:30 -05:00

406 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.