Consolidate shared Forge config and Arma cfg templates

- Add shared `bin/host/config.example.toml` and remove the old host example
- Split Arma config creation between `server.cfg` and `basic.cfg`
- Update docs and examples to reflect shared host-managed config
This commit is contained in:
Jacob Schmidt 2026-06-06 20:16:39 -05:00
parent c3531a5839
commit c676a9084e
26 changed files with 504 additions and 434 deletions

View File

@ -85,8 +85,23 @@ npm run host:build
``` ```
The app reads the shared `config.toml` from the repo root during development, or The app reads the shared `config.toml` from the repo root during development, or
from the current/executable directory in packaged use. If no shared config exists, from the current/executable directory in packaged use. Start from
it falls back to `bin/host/host.example.toml`; saving from the Settings view writes `bin/host/config.example.toml` when you want one shared config for Forge Host,
the active shared `config.toml`. The shared file includes the host process sections ICOM, and the Arma extension. Saving from the Settings view writes the active
plus the `[server]` section used by ICOM and the `[surreal]` section used by the shared `config.toml`.
Arma extension.
The shared file includes:
- `[server]` for the ICOM hub bind address.
- `[surreal]` for the Arma extension SurrealDB connection.
- `[surrealdb]`, `[icom]`, and `[arma]` for Forge Host managed processes.
Forge Host manages the two Arma dedicated server config files separately:
`server.cfg` is launched with `-config` for server rules, missions, passwords,
and admins, while `basic.cfg` is launched with `-cfg` for network tuning. The
Arma Server settings view can create either file from `bin/host/server.example.cfg`
or `bin/host/basic.example.cfg`, then open it in the built-in editor.
Component-specific examples still exist for narrow deployments:
`bin/icom/config.example.toml` contains only `[server]`, and
`arma/server/extension/config.example.toml` contains only `[surreal]`.

View File

@ -1,12 +1,22 @@
# Forge Server Configuration # Forge Arma server extension 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 # Copy this file to `config.toml` beside `forge_server_x64.dll`, or use the
# aligned with the running database. # shared config generated from `bin/host/config.example.toml`.
#
# The Arma extension reads the [surreal] section. Extra host/ICOM sections in a
# shared config are ignored by the extension.
[surreal] [surreal]
# SurrealDB HTTP endpoint. This must match the running SurrealDB --bind value.
endpoint = "127.0.0.1:8000" endpoint = "127.0.0.1:8000"
# Namespace and database selected by the extension after connecting.
namespace = "forge" namespace = "forge"
database = "main" database = "main"
# Local development defaults. Use a real password for shared or public servers.
username = "root" username = "root"
password = "root" password = "root"
# Initial connection timeout in milliseconds.
connect_timeout_ms = 5000 connect_timeout_ms = 5000

View File

@ -11,7 +11,10 @@ This extension build targets SurrealDB `3.x`.
Before starting the Arma server with Forge enabled: Before starting the Arma server with Forge enabled:
1. Start SurrealDB. 1. Start SurrealDB.
2. Copy `config.example.toml` to `config.toml` beside `forge_server_x64.dll`. 2. Create Forge's `config.toml`. Copy `config.example.toml` beside
`forge_server_x64.dll` for an extension-only deployment, or use the shared
repo-root config from `bin/host/config.example.toml` when Forge Host manages
local services.
3. Match the `config.toml` endpoint, namespace, database, username, and password 3. Match the `config.toml` endpoint, namespace, database, username, and password
to the running SurrealDB instance. to the running SurrealDB instance.
@ -23,7 +26,8 @@ normal gameplay.
- Register extension command groups for actor, bank, garage, locker, org, - Register extension command groups for actor, bank, garage, locker, org,
phone, store, task, CAD, terrain, and transport systems. phone, store, task, CAD, terrain, and transport systems.
- Load extension configuration from `@forge_server/config.toml`. - Load extension configuration from `@forge_server/config.toml`, `config.toml`
in the working directory, or `config.toml` beside the extension DLL.
- Connect to SurrealDB and apply schema modules on startup. - Connect to SurrealDB and apply schema modules on startup.
- Keep SQF-facing command handlers thin while service crates own domain rules. - Keep SQF-facing command handlers thin while service crates own domain rules.
@ -31,14 +35,24 @@ normal gameplay.
```toml ```toml
[surreal] [surreal]
# SurrealDB HTTP endpoint.
endpoint = "127.0.0.1:8000" endpoint = "127.0.0.1:8000"
# Namespace and database selected after connecting.
namespace = "forge" namespace = "forge"
database = "main" database = "main"
# Local development credentials.
username = "root" username = "root"
password = "root" password = "root"
# Initial connection timeout in milliseconds.
connect_timeout_ms = 5000 connect_timeout_ms = 5000
``` ```
The extension reads only `[surreal]`. Extra sections from the shared Forge Host
config are ignored.
## Status ## Status
```sqf ```sqf

View File

@ -1,16 +1,22 @@
# Forge Server Configuration # Forge Arma server extension 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 # Copy this file to `config.toml` beside `forge_server_x64.dll`, or use the
# aligned with the running database. # shared config generated from `bin/host/config.example.toml`.
#
# The Arma extension reads the [surreal] section. Extra host/ICOM sections in a
# shared config are ignored by the extension.
[surreal] [surreal]
# SurrealDB HTTP endpoint. Use "127.0.0.1:8000" for a local server. # SurrealDB HTTP endpoint. This must match the running SurrealDB --bind value.
endpoint = "127.0.0.1:8000" endpoint = "127.0.0.1:8000"
# Namespace and database selected by the extension after connecting.
namespace = "forge" namespace = "forge"
database = "main" database = "main"
# Optional authentication. # Local development defaults. Use a real password for shared or public servers.
username = "root" username = "root"
password = "root" password = "root"
# Initial connection timeout in milliseconds.
connect_timeout_ms = 5000 connect_timeout_ms = 5000

View File

@ -1,4 +1,8 @@
// Basic Network Configuration // Arma 3 dedicated server basic.cfg
//
// Forge Host launches this file with -cfg=basic.cfg. Use it for network and
// performance tuning. Gameplay rules, missions, passwords, and admin settings
// belong in server.cfg, which is launched with -config=server.cfg.
language = "English"; language = "English";
adapter = -1; adapter = -1;
@ -6,12 +10,19 @@ adapter = -1;
Resolution_W = 0; Resolution_W = 0;
Resolution_H = 0; Resolution_H = 0;
Resolution_Bpp = 32; Resolution_Bpp = 32;
// Bandwidth limits in bits per second. Raise MaxBandwidth for hosted servers
// with a reliable uplink; keep MinBandwidth conservative for local testing.
MinBandwidth = 131072; MinBandwidth = 131072;
MaxBandwidth = 10000000000; MaxBandwidth = 10000000000;
// Message sizing and send cadence. Higher values can improve responsiveness on
// healthy networks but may increase bandwidth and CPU pressure.
MaxMsgSend = 128; MaxMsgSend = 128;
MaxSizeGuaranteed = 512; MaxSizeGuaranteed = 512;
MaxSizeNonguaranteed = 256; MaxSizeNonguaranteed = 256;
MinErrorToSend = 0.001; MinErrorToSend = 0.001;
MinErrorToSendNear = 0.01; MinErrorToSendNear = 0.01;
// Prevent clients from uploading custom face/sound files to the server.
MaxCustomFileSize = 0; MaxCustomFileSize = 0;
Windowed = 0; Windowed = 0;

View File

@ -0,0 +1,71 @@
# Forge shared host configuration.
#
# Copy this file to `config.toml` at the repository root for development, or
# place it beside the packaged Forge Host executable for deployment. Forge Host
# writes the active config to that shared `config.toml`.
#
# This file intentionally contains:
# - [server], used by the standalone ICOM hub.
# - [surreal], used by the Arma server extension.
# - [surrealdb], [icom], and [arma], used by Forge Host to manage processes.
[server]
# ICOM hub bind address. Use "127.0.0.1" for same-machine testing only, or
# "0.0.0.0" when remote Arma servers need to connect through the firewall.
host = "0.0.0.0"
port = 9090
[surreal]
# SurrealDB HTTP endpoint used by the Arma server extension.
# Keep this aligned with [surrealdb].args --bind.
endpoint = "127.0.0.1:8000"
# Namespace and database selected after the extension connects.
namespace = "forge"
database = "main"
# Local development defaults. Use a real password for shared or public servers.
username = "root"
password = "root"
# How long the extension waits for the initial SurrealDB connection.
connect_timeout_ms = 5000
[surrealdb]
# Managed SurrealDB process. Forge Host can install SurrealDB into the user's
# local app data directory and will prefer that executable when available.
enabled = true
command = "surreal"
# Persistent local database using RocksDB. The bind address must match
# [surreal].endpoint and health_host/health_port.
args = ["start", "--user", "root", "--pass", "root", "--bind", "127.0.0.1:8000", "rocksdb://forge.db"]
# Relative paths are resolved from the active config.toml directory.
working_dir = "arma/server/surrealdb"
health_host = "127.0.0.1"
health_port = 8000
[icom]
# Managed ICOM hub process. Build it first with:
# cargo build --release -p forge-icom
enabled = true
command = "target/release/forge-icom.exe"
args = []
working_dir = "."
health_host = "127.0.0.1"
health_port = 9090
[arma]
# Managed Arma 3 dedicated server process. This is disabled by default because
# every installation needs its own arma3server_x64.exe path and server config.
enabled = false
command = "arma3server_x64.exe"
# Forge Host can create/edit both Arma config files:
# - -config=server.cfg controls game server rules, missions, passwords, and admins.
# - -cfg=basic.cfg controls dedicated server network tuning.
args = ["-config=server.cfg", "-cfg=basic.cfg", "-port=2302", "-profiles=serverprofiles", "-name=server", "-noBattlEye"]
working_dir = ""
health_host = "127.0.0.1"
health_port = 2302

View File

@ -1,35 +0,0 @@
[server]
host = "0.0.0.0"
port = 9090
[surreal]
endpoint = "127.0.0.1:8000"
namespace = "forge"
database = "main"
username = "root"
password = "root"
connect_timeout_ms = 5000
[surrealdb]
enabled = true
command = "surreal"
args = ["start", "--user", "root", "--pass", "root", "--bind", "127.0.0.1:8000", "rocksdb://forge.db"]
working_dir = "../../arma/server/surrealdb"
health_host = "127.0.0.1"
health_port = 8000
[icom]
enabled = true
command = "target/release/forge-icom.exe"
args = []
working_dir = "../.."
health_host = "127.0.0.1"
health_port = 9090
[arma]
enabled = false
command = "arma3server_x64.exe"
args = ["-port=2302", "-profiles=serverprofiles", "-name=server", "-noBattlEye"]
working_dir = ""
health_host = "127.0.0.1"
health_port = 2302

View File

@ -202,7 +202,10 @@ impl AppState {
fn load_example_config() -> Result<HostConfig, String> { fn load_example_config() -> Result<HostConfig, String> {
if let Some(repo_root) = find_repo_root() { if let Some(repo_root) = find_repo_root() {
let example = repo_root.join("bin").join("host").join("host.example.toml"); let example = repo_root
.join("bin")
.join("host")
.join("config.example.toml");
return load_config(&example); return load_config(&example);
} }
@ -426,13 +429,7 @@ fn create_arma_server_config(path: String, template: String) -> Result<(), Strin
.map_err(|error| format!("Failed to create config directory: {error}"))?; .map_err(|error| format!("Failed to create config directory: {error}"))?;
} }
let template_path = resolve_arma_config_template(&template)?; let content = arma_config_template(&template)?;
let content = fs::read_to_string(&template_path).map_err(|error| {
format!(
"Failed to read config template {}: {error}",
template_path.display()
)
})?;
fs::write(&path, content).map_err(|error| format!("Failed to write server config: {error}")) fs::write(&path, content).map_err(|error| format!("Failed to write server config: {error}"))
} }
@ -690,32 +687,12 @@ fn ensure_surreal_on_user_path(_install_dir: &Path) -> Result<(), String> {
Ok(()) Ok(())
} }
fn resolve_arma_config_template(template: &str) -> Result<PathBuf, String> { fn arma_config_template(template: &str) -> Result<&'static str, String> {
let file_name = match template { match template {
"basic" => "basic.example.cfg", "basic" => Ok(include_str!("../../basic.example.cfg")),
"server" | "" => "server.example.cfg", "server" | "" => Ok(include_str!("../../server.example.cfg")),
other => return Err(format!("Unknown Arma config template '{other}'")), other => Err(format!("Unknown Arma config template '{other}'")),
};
let mut candidates = Vec::new();
if let Some(repo_root) = find_repo_root() {
candidates.push(repo_root.join("bin").join("host").join(file_name));
} }
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
candidates.push(dir.join(file_name));
candidates.push(dir.join("bin").join("host").join(file_name));
}
}
if let Ok(cwd) = std::env::current_dir() {
candidates.push(cwd.join(file_name));
candidates.push(cwd.join("bin").join("host").join(file_name));
}
candidates
.into_iter()
.find(|path| path.exists())
.ok_or_else(|| format!("Unable to find Arma config template '{file_name}'"))
} }
fn service_config(kind: ServiceKind, config: &HostConfig) -> ServiceConfig { fn service_config(kind: ServiceKind, config: &HostConfig) -> ServiceConfig {
@ -971,7 +948,7 @@ fn default_surrealdb_service() -> ServiceConfig {
"127.0.0.1:8000".to_string(), "127.0.0.1:8000".to_string(),
"rocksdb://forge.db".to_string(), "rocksdb://forge.db".to_string(),
], ],
working_dir: "../../arma/server/surrealdb".to_string(), working_dir: "arma/server/surrealdb".to_string(),
health_host: "127.0.0.1".to_string(), health_host: "127.0.0.1".to_string(),
health_port: 8000, health_port: 8000,
} }
@ -982,7 +959,7 @@ fn default_icom_service() -> ServiceConfig {
enabled: true, enabled: true,
command: "target/release/forge-icom.exe".to_string(), command: "target/release/forge-icom.exe".to_string(),
args: Vec::new(), args: Vec::new(),
working_dir: "../..".to_string(), working_dir: ".".to_string(),
health_host: "127.0.0.1".to_string(), health_host: "127.0.0.1".to_string(),
health_port: 9090, health_port: 9090,
} }
@ -993,8 +970,10 @@ fn default_arma_service() -> ServiceConfig {
enabled: false, enabled: false,
command: "arma3server_x64.exe".to_string(), command: "arma3server_x64.exe".to_string(),
args: vec![ args: vec![
"-config=server.cfg".to_string(),
"-cfg=basic.cfg".to_string(),
"-port=2302".to_string(), "-port=2302".to_string(),
"-profiles=profiles".to_string(), "-profiles=serverprofiles".to_string(),
"-name=server".to_string(), "-name=server".to_string(),
"-noBattlEye".to_string(), "-noBattlEye".to_string(),
], ],

View File

@ -18,7 +18,6 @@ let activeSettingsService = "surrealdb";
let configDirty = false; let configDirty = false;
let activeEditorPath = ""; let activeEditorPath = "";
let surrealInstallInfo = null; let surrealInstallInfo = null;
let selectedArmaConfigTemplate = "server";
const views = { const views = {
overview: document.getElementById("overviewView"), overview: document.getElementById("overviewView"),
@ -340,20 +339,24 @@ function renderSettings(force = false) {
form.querySelectorAll("[data-create-server-config]").forEach((button) => { form.querySelectorAll("[data-create-server-config]").forEach((button) => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
const template = runAction(() =>
document.querySelector("[data-config-template]")?.value || createArmaServerConfig(
"server"; button.dataset.configTemplate || "server",
runAction(() => createArmaServerConfig(template)); button.dataset.configTarget || "config",
button.dataset.defaultFile || "server.cfg",
),
);
}); });
}); });
form.querySelectorAll("[data-edit-server-config]").forEach((button) => { form.querySelectorAll("[data-edit-server-config]").forEach((button) => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
const target = button.dataset.configTarget || "config";
const path = document const path = document
.querySelector('[data-arg-key="config"]') .querySelector(`[data-arg-key="${target}"]`)
?.value.trim(); ?.value.trim();
if (!path) { if (!path) {
alert("Select or create a server config first."); alert("Select or create a config first.");
return; return;
} }
runAction(() => openConfigEditor(resolveArmaConfigPath(path))); runAction(() => openConfigEditor(resolveArmaConfigPath(path)));
@ -373,12 +376,6 @@ function renderSettings(force = false) {
}); });
}); });
form.querySelectorAll("[data-config-template]").forEach((select) => {
select.addEventListener("change", () => {
selectedArmaConfigTemplate = select.value;
});
});
if (name === "surrealdb" && !surrealInstallInfo) { if (name === "surrealdb" && !surrealInstallInfo) {
loadSurrealInstallInfo(); loadSurrealInstallInfo();
} else if ( } else if (
@ -716,7 +713,7 @@ function renderArmaSettings(config) {
<label>Launch Options</label> <label>Launch Options</label>
<div class="args-list arma-args" data-service="arma" data-field="args"> <div class="args-list arma-args" data-service="arma" data-field="args">
${serverConfigInput(parsed.config)} ${serverConfigInput(parsed.config)}
${serverConfigTemplateInput()} ${basicConfigInput(parsed.cfg)}
${argumentInput("Port", "port", parsed.port)} ${argumentInput("Port", "port", parsed.port)}
${argumentInput("Profiles", "profiles", parsed.profiles)} ${argumentInput("Profiles", "profiles", parsed.profiles)}
${argumentInput("Name", "name", parsed.name)} ${argumentInput("Name", "name", parsed.name)}
@ -766,27 +763,45 @@ function argumentInput(label, key, value, className = "") {
} }
function serverConfigInput(value) { function serverConfigInput(value) {
return ` return configFileInput({
<div class="arg-field full"> label: "Server Config",
<label>Server Config</label> key: "config",
<div class="input-action action-three"> value,
<input class="arg-input" data-arg-key="config" value="${escapeAttr(value)}" placeholder="server.cfg" /> placeholder: "server.cfg",
<button class="icon-button" type="button" data-picker="config" data-service-target="arma" data-target='[data-arg-key="config"]' title="Select config" aria-label="Select config"><span class="svg-icon icon-browse"></span></button> template: "server",
<button class="icon-button" type="button" data-create-server-config title="Create default config" aria-label="Create default config"><span class="svg-icon icon-new"></span></button> defaultFile: "server.cfg",
<button class="icon-button" type="button" data-edit-server-config title="Edit config" aria-label="Edit config"><span class="svg-icon icon-edit"></span></button> });
</div>
</div>
`;
} }
function serverConfigTemplateInput() { function basicConfigInput(value) {
return configFileInput({
label: "Basic Network Config",
key: "cfg",
value,
placeholder: "basic.cfg",
template: "basic",
defaultFile: "basic.cfg",
});
}
function configFileInput({
label,
key,
value,
placeholder,
template,
defaultFile,
}) {
const target = `[data-arg-key="${key}"]`;
return ` return `
<div class="arg-field"> <div class="arg-field full">
<label>Config Template</label> <label>${label}</label>
<select data-config-template> <div class="input-action action-three">
<option value="server" ${selectedAttr(selectedArmaConfigTemplate === "server")}>Server</option> <input class="arg-input" data-arg-key="${key}" value="${escapeAttr(value)}" placeholder="${placeholder}" />
<option value="basic" ${selectedAttr(selectedArmaConfigTemplate === "basic")}>Basic Network</option> <button class="icon-button" type="button" data-picker="config" data-service-target="arma" data-target='${target}' title="Select config" aria-label="Select config"><span class="svg-icon icon-browse"></span></button>
</select> <button class="icon-button" type="button" data-create-server-config data-config-template="${template}" data-config-target="${key}" data-default-file="${defaultFile}" title="Create default config" aria-label="Create default config"><span class="svg-icon icon-new"></span></button>
<button class="icon-button" type="button" data-edit-server-config data-config-target="${key}" title="Edit config" aria-label="Edit config"><span class="svg-icon icon-edit"></span></button>
</div>
</div> </div>
`; `;
} }
@ -819,6 +834,7 @@ function readArgumentFields(service, container) {
if (service === "arma") { if (service === "arma") {
const args = []; const args = [];
if (value("config")) args.push(`-config=${value("config")}`); if (value("config")) args.push(`-config=${value("config")}`);
if (value("cfg")) args.push(`-cfg=${value("cfg")}`);
if (value("port")) args.push(`-port=${value("port")}`); if (value("port")) args.push(`-port=${value("port")}`);
if (value("profiles")) args.push(`-profiles=${value("profiles")}`); if (value("profiles")) args.push(`-profiles=${value("profiles")}`);
if (value("name")) args.push(`-name=${value("name")}`); if (value("name")) args.push(`-name=${value("name")}`);
@ -871,6 +887,7 @@ function parseSurrealArgs(args) {
function parseArmaArgs(args) { function parseArmaArgs(args) {
const parsed = { const parsed = {
config: "", config: "",
cfg: "",
port: "", port: "",
profiles: "", profiles: "",
name: "", name: "",
@ -888,6 +905,8 @@ function parseArmaArgs(args) {
if (lowerArg.startsWith("-config=")) { if (lowerArg.startsWith("-config=")) {
parsed.config = arg.slice("-config=".length); parsed.config = arg.slice("-config=".length);
} else if (lowerArg.startsWith("-cfg=")) {
parsed.cfg = arg.slice("-cfg=".length);
} else if (lowerArg.startsWith("-port=")) { } else if (lowerArg.startsWith("-port=")) {
parsed.port = arg.slice("-port=".length); parsed.port = arg.slice("-port=".length);
} else if (lowerArg.startsWith("-profiles=")) { } else if (lowerArg.startsWith("-profiles=")) {
@ -936,7 +955,10 @@ async function pickPath(kind, targetSelector, service) {
const input = document.querySelector(targetSelector); const input = document.querySelector(targetSelector);
if (!input) return; if (!input) return;
const finalPath = normalizePickedExecutable(service, selected); const finalPath =
kind === "file"
? normalizePickedExecutable(service, selected)
: selected;
input.value = finalPath; input.value = finalPath;
if (kind === "file" && targetSelector.includes('data-field="command"')) { if (kind === "file" && targetSelector.includes('data-field="command"')) {
@ -966,7 +988,7 @@ function pickerFilters(kind) {
return undefined; return undefined;
} }
async function createArmaServerConfig(template) { async function createArmaServerConfig(template, targetKey, defaultFile) {
const save = window.__TAURI__?.dialog?.save; const save = window.__TAURI__?.dialog?.save;
if (!save) { if (!save) {
throw new Error( throw new Error(
@ -977,7 +999,9 @@ async function createArmaServerConfig(template) {
const workingDir = document const workingDir = document
.querySelector('[data-service="arma"][data-field="working_dir"]') .querySelector('[data-service="arma"][data-field="working_dir"]')
?.value.trim(); ?.value.trim();
const defaultPath = workingDir ? `${workingDir}\\server.cfg` : "server.cfg"; const defaultPath = workingDir
? `${workingDir}\\${defaultFile}`
: defaultFile;
const selected = await save({ const selected = await save({
defaultPath, defaultPath,
filters: [{ name: "Arma Server Config", extensions: ["cfg"] }], filters: [{ name: "Arma Server Config", extensions: ["cfg"] }],
@ -988,7 +1012,7 @@ async function createArmaServerConfig(template) {
path: selected, path: selected,
template, template,
}); });
const input = document.querySelector('[data-arg-key="config"]'); const input = document.querySelector(`[data-arg-key="${targetKey}"]`);
if (input) { if (input) {
input.value = selected; input.value = selected;
configDirty = true; configDirty = true;
@ -1075,10 +1099,6 @@ function escapeAttr(value) {
return escapeHtml(value).replaceAll('"', "&quot;"); return escapeHtml(value).replaceAll('"', "&quot;");
} }
function selectedAttr(selected) {
return selected ? "selected" : "";
}
refresh(); refresh();
syncRefreshTimer(); syncRefreshTimer();
loadAppVersion(); loadAppVersion();

View File

@ -20,18 +20,21 @@ The ICOM server can be configured using a `config.toml` file. Create one from th
cp bin/icom/config.example.toml config.toml cp bin/icom/config.example.toml config.toml
``` ```
Place `config.toml` in the same directory as the `forge-icom` executable or in the current working directory. Place `config.toml` in the same directory as the `forge-icom` executable or in
the current working directory. If Forge Host manages the hub, use the shared
repo-root `config.toml` created from `bin/host/config.example.toml`; it includes
the same `[server]` section and the ICOM hub ignores unrelated sections.
### Configuration Options ### Configuration Options
```toml ```toml
[server] [server]
# Host to bind to # TCP address to bind.
# "0.0.0.0" = All interfaces (allows remote connections) # "0.0.0.0" = all interfaces, allowing remote servers.
# "127.0.0.1" = Localhost only # "127.0.0.1" = localhost only.
host = "0.0.0.0" host = "0.0.0.0"
# Port to listen on # TCP port used by extension `icom:connect`.
port = 9090 port = 9090
``` ```

View File

@ -1,22 +1,17 @@
# Forge ICOM Server Configuration # Forge ICOM hub configuration.
# Copy this file to config.toml and modify as needed #
# Place this file in the same directory as the forge-icom executable # Copy this file to `config.toml` beside `forge-icom.exe`, into the working
# directory used to launch the hub, or use the shared repo-root `config.toml`
# generated from `bin/host/config.example.toml`.
#
# The ICOM loader only reads the [server] section. Extra sections in the shared
# host config are ignored by the standalone hub.
[server] [server]
# Host to bind to # TCP address the ICOM hub binds to.
# - "0.0.0.0" = All network interfaces (default, allows remote connections) # - "0.0.0.0" listens on all network interfaces and allows remote servers.
# - "127.0.0.1" = Localhost only (for local testing) # - "127.0.0.1" listens only on localhost for same-machine testing.
host = "0.0.0.0" host = "0.0.0.0"
# Port to listen on # TCP port accepted by extension `icom:connect` calls.
port = 9090 port = 9090
# Example configurations for different environments:
# Development (localhost only)
# host = "127.0.0.1"
# port = 9090
# Production (all interfaces, custom port)
# host = "0.0.0.0"
# port = 19090

View File

@ -130,8 +130,11 @@ server addon fnc_extCall
## Configuration ## Configuration
The server extension reads `config.toml` next to the extension DLL. The current The server extension reads `config.toml` from the server working directory,
persistence section is: `@forge_server/config.toml`, or beside the extension DLL. When Forge Host is
used during development, the repo-root `config.toml` can be shared across Forge
Host, ICOM, and the extension. The extension reads the `[surreal]` section and
ignores the host-only sections. The current persistence section is:
```toml ```toml
[surreal] [surreal]
@ -143,12 +146,20 @@ password = "root"
connect_timeout_ms = 5000 connect_timeout_ms = 5000
``` ```
`config.toml` is a launch prerequisite for server owners and developers. The `config.toml` is a launch prerequisite for server owners and developers. Use
file must exist beside `forge_server_x64.dll`, and SurrealDB must already be `arma/server/extension/config.example.toml` for an extension-only config, or
running at the configured endpoint before starting a Forge-enabled dedicated `bin/host/config.example.toml` for the shared host-managed config. SurrealDB must
server or local multiplayer test. Clients and mission designers do not run this already be running at the configured endpoint before starting a Forge-enabled
configuration unless they are hosting locally, but the server they connect to dedicated server or local multiplayer test. Clients and mission designers do not
must have it in place. run this configuration unless they are hosting locally, but the server they
connect to must have it in place.
Arma's own dedicated server files remain separate from Forge's TOML config.
Launch `server.cfg` with `-config` for server rules, mission rotation, passwords,
and admin settings. Launch `basic.cfg` with `-cfg` for network and performance
tuning. Forge Host exposes both paths in the Arma Server settings view and can
create them from `bin/host/server.example.cfg` and
`bin/host/basic.example.cfg`.
For install links and role-based setup guidance, see For install links and role-based setup guidance, see
[SurrealDB Setup](./surrealdb-setup.md). [SurrealDB Setup](./surrealdb-setup.md).

View File

@ -20,12 +20,12 @@ through `arma/server/extension/src/icom.rs`.
## Components ## Components
| Component | Path | Role | | Component | Path | Role |
| --- | --- | --- | | ----------------------- | ----------------------------------------- | ------------------------------------------------------------------------- |
| ICOM hub binary | `bin/icom` | Standalone TCP router for connected servers. | | ICOM hub binary | `bin/icom` | Standalone TCP router for connected servers. |
| ICOM client library | `bin/icom/src/client.rs` | Rust client used by the Forge server extension and examples. | | ICOM client library | `bin/icom/src/client.rs` | Rust client used by the Forge server extension and examples. |
| Extension command group | `arma/server/extension/src/icom.rs` | Exposes `icom:*` commands to SQF and forwards inbound events to Arma. | | Extension command group | `arma/server/extension/src/icom.rs` | Exposes `icom:*` commands to SQF and forwards inbound events to Arma. |
| SQF callback bridge | `arma/server/addons/main/XEH_preInit.sqf` | Receives extension callbacks and re-emits `forge_icom_event` through CBA. | | SQF callback bridge | `arma/server/addons/main/XEH_preInit.sqf` | Receives extension callbacks and re-emits `forge_icom_event` through CBA. |
## Build and Run the Hub ## Build and Run the Hub
@ -46,11 +46,17 @@ The default bind address is `0.0.0.0:9090`.
## Hub Configuration ## Hub Configuration
Copy `bin/icom/config.example.toml` to `config.toml` beside the `forge-icom` Copy `bin/icom/config.example.toml` to `config.toml` beside the `forge-icom`
executable or into the working directory used to launch it. executable or into the working directory used to launch it. If you are using
Forge Host, prefer the shared repo-root config from `bin/host/config.example.toml`;
it contains the same `[server]` section and the standalone ICOM hub ignores the
extra host and extension sections.
```toml ```toml
[server] [server]
# Listen on all interfaces so remote Arma servers can connect.
host = "0.0.0.0" host = "0.0.0.0"
# TCP port used by extension `icom:connect`.
port = 9090 port = 9090
``` ```
@ -62,11 +68,11 @@ layer.
ICOM commands are exposed through the `icom` command group in `forge_server`. ICOM commands are exposed through the `icom` command group in `forge_server`.
| Command | Arguments | Returns | | Command | Arguments | Returns |
| --- | --- | --- | | ----------------- | ------------------------------------------ | ----------------------------------------------------- |
| `icom:connect` | `address`, `server_id` | `Connection initiated` or `ERROR: Already connected`. | | `icom:connect` | `address`, `server_id` | `Connection initiated` or `ERROR: Already connected`. |
| `icom:send_event` | `target_server`, `event_name`, `data_json` | `OK` or `ERROR: <reason>`. | | `icom:send_event` | `target_server`, `event_name`, `data_json` | `OK` or `ERROR: <reason>`. |
| `icom:broadcast` | `event_name`, `data_json` | `OK` or `ERROR: <reason>`. | | `icom:broadcast` | `event_name`, `data_json` | `OK` or `ERROR: <reason>`. |
The current extension connects when `icom:connect` is called. Start the ICOM hub The current extension connects when `icom:connect` is called. Start the ICOM hub
first, then connect each Arma server with a unique `server_id`. first, then connect each Arma server with a unique `server_id`.
@ -144,8 +150,8 @@ registration payload:
```json ```json
{ {
"type": "register", "type": "register",
"server_id": "server_1" "server_id": "server_1"
} }
``` ```
@ -153,12 +159,12 @@ Targeted events use `type: "event"`:
```json ```json
{ {
"type": "event", "type": "event",
"target_server": "server_2", "target_server": "server_2",
"event_name": "supply_drop", "event_name": "supply_drop",
"data": { "data": {
"coords": [1234, 5678, 0] "coords": [1234, 5678, 0]
} }
} }
``` ```

View File

@ -28,11 +28,18 @@ Before starting a Forge-enabled dedicated server or local multiplayer test,
server owners and developers must: server owners and developers must:
1. Start SurrealDB. 1. Start SurrealDB.
2. Place `config.toml` beside `forge_server_x64.dll`. 2. Place a Forge `config.toml` where the extension can find it. Use
`arma/server/extension/config.example.toml` for an extension-only config, or
use `bin/host/config.example.toml` at the repo root when Forge Host should
manage SurrealDB, ICOM, and the Arma server.
3. Keep the `config.toml` SurrealDB endpoint, namespace, database, username, 3. Keep the `config.toml` SurrealDB endpoint, namespace, database, username,
and password aligned with the running database. and password aligned with the running database.
4. Load `@forge_mod` with the server's normal mod list and `@forge_server` as 4. Load `@forge_mod` with the server's normal mod list and `@forge_server` as
a server-only mod. a server-only mod.
5. Start Arma with both dedicated server config files: `server.cfg` through
`-config` for server rules and mission rotation, and `basic.cfg` through
`-cfg` for network tuning. Forge Host can create both files from
`bin/host/server.example.cfg` and `bin/host/basic.example.cfg`.
Mission designers and players do not need to run SurrealDB unless they are Mission designers and players do not need to run SurrealDB unless they are
hosting locally, but they do need `@forge_mod` for Forge mission config classes. hosting locally, but they do need `@forge_mod` for Forge mission config classes.

View File

@ -9,8 +9,10 @@ comes down to running a reachable database and matching the Forge config.
Before launching an Arma server or local multiplayer test with Forge enabled: 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. 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 2. Create Forge's `config.toml`. Use `bin/host/config.example.toml` at the repo
`forge_server_x64.dll`. root when Forge Host manages local services, or copy
`arma/server/extension/config.example.toml` beside `forge_server_x64.dll`
for an extension-only deployment.
3. Make sure `config.toml` matches the running SurrealDB endpoint, namespace, 3. Make sure `config.toml` matches the running SurrealDB endpoint, namespace,
database, username, and password. database, username, and password.
@ -70,9 +72,12 @@ surreal start --user root --pass root --bind 127.0.0.1:8000 rocksdb://forge.db
`root`/`root` is only the local development default. For a public or shared `root`/`root` is only the local development default. For a public or shared
server, set a real password and keep `config.toml` aligned. server, set a real password and keep `config.toml` aligned.
Then copy `arma/server/extension/config.example.toml` to `config.toml` next to Then create Forge's `config.toml` and keep the values aligned with the database
`forge_server_x64.dll` and keep the values aligned with the database you you started. If you use Forge Host locally, copy `bin/host/config.example.toml`
started: to the repo root as `config.toml`; the host will use the same file for managed
processes, ICOM, and extension settings. If you only need the Arma extension
config, copy `arma/server/extension/config.example.toml` next to
`forge_server_x64.dll`.
```toml ```toml
[surreal] [surreal]

View File

@ -9,8 +9,10 @@ comes down to running a reachable database and matching the Forge config.
Before launching an Arma server or local multiplayer test with Forge enabled: 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. 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 2. Create Forge's `config.toml`. Use `bin/host/config.example.toml` at the repo
`forge_server_x64.dll`. root when Forge Host manages local services, or copy
`arma/server/extension/config.example.toml` beside `forge_server_x64.dll`
for an extension-only deployment.
3. Make sure `config.toml` matches the running SurrealDB endpoint, namespace, 3. Make sure `config.toml` matches the running SurrealDB endpoint, namespace,
database, username, and password. database, username, and password.
@ -70,9 +72,12 @@ surreal start --user root --pass root --bind 127.0.0.1:8000 rocksdb://forge.db
`root`/`root` is only the local development default. For a public or shared `root`/`root` is only the local development default. For a public or shared
server, set a real password and keep `config.toml` aligned. server, set a real password and keep `config.toml` aligned.
Then copy `arma/server/extension/config.example.toml` to `config.toml` next to Then create Forge's `config.toml` and keep the values aligned with the database
`forge_server_x64.dll` and keep the values aligned with the database you you started. If you use Forge Host locally, copy `bin/host/config.example.toml`
started: to the repo root as `config.toml`; the host will use the same file for managed
processes, ICOM, and extension settings. If you only need the Arma extension
config, copy `arma/server/extension/config.example.toml` next to
`forge_server_x64.dll`.
```toml ```toml
[surreal] [surreal]

View File

@ -129,8 +129,11 @@ server addon fnc_extCall
## Configuration ## Configuration
The server extension reads `config.toml` next to the extension DLL. The current The server extension reads `config.toml` from the server working directory,
persistence section is: `@forge_server/config.toml`, or beside the extension DLL. When Forge Host is
used during development, the repo-root `config.toml` can be shared across Forge
Host, ICOM, and the extension. The extension reads the `[surreal]` section and
ignores the host-only sections. The current persistence section is:
```toml ```toml
[surreal] [surreal]
@ -142,12 +145,20 @@ password = "root"
connect_timeout_ms = 5000 connect_timeout_ms = 5000
``` ```
`config.toml` is a launch prerequisite for server owners and developers. The `config.toml` is a launch prerequisite for server owners and developers. Use
file must exist beside `forge_server_x64.dll`, and SurrealDB must already be `arma/server/extension/config.example.toml` for an extension-only config, or
running at the configured endpoint before starting a Forge-enabled dedicated `bin/host/config.example.toml` for the shared host-managed config. SurrealDB must
server or local multiplayer test. Clients and mission designers do not run this already be running at the configured endpoint before starting a Forge-enabled
configuration unless they are hosting locally, but the server they connect to dedicated server or local multiplayer test. Clients and mission designers do not
must have it in place. run this configuration unless they are hosting locally, but the server they
connect to must have it in place.
Arma's own dedicated server files remain separate from Forge's TOML config.
Launch `server.cfg` with `-config` for server rules, mission rotation, passwords,
and admin settings. Launch `basic.cfg` with `-cfg` for network and performance
tuning. Forge Host exposes both paths in the Arma Server settings view and can
create them from `bin/host/server.example.cfg` and
`bin/host/basic.example.cfg`.
For install links and role-based setup guidance, see For install links and role-based setup guidance, see
[SurrealDB Setup](/getting-started/surrealdb-setup). [SurrealDB Setup](/getting-started/surrealdb-setup).

View File

@ -763,14 +763,26 @@ CAD dispatcher-requested generation.
The optional framework mission setup UI lets the setup operator choose runtime The optional framework mission setup UI lets the setup operator choose runtime
tuning such as opposing faction, mission cap, interval, location cooldown, tuning such as opposing faction, mission cap, interval, location cooldown,
reward ranges, reputation ranges, penalty ranges, time limits, and a generator reward ranges, reputation ranges, penalty ranges, time limits, and a generator
provider preference. It does not enable or disable generated missions; use the provider preference. It also exposes service pricing for medical spawn, heal,
CBA setting for that policy. repair, rearm, refuel, and transport defaults. It does not enable or disable
generated missions; use the CBA setting for that policy.
Task time limits can be disabled from the setup UI by turning off the task
timer. That stores `timeLimitMin = 0` and `timeLimitMax = 0`, which generated
tasks treat as no timer. Positive min/max values enable task timers and are
rolled in seconds.
If mission setup is enabled, the mission manager waits until the setup operator If mission setup is enabled, the mission manager waits until the setup operator
applies settings. Cancel, X, and Escape apply default values from CBA, mission applies settings. Cancel, X, and Escape apply default values from CBA, mission
parameters, and `CfgMissions`. There is no timeout that auto-applies defaults. parameters, and `CfgMissions`. There is no timeout that auto-applies defaults.
After settings are applied, the setup UI cannot be reopened. After settings are applied, the setup UI cannot be reopened.
Service pricing fallback values live in mission-local `CfgServicePricing.hpp`.
Mission `Params` with matching names, such as `medicalHealCost`,
`serviceRepairCost`, `serviceRearmCost`, `fuelCost`, `transportBaseFare`, and
`transportPricePerKm`, are read before the setup UI hydrates so mission makers
can keep a non-UI backup.
The setup UI stores the provider preference as `builtin` or `custom`. CAD/manual The setup UI stores the provider preference as `builtin` or `custom`. CAD/manual
generated task requests use the task provider registry and route to the selected generated task requests use the task provider registry and route to the selected
provider. Custom generators should register a provider or create CAD-visible provider. Custom generators should register a provider or create CAD-visible

View File

@ -8,8 +8,10 @@ description: "Forge uses SurrealDB for durable storage. The Rust server extensio
Before launching an Arma server or local multiplayer test with Forge enabled: 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. 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 2. Create Forge's `config.toml`. Use `bin/host/config.example.toml` at the repo
`forge_server_x64.dll`. root when Forge Host manages local services, or copy
`arma/server/extension/config.example.toml` beside `forge_server_x64.dll`
for an extension-only deployment.
3. Make sure `config.toml` matches the running SurrealDB endpoint, namespace, 3. Make sure `config.toml` matches the running SurrealDB endpoint, namespace,
database, username, and password. database, username, and password.
@ -35,45 +37,12 @@ Official SurrealDB resources:
- [SurrealDB install page](https://surrealdb.com/install) - [SurrealDB install page](https://surrealdb.com/install)
- [SurrealDB CLI `start` reference](https://surrealdb.com/docs/surrealdb/cli/start) - [SurrealDB CLI `start` reference](https://surrealdb.com/docs/surrealdb/cli/start)
Forge also includes helper scripts under `arma/server/surrealdb`: Forge Host includes the recommended local setup path. Open the SurrealDB view
to install or update SurrealDB, configure the bind address and database path,
```powershell and start or stop the local database. Enter `3` to install the latest compatible
cd arma/server/surrealdb SurrealDB 3.x release, enter an exact version such as `v3.1.2` to pin a
.\UpdateMe.bat release, or enter `latest` only after confirming compatibility with the Forge
.\RunMe.bat server extension.
```
On Windows, `UpdateMe.bat` is a wrapper around `UpdateSurrealDB.ps1`. By
default it installs or updates to the newest compatible SurrealDB 3.x release
reported by SurrealDB's official version endpoint. You can also pin an exact
release:
```powershell
.\UpdateMe.bat v3.1.2
.\UpdateSurrealDB.ps1 -Version v3.1.2
```
To intentionally install the latest stable SurrealDB release regardless of
major version, run:
```powershell
.\UpdateMe.bat latest
```
The `latest` option prompts for confirmation because a newer SurrealDB major
version can require rebuilding the Forge server extension from source with a
compatible `surrealdb` Rust crate.
`RunMe.bat` is a wrapper around `RunSurrealDB.ps1`, which starts the local
Forge database with the same defaults shown below.
On Linux or macOS:
```bash
cd arma/server/surrealdb
./setup.sh
./run.sh
```
Install SurrealDB with the official method for your platform: Install SurrealDB with the official method for your platform:
@ -102,9 +71,12 @@ surreal start --user root --pass root --bind 127.0.0.1:8000 rocksdb://forge.db
`root`/`root` is only the local development default. For a public or shared `root`/`root` is only the local development default. For a public or shared
server, set a real password and keep `config.toml` aligned. server, set a real password and keep `config.toml` aligned.
Then copy `arma/server/extension/config.example.toml` to `config.toml` next to Then create Forge's `config.toml` and keep the values aligned with the database
`forge_server_x64.dll` and keep the values aligned with the database you you started. If you use Forge Host locally, copy `bin/host/config.example.toml`
started: to the repo root as `config.toml`; the host will use the same file for managed
processes, ICOM, and extension settings. If you only need the Arma extension
config, copy `arma/server/extension/config.example.toml` next to
`forge_server_x64.dll`.
```toml ```toml
[surreal] [surreal]

View File

@ -1,158 +0,0 @@
---
title: "Git Workflow"
description: "This repository uses `master` as the clean framework branch. Mission folders are kept off `master` so the framework can be versioned without bundling local test missions or playable mission copies."
---
## Workflow Helper
The repository includes a small helper for the common branch checks and branch
switching commands:
```powershell
npm run workflow -- status
npm run workflow -- doctor
npm run workflow -- switch dev
npm run workflow -- switch missions
npm run workflow -- start-feature cad-task-request
npm run workflow -- release-check
```
The helper refuses branch switches and feature branch creation when the working
tree has uncommitted changes. Use the manual Git commands below when you need
more control.
## Branch Roles
- `master`: framework source, addon code, Rust extension code, docs, tooling,
and release tags.
- `missions/local-mission-copies`: local mission folders used for testing and
mission iteration. This branch is not pushed unless intentionally needed.
- `archive/pre-v0.1-history`: read-only archive of the previous full `master`
history before the `v0.1.0` baseline cleanup.
## Daily Framework Work
Start from the clean framework branch.
```powershell
git switch master
git pull
git status --short --branch
```
Create a short-lived feature branch for framework work.
```powershell
git switch -c feature/garage-marker-selection
```
Make the change, validate it, then commit.
```powershell
git status --short --branch
git add arma/client/addons/garage/functions/fnc_initContextService.sqf
git commit -m "Improve garage spawn marker selection"
```
Merge the work back into `master`. Squash merges keep future `master` history
compact.
```powershell
git switch master
git merge --squash feature/garage-marker-selection
git commit -m "Improve garage spawn marker selection"
git push
```
Remove the local feature branch when it is no longer needed.
```powershell
git branch -D feature/garage-marker-selection
```
## Mission Work
Switch to the local mission branch before editing mission folders.
```powershell
git switch missions/local-mission-copies
git status --short --branch
```
Mission folders currently tracked on that branch:
```text
arma/forge_framework.Malden
arma/forge_pmc_simulator.Tanoa
arma/forge_pmc_simulator_v2.Tanoa
```
Commit mission-only changes on the mission branch.
```powershell
git add arma/forge_pmc_simulator.Tanoa
git commit -m "Update PMC simulator mission setup"
```
Do not merge the mission branch into `master`. If a mission change becomes
framework code, copy only the reusable files or logic onto a framework feature
branch created from `master`.
Example:
```powershell
git switch master
git switch -c feature/cad-on-demand-task-request
# Bring over only the framework files needed from the mission branch.
git checkout missions/local-mission-copies -- arma/client/addons/cad/functions/fnc_initUIBridge.sqf
git checkout missions/local-mission-copies -- arma/server/addons/cad/XEH_preInit.sqf
git add arma/client/addons/cad/functions/fnc_initUIBridge.sqf arma/server/addons/cad/XEH_preInit.sqf
git commit -m "Add CAD on-demand mission task request bridge"
```
## Release Versioning
Use tags to mark framework releases.
Version guideline:
- Patch, such as `v0.1.1`: fixes and small compatible changes.
- Minor, such as `v0.2.0`: new modules or features.
- Major, such as `v1.0.0`: stable release line or breaking changes.
Create a release tag from `master`.
```powershell
git switch master
git pull
git status --short --branch
git tag -a v0.1.1 -m "v0.1.1"
git push origin master
git push origin v0.1.1
```
## Safety Checks
Before committing on `master`, check that no mission folders are staged.
```powershell
git status --short --branch
```
On `master`, these paths should not appear:
```text
arma/forge_framework.Malden
arma/forge_pmc_simulator.Tanoa
arma/forge_pmc_simulator_v2.Tanoa
```
If mission files appear while on `master`, stop and switch to the mission
branch before continuing.
```powershell
git switch missions/local-mission-copies
```

View File

@ -19,12 +19,12 @@ through `arma/server/extension/src/icom.rs`.
## Components ## Components
| Component | Path | Role | | Component | Path | Role |
| --- | --- | --- | | ----------------------- | ----------------------------------------- | ------------------------------------------------------------------------- |
| ICOM hub binary | `bin/icom` | Standalone TCP router for connected servers. | | ICOM hub binary | `bin/icom` | Standalone TCP router for connected servers. |
| ICOM client library | `bin/icom/src/client.rs` | Rust client used by the Forge server extension and examples. | | ICOM client library | `bin/icom/src/client.rs` | Rust client used by the Forge server extension and examples. |
| Extension command group | `arma/server/extension/src/icom.rs` | Exposes `icom:*` commands to SQF and forwards inbound events to Arma. | | Extension command group | `arma/server/extension/src/icom.rs` | Exposes `icom:*` commands to SQF and forwards inbound events to Arma. |
| SQF callback bridge | `arma/server/addons/main/XEH_preInit.sqf` | Receives extension callbacks and re-emits `forge_icom_event` through CBA. | | SQF callback bridge | `arma/server/addons/main/XEH_preInit.sqf` | Receives extension callbacks and re-emits `forge_icom_event` through CBA. |
## Build and Run the Hub ## Build and Run the Hub
@ -45,11 +45,17 @@ The default bind address is `0.0.0.0:9090`.
## Hub Configuration ## Hub Configuration
Copy `bin/icom/config.example.toml` to `config.toml` beside the `forge-icom` Copy `bin/icom/config.example.toml` to `config.toml` beside the `forge-icom`
executable or into the working directory used to launch it. executable or into the working directory used to launch it. If you are using
Forge Host, prefer the shared repo-root config from `bin/host/config.example.toml`;
it contains the same `[server]` section and the standalone ICOM hub ignores the
extra host and extension sections.
```toml ```toml
[server] [server]
# Listen on all interfaces so remote Arma servers can connect.
host = "0.0.0.0" host = "0.0.0.0"
# TCP port used by extension `icom:connect`.
port = 9090 port = 9090
``` ```
@ -61,11 +67,11 @@ layer.
ICOM commands are exposed through the `icom` command group in `forge_server`. ICOM commands are exposed through the `icom` command group in `forge_server`.
| Command | Arguments | Returns | | Command | Arguments | Returns |
| --- | --- | --- | | ----------------- | ------------------------------------------ | ----------------------------------------------------- |
| `icom:connect` | `address`, `server_id` | `Connection initiated` or `ERROR: Already connected`. | | `icom:connect` | `address`, `server_id` | `Connection initiated` or `ERROR: Already connected`. |
| `icom:send_event` | `target_server`, `event_name`, `data_json` | `OK` or `ERROR: <reason>`. | | `icom:send_event` | `target_server`, `event_name`, `data_json` | `OK` or `ERROR: <reason>`. |
| `icom:broadcast` | `event_name`, `data_json` | `OK` or `ERROR: <reason>`. | | `icom:broadcast` | `event_name`, `data_json` | `OK` or `ERROR: <reason>`. |
The current extension connects when `icom:connect` is called. Start the ICOM hub The current extension connects when `icom:connect` is called. Start the ICOM hub
first, then connect each Arma server with a unique `server_id`. first, then connect each Arma server with a unique `server_id`.
@ -143,8 +149,8 @@ registration payload:
```json ```json
{ {
"type": "register", "type": "register",
"server_id": "server_1" "server_id": "server_1"
} }
``` ```
@ -152,12 +158,12 @@ Targeted events use `type: "event"`:
```json ```json
{ {
"type": "event", "type": "event",
"target_server": "server_2", "target_server": "server_2",
"event_name": "supply_drop", "event_name": "supply_drop",
"data": { "data": {
"coords": [1234, 5678, 0] "coords": [1234, 5678, 0]
} }
} }
``` ```

View File

@ -1,6 +1,6 @@
--- ---
title: "Store Usage Guide" title: "Store Usage Guide"
description: "The store module processes checkout requests. It charges a payment source and grants purchased items to the player locker, virtual arsenal locker, virtual garage unlocks, and immediate unit spawn grants." description: "The store module processes checkout requests. It charges a payment source and grants purchased items to the player locker, virtual arsenal locker, and virtual garage unlocks. Unit purchases are fulfilled as immediate server-side spawn grants at discovered `unit_spawn` markers."
--- ---
## Server SQF Module ## Server SQF Module
@ -9,13 +9,10 @@ The server addon uses two long-lived module objects:
- `StorefrontStore` is the storefront workflow facade. It builds hydrate - `StorefrontStore` is the storefront workflow facade. It builds hydrate
payloads, validates checkout requests, calls the Rust `store:checkout` payloads, validates checkout requests, calls the Rust `store:checkout`
command, syncs UI patches, asks related module stores to save hot state, and command, syncs UI patches, and asks related module stores to save hot state.
spawns purchased units at discovered `unit_spawn` markers after the backend
charge succeeds.
- `StoreCatalogService` scans configured item and vehicle categories, builds - `StoreCatalogService` scans configured item and vehicle categories, builds
catalog responses, resolves checkout entries, and calculates authoritative catalog responses, resolves checkout entries, and calculates authoritative
prices. It also applies the optional mission `CfgStore` filter and overrides prices.
before payloads or checkout validation use catalog entries.
Editor-placed store entities are initialized by `fnc_initStore` during store Editor-placed store entities are initialized by `fnc_initStore` during store
post-init. The initializer matches non-null mission namespace objects whose post-init. The initializer matches non-null mission namespace objects whose
@ -35,6 +32,26 @@ Include `CfgStore.hpp` from `description.ext`:
```cpp ```cpp
class CfgStore { class CfgStore {
mode = "allowlist"; // dynamic, allowlist, or denylist mode = "allowlist"; // dynamic, allowlist, or denylist
modMode = "dynamic"; // dynamic, allowlist, or denylist
mods[] = {}; // ModSources child class names used when modMode is not dynamic
class ModSources {
class rhs {
patches[] = {"rhs_main", "rhsusf_main"};
addons[] = {"rhs_", "rhsusf_", "rhsgref_", "rhsafrf_"};
prefixes[] = {"rhs_", "rhsusf_", "rhsgref_", "rhsafrf_"};
contains[] = {"rhs_", "rhsusf_", "rhsgref_", "rhsafrf_"};
dlcs[] = {};
};
class ace3 {
patches[] = {"ace_main"};
addons[] = {"ace_"};
prefixes[] = {"ace_"};
contains[] = {"ace_"};
dlcs[] = {};
};
};
class Categories { class Categories {
primary[] = {"arifle_MX_F", "arifle_MXC_F"}; primary[] = {"arifle_MX_F", "arifle_MXC_F"};
@ -55,9 +72,40 @@ class CfgStore {
`dynamic` keeps the full generated catalog. `allowlist` only shows classnames `dynamic` keeps the full generated catalog. `allowlist` only shows classnames
listed for each category. `denylist` removes listed classnames. Overrides are listed for each category. `denylist` removes listed classnames. Overrides are
server-side and are used by both the UI payload and checkout validation. server-side and are used by both the UI payload and checkout validation.
`units[]` follows the same filter behavior and is fulfilled as an immediate `units[]` uses the same filter behavior as every other category.
server-side unit spawn at a discovered `unit_spawn` marker after checkout
succeeds. `modMode` applies before category filtering. `dynamic` means no mod-source
filtering. `allowlist` only keeps generated entries that match one of the
configured `mods[]`; `denylist` removes matching entries. Each `ModSources`
child can define `patches[]` to detect whether the mod is loaded, `addons[]`
for exact config source addon/source mod names, `prefixes[]` for classname,
source addon, or source mod prefixes, `contains[]` for classname/source
metadata tokens that can appear anywhere, and `dlcs[]` for DLC/source/author
labels used by Creator DLC content. If a mod source defines no patches, it is
treated as available and only the source/prefix/contains/DLC checks are used.
For example, to show only RHS-sourced generated inventory:
```cpp
modMode = "allowlist";
mods[] = {"rhs"};
```
The matching `class rhs` must exist under `ModSources`. Category `mode` is still
applied afterward, so leave `mode = "dynamic"` if the mod filter should be the
only inventory filter.
For Creator DLCs such as RF or WS, prefer both prefixes and DLC labels:
```cpp
class rf {
patches[] = {};
addons[] = {"lxrf_", "rf_"};
prefixes[] = {"lxrf_", "rf_"};
contains[] = {"lxrf", "_rf_", "_rf", "rf_"};
dlcs[] = {"rf", "reactionforces", "reaction forces"};
};
```
The current filter is global for the mission. Revisit per-store profile support The current filter is global for the mission. Revisit per-store profile support
if individual vendors need different inventories. if individual vendors need different inventories.

View File

@ -187,14 +187,21 @@ server-side.
The mission setup UI does not enable or disable generated missions. It applies The mission setup UI does not enable or disable generated missions. It applies
runtime tuning such as faction, caps, intervals, reward ranges, rating ranges, runtime tuning such as faction, caps, intervals, reward ranges, rating ranges,
penalties, time limits, and a generator provider preference. Generator penalties, time limits, service pricing, and a generator provider preference.
enablement remains controlled by the CBA setting above. Generator enablement remains controlled by the CBA setting above.
When `forge_server_task_enableMissionSetup` is enabled, the mission manager When `forge_server_task_enableMissionSetup` is enabled, the mission manager
waits for setup settings before starting. There is no timeout auto-apply. waits for setup settings before starting. There is no timeout auto-apply.
Pressing Cancel, X, or Escape applies default values from CBA, mission Pressing Cancel, X, or Escape applies default values from CBA, mission
parameters, and `CfgMissions`. parameters, and `CfgMissions`.
Service price defaults are stored in `CfgServicePricing`. Mission
`Params` with matching names override those defaults before the UI opens, and
submitted UI values override both. The supported names are
`medicalSpawnCost`, `medicalHealCost`, `serviceRepairCost`,
`serviceRearmCost`, `fuelCost`, `transportBaseFare`, and
`transportPricePerKm`.
The setup UI stores the provider preference in The setup UI stores the provider preference in
`forge_server_task_generatorProvider` as `builtin` or `custom`. CAD/manual `forge_server_task_generatorProvider` as `builtin` or `custom`. CAD/manual
generated task requests use the task provider registry and route to the selected generated task requests use the task provider registry and route to the selected
@ -623,6 +630,10 @@ Task time limits use `0` for no limit:
Positive values are measured in seconds. Do not pass `-1` as a no-limit value; Positive values are measured in seconds. Do not pass `-1` as a no-limit value;
the task runtime treats any non-zero task time limit as active. the task runtime treats any non-zero task time limit as active.
The mission setup UI uses the same rule. Turning off the task timer stores
`timeLimitMin = 0` and `timeLimitMax = 0`; turning it on uses the configured
positive min/max range for generated tasks.
Defuse IED timers are different. `iedTimer` must be greater than `0`, because Defuse IED timers are different. `iedTimer` must be greater than `0`, because
IEDs are expected to have an active countdown. The Eden defuse module defaults IEDs are expected to have an active countdown. The Eden defuse module defaults
to `300` seconds. to `300` seconds.

View File

@ -88,6 +88,12 @@ nearby vehicles, ships, aircraft, and player units. The scan ignores:
Use `transport_vehicle*` names for the actual boat, ferry, aircraft, or set Use `transport_vehicle*` names for the actual boat, ferry, aircraft, or set
dressing object that should not be moved as cargo. dressing object that should not be moved as cargo.
## Pricing
Default transport pricing comes from the mission setup UI or matching mission
`Params` entries named `transportBaseFare` and `transportPricePerKm`. If neither
is set, `CfgServicePricing` provides the fallback.
## Optional Per-Node Overrides ## Optional Per-Node Overrides
The default naming convention should cover normal missions. If a specific The default naming convention should cover normal missions. If a specific
@ -107,7 +113,8 @@ this setVariable ["transportCargoRadius", 25, true];
this setVariable ["transportIncludeCargo", true, true]; this setVariable ["transportIncludeCargo", true, true];
``` ```
Only use overrides when the default `transport*` convention is not appropriate. Only use overrides when the default `transport*` convention or mission-level
pricing is not appropriate.
## Image Checklist ## Image Checklist

View File

@ -27,6 +27,10 @@ calculates missing fuel from the vehicle config `fuelCapacity`, charges the
player's organization, and fills the vehicle only after the organization charge player's organization, and fills the vehicle only after the organization charge
succeeds. succeeds.
The refuel price per liter is controlled by `fuelCost`. The mission setup UI
can override it at startup; otherwise a mission `Params` entry named
`fuelCost` or `CfgServicePricing >> fuelCost` is used.
## Repair ## Repair
Repair is organization-funded. Repair is organization-funded.
@ -43,6 +47,9 @@ The target is only repaired after the organization charge succeeds.
The client garage UI forwards selected nearby vehicle repair requests through The client garage UI forwards selected nearby vehicle repair requests through
the same event. the same event.
The default repair charge is controlled by `serviceRepairCost`. A direct
service event can still pass a concrete `_cost` to override that request.
## Rearm ## Rearm
Rearm is organization-funded. Rearm is organization-funded.
@ -61,6 +68,9 @@ turrets, so the service broadcasts the ammo reset after billing succeeds.
The client garage UI forwards selected nearby vehicle rearm requests through The client garage UI forwards selected nearby vehicle rearm requests through
the same event. the same event.
The default rearm charge is controlled by `serviceRearmCost`. A direct service
event can still pass a concrete `_cost` to override that request.
## Medical ## Medical
Medical is player-funded first. Medical is player-funded first.
@ -77,6 +87,15 @@ The heal only completes after one of those charges succeeds. If personal
billing is unavailable, the heal does not fall back to organization funds billing is unavailable, the heal does not fall back to organization funds
because the server cannot verify that the player is unable to cover the fee. because the server cannot verify that the player is unable to cover the fee.
Medical pricing uses:
- `medicalHealCost` for heal billing.
- `medicalSpawnCost` for medical respawn billing. Respawn billing is
best-effort so a failed charge does not block the respawn flow.
Both values can be set in the mission setup UI, mission `Params`, or
`CfgServicePricing`.
## Medical Debt Repayment ## Medical Debt Repayment
Medical fallback debt uses the existing organization credit-line repayment Medical fallback debt uses the existing organization credit-line repayment

View File

@ -79,6 +79,25 @@ New owned garages are created with default unlocks from the Rust model.
| `owned:garage:delete` | `uid` | `OK`. | | `owned:garage:delete` | `uid` | `OK`. |
| `owned:garage:exists` | `uid` | `true` or `false`. | | `owned:garage:exists` | `uid` | `true` or `false`. |
## Starting Equipment And Unlocks
Missions can include `CfgStartingEquipment.hpp` from `description.ext` to set
new-player loadout and initial virtual arsenal or garage unlocks without
recompiling the framework.
```cpp
#include "CfgStartingEquipment.hpp"
```
`loadout[]` uses the standard Arma loadout array shape. `Unlocks.Locker`
supports `items[]`, `weapons[]`, `magazines[]`, and `backpacks[]`.
`Unlocks.Garage` supports `cars[]`, `armor[]`, `helis[]`, `planes[]`,
`naval[]`, and `other[]`.
The extension defaults are intentionally empty. The server seeds mission
starting unlocks only when a player does not already have persistent owned
locker or garage records.
## Add Virtual Arsenal Unlocks ## Add Virtual Arsenal Unlocks
```sqf ```sqf