feat: Implement task store reset functionality and update documentation for server launch prerequisites

This commit is contained in:
Jacob Schmidt 2026-05-16 22:12:57 -05:00
parent b7f7ae3a01
commit 96718996b2
26 changed files with 259 additions and 55 deletions

View File

@ -1,5 +1,6 @@
#include "script_component.hpp"
if !(isNil QGVAR(TaskStore)) then { GVAR(TaskStore) call ["resetMissionState", []]; };
if (isNil QEGVAR(common,EventBus)) then { call EFUNC(common,eventBus); };
if (isNil QGVAR(TaskLifecycleEventLogTokens)) then {
private _logTaskLifecycleEvent = {

View File

@ -37,6 +37,21 @@ GVAR(TaskStore) = createHashMapObject [[
["targets", createHashMap]
]];
_self call ["resetMissionState", []];
}],
["resetMissionState", compileFinal {
_self set ["participantRegistry", createHashMap];
_self set ["taskLifecycleRegistry", createHashMap];
_self set ["taskEntityRegistries", createHashMapFromArray [
["cargo", createHashMap],
["hostages", createHashMap],
["hvts", createHashMap],
["ieds", createHashMap],
["entities", createHashMap],
["shooters", createHashMap],
["targets", createHashMap]
]];
// Task extension state is mission-scoped and intentionally reset on
// startup rather than being treated as durable account data.
["task:reset", []] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
@ -44,9 +59,12 @@ GVAR(TaskStore) = createHashMapObject [[
!_isSuccess
|| { !(_result isEqualType "") }
|| { (_result find "Error:") == 0 }
) then {
) exitWith {
["WARNING", "Failed to reset task backend state during task store initialization."] call EFUNC(common,log);
false
};
true
}],
["callTaskStateEnvelope", compileFinal {
params [["_function", "", [""]], ["_arguments", [], [[]]]];

View File

@ -89,6 +89,18 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [
private _targets = _self getOrDefault ["targets", []];
{ !alive _x } count _targets
}],
["waitForAssignment", compileFinal {
private _taskID = _self getOrDefault ["taskID", ""];
if (_taskID isEqualTo "" || { !(_self getOrDefault ["useTaskStore", false]) }) exitWith { true };
waitUntil {
sleep 1;
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
};
true
}],
["tick", compileFinal {
private _startedAt = _self getOrDefault ["startedAt", -1];
private _timeLimit = _self getOrDefault ["timeLimit", 0];
@ -136,26 +148,7 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [
};
};
if (_timeLimit isNotEqualTo 0 && { _useTaskStore }) then {
private _catalogEntry = GVAR(TaskStore) call ["getTaskCatalogEntry", [_taskID]];
["INFO", format [
"Attack task %1 initial state before acceptance wait. Accepted=%2, RequesterUid='%3', Source='%4', TimeLimit=%5s",
_taskID,
_catalogEntry getOrDefault ["accepted", false],
_catalogEntry getOrDefault ["requesterUid", ""],
_catalogEntry getOrDefault ["source", ""],
_timeLimit
]] call EFUNC(common,log);
["INFO", format ["Attack task %1 waiting for acceptance before starting %2s time limit.", _taskID, _timeLimit]] call EFUNC(common,log);
waitUntil {
sleep 1;
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
};
["INFO", format ["Attack task %1 accepted. Starting %2s time limit.", _taskID, _timeLimit]] call EFUNC(common,log);
};
_self call ["waitForAssignment", []];
_self call ["markActive", []];
while { (_self call ["getStatus", []]) isEqualTo "active" } do {

View File

@ -114,6 +114,18 @@ GVAR(DefuseTaskBaseClass) = createHashMapFromArray [
true
}],
["waitForAssignment", compileFinal {
private _taskID = _self getOrDefault ["taskID", ""];
if (_taskID isEqualTo "" || { !(_self getOrDefault ["useTaskStore", false]) }) exitWith { true };
waitUntil {
sleep 1;
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
};
true
}],
["startIedControllers", compileFinal {
if ((_self getOrDefault ["iedControllers", []]) isNotEqualTo []) exitWith { true };
@ -234,6 +246,7 @@ GVAR(DefuseTaskBaseClass) = createHashMapFromArray [
}],
["runLoop", compileFinal {
_self call ["waitForRequiredEntities", []];
_self call ["waitForAssignment", []];
_self call ["startIedControllers", []];
_self call ["markActive", []];

View File

@ -78,11 +78,10 @@ GVAR(DeliveryTaskBaseClass) = createHashMapFromArray [
_self set ["maxDamaged", _maxDamaged];
true
}],
["waitForAssignmentIfTimed", compileFinal {
private _timeLimit = _self getOrDefault ["timeLimit", 0];
["waitForAssignment", compileFinal {
private _taskID = _self getOrDefault ["taskID", ""];
if (_timeLimit <= 0 || { _taskID isEqualTo "" } || { !(_self getOrDefault ["useTaskStore", false]) }) exitWith { true };
if (_taskID isEqualTo "" || { !(_self getOrDefault ["useTaskStore", false]) }) exitWith { true };
waitUntil {
sleep 1;
@ -175,7 +174,7 @@ GVAR(DeliveryTaskBaseClass) = createHashMapFromArray [
}],
["runLoop", compileFinal {
_self call ["waitForRequiredEntities", []];
_self call ["waitForAssignmentIfTimed", []];
_self call ["waitForAssignment", []];
_self call ["markActive", []];
while { (_self call ["getStatus", []]) isEqualTo "active" } do {

View File

@ -69,11 +69,10 @@ GVAR(DestroyTaskBaseClass) = createHashMapFromArray [
_self set ["requiredDestroyed", _requiredDestroyed];
true
}],
["waitForAssignmentIfTimed", compileFinal {
private _timeLimit = _self getOrDefault ["timeLimit", 0];
["waitForAssignment", compileFinal {
private _taskID = _self getOrDefault ["taskID", ""];
if (_timeLimit <= 0 || { _taskID isEqualTo "" } || { !(_self getOrDefault ["useTaskStore", false]) }) exitWith { true };
if (_taskID isEqualTo "" || { !(_self getOrDefault ["useTaskStore", false]) }) exitWith { true };
waitUntil {
sleep 1;
@ -155,7 +154,7 @@ GVAR(DestroyTaskBaseClass) = createHashMapFromArray [
}],
["runLoop", compileFinal {
_self call ["waitForRequiredEntities", []];
_self call ["waitForAssignmentIfTimed", []];
_self call ["waitForAssignment", []];
_self call ["markActive", []];
while { (_self call ["getStatus", []]) isEqualTo "active" } do {

View File

@ -37,7 +37,7 @@ GVAR(HVTEntityController) = createHashMapFromArray [
if (isNull _entity || { !alive _entity }) exitWith { false };
_entity setCaptive true;
doStop _entity;
_entity enableAIFeature ["MOVE", true];
true
}],
["runLoop", compileFinal {

View File

@ -115,11 +115,10 @@ GVAR(HVTTaskBaseClass) = createHashMapFromArray [
_self set ["hvtControllers", _controllers];
true
}],
["waitForAssignmentIfTimed", compileFinal {
private _timeLimit = _self getOrDefault ["timeLimit", 0];
["waitForAssignment", compileFinal {
private _taskID = _self getOrDefault ["taskID", ""];
if (_timeLimit <= 0 || { _taskID isEqualTo "" } || { !(_self getOrDefault ["useTaskStore", false]) }) exitWith { true };
if (_taskID isEqualTo "" || { !(_self getOrDefault ["useTaskStore", false]) }) exitWith { true };
waitUntil {
sleep 1;
@ -210,8 +209,8 @@ GVAR(HVTTaskBaseClass) = createHashMapFromArray [
}],
["runLoop", compileFinal {
_self call ["waitForRequiredEntities", []];
_self call ["waitForAssignment", []];
_self call ["startHvtControllers", []];
_self call ["waitForAssignmentIfTimed", []];
_self call ["markActive", []];
while { (_self call ["getStatus", []]) isEqualTo "active" } do {

View File

@ -183,11 +183,10 @@ GVAR(HostageTaskBaseClass) = createHashMapFromArray [
_self set ["maxHostageLosses", _maxHostageLosses];
true
}],
["waitForAssignmentIfTimed", compileFinal {
private _timeLimit = _self getOrDefault ["timeLimit", 0];
["waitForAssignment", compileFinal {
private _taskID = _self getOrDefault ["taskID", ""];
if (_timeLimit <= 0 || { _taskID isEqualTo "" } || { !(_self getOrDefault ["useTaskStore", false]) }) exitWith { true };
if (_taskID isEqualTo "" || { !(_self getOrDefault ["useTaskStore", false]) }) exitWith { true };
waitUntil {
sleep 1;
@ -337,8 +336,8 @@ GVAR(HostageTaskBaseClass) = createHashMapFromArray [
}],
["runLoop", compileFinal {
_self call ["waitForRequiredEntities", []];
_self call ["waitForAssignment", []];
_self call ["startHostageControllers", []];
_self call ["waitForAssignmentIfTimed", []];
_self call ["markActive", []];
while { (_self call ["getStatus", []]) isEqualTo "active" } do {

View File

@ -1,5 +1,7 @@
# Forge Server Configuration
# Copy this file to config.toml and place it beside forge_server_x64.dll.
# Start SurrealDB before launching the Arma server, and keep these values
# aligned with the running database.
[surreal]
endpoint = "127.0.0.1:8000"

View File

@ -21,7 +21,9 @@ SQF module
## Configuration
Copy `config.example.toml` to `config.toml` next to the extension DLL.
Copy `config.example.toml` to `config.toml` next to the extension DLL before
launching a Forge-enabled server. SurrealDB must also be running before the
server starts, and the values in `config.toml` must match that database.
```toml
[surreal]
@ -33,6 +35,9 @@ password = "root"
connect_timeout_ms = 5000
```
Players and mission designers do not need this file unless they are hosting
locally. Server owners and developers do.
For install links and Forge-specific setup steps, see
[SurrealDB Setup](../../../docs/surrealdb-setup.md).

View File

@ -6,6 +6,19 @@ persists durable state through SurrealDB.
This extension build targets SurrealDB `3.x`.
## Launch Prerequisites
Before starting the Arma server with Forge enabled:
1. Start SurrealDB.
2. Copy `config.example.toml` to `config.toml` beside `forge_server_x64.dll`.
3. Match the `config.toml` endpoint, namespace, database, username, and password
to the running SurrealDB instance.
The extension reads configuration during startup. If SurrealDB is offline or
the config values do not match, persistence-backed commands are not ready for
normal gameplay.
## Responsibilities
- Register extension command groups for actor, bank, garage, locker, org,

View File

@ -1,5 +1,7 @@
# Forge Server Configuration
# Copy this file to config.toml and place it beside forge_server_x64.dll.
# Start SurrealDB before launching the Arma server, and keep these values
# aligned with the running database.
[surreal]
# SurrealDB HTTP endpoint. Use "127.0.0.1:8000" for a local server.

View File

@ -5,7 +5,9 @@ This guide covers the usual path for adding or changing a Forge module.
## Local Checks
Before running storage-backed workflows locally, complete
[SurrealDB Setup](./surrealdb-setup.md).
[SurrealDB Setup](./surrealdb-setup.md). A local or dedicated server launch must
have SurrealDB running and a `config.toml` beside `forge_server_x64.dll` that
matches the running database.
Run these before pushing Rust or extension changes:

View File

@ -127,6 +127,13 @@ password = "root"
connect_timeout_ms = 5000
```
`config.toml` is a launch prerequisite for server owners and developers. The
file must exist beside `forge_server_x64.dll`, and SurrealDB must already be
running at the configured endpoint before starting a Forge-enabled dedicated
server or local multiplayer test. Clients and mission designers do not run this
configuration unless they are hosting locally, but the server they connect to
must have it in place.
For install links and role-based setup guidance, see
[SurrealDB Setup](./surrealdb-setup.md).

View File

@ -4,6 +4,20 @@ Forge is split into Arma client addons, Arma server addons, a Rust server
extension, shared Rust domain crates, and web UI build tooling. This directory
collects framework-level documentation for those pieces.
## Launch Prerequisites
Before starting a Forge-enabled dedicated server or local multiplayer test,
server owners and developers must:
1. Start SurrealDB.
2. Place `config.toml` beside `forge_server_x64.dll`.
3. Keep the `config.toml` SurrealDB endpoint, namespace, database, username,
and password aligned with the running database.
Mission designers and players do not need to run SurrealDB unless they are
hosting locally, but the server they join must have these prerequisites ready.
See [SurrealDB Setup](./surrealdb-setup.md) for the full setup path.
## Start Here
- [Framework Architecture](./FRAMEWORK_ARCHITECTURE.md): how SQF, web UIs,

View File

@ -4,6 +4,26 @@ Forge uses SurrealDB for durable storage. The Rust server extension connects to
SurrealDB on startup and applies Forge schema modules automatically, so setup
comes down to running a reachable database and matching the Forge config.
## Launch Requirement
Before launching an Arma server or local multiplayer test with Forge enabled:
1. Start SurrealDB and confirm it is listening on the endpoint Forge will use.
2. Copy `arma/server/extension/config.example.toml` to `config.toml` beside
`forge_server_x64.dll`.
3. Make sure `config.toml` matches the running SurrealDB endpoint, namespace,
database, username, and password.
Server owners and developers must do this before starting the dedicated server
or hosting a test session. Mission designers and players do not need their own
SurrealDB instance unless they are running the server locally, but the server
they connect to must have SurrealDB running and configured.
If SurrealDB is not running, or if `config.toml` points at the wrong endpoint
or credentials, persistence-backed systems such as actors, bank accounts,
garages, lockers, organizations, phone data, stores, and tasks will not be
ready for normal gameplay.
## Choose the Right Path
### Developer or Server Operator
@ -73,11 +93,11 @@ password = "root"
connect_timeout_ms = 5000
```
After that:
Before starting the game server, confirm SurrealDB is still running. After
launching the Arma server:
1. Start the Arma server with the Forge extension enabled.
2. Let the extension connect and apply the Forge schema modules.
3. Verify the connection state:
1. Let the extension connect and apply the Forge schema modules.
2. Verify the connection state:
```sqf
"forge_server" callExtension ["status", []];

View File

@ -4,6 +4,26 @@ Forge uses SurrealDB for durable storage. The Rust server extension connects to
SurrealDB on startup and applies Forge schema modules automatically, so setup
comes down to running a reachable database and matching the Forge config.
## Launch Requirement
Before launching an Arma server or local multiplayer test with Forge enabled:
1. Start SurrealDB and confirm it is listening on the endpoint Forge will use.
2. Copy `arma/server/extension/config.example.toml` to `config.toml` beside
`forge_server_x64.dll`.
3. Make sure `config.toml` matches the running SurrealDB endpoint, namespace,
database, username, and password.
Server owners and developers must do this before starting the dedicated server
or hosting a test session. Mission designers and players do not need their own
SurrealDB instance unless they are running the server locally, but the server
they connect to must have SurrealDB running and configured.
If SurrealDB is not running, or if `config.toml` points at the wrong endpoint
or credentials, persistence-backed systems such as actors, bank accounts,
garages, lockers, organizations, phone data, stores, and tasks will not be
ready for normal gameplay.
## Choose the Right Path
### Developer or Server Operator
@ -73,11 +93,11 @@ password = "root"
connect_timeout_ms = 5000
```
After that:
Before starting the game server, confirm SurrealDB is still running. After
launching the Arma server:
1. Start the Arma server with the Forge extension enabled.
2. Let the extension connect and apply the Forge schema modules.
3. Verify the connection state:
1. Let the extension connect and apply the Forge schema modules.
2. Verify the connection state:
```sqf
"forge_server" callExtension ["status", []];

View File

@ -11,6 +11,17 @@ Forge combines:
- shared Rust crates for models, repositories, and services
- SurrealDB for durable storage
## Launch Prerequisites
Before starting a Forge-enabled dedicated server or local multiplayer test,
server owners and developers must start SurrealDB and make sure
`config.toml` is beside `forge_server_x64.dll`. The config values must match
the running SurrealDB endpoint, namespace, database, username, and password.
Mission designers and players do not need their own SurrealDB instance unless
they are hosting locally, but the server they join must have these prerequisites
ready.
## Common Commands
```powershell

View File

@ -126,6 +126,13 @@ password = "root"
connect_timeout_ms = 5000
```
`config.toml` is a launch prerequisite for server owners and developers. The
file must exist beside `forge_server_x64.dll`, and SurrealDB must already be
running at the configured endpoint before starting a Forge-enabled dedicated
server or local multiplayer test. Clients and mission designers do not run this
configuration unless they are hosting locally, but the server they connect to
must have it in place.
For install links and role-based setup guidance, see
[SurrealDB Setup](/getting-started/surrealdb-setup).

View File

@ -6,7 +6,9 @@ description: "This guide covers the usual path for adding or changing a Forge mo
## Local Checks
Before running storage-backed workflows locally, complete
[SurrealDB Setup](/getting-started/surrealdb-setup).
[SurrealDB Setup](/getting-started/surrealdb-setup). A local or dedicated server launch must
have SurrealDB running and a `config.toml` beside `forge_server_x64.dll` that
matches the running database.
Run these before pushing Rust or extension changes:

View File

@ -3,6 +3,26 @@ title: "SurrealDB Setup"
description: "Forge uses SurrealDB for durable storage. The Rust server extension connects to SurrealDB on startup and applies Forge schema modules automatically, so setup comes down to running a reachable database and matching the Forge config."
---
## Launch Requirement
Before launching an Arma server or local multiplayer test with Forge enabled:
1. Start SurrealDB and confirm it is listening on the endpoint Forge will use.
2. Copy `arma/server/extension/config.example.toml` to `config.toml` beside
`forge_server_x64.dll`.
3. Make sure `config.toml` matches the running SurrealDB endpoint, namespace,
database, username, and password.
Server owners and developers must do this before starting the dedicated server
or hosting a test session. Mission designers and players do not need their own
SurrealDB instance unless they are running the server locally, but the server
they connect to must have SurrealDB running and configured.
If SurrealDB is not running, or if `config.toml` points at the wrong endpoint
or credentials, persistence-backed systems such as actors, bank accounts,
garages, lockers, organizations, phone data, stores, and tasks will not be
ready for normal gameplay.
## Choose the Right Path
### Developer or Server Operator
@ -72,11 +92,11 @@ password = "root"
connect_timeout_ms = 5000
```
After that:
Before starting the game server, confirm SurrealDB is still running. After
launching the Arma server:
1. Start the Arma server with the Forge extension enabled.
2. Let the extension connect and apply the Forge schema modules.
3. Verify the connection state:
1. Let the extension connect and apply the Forge schema modules.
2. Verify the connection state:
```sqf
"forge_server" callExtension ["status", []];

View File

@ -20,7 +20,9 @@ SQF module
## Configuration
Copy `config.example.toml` to `config.toml` next to the extension DLL.
Copy `config.example.toml` to `config.toml` next to the extension DLL before
launching a Forge-enabled server. SurrealDB must also be running before the
server starts, and the values in `config.toml` must match that database.
```toml
[surreal]
@ -32,6 +34,9 @@ password = "root"
connect_timeout_ms = 5000
```
Players and mission designers do not need this file unless they are hosting
locally. Server owners and developers do.
For install links and Forge-specific setup steps, see
[SurrealDB Setup](/getting-started/surrealdb-setup).

View File

@ -16,6 +16,10 @@ browser-backed player interfaces.
Use these docs to understand the runtime architecture, extension API surface,
server gameplay modules, and client addon integration patterns.
Server owners and developers must start SurrealDB and place a matching
`config.toml` beside `forge_server_x64.dll` before launching a
Forge-enabled server or local multiplayer test.
#links
:::u-button
---

View File

@ -80,9 +80,10 @@ impl<R: TaskRepository> TaskStateService<R> {
self.repository
.save_ownership(entry_id.clone(), ownership.clone())?;
let accepted = !ownership.requester_uid.trim().is_empty();
let entry = self.patch_catalog_ownership(
&entry_id,
true,
accepted,
&ownership.requester_uid,
&ownership.org_id,
)?;
@ -326,6 +327,39 @@ mod tests {
);
}
#[test]
fn bind_ownership_without_requester_does_not_accept_task() {
let repository = InMemoryTaskRepository::new();
let service = TaskStateService::new(repository.clone());
service
.upsert_catalog_entry("task-1".to_string(), r#"{"title":"Hostage"}"#.to_string())
.expect("catalog upsert should succeed");
let result = service
.bind_ownership(
"task-1".to_string(),
r#"{"requesterUid":"","orgId":"default"}"#.to_string(),
)
.expect("bind should succeed");
assert_eq!(result.requester_uid, "");
assert_eq!(result.org_id, "default");
assert_eq!(
result.entry.get("accepted").and_then(Value::as_bool),
Some(false)
);
let stored = repository
.get_catalog_entry("task-1")
.expect("catalog lookup should succeed")
.expect("catalog entry should exist");
assert_eq!(
stored.fields.get("requesterUid").and_then(Value::as_str),
Some("")
);
}
#[test]
fn get_status_falls_back_to_completed_status() {
let repository = InMemoryTaskRepository::new();

View File

@ -171,6 +171,10 @@ browser-backed player interfaces.
Use these docs to understand the runtime architecture, extension API surface,
server gameplay modules, and client addon integration patterns.
Server owners and developers must start SurrealDB and place a matching
\`config.toml\` beside \`forge_server_x64.dll\` before launching a
Forge-enabled server or local multiplayer test.
#links
:::u-button
---
@ -372,6 +376,17 @@ Forge combines:
- shared Rust crates for models, repositories, and services
- SurrealDB for durable storage
## Launch Prerequisites
Before starting a Forge-enabled dedicated server or local multiplayer test,
server owners and developers must start SurrealDB and make sure
\`config.toml\` is beside \`forge_server_x64.dll\`. The config values must match
the running SurrealDB endpoint, namespace, database, username, and password.
Mission designers and players do not need their own SurrealDB instance unless
they are hosting locally, but the server they join must have these prerequisites
ready.
## Common Commands
\`\`\`powershell