Compare commits

...

No commits in common. "archive/pre-v0.1-history" and "master" have entirely different histories.

122 changed files with 144 additions and 14481 deletions

View File

@ -50,6 +50,7 @@ npm run build:webui
- [Framework Architecture](./docs/FRAMEWORK_ARCHITECTURE.md)
- [Module Reference](./docs/MODULE_REFERENCE.md)
- [Development Guide](./docs/DEVELOPMENT_GUIDE.md)
- [Git Workflow](./docs/GIT_WORKFLOW.md)
## Extension Status

View File

@ -1,58 +0,0 @@
/*
* Runtime enemy faction discovery controls for the PMC simulator setup flow.
*
* Consumers:
* - forge_pmc_fnc_getEnemyFactionOptions scans loaded CfgFactionClasses and
* CfgVehicles at runtime, then uses this config to filter and polish the UI.
* - forge_pmc_fnc_resolveEnemyFactionParam uses override values here to keep
* legacy mission params compatible when the setup UI is cancelled.
* - Mission generators use ENEMY_FACTION_STR and ENEMY_SIDE after setup has
* applied the selected option.
*
* This config is intentionally not the full faction list. Any loaded mod
* faction on an allowed side can appear automatically if it has spawnable
* infantry or a CfgFactionUnitMap override.
*/
class CfgEnemyFactions {
/*
* Arma side IDs allowed as generated enemy factions:
* - 0: EAST / OPFOR
* - 2: RESISTANCE / Independent
*
* WEST / BLUFOR is intentionally excluded because generated missions use
* these options as opposing forces.
*/
sides[] = {0, 2};
/*
* Factions that should never be offered even if present in the active
* modset. Keep this for factions that are unsuitable for PMC contracts.
*/
denylist[] = {
"IND_UN_lxWS"
};
/*
* Optional display/order/value metadata for known factions.
*
* - value keeps legacy Params::enemyFaction values stable.
* - order controls setup UI ordering before dynamically discovered factions.
* - display overrides raw CfgFactionClasses names when we want cleaner text.
*
* Factions not listed here are still discovered automatically and sorted
* after these known options.
*/
class Overrides {
class OPF_F { value = 0; order = 0; display = "CSAT"; };
class OPF_T_F { value = 1; order = 1; display = "CSAT (Pacific)"; };
class OPF_R_F { value = 2; order = 2; display = "Spetnaz"; };
class OPF_SFIA_lxWS { value = 3; order = 3; display = "SFIA"; };
class OPF_TURA_lxWS { value = 4; order = 4; display = "Tura"; };
class IND_F { value = 5; order = 5; display = "AAF"; };
class IND_G_F { value = 6; order = 6; display = "FIA"; };
class IND_E_F { value = 7; order = 7; display = "LDF"; };
class IND_C_F { value = 8; order = 8; display = "Syndikat"; };
class IND_L_F { value = 9; order = 9; display = "Looters"; };
class IND_TURA_lxWS { value = 10; order = 10; display = "Tura"; };
};
};

View File

@ -1,34 +0,0 @@
/*
* Optional faction-to-unit override map.
*
* Current behavior:
* - forge_pmc_fnc_getEnemyFactionOptions treats a mapped faction as selectable
* when at least one mapped vehicle exists.
* - forge_pmc_fnc_getEnemyFactionUnitPool checks this map first.
* - If a selected faction has a class here, the listed Units are used as the
* deterministic spawn pool for generated mission enemies.
* - If no class exists here, the helper falls back to CfgVehicles traversal for
* units whose faction and side match the selected faction.
*
* Most mod factions do not need an entry here. Add a class only when a faction
* needs a curated or corrected spawn pool.
*/
class CfgFactionUnitMap {
/*
* Mapping key should match the selected faction classname from
* CfgFactionClasses, such as "IND_G_F".
*/
class IND_G_F {
/*
* Unit template fields:
* - vehicle: unit classname to spawn.
* - rank: Arma rank string applied after spawn.
* - position[]: base local offset from the generated mission position.
*
* Generators may add small random jitter to the position offset.
*/
class Units {
class Unit0 { vehicle = "Ind_G_Unit1_F"; rank = "SERGEANT"; position[] = {0,0,0}; };
};
};
};

View File

@ -1,46 +0,0 @@
class CfgFunctions {
class forge_pmc {
tag = "forge_pmc";
class helpers {
file = "functions\helpers";
class getAllEnemyFactions {};
class getEnemyFactionListboxSelection {};
class getEnemyFactionOptions {};
class getEnemyFactionSide {};
class getEnemyFactionUnitPool {};
class getMissionSettingRange {};
class populateEnemyFactionListbox {};
class resolveEnemyFactionParam {};
};
class missionSetup {
file = "functions\missionSetup";
class handleMissionSetupUIEvents {};
class openMissionSetupUI {};
class setupMenu_applySettings {};
};
class missionManager {
file = "functions\missionManager";
class missionManager {
postInit = 1;
};
class persistentCadMissionManager {};
class requestMissionTask {};
class updateEnemyCountFromActivePlayers {};
};
class missionGenerators {
file = "functions\missionGenerators";
class attackMissionGenerator {};
class defendMissionGenerator {};
class destroyMissionGenerator {};
class deliveryMissionGenerator {};
class defuseMissionGenerator {};
class hostageMissionGenerator {};
class hvtMissionGenerator {};
class captureHvtMissionGenerator {};
};
};
};

View File

@ -1,223 +0,0 @@
/*
* PMC simulator dynamic mission configuration.
*
* This file is read by the mission setup UI, the mission manager, and the
* mission generators under functions\missionGenerators.
*
* Startup UI behavior:
* - Values in this config provide defaults for the mission setup UI.
* - If the setup UI is cancelled, Arma mission params/defaults are applied.
* - If the setup UI is submitted, UI values override compatible ranges.
*
* Generator behavior:
* - maxConcurrentMissions and missionInterval are copied into
* forge_pmc_missionSettings by forge_pmc_fnc_setupMenu_applySettings.
* - Reward, reputation, penalty, and timeLimit ranges are read through
* forge_pmc_fnc_getMissionSettingRange so UI overrides and config fallbacks
* use the same path.
*/
class CfgMissions {
// Maximum number of generated missions allowed to be active at once.
maxConcurrentMissions = 3;
// Seconds between mission generation attempts.
missionInterval = 300;
// Seconds before a generated mission location can be reused.
locationReuseCooldown = 900;
// Enemy faction selection is ultimately exported to ENEMY_FACTION_STR and
// ENEMY_SIDE for server-side generators.
class EnemyFactionConfig {
// Mission param key used by fallback/default setup application.
enemyFactionParam = "enemyFaction";
};
// Relative generation weights. The values do not need to add to 1; the
// mission manager treats them as weighted proportions.
class MissionWeights {
attack = 0.2;
defend = 0.2;
hostage = 0.2;
hvtkill = 0.15;
hvtcapture = 0.15;
defuse = 0.15;
delivery = 0.1;
destroy = 0.2;
};
/*
* Mission type settings.
*
* Common fields:
* - Rewards.money[]: min/max funds reward.
* - Rewards.reputation[]: min/max reputation reward.
* - Rewards.<category>[]: item reward rolls as {classname, chance}.
* - penalty[]: min/max reputation penalty on failure.
* - timeLimit[]: min/max task time limit in seconds.
*/
class MissionTypes {
// Search-and-destroy infantry engagement.
class Attack {
minUnits = 4;
maxUnits = 8;
class Rewards {
money[] = {25000, 60000};
reputation[] = {6, 14};
equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-8, -3};
timeLimit[] = {900, 1800};
};
// Hold a generated position through multiple enemy waves.
class Defend {
minWaves = 3;
maxWaves = 8;
// Min/max units spawned per wave before active-player scaling.
unitsPerWave[] = {4, 8};
// Seconds between wave spawns.
waveCooldown = 300;
class Rewards {
money[] = {40000, 90000};
reputation[] = {8, 18};
equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-12, -4};
timeLimit[] = {300, 1800};
};
// Rescue a hostage from a generated hostile site.
class Hostage {
// Candidate hostage classnames by broad source category.
class Hostages {
civilian[] = {"C_journalist_F", "C_Journalist_01_War_F", "C_Man_Paramedic_01_F", "C_scientist_F", "C_IDAP_Pilot_RF", "C_IDAP_Man_Paramedic_01_F", "C_IDAP_Pilot_01_F", "C_IDAP_Man_AidWorker_01_F", "C_IDAP_Man_AidWorker_05_F", "C_pilot_story_RF", "C_pilot2_story_RF", "C_Orestes", "C_Nikos", "C_Journalist_lxWS"};
military[] = {"B_helicrew_F", "B_Helipilot_F", "B_officer_F", "B_Fighter_Pilot_F", "B_Captain_Jay_F", "B_CTRG_soldier_M_medic_F", "B_Story_Pilot_F", "B_CTRG_soldier_GL_LAT_F", "B_Captain_Pettka_F", "B_Survivor_F", "B_Pilot_F"};
};
class Rewards {
money[] = {60000, 140000};
reputation[] = {12, 25};
equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-16, -6};
timeLimit[] = {600, 900};
};
// Eliminate a high-value target with escort security.
class HVTKill {
// Candidate target classnames by role.
class Targets {
officer[] = {"O_officer_F"};
sniper[] = {"O_sniper_F"};
};
// Number of escort units to attempt around the target.
escorts = 4;
class Rewards {
money[] = {50000, 120000};
reputation[] = {10, 22};
equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-14, -5};
timeLimit[] = {900, 1800};
};
// Capture and extract a high-value target.
class HVTCapture {
// Candidate capturable target classnames.
class Targets {
civilian[] = {"C_journalist_F", "C_Journalist_01_War_F", "C_Man_Paramedic_01_F", "C_scientist_F", "C_IDAP_Pilot_RF", "C_IDAP_Man_Paramedic_01_F", "C_IDAP_Pilot_01_F", "C_IDAP_Man_AidWorker_01_F", "C_IDAP_Man_AidWorker_05_F", "C_pilot_story_RF", "C_pilot2_story_RF", "C_Orestes", "C_Nikos", "C_Journalist_lxWS"};
};
// Number of escort units to attempt around the target.
escorts = 4;
class Rewards {
money[] = {50000, 120000};
reputation[] = {10, 22};
equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-14, -5};
timeLimit[] = {900, 1800};
};
// Defuse explosive devices and protect nearby critical objects.
class Defuse {
// Device and protected-object candidate classnames.
class Devices {
small[] = {"DemoCharge_F", "IEDLandSmall_F", "IEDUrbanSmall_F", "ACE_IEDLandSmall_Range", "ACE_IEDUrbanSmall_Range"};
large[] = {"SatchelCharge_F", "IEDLandBig_F", "IEDUrbanBig_F", "ACE_IEDLandBig_Range", "ACE_IEDUrbanBig_Range"};
protected[] = {"CargoNet_01_barrels_F", "CargoNet_01_box_F", "B_CargoNet_01_ammo_F", "C_IDAP_CargoNet_01_supplies_F", "Box_NATO_AmmoVeh_F", "B_supplyCrate_F"};
};
// Maximum explosive devices to place for one generated task.
maxDevices = 1;
class Rewards {
money[] = {20000, 50000};
reputation[] = {5, 12};
equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-9, -3};
timeLimit[] = {600, 900};
};
// Deliver cargo or vehicles between generated locations.
class Delivery {
// Candidate delivery objects grouped by cargo type.
class Cargo {
supplies[] = {"CargoNet_01_barrels_F", "CargoNet_01_box_F", "B_CargoNet_01_ammo_F", "C_IDAP_CargoNet_01_supplies_F", "Box_NATO_AmmoVeh_F", "B_supplyCrate_F"};
vehicles[] = {"B_MRAP_01_F", "B_Truck_01_transport_F", "B_Heli_Transport_03_F", "B_Heli_Transport_03_unarmed_F", "B_Heli_Light_01_F", "B_Heli_Transport_01_F"};
};
class Rewards {
money[] = {10000, 30000};
reputation[] = {3, 8};
equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-6, -2};
timeLimit[] = {900, 1800};
};
// Destroy generated infrastructure targets.
class Destroy {
// Candidate destructible target classnames.
class Bomb {
building[] = {"Land_Radar_F", "Land_Radar_Small_F", "Land_MobileRadar_01_radar_F", "Land_MobileRadar_01_generator_F", "Land_Communication_F", "Land_spp_Tower_F", "Land_TTowerSmall_1_F", "Land_TTowerSmall_2_F", "Land_TTowerBig_1_F", "Land_TTowerBig_2_F"};
};
class Rewards {
money[] = {10000, 30000};
reputation[] = {3, 8};
equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-6, -2};
timeLimit[] = {900, 1800};
};
};
};

View File

@ -1,88 +0,0 @@
/*
* Mission lobby fallback params.
*
* The startup setup UI now discovers selectable factions dynamically from the
* active modset. Params remain intentionally static because Arma evaluates
* them before mission runtime scripts can scan loaded factions. If the setup UI
* is cancelled or never opened, these values provide the default fallback.
*/
class Params {
class enemyFaction {
title = "Enemy Faction";
values[] = {0,1,2,3,4,5,6,7,8,9,10};
texts[] = {
"CSAT",
"CSAT (Pacific)",
"Spetnaz",
"SFIA (OPFOR)",
"Tura (OPFOR)",
"AAF",
"FIA",
"LDF",
"Syndikat",
"Looters",
"Tura (Independent)"
};
default = 6;
};
class maxConcurrentMissions {
title = "Max Concurrent Missions";
values[] = {1,2,3,4,5};
default = 3;
};
class missionInterval {
title = "Mission Interval (seconds)";
values[] = {60,120,300,600,900};
default = 300;
};
class moneyMin {
title = "Money Min";
values[] = {0,10000,25000,40000};
default = 25000;
};
class moneyMax {
title = "Money Max";
values[] = {20000,40000,60000,80000,120000};
default = 60000;
};
class reputationMin {
title = "Reputation Min";
values[] = {0,2,4,6,8,10};
default = 6;
};
class reputationMax {
title = "Reputation Max";
values[] = {10,12,14,18,22,30};
default = 14;
};
class penaltyMin {
title = "Penalty Min";
values[] = {-20,-16,-12,-8,-6,-4,-3};
default = -8;
};
class penaltyMax {
title = "Penalty Max";
values[] = {-20,-16,-12,-8,-6,-4,-3};
default = -3;
};
class timeLimitMin {
title = "Time Limit Min (seconds)";
values[] = {300,600,900,1200,1800};
default = 900;
};
class timeLimitMax {
title = "Time Limit Max (seconds)";
values[] = {600,900,1800,2400,3600};
default = 1800;
};
};

View File

@ -1,136 +0,0 @@
Mission updates
Implemented in mission sandbox:
- Mission manager now skips unavailable generators instead of throwing undefined-variable errors.
- Enemy-count scaling now executes through `forge_pmc_fnc_updateEnemyCountFromActivePlayers` and is applied by generated enemy spawns.
- HVT kill/capture generators no longer call an undefined patrol method; they spawn the HVT plus escorts directly.
- Capture HVT now publishes `CaptureHvtMissionGenerator` without overwriting `HvtMissionGenerator`.
- Delivery zones are placed at a random safe map location away from the cargo pickup.
- Destroy missions prefer nearby map-placed configured target buildings before spawning a fallback target.
- Defuse, Hostage, and HVT generators can use nearby building positions where available.
- ~~all missions require player count check when spawning~~
- Added `forge_pmc_fnc_updateEnemyCountFromActivePlayers` execution to generated enemy spawn paths. Attack, Defend, Defuse, Destroy, Hostage, HVT Kill, and HVT Capture now apply the active-player multiplier where they spawn enemy units or escorts.
{Mission manager}
Wants:
- ~~Prevent a missing/failed generator from breaking mission manager startup.~~
- Mission manager now builds the generator list from variable names and skips unavailable/invalid generators with a warning.
- ~~Support both HVT mission variants in weighted selection.~~
- Registered generator keys now match `CfgMissions` weights: `hvtkill` and `hvtcapture`.
Errors:
~~Resolved: undefined `DefuseMissionGenerator` could break mission manager startup.~~
```15:57:53 Error in expression <d", DefendMissionGenerator],
["defuse", DefuseMissionGenerator],
["delivery", De>
15:57:53 Error position: <DefuseMissionGenerator],
["delivery", De>
15:57:53 Error Undefined variable in expression: defusemissiongenerator
15:57:53 File C:\Users\cypha\OneDrive\Documents\Arma 3 - Other Profiles\MrPakeha\mpmissions\Forge\forge_pmc_simulator_v2.Tanoa\functions\fn_missionManager.sqf..., line 67
```
-------------------------------------------------------------------------------------------------
{Attack}
Wants:
- ~~Apply active-player enemy count scaling.~~
- Attack generated patrol size now uses the active-player multiplier.
Errors:
-------------------------------------------------------------------------------------------------
{Defence}
Wants:
- ~~Apply active-player enemy count scaling.~~
- Defend wave template size now uses the active-player multiplier.
Errors:
-------------------------------------------------------------------------------------------------
{Defuse}
Wants:
- ~~add building to possible spawnable locations~~
- Defuse protected object/device placement now attempts nearby building positions before falling back outdoors.
- ~~Apply active-player enemy count scaling.~~
- Defuse patrol size now uses the active-player multiplier.
Errors:
~~Resolved: invalid `exitWith {0}` syntax in building-position selection.~~
```- 15:58:01 Error in expression <n {
_buildingPos = _candidate;
exitWith {0};
};
};
};
private _protectedPos = [>
15:58:01 Error position: <{0};
};
};
};
private _protectedPos = [>
15:58:01 Error Missing ;
15:58:01 File C:\Users\cypha\OneDrive\Documents\Arma 3 - Other Profiles\MrPakeha\mpmissions\Forge\forge_pmc_simulator_v2.Tanoa\missionGenerator\fn_defuseMissionGenerator.sqf..., line 321
```
-------------------------------------------------------------------------------------------------
{Delivery}
Wants:
- ~~add code to move marker to random location around the map~~
- Delivery zone marker now moves to a random safe map location away from the cargo pickup, with fallback placement if a safe position cannot be found.
Errors:
untested
Changes:
- added defined location for spawning cargo
-- logic chain: spawn near "cargoSpawn", if not found, spawn near "ExtZone", if not found, revert to original offset
- added random destination-zone placement around the map
-------------------------------------------------------------------------------------------------
{Destroy}
Wants:
- ~~add possibility of map placed building as destroyable targets~~
- Destroy now prefers nearby map-placed configured target objects before spawning a fallback target.
- ~~Apply active-player enemy count scaling.~~
- Destroy patrol size now uses the active-player multiplier.
Errors:
untested
-------------------------------------------------------------------------------------------------
{Hostage}
Wants:
- ~~add building as possible spawning location~~
- Hostages and shooters now receive nearby building positions from location selection and use them when available.
- ~~have code look for marker "extMarker", if not found, find safe location within "blkMarker", if that fails, find safe location >2km away from task~~
- Hostage extraction already searches `ExtZone`, falls back through blacklist-area safe placement, then falls back to a safe location over 2km from the task.
- ~~Apply active-player enemy count scaling.~~
- Hostage patrol size now uses the active-player multiplier.
Errors:
untested
Changes:
- added a search for ExtZone
- if "ExtZone" is not found, will seach for a safe location within "blklist", if that fails, find a safe location >2km from task.
- Will attempt to spawn shooter and hostage inside a building if possible.
-------------------------------------------------------------------------------------------------
{HVT}
Wants:
- ~~add buildings as a possible spawning location~~
- HVT Kill and HVT Capture now gather nearby building positions and use them for the HVT/escort spawn when available.
- ~~have code look for marker "extMarker", if not found find safe location within "blkMarker", if that fails find safe location >2km away from task~~
- HVT Kill and HVT Capture extraction zones now search `ExtZone`/`extMarker`, fall back through `blklist`/`blkMarker`, then fall back to a safe location over 2km from the task.
- ~~Apply active-player enemy count scaling.~~
- HVT escort count now uses the active-player multiplier.
- ~~Keep HVT Kill and HVT Capture as separate generators.~~
- Capture HVT now publishes `CaptureHvtMissionGenerator` instead of overwriting `HvtMissionGenerator`.
Errors:
untested
Changes:
- created a second hvt mission that defines a capture mission instead of kill
- fixed HVT generators calling an undefined patrol method

View File

@ -1,5 +0,0 @@
force forge_client_actor_enableLoc = false;
force forge_client_actor_enableGear = true;
force forge_client_actor_enableVA = true;
force forge_client_actor_enableVG = true;
force forge_task_enableGenerator = false;

View File

@ -1,42 +0,0 @@
author = "IDSolutions";
onLoadName = "FORGE - Dev Environment v2.0";
onLoadMission = "A dev environment for the FORGE Framework";
loadScreen = "";
class Header {
gameType = "Sandbox";
minPlayers = 1;
maxPlayers = 22;
};
respawn = 3;
respawnButton = 1;
respawnDelay = 5;
respawnDialog = 0;
disabledAI = 1;
enableDebugConsole = 1;
enableTargetDebug = 1;
allowProfileGlasses = 0;
cba_settings_hasSettingsFile = 1;
corpseManagerMode = 0;
#include "CfgParams.hpp"
#include "CfgEnemyFactions.hpp"
#include "CfgFactionUnitMap.hpp"
#include "CfgMissions.hpp"
#include "CfgFunctions.hpp"
#include "ui\baseControls.hpp"
#include "ui\MissionSetup.hpp"
class CfgRemoteExec {
class Functions {
mode = 1;
jip = 0;
class forge_pmc_fnc_setupMenu_applySettings {
allowedTargets = 2;
};
};
};

View File

@ -1,19 +0,0 @@
# Helper Functions
Helper functions provide reusable lookups and conversions for the PMC simulator mission.
## Registered Functions
- `forge_pmc_fnc_getAllEnemyFactions` returns available non-BLUFOR faction classnames.
- `forge_pmc_fnc_getEnemyFactionOptions` scans the active modset for selectable OPFOR/Independent factions and applies `CfgEnemyFactions` filters/labels.
- `forge_pmc_fnc_resolveEnemyFactionParam` converts a mission parameter value into a faction classname.
- `forge_pmc_fnc_getEnemyFactionSide` resolves a faction classname to an Arma side.
- `forge_pmc_fnc_getEnemyFactionUnitPool` builds the unit pool used by generated enemy spawns.
- `forge_pmc_fnc_getMissionSettingRange` resolves generator ranges from applied mission settings with config fallbacks.
- `forge_pmc_fnc_getEnemyFactionListboxSelection` and `forge_pmc_fnc_populateEnemyFactionListbox` support faction picker UI/listbox flows.
## Notes
The mission setup UI is populated dynamically from loaded `CfgFactionClasses` and `CfgVehicles`. `CfgEnemyFactions` only controls allowed sides, denylisted factions, and friendly labels/order for known factions.
`forge_pmc_fnc_getEnemyFactionUnitPool` checks `CfgFactionUnitMap` first. Add a faction class there when the automatic `CfgVehicles` faction lookup is too broad, too sparse, or needs a curated unit pool.
These functions are registered under the `forge_pmc` tag, so their public names do not include the folder name. Moving a helper within this folder should not change callers as long as `CfgFunctions.hpp` remains updated.

View File

@ -1,62 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Returns candidate enemy faction classnames available to the mission.
* This is a runtime helper only. The setup UI uses
* forge_pmc_fnc_getEnemyFactionOptions so it can filter to spawnable factions.
*
* Arguments:
* 0: Exclude BLUFOR/WEST factions <BOOL> (Default: true)
* 1: Required faction side text <STRING> (Default: "")
*
* Return Value:
* Enemy faction classnames <ARRAY>
*
* Public: No
*/
params [
["_excludeBLUFOR", true],
["_minSide", ""]
];
private _out = [];
// Candidate sources (may include duplicates and may contain non-faction entries depending on mods)
private _candidates = [];
// Modded missions often populate this; safe fallback to config traversal.
{
if (_x isEqualType "" && { _x != "" }) then { _candidates pushBackUnique _x; };
} forEach [
"OPF_F", "IND_F" // common containers; harmless if not present in this context
];
// Traverse side->factions in config when possible.
// We use configClasses on CfgFactionClasses.
private _cfg = configFile >> "CfgFactionClasses";
if (isClass _cfg) then {
{
private _factionClass = configName _x;
private _sideText = toUpperANSI getText (_x >> "side");
if (_factionClass isEqualType "" && { _factionClass != "" }) then {
if (_excludeBLUFOR) then {
// Common side strings: "WEST", "EAST", "GUER"
if (_sideText == "WEST") then { continue; };
};
if (_minSide != "") then {
if (_sideText != toUpperANSI _minSide) then { continue; };
};
_out pushBackUnique _factionClass;
};
} forEach ("true" configClasses _cfg);
};
// Fallback: if config traversal failed, at least return known keys used by the mission.
if (_out isEqualTo []) then {
_out = ["OPF_F", "IND_G_F"];
_out pushBackUnique "IND_G_F";
};
_out

View File

@ -1,40 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Reads the selected enemy faction classname from a populated listbox or
* combo control.
*
* Arguments:
* 0: Listbox/combo control or display <CONTROL|DISPLAY>
* 1: Control IDC when parameter 0 is a display <NUMBER> (Default: -1)
* 2: Fallback faction classname <STRING> (Default: "IND_G_F")
*
* Return Value:
* Selected faction classname <STRING>
*
* Public: No
*/
params [
["_controlOrDisplay", controlNull, [controlNull, displayNull]],
["_idc", -1, [0]],
["_fallback", "IND_G_F", [""]]
];
private _control = controlNull;
if (_controlOrDisplay isEqualType displayNull) then {
if (_idc >= 0) then {
_control = _controlOrDisplay displayCtrl _idc;
};
} else {
_control = _controlOrDisplay;
};
if (isNull _control) exitWith { _fallback };
private _index = lbCurSel _control;
if (_index < 0) exitWith { _fallback };
private _faction = _control lbData _index;
if (_faction isEqualTo "") exitWith { _fallback };
_faction

View File

@ -1,135 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPakeha
* Builds setup UI faction options from the active modset. The helper scans
* loaded faction classes and only returns allowed OPFOR/Independent factions
* that can provide infantry through CfgVehicles or CfgFactionUnitMap.
*
* Arguments:
* None
*
* Return Value:
* Enemy faction options as [factionClassname, displayName, paramValue] <ARRAY>
*
* Public: No
*/
private _config = missionConfigFile >> "CfgEnemyFactions";
private _allowedSides = getArray (_config >> "sides");
if (_allowedSides isEqualTo []) then {
_allowedSides = [0, 2];
};
private _denylist = getArray (_config >> "denylist");
private _overridesConfig = _config >> "Overrides";
private _spawnableFactions = createHashMap;
{
if (getNumber (_x >> "scope") < 2) then { continue; };
if !(configName _x isKindOf "CAManBase") then { continue; };
private _faction = getText (_x >> "faction");
if (_faction isEqualTo "") then { continue; };
private _side = getNumber (_x >> "side");
if !(_side in _allowedSides) then { continue; };
_spawnableFactions set [_faction, true];
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
private _mappedFactions = createHashMap;
{
private _unitsConfig = _x >> "Units";
if (!(isClass _unitsConfig)) then { continue; };
private _hasUnits = false;
{
private _vehicle = getText (_x >> "vehicle");
if (_vehicle isNotEqualTo "" && { isClass (configFile >> "CfgVehicles" >> _vehicle) }) exitWith {
_hasUnits = true;
};
} forEach ("true" configClasses _unitsConfig);
if (_hasUnits) then {
_mappedFactions set [configName _x, true];
};
} forEach ("true" configClasses (missionConfigFile >> "CfgFactionUnitMap"));
private _getFactionSideNumber = {
params ["_factionConfig"];
if (isNumber (_factionConfig >> "side")) exitWith {
getNumber (_factionConfig >> "side")
};
switch (toUpperANSI getText (_factionConfig >> "side")) do {
case "0";
case "EAST";
case "OPFOR": { 0 };
case "2";
case "GUER";
case "GUERRILA";
case "GUERRILLA";
case "INDEPENDENT";
case "RESISTANCE": { 2 };
default { -1 };
};
};
private _records = [];
private _dynamicIndex = 0;
{
private _faction = configName _x;
if (_faction isEqualTo "") then { continue; };
if (_faction in _denylist) then { continue; };
private _side = [_x] call _getFactionSideNumber;
if !(_side in _allowedSides) then { continue; };
if (!(_spawnableFactions getOrDefault [_faction, false]) && {
!(_mappedFactions getOrDefault [_faction, false])
}) then {
continue;
};
private _override = _overridesConfig >> _faction;
private _display = getText (_x >> "displayName");
private _order = 1000 + _dynamicIndex;
private _value = 1000 + _dynamicIndex;
if (isClass _override) then {
private _overrideDisplay = getText (_override >> "display");
if (_overrideDisplay isNotEqualTo "") then {
_display = _overrideDisplay;
};
if (isNumber (_override >> "order")) then {
_order = getNumber (_override >> "order");
};
if (isNumber (_override >> "value")) then {
_value = getNumber (_override >> "value");
};
};
if (_display isEqualTo "") then {
_display = _faction;
};
_records pushBack [_order, _display, _faction, _value];
_dynamicIndex = _dynamicIndex + 1;
} forEach ("true" configClasses (configFile >> "CfgFactionClasses"));
_records sort true;
private _options = [];
{
_x params ["_order", "_display", "_faction", "_value"];
_options pushBack [_faction, _display, _value];
} forEach _records;
if (_options isEqualTo []) then {
_options = [
["OPF_F", "CSAT", 0],
["IND_G_F", "FIA", 6]
];
};
_options

View File

@ -1,62 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Resolves a faction classname to an Arma side. Prefers CfgFactionClasses
* data and falls back to common faction-prefix conventions.
*
* Arguments:
* 0: Enemy faction classname <STRING>
* 1: Fallback side <SIDE> (Default: east)
*
* Return Value:
* Resolved side <SIDE>
*
* Public: No
*/
params [
["_enemyFaction", ""],
["_fallbackSide", east]
];
private _f = toUpperANSI _enemyFaction;
if (_f isEqualTo "") exitWith { _fallbackSide };
// Try CfgFactionClasses first.
private _side = _fallbackSide;
private _cfgFaction = configFile >> "CfgFactionClasses" >> _enemyFaction;
if (isClass _cfgFaction) then {
private _hasSideNumber = isNumber (_cfgFaction >> "side");
private _sideNumber = if (_hasSideNumber) then { getNumber (_cfgFaction >> "side") } else { -1 };
private _sideText = toUpperANSI getText (_cfgFaction >> "side");
if (_hasSideNumber || { _sideText in ["0", "1", "2"] }) then {
if (!_hasSideNumber) then {
_sideNumber = parseNumber _sideText;
};
switch (_sideNumber) do {
case 0: { _side = east; };
case 2: { _side = resistance; };
case 1: { _side = _fallbackSide; }; // BLUFOR excluded
default { _side = _fallbackSide; };
};
} else {
switch (_sideText) do {
case "EAST": { _side = east; };
case "OPFOR": { _side = east; };
case "GUER": { _side = resistance; };
case "GUERRILA": { _side = resistance; };
case "GUERRILLA": { _side = resistance; };
case "INDEPENDENT": { _side = resistance; };
case "RESISTANCE": { _side = resistance; };
case "WEST": { _side = _fallbackSide; }; // BLUFOR excluded
default { _side = _fallbackSide; };
};
};
} else {
// Fallback: try to infer via mission CfgMissions AIGroups unit side fields.
// Since the current spawn system uses side, this is good enough.
// If faction isn't found, keep fallback.
};
_side

View File

@ -1,96 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Builds an infantry unit pool for the selected enemy faction. The returned
* entries match the generator spawn format.
*
* Arguments:
* 0: Faction classname <STRING> (Default: ENEMY_FACTION_STR or "IND_G_F")
* 1: Fallback side <SIDE> (Default: ENEMY_SIDE or east)
* 2: Allow side-default fallback units when no faction units exist <BOOL>
* (Default: true)
*
* Return Value:
* Unit definitions with vehicle, rank, and position keys <ARRAY>
*
* Public: No
*/
params [
["_faction", missionNamespace getVariable ["ENEMY_FACTION_STR", "IND_G_F"], [""]],
["_fallbackSide", missionNamespace getVariable ["ENEMY_SIDE", east], [east]],
["_allowSideFallback", true, [false]]
];
if (_faction isEqualTo "") then {
_faction = "IND_G_F";
};
private _pool = [];
private _sideNumber = [_fallbackSide] call BIS_fnc_sideID;
// Check CfgFactionUnitMap first for explicit faction unit definitions
private _factionMapConfig = missionConfigFile >> "CfgFactionUnitMap" >> _faction;
if (isClass _factionMapConfig) then {
{
private _vehicle = getText (_x >> "vehicle");
if (_vehicle isEqualTo "" || { !(isClass (configFile >> "CfgVehicles" >> _vehicle)) }) then {
continue;
};
_pool pushBack createHashMapFromArray [
["vehicle", _vehicle],
["rank", getText (_x >> "rank")],
["position", getArray (_x >> "position")]
];
} forEach ("true" configClasses (_factionMapConfig >> "Units"));
};
// Fall back to config traversal if no explicit mapping exists.
if (_pool isEqualTo []) then {
private _factionFallback = _faction;
{
if (getNumber (_x >> "scope") < 2) then { continue; };
private _unitFaction = getText (_x >> "faction");
if ((_unitFaction isNotEqualTo _faction) && (_unitFaction isNotEqualTo _factionFallback)) then { continue; };
if (getNumber (_x >> "side") isNotEqualTo _sideNumber) then { continue; };
if !(configName _x isKindOf "CAManBase") then { continue; };
private _className = configName _x;
private _upperClassName = toUpperANSI _className;
private _rank = "PRIVATE";
if (
(_upperClassName find "_SL_" >= 0)
|| { _upperClassName find "_TL_" >= 0 }
|| { _upperClassName find "OFFICER" >= 0 }
|| { _upperClassName find "COMMANDER" >= 0 }
) then {
_rank = "SERGEANT";
};
_pool pushBack createHashMapFromArray [
["vehicle", _className],
["rank", _rank],
["position", [0, 0, 0]]
];
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
};
if (_pool isEqualTo [] && { _allowSideFallback }) then {
private _fallbackUnits = switch (_fallbackSide) do {
case east: { ["O_Soldier_SL_F", "O_Soldier_TL_F", "O_Soldier_F", "O_Soldier_AR_F", "O_Soldier_GL_F", "O_medic_F"] };
case resistance: { ["I_G_Soldier_SL_F", "I_G_Soldier_TL_F", "I_G_Soldier_F", "I_G_Soldier_AR_F", "I_G_medic_F"] };
default { ["O_Soldier_SL_F", "O_Soldier_TL_F", "O_Soldier_F", "O_Soldier_AR_F", "O_medic_F"] };
};
{
_pool pushBack createHashMapFromArray [
["vehicle", _x],
["rank", if (_forEachIndex == 0) then { "SERGEANT" } else { "PRIVATE" }],
["position", [0, 0, 0]]
];
} forEach _fallbackUnits;
};
_pool

View File

@ -1,49 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Resolves a numeric mission range, preferring startup UI settings when
* present and falling back to CfgMissions values.
*
* Arguments:
* 0: Root config class <CONFIG>
* 1: Config path segments to the range array <ARRAY>
* 2: Mission settings min key <STRING>
* 3: Mission settings max key <STRING>
* 4: Fallback [min, max] range <ARRAY> (Default: [0, 0])
*
* Return Value:
* Normalized [min, max] range <ARRAY>
*
* Public: No
*/
params [
["_config", configNull, [configNull]],
["_path", [], [[]]],
["_minKey", "", [""]],
["_maxKey", "", [""]],
["_fallback", [0, 0], [[]]]
];
private _rangeConfig = _config;
{
_rangeConfig = _rangeConfig >> _x;
} forEach _path;
private _range = getArray _rangeConfig;
private _fallbackMin = _fallback param [0, 0, [0]];
private _fallbackMax = _fallback param [1, _fallbackMin, [0]];
private _min = _range param [0, _fallbackMin, [0]];
private _max = _range param [1, _fallbackMax, [0]];
private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap];
if (_settings isEqualType createHashMap) then {
_min = _settings getOrDefault [_minKey, _min];
_max = _settings getOrDefault [_maxKey, _max];
};
if (_max < _min) then {
_max = _min;
};
[_min, _max]

View File

@ -1,59 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Populates a listbox or combo control with dynamically discovered enemy
* faction options.
*
* Arguments:
* 0: Listbox/combo control or display <CONTROL|DISPLAY>
* 1: Control IDC when parameter 0 is a display <NUMBER> (Default: -1)
* 2: Selected faction classname <STRING> (Default: current enemy faction)
*
* Return Value:
* Number of options added <NUMBER>
*
* Public: No
*/
params [
["_controlOrDisplay", controlNull, [controlNull, displayNull]],
["_idc", -1, [0]],
["_selectedFaction", missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]], [""]]
];
private _control = controlNull;
if (_controlOrDisplay isEqualType displayNull) then {
if (_idc >= 0) then {
_control = _controlOrDisplay displayCtrl _idc;
};
} else {
_control = _controlOrDisplay;
};
if (isNull _control) exitWith { 0 };
lbClear _control;
private _selectedIndex = -1;
private _options = [] call forge_pmc_fnc_getEnemyFactionOptions;
{
_x params ["_faction", "_display"];
private _index = _control lbAdd _display;
_control lbSetData [_index, _faction];
_control lbSetTooltip [_index, _faction];
if (_faction isEqualTo _selectedFaction) then {
_selectedIndex = _index;
};
} forEach _options;
if (_selectedIndex < 0 && { count _options > 0 }) then {
_selectedIndex = 0;
};
if (_selectedIndex >= 0) then {
_control lbSetCurSel _selectedIndex;
};
count _options

View File

@ -1,35 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Resolves a mission param value or faction classname into the selected
* enemy faction classname.
*
* Arguments:
* 0: Param value from Params::enemyFaction <NUMBER|STRING> (Default: 6)
* 1: Fallback faction classname <STRING> (Default: "IND_G_F")
*
* Return Value:
* Faction classname <STRING>
*
* Public: No
*/
params [
["_value", 6, [0, ""]],
["_fallback", "IND_G_F", [""]]
];
if (_value isEqualType "") then {
if (_value isEqualTo "") exitWith { _fallback };
if (isClass (configFile >> "CfgFactionClasses" >> _value)) exitWith { _value };
_value = parseNumber _value;
};
private _faction = _fallback;
{
_x params ["_optionFaction", "_display", "_optionValue"];
if (_optionValue isEqualTo _value) exitWith {
_faction = _optionFaction;
};
} forEach ([] call forge_pmc_fnc_getEnemyFactionOptions);
_faction

View File

@ -1,29 +0,0 @@
# Mission Generators
This folder contains per-mission-type dynamic mission generators for the PMC simulator mission.
## Naming
- `fn_<missionType>MissionGenerator.sqf`
## Task usage
Generators follow the same runtime shape as the original attack generator:
- Base class stored as a HashMap-backed "object" with compiled functions.
- Provides: `startMission` and `completeMission`.
- Spawns AI/objects appropriate to the mission type and registers tasks via `forge_server_task_fnc_startTask`.
## Location
Mission functions live under `functions/`; these generator files are registered through `CfgFunctions.hpp` with `file = "functions\missionGenerators"`.
## Registered Generators
- `forge_pmc_fnc_attackMissionGenerator`
- `forge_pmc_fnc_defendMissionGenerator`
- `forge_pmc_fnc_destroyMissionGenerator`
- `forge_pmc_fnc_deliveryMissionGenerator`
- `forge_pmc_fnc_defuseMissionGenerator`
- `forge_pmc_fnc_hostageMissionGenerator`
- `forge_pmc_fnc_hvtMissionGenerator`
- `forge_pmc_fnc_captureHvtMissionGenerator`
## Settings
Generators should read shared reward/time ranges through `forge_pmc_fnc_getMissionSettingRange` so the startup UI and mission-param fallback stay consistent.

View File

@ -1,414 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Defines the Attack mission generator base class used by the dynamic
* mission manager. The generator selects a location, spawns required
* entities, registers a Forge task, and cleans up manager state when the
* task completes.
*
* Arguments:
* None
*
* Return Value:
* N/A. Defines AttackMissionGeneratorBaseClass in missionNamespace.
*
* Public: No
*/
AttackMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
["#type", "AttackMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
_self set ["generatorType", "attack"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "attack"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "attack") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
private _isTooClose = false;
{
private _prevPos = _x param [0, [], [[]]];
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
_isTooClose = true;
};
} forEach _recentLocationRegistry;
if (_isTooClose) then { continue; };
{
if (_candidate distance2D _x < 500) exitWith {
_isTooClose = true;
};
} forEach _activeMissionPositions;
if (_isTooClose) then { continue; };
private _inBlkList = false;
{
if (_candidate inArea _x) exitWith {
_inBlkList = true;
};
} forEach _blkListMarkers;
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
if (_taskPos isEqualTo []) exitWith {
["WARNING", "Attack mission generator: selectLocation failed to find a valid dynamic position."] call forge_server_common_fnc_log;
createHashMap
};
createHashMapFromArray [
["position", _taskPos],
["grid", mapGridPosition _taskPos]
]
}],
["spawnAttackGroup", compileFinal {
params [["_position", [0, 0, 0], [[]]]];
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
private _groups = [];
{
if ("attack" in getArray (_x >> "suitable")) then {
_groups pushBack _x;
};
} forEach ("true" configClasses _aiGroupsConfig);
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
private _sideText = str _side;
private _group = createGroup _side;
[] call forge_pmc_fnc_updateEnemyCountFromActivePlayers;
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
private _minUnitsBase = getNumber (_attackConfig >> "minUnits");
private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits");
private _patrolRadius = getNumber (_attackConfig >> "patrolRadius");
if (_minUnitsBase <= 0) then { _minUnitsBase = 4; };
if (_maxUnitsBase < _minUnitsBase) then { _maxUnitsBase = _minUnitsBase; };
if (_patrolRadius <= 0) then { _patrolRadius = 200; };
private _minUnits = floor ((_minUnitsBase max 1) * _enemyMult);
private _maxUnits = ceil ((_maxUnitsBase max _minUnitsBase) * _enemyMult);
if (_minUnits <= 0) then { _minUnits = 1; };
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1];
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
private _unitPool = [_enemyFaction, _side] call forge_pmc_fnc_getEnemyFactionUnitPool;
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
{
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
{
_unitPool pushBack createHashMapFromArray [
["vehicle", getText (_x >> "vehicle")],
["rank", getText (_x >> "rank")],
["position", getArray (_x >> "position")]
];
} forEach ("true" configClasses (_x >> "Units"));
} forEach _groups;
};
if (_unitPool isEqualTo []) exitWith {
["WARNING", format ["Attack mission generator: selected AI group side '%1' produced an empty unit pool.", _side]] call forge_server_common_fnc_log;
deleteGroup _group;
grpNull
};
private _leaderPool = _unitPool select {
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
};
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
private _spawnDefs = [selectRandom _leaderPool];
for "_i" from 1 to (_targetUnitCount - 1) do {
_spawnDefs pushBack (selectRandom _unitPool);
};
{
private _unitClass = _x getOrDefault ["vehicle", ""];
if (_unitClass isEqualTo "") then { continue; };
private _unitOffset = +(_x getOrDefault ["position", [0, 0, 0]]);
if (count _unitOffset < 3) then { _unitOffset resize 3; };
_unitOffset set [0, (_unitOffset # 0) + (random 6 - 3)];
_unitOffset set [1, (_unitOffset # 1) + (random 6 - 3)];
private _unit = _group createUnit [_unitClass, _position vectorAdd _unitOffset, [], 0, "NONE"];
_unit setRank (_x getOrDefault ["rank", "PRIVATE"]);
} forEach _spawnDefs;
[_group, _position, _patrolRadius] call BIS_fnc_taskPatrol;
["INFO", format [
"Attack mission generator: spawned attack group. Side=%1, Units=%2, PatrolRadius=%3, Position=%4",
_side,
count (units _group),
_patrolRadius,
_position
]] call forge_server_common_fnc_log;
_group
}],
["rollRewards", compileFinal {
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
private _equipmentRewards = [];
private _supplyRewards = [];
private _weaponRewards = [];
private _vehicleRewards = [];
private _specialRewards = [];
{
private _category = _x;
{
_x params ["_item", "_chance"];
if (random 1 < _chance) then {
switch (_category) do {
case "equipment": { _equipmentRewards pushBack _item; };
case "supplies": { _supplyRewards pushBack _item; };
case "weapons": { _weaponRewards pushBack _item; };
case "vehicles": { _vehicleRewards pushBack _item; };
case "special": { _specialRewards pushBack _item; };
};
};
} forEach (getArray (_attackConfig >> "Rewards" >> _category));
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
createHashMapFromArray [
["equipment", _equipmentRewards],
["supplies", _supplyRewards],
["weapons", _weaponRewards],
["vehicles", _vehicleRewards],
["special", _specialRewards]
]
}],
["startMission", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
private _locationData = _self call ["selectLocation", [_manager]];
if (_locationData isEqualTo createHashMap) exitWith { "" };
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
["INFO", format [
"Attack mission generator: selected location. Grid=%1, Position=%2",
_grid,
_position
]] call forge_server_common_fnc_log;
private _group = _self call ["spawnAttackGroup", [_position]];
if (isNull _group) exitWith {
["WARNING", format [
"Attack mission generator: spawnAttackGroup failed for Grid=%1, Position=%2",
_grid,
_position
]] call forge_server_common_fnc_log;
""
};
private _units = units _group;
if (_units isEqualTo []) exitWith {
["WARNING", format [
"Attack mission generator: spawned group has no units. Grid=%1, Group=%2",
_grid,
_group
]] call forge_server_common_fnc_log;
deleteGroup _group;
""
};
private _taskID = format ["task_attack_%1", round (diag_tickTime * 1000)];
private _rewardRange = [_attackConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [25000, 60000]] call forge_pmc_fnc_getMissionSettingRange;
private _reputationRange = [_attackConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [6, 14]] call forge_pmc_fnc_getMissionSettingRange;
private _penaltyRange = [_attackConfig, ["penalty"], "penaltyMin", "penaltyMax", [-8, -3]] call forge_pmc_fnc_getMissionSettingRange;
private _timeRange = [_attackConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call forge_pmc_fnc_getMissionSettingRange;
private _rewards = _self call ["rollRewards"];
private _fundsReward = _rewardRange call BIS_fnc_randomNum;
private _reputationReward = _reputationRange call BIS_fnc_randomNum;
private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum;
private _timeLimit = _timeRange call BIS_fnc_randomNum;
["INFO", format [
"Attack mission generator: creating task. TaskID=%1, Grid=%2, Units=%3",
_taskID,
_grid,
count _units
]] call forge_server_common_fnc_log;
private _success = [
"attack",
_taskID,
_position,
format ["Attack: Grid %1", _grid],
format ["Eliminate hostile forces operating near grid %1.", _grid],
createHashMapFromArray [["targets", _units]],
createHashMapFromArray [
["limitFail", 0],
["limitSuccess", count _units],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"]
],
0,
"",
"mission_manager"
] call forge_server_task_fnc_startTask;
if !(_success) exitWith {
["WARNING", format [
"Attack mission generator: startTask failed. TaskID=%1, Grid=%2, Units=%3",
_taskID,
_grid,
count _units
]] call forge_server_common_fnc_log;
""
};
["INFO", format [
"Attack mission generator: task registered. TaskID=%1, Source=mission_manager, TimeLimit=%2s, LimitSuccess=%3",
_taskID,
_timeLimit,
count _units
]] call forge_server_common_fnc_log;
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
_activeMissionRegistry set [_taskID, createHashMapFromArray [
["generatorType", _self call ["getGeneratorType", []]],
["position", _position],
["startedAt", serverTime]
]];
_manager set ["activeMissionRegistry", _activeMissionRegistry];
["INFO", format [
"Attack mission generator: mission started successfully. TaskID=%1, Grid=%2",
_taskID,
_grid
]] call forge_server_common_fnc_log;
_taskID
}],
["completeMission", compileFinal {
params [
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
["_taskID", "", [""]]
];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
private _position = _missionRecord getOrDefault ["position", []];
_activeMissionRegistry deleteAt _taskID;
_manager set ["activeMissionRegistry", _activeMissionRegistry];
if (_position isEqualType [] && { count _position >= 2 }) then {
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
_recentLocationRegistry pushBack [_position, serverTime];
_manager set ["recentLocationRegistry", _recentLocationRegistry];
};
true
}]
];
publicVariable "AttackMissionGeneratorBaseClass";
AttackMissionGenerator = createHashMapObject [AttackMissionGeneratorBaseClass];
publicVariable "AttackMissionGenerator";

View File

@ -1,444 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Defines the HVT capture mission generator base class used by the dynamic
* mission manager. The generator selects a location, spawns required
* entities, registers a Forge task, and cleans up manager state when the
* task completes.
*
* Arguments:
* None
*
* Return Value:
* N/A. Defines CaptureHvtMissionGeneratorBaseClass in missionNamespace.
*
* Public: No
*/
CaptureHvtMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
["#type", "CaptureHvtMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["hvtConfig", (_missionConfig >> "MissionTypes" >> "HVTCapture")];
_self set ["generatorType", "hvtcapture"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "hvtcapture"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "hvtcapture") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
private _isTooClose = false;
{
private _prevPos = _x param [0, [], [[]]];
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
_isTooClose = true;
};
} forEach _recentLocationRegistry;
if (_isTooClose) then { continue; };
{
if (_candidate distance2D _x < 500) exitWith {
_isTooClose = true;
};
} forEach _activeMissionPositions;
if (_isTooClose) then { continue; };
private _inBlkList = false;
{
if (_candidate inArea _x) exitWith {
_inBlkList = true;
};
} forEach _blkListMarkers;
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
if (_taskPos isEqualTo []) exitWith {
["WARNING", "Capture HVT mission generator: selectLocation failed to find a valid dynamic position."] call forge_server_common_fnc_log;
createHashMap
};
private _building = objNull;
private _buildingCandidates = nearestObjects [
_taskPos,
["House_F", "House", "Building", "BuildingBase"],
200
];
if (_buildingCandidates isNotEqualTo []) then {
_building = selectRandom _buildingCandidates;
};
private _buildingPositions = [];
if !(isNull _building) then {
for "_i" from 0 to 100 do {
private _buildingPos = _building buildingPos _i;
if (_buildingPos isEqualTo [0, 0, 0]) exitWith {};
_buildingPositions pushBack _buildingPos;
};
};
createHashMapFromArray [
["position", _taskPos],
["grid", mapGridPosition _taskPos],
["buildingPositions", _buildingPositions]
]
}],
["spawnHvtTarget", compileFinal {
params [['_position', [0, 0, 0], [[]]], ["_buildingPositions", [], [[]]]];
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
private _unitPool = [_enemyFaction, _side] call forge_pmc_fnc_getEnemyFactionUnitPool;
if (_unitPool isEqualTo []) exitWith { [] };
private _leaderPool = _unitPool select {
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
};
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
private _targetDef = selectRandom _leaderPool;
private _targetClass = _targetDef getOrDefault ["vehicle", ""];
if (_targetClass isEqualTo "") exitWith { [] };
private _group = createGroup _side;
private _leaderPos = if (_buildingPositions isEqualTo []) then {
_position vectorAdd [(random 20 - 10), (random 20 - 10), 0]
} else {
selectRandom _buildingPositions
};
private _leader = _group createUnit [_targetClass, _leaderPos, [], 0, "NONE"];
if (isNull _leader) exitWith {
deleteGroup _group;
[]
};
_leader setRank "LIEUTENANT";
[] call forge_pmc_fnc_updateEnemyCountFromActivePlayers;
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
private _escortCount = getNumber (_hvtConfig >> "escorts");
if (_escortCount < 0) then { _escortCount = 0; };
_escortCount = floor (_escortCount * _enemyMult);
private _escortUnits = [];
for "_i" from 1 to _escortCount do {
private _escortDef = selectRandom _unitPool;
private _escortClass = _escortDef getOrDefault ["vehicle", ""];
if (_escortClass isEqualTo "") then { continue; };
private _escortPos = if (_buildingPositions isEqualTo []) then {
_position vectorAdd [(random 35 - 17), (random 35 - 17), 0]
} else {
selectRandom _buildingPositions
};
private _escort = _group createUnit [_escortClass, _escortPos, [], 0, "NONE"];
if !(isNull _escort) then {
_escort setRank (_escortDef getOrDefault ["rank", "PRIVATE"]);
_escortUnits pushBack _escort;
};
};
private _groupUnits = [_leader] + _escortUnits;
[_group, _position, 200] call BIS_fnc_taskPatrol;
[_leader, _groupUnits]
}],
["rollRewards", compileFinal {
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
private _equipmentRewards = [];
private _supplyRewards = [];
private _weaponRewards = [];
private _vehicleRewards = [];
private _specialRewards = [];
{
private _category = _x;
{
_x params ["_item", "_chance"];
if (random 1 < _chance) then {
switch (_category) do {
case "equipment": { _equipmentRewards pushBack _item; };
case "supplies": { _supplyRewards pushBack _item; };
case "weapons": { _weaponRewards pushBack _item; };
case "vehicles": { _vehicleRewards pushBack _item; };
case "special": { _specialRewards pushBack _item; };
};
};
} forEach (getArray (_hvtConfig >> "Rewards" >> _category));
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
createHashMapFromArray [
["equipment", _equipmentRewards],
["supplies", _supplyRewards],
["weapons", _weaponRewards],
["vehicles", _vehicleRewards],
["special", _specialRewards]
]
}],
["startMission", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
private _locationData = _self call ["selectLocation", [_manager]];
if (_locationData isEqualTo createHashMap) exitWith { "" };
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
private _buildingPositions = _locationData getOrDefault ["buildingPositions", []];
["INFO", format [
"Capture HVT mission generator: selected location. Grid=%1, Position=%2",
_grid,
_position
]] call forge_server_common_fnc_log;
private _taskID = format ["task_capture_hvt_%1", round (diag_tickTime * 1000)];
private _rewardRange = [_hvtConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [50000, 120000]] call forge_pmc_fnc_getMissionSettingRange;
private _reputationRange = [_hvtConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [10, 22]] call forge_pmc_fnc_getMissionSettingRange;
private _penaltyRange = [_hvtConfig, ["penalty"], "penaltyMin", "penaltyMax", [-14, -5]] call forge_pmc_fnc_getMissionSettingRange;
private _timeRange = [_hvtConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call forge_pmc_fnc_getMissionSettingRange;
private _rewards = _self call ["rollRewards"];
private _spawnResult = _self call ["spawnHvtTarget", [_position, _buildingPositions]];
if !(_spawnResult isEqualType [] && { count _spawnResult >= 2 }) exitWith { "" };
private _hvtTarget = _spawnResult select 0;
private _hvtGroupUnits = _spawnResult select 1;
if (isNull _hvtTarget || _hvtGroupUnits isEqualTo []) exitWith { "" };
private _fundsReward = _rewardRange call BIS_fnc_randomNum;
private _reputationReward = _reputationRange call BIS_fnc_randomNum;
private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum;
private _timeLimit = _timeRange call BIS_fnc_randomNum;
private _extZone = format ["forge_hvt_ext_zone_%1", _taskID];
private _extPos = [0, 0, 0];
private _extZoneMarkers = allMapMarkers select {
(toLowerANSI (markerText _x) find "extzone") == 0
|| { (toLowerANSI _x find "extzone") == 0 }
|| { (toLowerANSI (markerText _x) find "extmarker") == 0 }
|| { (toLowerANSI _x find "extmarker") == 0 }
};
if (_extZoneMarkers isNotEqualTo []) then {
_extPos = getMarkerPos (selectRandom _extZoneMarkers);
_extPos set [2, 0];
} else {
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
|| { (toLowerANSI _x find "blkmarker") == 0 }
|| { (toLowerANSI (markerText _x) find "blkmarker") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
if (_blkListMarkers isNotEqualTo []) then {
private _selectedBlk = selectRandom _blkListMarkers;
private _attempt = 0;
while { _attempt < 60 && { _extPos isEqualTo [0, 0, 0] } } do {
_attempt = _attempt + 1;
private _candidate = [getMarkerPos _selectedBlk, 0, 2000, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if !(_candidate inArea _selectedBlk) then { continue; };
_candidate set [2, 0];
_extPos = _candidate;
};
};
if (_extPos isEqualTo [0, 0, 0]) then {
private _attempt = 0;
while { _attempt < 80 && { _extPos isEqualTo [0, 0, 0] } } do {
_attempt = _attempt + 1;
private _probe = [random worldSize, random worldSize, 0];
if ((_probe distance2D _position) < 2000) then { continue; };
private _safe = [_probe, 0, 500, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_safe isEqualTo [0, 0, 0]) then { continue; };
_safe set [2, 0];
_extPos = _safe;
};
};
if (_extPos isEqualTo [0, 0, 0]) then {
_extPos = _position vectorAdd [2500, 0, 0];
_extPos set [2, 0];
};
};
createMarker [_extZone, _extPos];
_extZone setMarkerShape "ELLIPSE";
_extZone setMarkerSize [160, 160];
_extZone setMarkerText format ["HVT Extraction Zone %1", _grid];
_extZone setMarkerAlpha 0.5;
_extZone setMarkerBrush "DiagGrid";
_extZone setMarkerColor "ColorOrange";
private _success = [
"hvt",
_taskID,
_position,
format ["HVT: Grid %1", _grid],
format ["Capture the high-value target near grid %1.", _grid],
createHashMapFromArray [["hvts", [_hvtTarget]]],
createHashMapFromArray [
["limitFail", 0],
["limitSuccess", 1],
["extractionZone", _extZone],
["captureHvt", true],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"]
],
0,
"",
"mission_manager"
] call forge_server_task_fnc_startTask;
if !(_success) exitWith {
deleteMarker _extZone;
""
};
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
_activeMissionRegistry set [_taskID, createHashMapFromArray [
["generatorType", _self call ["getGeneratorType", []]],
["position", _position],
["markers", [_extZone]],
["startedAt", serverTime]
]];
_manager set ["activeMissionRegistry", _activeMissionRegistry];
_taskID
}],
["completeMission", compileFinal {
params [
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
["_taskID", "", [""]]
];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
private _position = _missionRecord getOrDefault ["position", []];
private _markers = _missionRecord getOrDefault ["markers", []];
{
if (_x isEqualType "" && { _x in allMapMarkers }) then {
deleteMarker _x;
};
} forEach _markers;
_activeMissionRegistry deleteAt _taskID;
_manager set ["activeMissionRegistry", _activeMissionRegistry];
if (_position isEqualType [] && { count _position >= 2 }) then {
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
_recentLocationRegistry pushBack [_position, serverTime];
_manager set ["recentLocationRegistry", _recentLocationRegistry];
};
true
}]
];
publicVariable "CaptureHvtMissionGeneratorBaseClass";
CaptureHvtMissionGenerator = createHashMapObject [CaptureHvtMissionGeneratorBaseClass];
publicVariable "CaptureHvtMissionGenerator";

View File

@ -1,384 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Defines the Defend mission generator base class used by the dynamic
* mission manager. The generator selects a location, spawns required
* entities, registers a Forge task, and cleans up manager state when the
* task completes.
*
* Arguments:
* None
*
* Return Value:
* N/A. Defines DefendMissionGeneratorBaseClass in missionNamespace.
*
* Public: No
*/
DefendMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
["#type", "DefendMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["defendConfig", (_missionConfig >> "MissionTypes" >> "Defend")];
_self set ["generatorType", "defend"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "defend"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "defend") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
private _isTooClose = false;
{
private _prevPos = _x param [0, [], [[]]];
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
_isTooClose = true;
};
} forEach _recentLocationRegistry;
if (_isTooClose) then { continue; };
{
if (_candidate distance2D _x < 500) exitWith {
_isTooClose = true;
};
} forEach _activeMissionPositions;
if (_isTooClose) then { continue; };
private _inBlkList = false;
{
if (_candidate inArea _x) exitWith {
_inBlkList = true;
};
} forEach _blkListMarkers;
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
if (_taskPos isEqualTo []) exitWith {
["WARNING", "Defend mission generator: selectLocation failed to find a valid dynamic position."] call forge_server_common_fnc_log;
createHashMap
};
createHashMapFromArray [
["position", _taskPos],
["grid", mapGridPosition _taskPos]
]
}],
["buildDefendTemplateGroups", compileFinal {
params [['_position', [0, 0, 0], [[]]]];
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
private _defendConfig = _self getOrDefault ["defendConfig", configNull];
private _groups = [];
{
if ("defend" in getArray (_x >> "suitable")) then {
_groups pushBack _x;
};
} forEach ("true" configClasses _aiGroupsConfig);
if (_groups isEqualTo []) then {
{
if ("attack" in getArray (_x >> "suitable")) then {
_groups pushBack _x;
};
} forEach ("true" configClasses _aiGroupsConfig);
};
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
private _sideText = str _side;
[] call forge_pmc_fnc_updateEnemyCountFromActivePlayers;
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
private _unitCountConfig = getArray (_defendConfig >> "unitsPerWave");
private _minUnits = _unitCountConfig select 0;
private _maxUnits = _unitCountConfig select 1;
if (_minUnits <= 0) then { _minUnits = 4; };
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
_minUnits = floor ((_minUnits max 1) * _enemyMult);
_maxUnits = ceil ((_maxUnits max _minUnits) * _enemyMult);
if (_minUnits <= 0) then { _minUnits = 1; };
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
private _targetUnitCount = _minUnits + floor random ((_maxUnits - _minUnits) + 1);
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
private _unitPool = [_enemyFaction, _side] call forge_pmc_fnc_getEnemyFactionUnitPool;
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
{
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
{
_unitPool pushBack createHashMapFromArray [
["vehicle", getText (_x >> "vehicle")],
["rank", getText (_x >> "rank")],
["position", getArray (_x >> "position")]
];
} forEach ("true" configClasses (_x >> "Units"));
} forEach _groups;
};
if (_unitPool isEqualTo []) exitWith { [] };
private _templateGroup = [];
for "_i" from 1 to _targetUnitCount do {
private _unitDef = selectRandom _unitPool;
private _unitClass = _unitDef getOrDefault ["vehicle", ""];
if (_unitClass isNotEqualTo "") then {
_templateGroup pushBack createHashMapFromArray [
["type", _unitClass],
["side", _side],
["rank", _unitDef getOrDefault ["rank", "PRIVATE"]],
["skill", 0.45 + random 0.25]
];
};
};
if (_templateGroup isEqualTo []) exitWith { [] };
[_templateGroup]
}],
["rollRewards", compileFinal {
private _defendConfig = _self getOrDefault ["defendConfig", configNull];
private _equipmentRewards = [];
private _supplyRewards = [];
private _weaponRewards = [];
private _vehicleRewards = [];
private _specialRewards = [];
{
private _category = _x;
{
_x params ["_item", "_chance"];
if (random 1 < _chance) then {
switch (_category) do {
case "equipment": { _equipmentRewards pushBack _item; };
case "supplies": { _supplyRewards pushBack _item; };
case "weapons": { _weaponRewards pushBack _item; };
case "vehicles": { _vehicleRewards pushBack _item; };
case "special": { _specialRewards pushBack _item; };
};
};
} forEach (getArray (_defendConfig >> "Rewards" >> _category));
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
createHashMapFromArray [
["equipment", _equipmentRewards],
["supplies", _supplyRewards],
["weapons", _weaponRewards],
["vehicles", _vehicleRewards],
["special", _specialRewards]
]
}],
["startMission", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _defendConfig = _self getOrDefault ["defendConfig", configNull];
private _locationData = _self call ["selectLocation", [_manager]];
if (_locationData isEqualTo createHashMap) exitWith { "" };
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
private _taskID = format ["task_defend_%1", round (diag_tickTime * 1000)];
private _rewardRange = [_defendConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [40000, 90000]] call forge_pmc_fnc_getMissionSettingRange;
private _reputationRange = [_defendConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [8, 18]] call forge_pmc_fnc_getMissionSettingRange;
private _penaltyRange = [_defendConfig, ["penalty"], "penaltyMin", "penaltyMax", [-12, -4]] call forge_pmc_fnc_getMissionSettingRange;
private _timeRange = [_defendConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [300, 1800]] call forge_pmc_fnc_getMissionSettingRange;
private _rewards = _self call ["rollRewards"];
private _enemyTemplates = _self call ["buildDefendTemplateGroups", [_position]];
if (_enemyTemplates isEqualTo []) exitWith { "" };
private _fundsReward = _rewardRange call BIS_fnc_randomNum;
private _reputationReward = _reputationRange call BIS_fnc_randomNum;
private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum;
private _timeLimit = _timeRange call BIS_fnc_randomNum;
private _minWaves = getNumber (_defendConfig >> "minWaves");
if (_minWaves <= 0) then { _minWaves = 3; };
private _maxWaves = getNumber (_defendConfig >> "maxWaves");
if (_maxWaves < _minWaves) then { _maxWaves = _minWaves; };
private _limitSuccess = _minWaves + floor random ((_maxWaves - _minWaves) + 1);
private _waveCooldown = getNumber (_defendConfig >> "waveCooldown");
if (_waveCooldown <= 0) then { _waveCooldown = 300; };
private _minBlufor = 1;
private _defenseZone = format ["forge_defend_zone_%1", _taskID];
createMarker [_defenseZone, _position];
_defenseZone setMarkerShape "ELLIPSE";
_defenseZone setMarkerSize [25, 25];
_defenseZone setMarkerText format ["Defense Zone %1", _grid];
_defenseZone setMarkerAlpha 0.5;
_defenseZone setMarkerBrush "DiagGrid";
_defenseZone setMarkerColor "ColorOrange";
private _success = [
"defend",
_taskID,
_position,
format ["Defend: Grid %1", _grid],
format ["Hold the area in and around grid %1.", _grid],
createHashMapFromArray [],
createHashMapFromArray [
["limitFail", 0],
["limitSuccess", _limitSuccess],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"],
["defenseZone", _defenseZone],
["defendTime", _timeLimit],
["waveCount", _limitSuccess],
["waveCooldown", _waveCooldown],
["minBlufor", _minBlufor],
["enemyTemplates", _enemyTemplates]
],
0,
"",
"mission_manager"
] call forge_server_task_fnc_startTask;
if !(_success) exitWith {
deleteMarker _defenseZone;
""
};
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
_activeMissionRegistry set [_taskID, createHashMapFromArray [
["generatorType", _self call ["getGeneratorType", []]],
["position", _position],
["markers", [_defenseZone]],
["startedAt", serverTime]
]];
_manager set ["activeMissionRegistry", _activeMissionRegistry];
_taskID
}],
["completeMission", compileFinal {
params [
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
["_taskID", "", [""]]
];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
private _position = _missionRecord getOrDefault ["position", []];
private _markers = _missionRecord getOrDefault ["markers", []];
{
if (_x isEqualType "" && { _x in allMapMarkers }) then {
deleteMarker _x;
};
} forEach _markers;
_activeMissionRegistry deleteAt _taskID;
_manager set ["activeMissionRegistry", _activeMissionRegistry];
if (_position isEqualType [] && { count _position >= 2 }) then {
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
_recentLocationRegistry pushBack [_position, serverTime];
_manager set ["recentLocationRegistry", _recentLocationRegistry];
};
true
}]
];
publicVariable "DefendMissionGeneratorBaseClass";
DefendMissionGenerator = createHashMapObject [DefendMissionGeneratorBaseClass];
publicVariable "DefendMissionGenerator";

View File

@ -1,511 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Defines the Defuse mission generator base class used by the dynamic
* mission manager. The generator selects a location, spawns required
* entities, registers a Forge task, and cleans up manager state when the
* task completes.
*
* Arguments:
* None
*
* Return Value:
* N/A. Defines DefuseMissionGeneratorBaseClass in missionNamespace.
*
* Public: No
*/
DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
["#type", "DefuseMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
_self set ["defuseConfig", (_missionConfig >> "MissionTypes" >> "Defuse")];
_self set ["generatorType", "defuse"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "defuse"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "defuse") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
private _isTooClose = false;
{
private _prevPos = _x param [0, [], [[]]];
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
_isTooClose = true;
};
} forEach _recentLocationRegistry;
if (_isTooClose) then { continue; };
{
if (_candidate distance2D _x < 500) exitWith {
_isTooClose = true;
};
} forEach _activeMissionPositions;
if (_isTooClose) then { continue; };
private _inBlkList = false;
{
if (_candidate inArea _x) exitWith {
_inBlkList = true;
};
} forEach _blkListMarkers;
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
if (_taskPos isEqualTo []) exitWith {
["WARNING", "Defuse mission generator: selectLocation failed to find a valid dynamic position."] call forge_server_common_fnc_log;
createHashMap
};
createHashMapFromArray [
["position", _taskPos],
["grid", mapGridPosition _taskPos]
]
}],
["spawnPatrolGroup", compileFinal {
params [["_position", [0, 0, 0], [[]]]];
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
private _groups = [];
{
if ("attack" in getArray (_x >> "suitable")) then {
_groups pushBack _x;
};
} forEach ("true" configClasses _aiGroupsConfig);
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
private _sideText = str _side;
private _group = createGroup _side;
[] call forge_pmc_fnc_updateEnemyCountFromActivePlayers;
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
private _minUnitsBase = getNumber (_attackConfig >> "minUnits");
private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits");
private _patrolRadius = getNumber (_attackConfig >> "patrolRadius");
if (_minUnitsBase <= 0) then { _minUnitsBase = 4; };
if (_maxUnitsBase < _minUnitsBase) then { _maxUnitsBase = _minUnitsBase; };
if (_patrolRadius <= 0) then { _patrolRadius = 200; };
private _minUnits = floor ((_minUnitsBase max 1) * _enemyMult);
private _maxUnits = ceil ((_maxUnitsBase max _minUnitsBase) * _enemyMult);
if (_minUnits <= 0) then { _minUnits = 1; };
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1];
diag_log format ["Defuse: Unit Count %1", _targetUnitCount];
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
private _unitPool = [_enemyFaction, _side] call forge_pmc_fnc_getEnemyFactionUnitPool;
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
{
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
{
_unitPool pushBack createHashMapFromArray [
["vehicle", getText (_x >> "vehicle")],
["rank", getText (_x >> "rank")],
["position", getArray (_x >> "position")]
];
} forEach ("true" configClasses (_x >> "Units"));
} forEach _groups;
};
if (_unitPool isEqualTo []) exitWith {
["WARNING", format ["Defuse mission generator: selected AI group side '%1' produced an empty unit pool.", _side]] call forge_server_common_fnc_log;
deleteGroup _group;
grpNull
};
private _leaderPool = _unitPool select {
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
};
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
private _spawnDefs = [selectRandom _leaderPool];
for "_i" from 1 to (_targetUnitCount - 1) do {
_spawnDefs pushBack (selectRandom _unitPool);
};
{
private _unitClass = _x getOrDefault ["vehicle", ""];
if (_unitClass isEqualTo "") then { continue; };
private _unitOffset = +(_x getOrDefault ["position", [0, 0, 0]]);
if (count _unitOffset < 3) then { _unitOffset resize 3; };
_unitOffset set [0, (_unitOffset # 0) + (random 6 - 3)];
_unitOffset set [1, (_unitOffset # 1) + (random 6 - 3)];
private _unit = _group createUnit [_unitClass, _position vectorAdd _unitOffset, [], 0, "NONE"];
_unit setRank (_x getOrDefault ["rank", "PRIVATE"]);
} forEach _spawnDefs;
[_group, _position, _patrolRadius] call BIS_fnc_taskPatrol;
["INFO", format [
"Defuse mission generator: spawned attack group. Side=%1, Units=%2, PatrolRadius=%3, Position=%4",
_side,
count (units _group),
_patrolRadius,
_position
]] call forge_server_common_fnc_log;
_group
}],
["rollRewards", compileFinal {
private _defuseConfig = _self getOrDefault ["defuseConfig", configNull];
private _equipmentRewards = [];
private _supplyRewards = [];
private _weaponRewards = [];
private _vehicleRewards = [];
private _specialRewards = [];
{
private _category = _x;
{
_x params ["_item", "_chance"];
if (random 1 < _chance) then {
switch (_category) do {
case "equipment": { _equipmentRewards pushBack _item; };
case "supplies": { _supplyRewards pushBack _item; };
case "weapons": { _weaponRewards pushBack _item; };
case "vehicles": { _vehicleRewards pushBack _item; };
case "special": { _specialRewards pushBack _item; };
};
};
} forEach (getArray (_defuseConfig >> "Rewards" >> _category));
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
createHashMapFromArray [
["equipment", _equipmentRewards],
["supplies", _supplyRewards],
["weapons", _weaponRewards],
["vehicles", _vehicleRewards],
["special", _specialRewards]
]
}],
["spawnDefuseDevices", compileFinal {
params [['_position', [0, 0, 0], [[]]]];
private _defuseConfig = _self getOrDefault ["defuseConfig", configNull];
private _smallDevices = getArray (_defuseConfig >> "Devices" >> "small");
private _largeDevices = getArray (_defuseConfig >> "Devices" >> "large");
private _protectedClasses = getArray (_defuseConfig >> "Devices" >> "protected");
private _devicePool = _smallDevices + _largeDevices;
if (_devicePool isEqualTo [] || _protectedClasses isEqualTo []) exitWith { [] };
private _maxDevices = getNumber (_defuseConfig >> "maxDevices");
if (_maxDevices <= 0) then { _maxDevices = 1; };
private _deviceCount = 1 + floor (random _maxDevices);
private _protectedClass = selectRandom _protectedClasses;
// Try to spawn inside a building if there is a suitable building near the selected location.
// This will attempt up to N building positions before falling back to outdoor offsets.
private _buildingSpawnAttempts = 10;
private _buildingPos = [];
private _nearBuildings = nearestObjects [_position, ["House"], 50];
private _building = objNull;
if (_nearBuildings isNotEqualTo []) then {
// prefer the closest building that actually contains the position
{
if !(isNull _x && { _position inArea _x }) exitWith {
_building = _x;
};
} forEach _nearBuildings;
if (isNull _building) then {
// fallback: pick nearest
_building = _nearBuildings select 0;
{
if (_position distance2D _x < _position distance2D _building) then {
_building = _x;
};
} forEach _nearBuildings;
};
};
if !(isNull _building) then {
for "_i" from 1 to _buildingSpawnAttempts do {
private _posIndex = floor random 1000;
private _candidate = _building buildingPos _posIndex;
// buildingPos returns [0,0,0] for invalid positions
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
// ensure candidate is still inside the building footprint
if !((_candidate isEqualType [])) then { continue; };
if ((_candidate vectorDistance _position) <= 60) exitWith {
_buildingPos = _candidate;
};
};
};
private _protectedPos = [0,0,0];
if !((_buildingPos isEqualTo [])) then {
_protectedPos = _buildingPos;
} else {
// Outdoor fallback: keep previous behavior
_protectedPos = _position vectorAdd [(random 20 - 10), (random 20 - 10), 0];
};
private _protectedObject = createVehicle [_protectedClass, _protectedPos, [], 0, "NONE"];
private _protectedObjects = [];
if !(isNull _protectedObject) then {
_protectedObjects pushBack _protectedObject;
};
private _deviceRadiusMin = 2;
private _deviceRadiusMax = 5;
private _devices = [];
for "_i" from 1 to _deviceCount do {
private _deviceClass = selectRandom _devicePool;
// If we managed to pick a building position, keep devices clustered relative to it.
// This keeps them inside the building volume more reliably than using ground offsets.
private _angle = random 2 * pi;
private _radius = _deviceRadiusMin + random (_deviceRadiusMax - _deviceRadiusMin);
private _deviceOffset = [_radius * cos _angle, _radius * sin _angle, 0];
private _devicePos = _protectedPos vectorAdd _deviceOffset;
private _deviceObject = createVehicle [_deviceClass, _devicePos, [], 0, "NONE"];
if !(isNull _deviceObject) then {
_devices pushBack _deviceObject;
};
};
[_devices, _protectedObjects]
}],
["startMission", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _defuseConfig = _self getOrDefault ["defuseConfig", configNull];
private _locationData = _self call ["selectLocation", [_manager]];
if (_locationData isEqualTo createHashMap) exitWith { "" };
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
["INFO", format [
"Defuse mission generator: selected location. Grid=%1, Position=%2",
_grid,
_position
]] call forge_server_common_fnc_log;
private _group = _self call ["spawnPatrolGroup", [_position]];
if (isNull _group) exitWith {
["WARNING", format [
"Defuse mission generator: spawnPatrolGroup failed for Grid=%1, Position=%2",
_grid,
_position
]] call forge_server_common_fnc_log;
""
};
private _units = units _group;
if (_units isEqualTo []) exitWith {
["WARNING", format [
"Defuse mission generator: spawned group has no units. Grid=%1, Group=%2",
_grid,
_group
]] call forge_server_common_fnc_log;
deleteGroup _group;
""
};
private _taskID = format ["task_defuse_%1", round (diag_tickTime * 1000)];
private _rewardRange = [_defuseConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [20000, 50000]] call forge_pmc_fnc_getMissionSettingRange;
private _reputationRange = [_defuseConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [5, 12]] call forge_pmc_fnc_getMissionSettingRange;
private _penaltyRange = [_defuseConfig, ["penalty"], "penaltyMin", "penaltyMax", [-9, -3]] call forge_pmc_fnc_getMissionSettingRange;
private _timeRange = [_defuseConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [600, 900]] call forge_pmc_fnc_getMissionSettingRange;
private _rewards = _self call ["rollRewards"];
private _spawnResult = _self call ["spawnDefuseDevices", [_position]];
private _devices = _spawnResult select 0;
private _protectedObjects = _spawnResult select 1;
if (_devices isEqualTo [] || _protectedObjects isEqualTo []) exitWith { "" };
private _fundsReward = _rewardRange call BIS_fnc_randomNum;
private _reputationReward = _reputationRange call BIS_fnc_randomNum;
private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum;
private _timeLimit = _timeRange call BIS_fnc_randomNum;
private _iedTimer = 300;
private _targetCount = count _devices;
private _defuseZone = format ["forge_defuse_zone_%1", _taskID];
createMarker [_defuseZone, _position];
_defuseZone setMarkerShape "ELLIPSE";
_defuseZone setMarkerSize [120, 120];
_defuseZone setMarkerText format ["Defuse Area %1", _grid];
private _success = [
"defuse",
_taskID,
_position,
format ["Defuse: Grid %1", _grid],
format ["Defuse explosives operating near grid %1.", _grid],
createHashMapFromArray [["ieds", _devices], ["protected", _protectedObjects]],
createHashMapFromArray [
["limitFail", 0],
["limitSuccess", _targetCount],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"],
["iedTimer", _iedTimer],
["defuseZone", _defuseZone]
],
0,
"",
"mission_manager"
] call forge_server_task_fnc_startTask;
if !(_success) exitWith {
deleteMarker _defuseZone;
""
};
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
_activeMissionRegistry set [_taskID, createHashMapFromArray [
["generatorType", _self call ["getGeneratorType", []]],
["position", _position],
["markers", [_defuseZone]],
["startedAt", serverTime]
]];
_manager set ["activeMissionRegistry", _activeMissionRegistry];
_taskID
}],
["completeMission", compileFinal {
params [
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
["_taskID", "", [""]]
];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
private _position = _missionRecord getOrDefault ["position", []];
private _markers = _missionRecord getOrDefault ["markers", []];
{
if (_x isEqualType "" && { _x in allMapMarkers }) then {
deleteMarker _x;
};
} forEach _markers;
_activeMissionRegistry deleteAt _taskID;
_manager set ["activeMissionRegistry", _activeMissionRegistry];
if (_position isEqualType [] && { count _position >= 2 }) then {
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
_recentLocationRegistry pushBack [_position, serverTime];
_manager set ["recentLocationRegistry", _recentLocationRegistry];
};
true
}]
];
publicVariable "DefuseMissionGeneratorBaseClass";
DefuseMissionGenerator = createHashMapObject [DefuseMissionGeneratorBaseClass];
publicVariable "DefuseMissionGenerator";

View File

@ -1,378 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Defines the Delivery mission generator base class used by the dynamic
* mission manager. The generator selects a location, spawns required
* entities, registers a Forge task, and cleans up manager state when the
* task completes.
*
* Arguments:
* None
*
* Return Value:
* N/A. Defines DeliveryMissionGeneratorBaseClass in missionNamespace.
*
* Public: No
*/
DeliveryMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
["#type", "DeliveryMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["deliveryConfig", (_missionConfig >> "MissionTypes" >> "Delivery")];
_self set ["generatorType", "delivery"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "delivery"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "delivery") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
private _isTooClose = false;
{
private _prevPos = _x param [0, [], [[]]];
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
_isTooClose = true;
};
} forEach _recentLocationRegistry;
if (_isTooClose) then { continue; };
{
if (_candidate distance2D _x < 500) exitWith {
_isTooClose = true;
};
} forEach _activeMissionPositions;
if (_isTooClose) then { continue; };
private _inBlkList = false;
{
if (_candidate inArea _x) exitWith {
_inBlkList = true;
};
} forEach _blkListMarkers;
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
if (_taskPos isEqualTo []) exitWith {
["WARNING", "Delivery mission generator: selectLocation failed to find a valid dynamic position."] call forge_server_common_fnc_log;
createHashMap
};
createHashMapFromArray [
["position", _taskPos],
["grid", mapGridPosition _taskPos]
]
}],
["rollRewards", compileFinal {
private _deliveryConfig = _self getOrDefault ["deliveryConfig", configNull];
private _equipmentRewards = [];
private _supplyRewards = [];
private _weaponRewards = [];
private _vehicleRewards = [];
private _specialRewards = [];
{
private _category = _x;
{
_x params ["_item", "_chance"];
if (random 1 < _chance) then {
switch (_category) do {
case "equipment": { _equipmentRewards pushBack _item; };
case "supplies": { _supplyRewards pushBack _item; };
case "weapons": { _weaponRewards pushBack _item; };
case "vehicles": { _vehicleRewards pushBack _item; };
case "special": { _specialRewards pushBack _item; };
};
};
} forEach (getArray (_deliveryConfig >> "Rewards" >> _category));
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
createHashMapFromArray [
["equipment", _equipmentRewards],
["supplies", _supplyRewards],
["weapons", _weaponRewards],
["vehicles", _vehicleRewards],
["special", _specialRewards]
]
}],
["spawnDeliveryCargo", compileFinal {
params [['_position', [0, 0, 0], [[]]]];
private _deliveryConfig = _self getOrDefault ["deliveryConfig", configNull];
private _supplyCargo = getArray (_deliveryConfig >> "Cargo" >> "supplies");
private _vehicleCargo = getArray (_deliveryConfig >> "Cargo" >> "vehicles");
private _cargoPool = _supplyCargo + _vehicleCargo;
private _cargoCount = 1 + floor (random 2);
private _cargoObjects = [];
if (_cargoPool isEqualTo []) exitWith { [] };
private _cargoSpawnObj = objNull;
if !(isNil "cargoSpawn") then { _cargoSpawnObj = cargoSpawn; };
if (isNull _cargoSpawnObj) then { _cargoSpawnObj = missionNamespace getVariable ["cargoSpawn", objNull]; };
private _extZoneObj = objNull;
if !(isNil "ExtZone") then { _extZoneObj = ExtZone; };
if (isNull _extZoneObj) then { _extZoneObj = missionNamespace getVariable ["ExtZone", objNull]; };
for "_i" from 1 to _cargoCount do {
private _cargoClass = selectRandom _cargoPool;
private _spawnPos = [0, 0, 0];
if !(isNull _cargoSpawnObj) then {
private _basePos = getPosATL _cargoSpawnObj;
_spawnPos = _basePos vectorAdd [(random 12 - 6), (random 12 - 6), 0];
} else {
if !(isNull _extZoneObj) then {
private _basePos = getPosATL _extZoneObj;
_spawnPos = _basePos vectorAdd [(random 12 - 6), (random 12 - 6), 0];
} else {
_spawnPos = _position vectorAdd [(random 80 - 40), (random 80 - 40), 0];
};
};
private _cargoObject = createVehicle [_cargoClass, _spawnPos, [], 0, "NONE"];
if !(isNull _cargoObject) then {
_cargoObjects pushBack _cargoObject;
};
};
_cargoObjects
}],
["startMission", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _deliveryConfig = _self getOrDefault ["deliveryConfig", configNull];
private _locationData = _self call ["selectLocation", [_manager]];
if (_locationData isEqualTo createHashMap) exitWith { "" };
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
private _taskID = format ["task_delivery_%1", round (diag_tickTime * 1000)];
private _pickupMarker = format ["forge_delivery_pickup_%1", _taskID];
private _deliveryZone = format ["forge_delivery_zone_%1", _taskID];
private _dropoffMarker = format ["forge_delivery_dropoff_%1", _taskID];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _deliveryPos = [0, 0, 0];
private _attempt = 0;
private _deliverySearchRadius = (_worldSize / 2 - 1000) max 500;
while { _attempt < 80 && { _deliveryPos isEqualTo [0, 0, 0] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, 0, _deliverySearchRadius, 10, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if ((_candidate distance2D _position) < 1200) then { continue; };
_candidate set [2, 0];
_deliveryPos = _candidate;
};
if (_deliveryPos isEqualTo [0, 0, 0]) then {
_deliveryPos = [_position, 1200, 2500, 10, 0, 0.3, 0] call BIS_fnc_findSafePos;
};
if (_deliveryPos isEqualTo [0, 0, 0]) then {
_deliveryPos = _position vectorAdd [1500, 0, 0];
};
private _deliveryGrid = mapGridPosition _deliveryPos;
private _rewardRange = [_deliveryConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [10000, 30000]] call forge_pmc_fnc_getMissionSettingRange;
private _reputationRange = [_deliveryConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [3, 8]] call forge_pmc_fnc_getMissionSettingRange;
private _penaltyRange = [_deliveryConfig, ["penalty"], "penaltyMin", "penaltyMax", [-6, -2]] call forge_pmc_fnc_getMissionSettingRange;
private _timeRange = [_deliveryConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call forge_pmc_fnc_getMissionSettingRange;
private _rewards = _self call ["rollRewards"];
private _cargoObjects = _self call ["spawnDeliveryCargo", [_position]];
if (_cargoObjects isEqualTo []) exitWith { "" };
createMarker [_pickupMarker, _position];
_pickupMarker setMarkerType "hd_pickup";
_pickupMarker setMarkerColor "ColorBLUFOR";
_pickupMarker setMarkerText format ["Pickup %1", _grid];
createMarker [_deliveryZone, _deliveryPos];
_deliveryZone setMarkerShape "ELLIPSE";
_deliveryZone setMarkerSize [25, 25];
_deliveryZone setMarkerText format ["Delivery Zone %1", _deliveryGrid];
_deliveryZone setMarkerAlpha 0.5;
_deliveryZone setMarkerBrush "DiagGrid";
_deliveryZone setMarkerColor "ColorOrange";
createMarker [_dropoffMarker, _deliveryPos];
_dropoffMarker setMarkerType "hd_end";
_dropoffMarker setMarkerColor "ColorBLUFOR";
_dropoffMarker setMarkerText format ["Drop-off %1", _deliveryGrid];
private _fundsReward = _rewardRange call BIS_fnc_randomNum;
private _reputationReward = _reputationRange call BIS_fnc_randomNum;
private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum;
private _timeLimit = _timeRange call BIS_fnc_randomNum;
private _cargoCount = count _cargoObjects;
private _success = [
"delivery",
_taskID,
_position,
format ["Delivery: Grid %1", _grid],
format ["Move cargo from grid %1 to the delivery zone near grid %2.", _grid, _deliveryGrid],
createHashMapFromArray [["cargo", _cargoObjects]],
createHashMapFromArray [
["limitFail", 1],
["limitSuccess", _cargoCount],
["deliveryZone", _deliveryZone],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"]
],
0,
"",
"mission_manager"
] call forge_server_task_fnc_startTask;
if !(_success) exitWith {
deleteMarker _pickupMarker;
deleteMarker _deliveryZone;
deleteMarker _dropoffMarker;
""
};
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
_activeMissionRegistry set [_taskID, createHashMapFromArray [
["generatorType", _self call ["getGeneratorType", []]],
["position", _position],
["markers", [_pickupMarker, _deliveryZone, _dropoffMarker]],
["startedAt", serverTime]
]];
_manager set ["activeMissionRegistry", _activeMissionRegistry];
_taskID
}],
["completeMission", compileFinal {
params [
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
["_taskID", "", [""]]
];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
private _position = _missionRecord getOrDefault ["position", []];
private _markers = _missionRecord getOrDefault ["markers", []];
{
if (_x isEqualType "" && { _x in allMapMarkers }) then {
deleteMarker _x;
};
} forEach _markers;
_activeMissionRegistry deleteAt _taskID;
_manager set ["activeMissionRegistry", _activeMissionRegistry];
if (_position isEqualType [] && { count _position >= 2 }) then {
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
_recentLocationRegistry pushBack [_position, serverTime];
_manager set ["recentLocationRegistry", _recentLocationRegistry];
};
true
}]
];
publicVariable "DeliveryMissionGeneratorBaseClass";
DeliveryMissionGenerator = createHashMapObject [DeliveryMissionGeneratorBaseClass];
publicVariable "DeliveryMissionGenerator";

View File

@ -1,467 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Defines the Destroy mission generator base class used by the dynamic
* mission manager. The generator selects a location, spawns required
* entities, registers a Forge task, and cleans up manager state when the
* task completes.
*
* Arguments:
* None
*
* Return Value:
* N/A. Defines DestroyMissionGeneratorBaseClass in missionNamespace.
*
* Public: No
*/
DestroyMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
["#type", "DestroyMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
_self set ["destroyConfig", (_missionConfig >> "MissionTypes" >> "Destroy")];
_self set ["generatorType", "destroy"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "destroy"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "destroy") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
private _isTooClose = false;
{
private _prevPos = _x param [0, [], [[]]];
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
_isTooClose = true;
};
} forEach _recentLocationRegistry;
if (_isTooClose) then { continue; };
{
if (_candidate distance2D _x < 500) exitWith {
_isTooClose = true;
};
} forEach _activeMissionPositions;
if (_isTooClose) then { continue; };
private _inBlkList = false;
{
if (_candidate inArea _x) exitWith {
_inBlkList = true;
};
} forEach _blkListMarkers;
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
if (_taskPos isEqualTo []) exitWith {
["WARNING", "Destroy mission generator: selectLocation failed to find a valid dynamic position."] call forge_server_common_fnc_log;
createHashMap
};
createHashMapFromArray [
["position", _taskPos],
["grid", mapGridPosition _taskPos]
]
}],
["spawnPatrolGroup", compileFinal {
params [
["_position", [0, 0, 0], [[]]],
["_behavior", "patrol", [""]]
];
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
private _groups = [];
{
if ("attack" in getArray (_x >> "suitable")) then {
_groups pushBack _x;
};
} forEach ("true" configClasses _aiGroupsConfig);
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
private _sideText = str _side;
private _group = createGroup _side;
[] call forge_pmc_fnc_updateEnemyCountFromActivePlayers;
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
private _minUnitsBase = getNumber (_attackConfig >> "minUnits");
private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits");
private _patrolRadius = getNumber (_attackConfig >> "patrolRadius");
if (_minUnitsBase <= 0) then { _minUnitsBase = 4; };
if (_maxUnitsBase < _minUnitsBase) then { _maxUnitsBase = _minUnitsBase; };
if (_patrolRadius <= 0) then { _patrolRadius = 200; };
private _minUnits = floor ((_minUnitsBase max 1) * _enemyMult);
private _maxUnits = ceil ((_maxUnitsBase max _minUnitsBase) * _enemyMult);
if (_minUnits <= 0) then { _minUnits = 1; };
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1];
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
private _unitPool = [_enemyFaction, _side] call forge_pmc_fnc_getEnemyFactionUnitPool;
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
{
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
{
_unitPool pushBack createHashMapFromArray [
["vehicle", getText (_x >> "vehicle")],
["rank", getText (_x >> "rank")],
["position", getArray (_x >> "position")]
];
} forEach ("true" configClasses (_x >> "Units"));
} forEach _groups;
};
if (_unitPool isEqualTo []) exitWith {
["WARNING", format ["Destroy mission generator: selected AI group side '%1' produced an empty unit pool.", _side]] call forge_server_common_fnc_log;
deleteGroup _group;
grpNull
};
private _leaderPool = _unitPool select {
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
};
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
private _spawnDefs = [selectRandom _leaderPool];
for "_i" from 1 to (_targetUnitCount - 1) do {
_spawnDefs pushBack (selectRandom _unitPool);
};
{
private _unitClass = _x getOrDefault ["vehicle", ""];
if (_unitClass isEqualTo "") then { continue; };
private _unitOffset = +(_x getOrDefault ["position", [0, 0, 0]]);
if (count _unitOffset < 3) then { _unitOffset resize 3; };
_unitOffset set [0, (_unitOffset # 0) + (random 6 - 3)];
_unitOffset set [1, (_unitOffset # 1) + (random 6 - 3)];
private _unit = _group createUnit [_unitClass, _position vectorAdd _unitOffset, [], 0, "NONE"];
_unit setRank (_x getOrDefault ["rank", "PRIVATE"]);
} forEach _spawnDefs;
if (_behavior isEqualTo "defend") then {
[_group, _position] call BIS_fnc_taskDefend;
} else {
[_group, _position, _patrolRadius] call BIS_fnc_taskPatrol;
};
["INFO", format [
"Destroy mission generator: spawned %1 group. Side=%2, Units=%3, PatrolRadius=%4, Position=%5",
_behavior,
_side,
count (units _group),
_patrolRadius,
_position
]] call forge_server_common_fnc_log;
_group
}],
["spawnDestroyTargets", compileFinal {
params [['_position', [0, 0, 0], [[]]]];
private _destroyConfig = _self getOrDefault ["destroyConfig", configNull];
private _targetClasses = getArray (_destroyConfig >> "Bomb" >> "building");
if (_targetClasses isEqualTo []) exitWith { [] };
private _targetClass = selectRandom _targetClasses;
private _nearTargets = nearestObjects [_position, _targetClasses, 250] select {
!isNull _x && { alive _x } && { !(_x getVariable ["forge_destroy_reserved", false]) }
};
private _targetObject = objNull;
if (_nearTargets isNotEqualTo []) then {
_targetObject = selectRandom _nearTargets;
_targetObject setVariable ["forge_destroy_reserved", true, true];
} else {
private _spawnPos = _position vectorAdd [(random 60 - 30), (random 60 - 30), 0];
_targetObject = createVehicle [_targetClass, _spawnPos, [], 0, "NONE"];
};
if (isNull _targetObject) exitWith { [] };
[_targetObject]
}],
["rollRewards", compileFinal {
private _destroyConfig = _self getOrDefault ["destroyConfig", configNull];
private _equipmentRewards = [];
private _supplyRewards = [];
private _weaponRewards = [];
private _vehicleRewards = [];
private _specialRewards = [];
{
private _category = _x;
{
_x params ["_item", "_chance"];
if (random 1 < _chance) then {
switch (_category) do {
case "equipment": { _equipmentRewards pushBack _item; };
case "supplies": { _supplyRewards pushBack _item; };
case "weapons": { _weaponRewards pushBack _item; };
case "vehicles": { _vehicleRewards pushBack _item; };
case "special": { _specialRewards pushBack _item; };
};
};
} forEach (getArray (_destroyConfig >> "Rewards" >> _category));
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
createHashMapFromArray [
["equipment", _equipmentRewards],
["supplies", _supplyRewards],
["weapons", _weaponRewards],
["vehicles", _vehicleRewards],
["special", _specialRewards]
]
}],
["startMission", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _destroyConfig = _self getOrDefault ["destroyConfig", configNull];
private _locationData = _self call ["selectLocation", [_manager]];
if (_locationData isEqualTo createHashMap) exitWith { "" };
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
["INFO", format [
"Destroy mission generator: selected location. Grid=%1, Position=%2",
_grid,
_position
]] call forge_server_common_fnc_log;
private _group = _self call ["spawnPatrolGroup", [_position]];
if (isNull _group) exitWith {
["WARNING", format [
"Destroy mission generator: spawnPatrolGroup failed for Grid=%1, Position=%2",
_grid,
_position
]] call forge_server_common_fnc_log;
""
};
private _units = units _group;
if (_units isEqualTo []) exitWith {
["WARNING", format [
"Destroy mission generator: spawned group has no units. Grid=%1, Group=%2",
_grid,
_group
]] call forge_server_common_fnc_log;
deleteGroup _group;
""
};
private _defendGroup = _self call ["spawnPatrolGroup", [_position, "defend"]];
if (isNull _defendGroup || { units _defendGroup isEqualTo [] }) then {
["WARNING", format [
"Destroy mission generator: defensive task group failed for Grid=%1, Position=%2",
_grid,
_position
]] call forge_server_common_fnc_log;
if !(isNull _defendGroup) then {
deleteGroup _defendGroup;
};
};
private _spawnedGroups = [_group];
if !(isNull _defendGroup && { units _defendGroup isNotEqualTo [] }) then {
_spawnedGroups pushBack _defendGroup;
};
private _taskID = format ["task_destroy_%1", round (diag_tickTime * 1000)];
private _rewardRange = [_destroyConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [10000, 30000]] call forge_pmc_fnc_getMissionSettingRange;
private _reputationRange = [_destroyConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [3, 8]] call forge_pmc_fnc_getMissionSettingRange;
private _penaltyRange = [_destroyConfig, ["penalty"], "penaltyMin", "penaltyMax", [-6, -2]] call forge_pmc_fnc_getMissionSettingRange;
private _timeRange = [_destroyConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call forge_pmc_fnc_getMissionSettingRange;
private _rewards = _self call ["rollRewards"];
private _destroyTargets = _self call ["spawnDestroyTargets", [_position]];
if (_destroyTargets isEqualTo []) exitWith {
{
{ deleteVehicle _x; } forEach (units _x);
deleteGroup _x;
} forEach _spawnedGroups;
""
};
private _fundsReward = _rewardRange call BIS_fnc_randomNum;
private _reputationReward = _reputationRange call BIS_fnc_randomNum;
private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum;
private _timeLimit = _timeRange call BIS_fnc_randomNum;
private _targetCount = count _destroyTargets;
private _success = [
"destroy",
_taskID,
_position,
format ["Destroy: Grid %1", _grid],
format ["Destroy hostile assets operating near grid %1.", _grid],
createHashMapFromArray [["targets", _destroyTargets]],
createHashMapFromArray [
["limitFail", 0],
["limitSuccess", _targetCount],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"]
],
0,
"",
"mission_manager"
] call forge_server_task_fnc_startTask;
if !(_success) exitWith {
{
{ deleteVehicle _x; } forEach (units _x);
deleteGroup _x;
} forEach _spawnedGroups;
""
};
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
_activeMissionRegistry set [_taskID, createHashMapFromArray [
["generatorType", _self call ["getGeneratorType", []]],
["position", _position],
["groups", _spawnedGroups],
["startedAt", serverTime]
]];
_manager set ["activeMissionRegistry", _activeMissionRegistry];
_taskID
}],
["completeMission", compileFinal {
params [
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
["_taskID", "", [""]]
];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
private _position = _missionRecord getOrDefault ["position", []];
private _groups = _missionRecord getOrDefault ["groups", []];
{
if !(isNull _x) then {
{ deleteVehicle _x; } forEach (units _x);
deleteGroup _x;
};
} forEach _groups;
_activeMissionRegistry deleteAt _taskID;
_manager set ["activeMissionRegistry", _activeMissionRegistry];
if (_position isEqualType [] && { count _position >= 2 }) then {
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
_recentLocationRegistry pushBack [_position, serverTime];
_manager set ["recentLocationRegistry", _recentLocationRegistry];
};
true
}]
];
publicVariable "DestroyMissionGeneratorBaseClass";
DestroyMissionGenerator = createHashMapObject [DestroyMissionGeneratorBaseClass];
publicVariable "DestroyMissionGenerator";

View File

@ -1,651 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Defines the Hostage mission generator base class used by the dynamic
* mission manager. The generator selects a location, spawns required
* entities, registers a Forge task, and cleans up manager state when the
* task completes.
*
* Arguments:
* None
*
* Return Value:
* N/A. Defines HostageMissionGeneratorBaseClass in missionNamespace.
*
* Public: No
*/
HostageMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
["#type", "HostageMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
_self set ["hostageConfig", (_missionConfig >> "MissionTypes" >> "Hostage")];
_self set ["generatorType", "hostage"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "hostage"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "hostage") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
private _isTooClose = false;
{
private _prevPos = _x param [0, [], [[]]];
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
_isTooClose = true;
};
} forEach _recentLocationRegistry;
if (_isTooClose) then { continue; };
{
if (_candidate distance2D _x < 500) exitWith {
_isTooClose = true;
};
} forEach _activeMissionPositions;
if (_isTooClose) then { continue; };
private _inBlkList = false;
{
if (_candidate inArea _x) exitWith {
_inBlkList = true;
};
} forEach _blkListMarkers;
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
if (_taskPos isEqualTo []) exitWith {
["WARNING", "Hostage mission generator: selectLocation failed to find a valid dynamic position."] call forge_server_common_fnc_log;
createHashMap
};
// Try to bias hostage/shooter spawns to buildings.
// We pick a nearby house-like building and later use building positions for spawn points.
private _building = objNull;
private _buildingCandidates = nearestObjects [
_taskPos,
["House_F","House","Building","BuildingBase"],
200
];
if (_buildingCandidates isNotEqualTo []) then {
_building = selectRandom _buildingCandidates;
};
private _buildingPositions = [];
if !(isNull _building) then {
// buildingPos returns positions for building interiors; we random-pick from these.
for "_i" from 0 to 100 do {
private _bp = _building buildingPos _i;
if (_bp isEqualTo [0,0,0]) exitWith {};
_buildingPositions pushBack _bp;
};
};
createHashMapFromArray [
["position", _taskPos],
["grid", mapGridPosition _taskPos],
["building", _building],
["buildingPositions", _buildingPositions]
]
}],
["spawnPatrolGroup", compileFinal {
params [["_position", [0, 0, 0], [[]]]];
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
private _groups = [];
{
if ("attack" in getArray (_x >> "suitable")) then {
_groups pushBack _x;
};
} forEach ("true" configClasses _aiGroupsConfig);
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
private _sideText = str _side;
private _group = createGroup _side;
[] call forge_pmc_fnc_updateEnemyCountFromActivePlayers;
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
private _minUnitsBase = getNumber (_attackConfig >> "minUnits");
private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits");
private _patrolRadius = getNumber (_attackConfig >> "patrolRadius");
if (_minUnitsBase <= 0) then { _minUnitsBase = 4; };
if (_maxUnitsBase < _minUnitsBase) then { _maxUnitsBase = _minUnitsBase; };
if (_patrolRadius <= 0) then { _patrolRadius = 200; };
private _minUnits = floor ((_minUnitsBase max 1) * _enemyMult);
private _maxUnits = ceil ((_maxUnitsBase max _minUnitsBase) * _enemyMult);
if (_minUnits <= 0) then { _minUnits = 1; };
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1];
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
private _unitPool = [_enemyFaction, _side] call forge_pmc_fnc_getEnemyFactionUnitPool;
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
{
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
{
_unitPool pushBack createHashMapFromArray [
["vehicle", getText (_x >> "vehicle")],
["rank", getText (_x >> "rank")],
["position", getArray (_x >> "position")]
];
} forEach ("true" configClasses (_x >> "Units"));
} forEach _groups;
};
if (_unitPool isEqualTo []) exitWith {
["WARNING", format ["Hostage mission generator: selected AI group side '%1' produced an empty unit pool.", _side]] call forge_server_common_fnc_log;
deleteGroup _group;
grpNull
};
private _leaderPool = _unitPool select {
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
};
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
private _spawnDefs = [selectRandom _leaderPool];
for "_i" from 1 to (_targetUnitCount - 1) do {
_spawnDefs pushBack (selectRandom _unitPool);
};
{
private _unitClass = _x getOrDefault ["vehicle", ""];
if (_unitClass isEqualTo "") then { continue; };
private _unitOffset = +(_x getOrDefault ["position", [0, 0, 0]]);
if (count _unitOffset < 3) then { _unitOffset resize 3; };
_unitOffset set [0, (_unitOffset # 0) + (random 6 - 3)];
_unitOffset set [1, (_unitOffset # 1) + (random 6 - 3)];
private _unit = _group createUnit [_unitClass, _position vectorAdd _unitOffset, [], 0, "NONE"];
_unit setRank (_x getOrDefault ["rank", "PRIVATE"]);
} forEach _spawnDefs;
[_group, _position, _patrolRadius] call BIS_fnc_taskPatrol;
["INFO", format [
"Hostage mission generator: spawned attack group. Side=%1, Units=%2, PatrolRadius=%3, Position=%4",
_side,
count (units _group),
_patrolRadius,
_position
]] call forge_server_common_fnc_log;
_group
}],
["spawnHostageUnits", compileFinal {
params [['_position', [0, 0, 0], [[]]], ['_buildingPositions', []]];
private _hostageConfig = _self getOrDefault ["hostageConfig", configNull];
private _hostageClasses = getArray (_hostageConfig >> "Hostages" >> "civilian") + getArray (_hostageConfig >> "Hostages" >> "military");
if (_hostageClasses isEqualTo []) exitWith { [] };
// Prefer interior building positions when available.
private _spawnBasePos = _position;
private _useBuildingPositions = (_buildingPositions isEqualTo []);
if (_buildingPositions isNotEqualTo []) then {
_useBuildingPositions = false;
};
private _hostageCount = 1 + floor (random 2);
private _hostageGroup = createGroup civilian;
private _hostages = [];
for "_i" from 1 to _hostageCount do {
private _hostageClass = selectRandom _hostageClasses;
private _hostagePos = [0,0,0];
if !(_useBuildingPositions) then {
private _bp = selectRandom _buildingPositions;
_hostagePos = _bp;
} else {
_hostagePos = _spawnBasePos vectorAdd [(random 40 - 20), (random 40 - 20), 0];
};
private _hostage = _hostageGroup createUnit [_hostageClass, _hostagePos, [], 0, "NONE"];
if !(isNull _hostage) then {
_hostage setCaptive true;
_hostages pushBack _hostage;
};
};
_hostages
}],
["spawnHostageShooters", compileFinal {
params [['_position', [0, 0, 0], [[]]], ['_buildingPositions', []]];
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
private _groups = [];
{
if ("hostage" in getArray (_x >> "suitable")) then {
_groups pushBack _x;
};
} forEach ("true" configClasses _aiGroupsConfig);
if (_groups isEqualTo []) then {
{
if ("attack" in getArray (_x >> "suitable")) then {
_groups pushBack _x;
};
} forEach ("true" configClasses _aiGroupsConfig);
};
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
private _sideText = str _side;
private _group = createGroup _side;
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
private _unitPool = [_enemyFaction, _side] call forge_pmc_fnc_getEnemyFactionUnitPool;
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
{
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
{
_unitPool pushBack createHashMapFromArray [
["vehicle", getText (_x >> "vehicle")],
["rank", getText (_x >> "rank")],
["position", getArray (_x >> "position")]
];
} forEach ("true" configClasses (_x >> "Units"));
} forEach _groups;
};
if (_unitPool isEqualTo []) exitWith {
deleteGroup _group;
[]
};
private _shooterCount = 1 + floor (random 3);
private _shooterDefs = [];
for "_i" from 1 to _shooterCount do {
_shooterDefs pushBack (selectRandom _unitPool);
};
private _shooters = [];
// Prefer exterior/adjacent building positions when available.
private _shootBasePos = _position;
if (_buildingPositions isNotEqualTo []) then {
_shootBasePos = selectRandom _buildingPositions;
};
{
private _unitClass = _x getOrDefault ["vehicle", ""];
if (_unitClass isEqualTo "") exitWith { };
private _unitOffset = +(_x getOrDefault ["position", [0, 0, 0]]);
if (count _unitOffset < 3) then { _unitOffset resize 3; };
_unitOffset set [0, (_unitOffset # 0) + (random 10 - 5)];
_unitOffset set [1, (_unitOffset # 1) + (random 10 - 5)];
private _shooter = _group createUnit [_unitClass, _shootBasePos vectorAdd _unitOffset, [], 0, "NONE"];
if !(isNull _shooter) then {
_shooter setRank (_x getOrDefault ["rank", "PRIVATE"]);
_shooters pushBack _shooter;
};
} forEach _shooterDefs;
_shooters
}],
["rollRewards", compileFinal {
private _hostageConfig = _self getOrDefault ["hostageConfig", configNull];
private _equipmentRewards = [];
private _supplyRewards = [];
private _weaponRewards = [];
private _vehicleRewards = [];
private _specialRewards = [];
{
private _category = _x;
{
_x params ["_item", "_chance"];
if (random 1 < _chance) then {
switch (_category) do {
case "equipment": { _equipmentRewards pushBack _item; };
case "supplies": { _supplyRewards pushBack _item; };
case "weapons": { _weaponRewards pushBack _item; };
case "vehicles": { _vehicleRewards pushBack _item; };
case "special": { _specialRewards pushBack _item; };
};
};
} forEach (getArray (_hostageConfig >> "Rewards" >> _category));
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
createHashMapFromArray [
["equipment", _equipmentRewards],
["supplies", _supplyRewards],
["weapons", _weaponRewards],
["vehicles", _vehicleRewards],
["special", _specialRewards]
]
}],
["startMission", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _hostageConfig = _self getOrDefault ["hostageConfig", configNull];
private _locationData = _self call ["selectLocation", [_manager]];
if (_locationData isEqualTo createHashMap) exitWith { "" };
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
private _buildingPositions = _locationData getOrDefault ["buildingPositions", []];
["INFO", format [
"Hostage mission generator: selected location. Grid=%1, Position=%2",
_grid,
_position
]] call forge_server_common_fnc_log;
private _group = _self call ["spawnPatrolGroup", [_position]];
if (isNull _group) exitWith {
["WARNING", format [
"Hostage mission generator: spawnPatrolGroup failed for Grid=%1, Position=%2",
_grid,
_position
]] call forge_server_common_fnc_log;
""
};
private _units = units _group;
if (_units isEqualTo []) exitWith {
["WARNING", format [
"Hostage mission generator: spawned group has no units. Grid=%1, Group=%2",
_grid,
_group
]] call forge_server_common_fnc_log;
deleteGroup _group;
""
};
private _taskID = format ["task_hostage_%1", round (diag_tickTime * 1000)];
private _rewardRange = [_hostageConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [60000, 140000]] call forge_pmc_fnc_getMissionSettingRange;
private _reputationRange = [_hostageConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [12, 25]] call forge_pmc_fnc_getMissionSettingRange;
private _penaltyRange = [_hostageConfig, ["penalty"], "penaltyMin", "penaltyMax", [-16, -6]] call forge_pmc_fnc_getMissionSettingRange;
private _timeRange = [_hostageConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [600, 900]] call forge_pmc_fnc_getMissionSettingRange;
private _rewards = _self call ["rollRewards"];
private _hostageUnits = _self call ["spawnHostageUnits", [_position, _buildingPositions]];
private _shooterUnits = _self call ["spawnHostageShooters", [_position, _buildingPositions]];
if (_hostageUnits isEqualTo [] || _shooterUnits isEqualTo []) exitWith { "" };
private _fundsReward = _rewardRange call BIS_fnc_randomNum;
private _reputationReward = _reputationRange call BIS_fnc_randomNum;
private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum;
private _timeLimit = _timeRange call BIS_fnc_randomNum;
private _extZone = format ["forge_hostage_ext_zone_%1", _taskID];
// Choose extraction marker position:
// 1) Prefer editor-placed marker containing "ExtZone".
// 2) Else, pick a safe point inside a marker containing "blklist".
// 3) Else, pick a safe point anywhere on the map at least 2km away from task position.
private _extPos = [0, 0, 0];
private _extZoneMarkers = allMapMarkers select {
(toLowerANSI (markerText _x) find "extzone") == 0
|| { (toLowerANSI _x find "extzone") == 0 }
};
if (_extZoneMarkers isNotEqualTo []) then {
private _mPos = getMarkerPos (selectRandom _extZoneMarkers);
// Put marker on ground.
private _ground = +_mPos;
private _safe = [_ground, 0, 30, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if !((_safe isEqualTo [0, 0, 0])) then {
_ground = _safe;
};
_ground set [2, 0];
_extPos = _ground;
} else {
// Collect blklist-like markers (rectangle/ellipse) that already exist.
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
if (_blkListMarkers isNotEqualTo []) then {
private _selectedBlk = selectRandom _blkListMarkers;
private _attempt = 0;
private _maxAttempts = 60;
private _found = false;
while { _attempt < _maxAttempts && { !_found } } do {
_attempt = _attempt + 1;
private _markerSize = getMarkerSize _selectedBlk;
private _markerRadius = ((_markerSize param [0, 250, [0]]) max (_markerSize param [1, 250, [0]])) max 250;
private _candidate = [getMarkerPos _selectedBlk, 0, _markerRadius, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if !(_candidate inArea _selectedBlk) then { continue; };
// Ensure it's on land.
private _try = +_candidate;
_try set [2, 0];
_extPos = _try;
_found = true;
};
};
if (_extPos isEqualTo [0, 0, 0]) then {
// Fallback: anywhere on map, at least 2km from task location.
private _taskPos2D = +_position;
_taskPos2D set [2, 0];
private _worldMin = 0;
private _worldMax = worldSize;
private _attempt = 0;
private _maxAttempts = 80;
private _found = false;
while { _attempt < _maxAttempts && { !_found } } do {
_attempt = _attempt + 1;
private _randX = _worldMin + random (_worldMax - _worldMin);
private _randY = _worldMin + random (_worldMax - _worldMin);
private _probe = [_randX, _randY, 0];
if ((_probe distance2D _taskPos2D) < 2000) then { continue; };
private _safe = [_probe, 0, 500, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_safe isEqualTo [0, 0, 0]) then { continue; };
if ((_safe distance2D _taskPos2D) < 2000) then { continue; };
_safe set [2, 0];
_extPos = _safe;
_found = true;
};
// Absolute last resort.
if (_extPos isEqualTo [0, 0, 0]) then {
private _fallback = _position vectorAdd [2500, 0, 0];
_fallback set [2, 0];
_extPos = _fallback;
};
};
};
createMarker [_extZone, _extPos];
_extZone setMarkerShape "ELLIPSE";
_extZone setMarkerSize [25, 25];
_extZone setMarkerText format ["Hostage Extraction %1", _grid];
_extZone setMarkerAlpha 0.5;
_extZone setMarkerBrush "DiagGrid";
_extZone setMarkerColor "ColorOrange";
private _hostageCount = count _hostageUnits;
private _limitFail = 1;
private _success = [
"hostage",
_taskID,
_position,
format ["Hostage: Grid %1", _grid],
format ["Rescue hostages operating near grid %1.", _grid],
createHashMapFromArray [["hostages", _hostageUnits], ["shooters", _shooterUnits]],
createHashMapFromArray [
["limitFail", _limitFail],
["limitSuccess", _hostageCount],
["extractionZone", _extZone],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"],
["execution", true],
["cbrn", false]
],
0,
"",
"mission_manager"
] call forge_server_task_fnc_startTask;
if !(_success) exitWith {
deleteMarker _extZone;
""
};
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
_activeMissionRegistry set [_taskID, createHashMapFromArray [
["generatorType", _self call ["getGeneratorType", []]],
["position", _position],
["markers", [_extZone]],
["startedAt", serverTime]
]];
_manager set ["activeMissionRegistry", _activeMissionRegistry];
_taskID
}],
["completeMission", compileFinal {
params [
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
["_taskID", "", [""]]
];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
private _position = _missionRecord getOrDefault ["position", []];
private _markers = _missionRecord getOrDefault ["markers", []];
{
if (_x isEqualType "" && { _x in allMapMarkers }) then {
deleteMarker _x;
};
} forEach _markers;
_activeMissionRegistry deleteAt _taskID;
_manager set ["activeMissionRegistry", _activeMissionRegistry];
if (_position isEqualType [] && { count _position >= 2 }) then {
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
_recentLocationRegistry pushBack [_position, serverTime];
_manager set ["recentLocationRegistry", _recentLocationRegistry];
};
true
}]
];
publicVariable "HostageMissionGeneratorBaseClass";
HostageMissionGenerator = createHashMapObject [HostageMissionGeneratorBaseClass];
publicVariable "HostageMissionGenerator";

View File

@ -1,375 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Defines the HVT kill mission generator base class used by the dynamic
* mission manager. The generator selects a location, spawns required
* entities, registers a Forge task, and cleans up manager state when the
* task completes.
*
* Arguments:
* None
*
* Return Value:
* N/A. Defines KillHvtMissionGeneratorBaseClass in missionNamespace.
*
* Public: No
*/
KillHvtMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
["#type", "KillHvtMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["hvtConfig", (_missionConfig >> "MissionTypes" >> "HVTKill")];
_self set ["generatorType", "hvtkill"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "hvtkill"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "hvtkill") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
private _isTooClose = false;
{
private _prevPos = _x param [0, [], [[]]];
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
_isTooClose = true;
};
} forEach _recentLocationRegistry;
if (_isTooClose) then { continue; };
{
if (_candidate distance2D _x < 500) exitWith {
_isTooClose = true;
};
} forEach _activeMissionPositions;
if (_isTooClose) then { continue; };
private _inBlkList = false;
{
if (_candidate inArea _x) exitWith {
_inBlkList = true;
};
} forEach _blkListMarkers;
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
if (_taskPos isEqualTo []) exitWith {
["WARNING", "Kill HVT mission generator: selectLocation failed to find a valid dynamic position."] call forge_server_common_fnc_log;
createHashMap
};
private _building = objNull;
private _buildingCandidates = nearestObjects [
_taskPos,
["House_F", "House", "Building", "BuildingBase"],
200
];
if (_buildingCandidates isNotEqualTo []) then {
_building = selectRandom _buildingCandidates;
};
private _buildingPositions = [];
if !(isNull _building) then {
for "_i" from 0 to 100 do {
private _buildingPos = _building buildingPos _i;
if (_buildingPos isEqualTo [0, 0, 0]) exitWith {};
_buildingPositions pushBack _buildingPos;
};
};
createHashMapFromArray [
["position", _taskPos],
["grid", mapGridPosition _taskPos],
["buildingPositions", _buildingPositions]
]
}],
["spawnHvtTarget", compileFinal {
params [['_position', [0, 0, 0], [[]]], ["_buildingPositions", [], [[]]]];
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
private _unitPool = [_enemyFaction, _side] call forge_pmc_fnc_getEnemyFactionUnitPool;
if (_unitPool isEqualTo []) exitWith { [] };
private _leaderPool = _unitPool select {
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
};
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
private _targetDef = selectRandom _leaderPool;
private _targetClass = _targetDef getOrDefault ["vehicle", ""];
if (_targetClass isEqualTo "") exitWith { [] };
private _group = createGroup _side;
private _leaderPos = if (_buildingPositions isEqualTo []) then {
_position vectorAdd [(random 20 - 10), (random 20 - 10), 0]
} else {
selectRandom _buildingPositions
};
private _leader = _group createUnit [_targetClass, _leaderPos, [], 0, "NONE"];
if (isNull _leader) exitWith {
deleteGroup _group;
[]
};
_leader setRank "LIEUTENANT";
[] call forge_pmc_fnc_updateEnemyCountFromActivePlayers;
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
private _escortCount = getNumber (_hvtConfig >> "escorts");
if (_escortCount < 0) then { _escortCount = 0; };
_escortCount = floor (_escortCount * _enemyMult);
private _escortUnits = [];
for "_i" from 1 to _escortCount do {
private _escortDef = selectRandom _unitPool;
private _escortClass = _escortDef getOrDefault ["vehicle", ""];
if (_escortClass isEqualTo "") then { continue; };
private _escortPos = if (_buildingPositions isEqualTo []) then {
_position vectorAdd [(random 35 - 17), (random 35 - 17), 0]
} else {
selectRandom _buildingPositions
};
private _escort = _group createUnit [_escortClass, _escortPos, [], 0, "NONE"];
if !(isNull _escort) then {
_escort setRank (_escortDef getOrDefault ["rank", "PRIVATE"]);
_escortUnits pushBack _escort;
};
};
private _groupUnits = [_leader] + _escortUnits;
[_group, _position, 200] call BIS_fnc_taskPatrol;
[_leader, _groupUnits]
}],
["rollRewards", compileFinal {
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
private _equipmentRewards = [];
private _supplyRewards = [];
private _weaponRewards = [];
private _vehicleRewards = [];
private _specialRewards = [];
{
private _category = _x;
{
_x params ["_item", "_chance"];
if (random 1 < _chance) then {
switch (_category) do {
case "equipment": { _equipmentRewards pushBack _item; };
case "supplies": { _supplyRewards pushBack _item; };
case "weapons": { _weaponRewards pushBack _item; };
case "vehicles": { _vehicleRewards pushBack _item; };
case "special": { _specialRewards pushBack _item; };
};
};
} forEach (getArray (_hvtConfig >> "Rewards" >> _category));
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
createHashMapFromArray [
["equipment", _equipmentRewards],
["supplies", _supplyRewards],
["weapons", _weaponRewards],
["vehicles", _vehicleRewards],
["special", _specialRewards]
]
}],
["startMission", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
private _locationData = _self call ["selectLocation", [_manager]];
if (_locationData isEqualTo createHashMap) exitWith { "" };
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
private _buildingPositions = _locationData getOrDefault ["buildingPositions", []];
["INFO", format [
"Kill HVT mission generator: selected location. Grid=%1, Position=%2",
_grid,
_position
]] call forge_server_common_fnc_log;
private _taskID = format ["task_kill_hvt_%1", round (diag_tickTime * 1000)];
private _rewardRange = [_hvtConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [50000, 120000]] call forge_pmc_fnc_getMissionSettingRange;
private _reputationRange = [_hvtConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [10, 22]] call forge_pmc_fnc_getMissionSettingRange;
private _penaltyRange = [_hvtConfig, ["penalty"], "penaltyMin", "penaltyMax", [-14, -5]] call forge_pmc_fnc_getMissionSettingRange;
private _timeRange = [_hvtConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call forge_pmc_fnc_getMissionSettingRange;
private _rewards = _self call ["rollRewards"];
private _spawnResult = _self call ["spawnHvtTarget", [_position, _buildingPositions]];
if !(_spawnResult isEqualType [] && { count _spawnResult >= 2 }) exitWith { "" };
private _hvtTarget = _spawnResult select 0;
private _hvtGroupUnits = _spawnResult select 1;
if (isNull _hvtTarget || _hvtGroupUnits isEqualTo []) exitWith { "" };
private _fundsReward = _rewardRange call BIS_fnc_randomNum;
private _reputationReward = _reputationRange call BIS_fnc_randomNum;
private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum;
private _timeLimit = _timeRange call BIS_fnc_randomNum;
private _success = [
"hvt",
_taskID,
_position,
format ["HVT: Grid %1", _grid],
format ["Eliminate a high-value target near grid %1.", _grid],
createHashMapFromArray [["hvts", [_hvtTarget]]],
createHashMapFromArray [
["limitFail", 0],
["limitSuccess", 1],
["captureHvt", false],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"]
],
0,
"",
"mission_manager"
] call forge_server_task_fnc_startTask;
if !(_success) exitWith { "" };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
_activeMissionRegistry set [_taskID, createHashMapFromArray [
["generatorType", _self call ["getGeneratorType", []]],
["position", _position],
["startedAt", serverTime]
]];
_manager set ["activeMissionRegistry", _activeMissionRegistry];
_taskID
}],
["completeMission", compileFinal {
params [
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
["_taskID", "", [""]]
];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
private _position = _missionRecord getOrDefault ["position", []];
private _markers = _missionRecord getOrDefault ["markers", []];
{
if (_x isEqualType "" && { _x in allMapMarkers }) then {
deleteMarker _x;
};
} forEach _markers;
_activeMissionRegistry deleteAt _taskID;
_manager set ["activeMissionRegistry", _activeMissionRegistry];
if (_position isEqualType [] && { count _position >= 2 }) then {
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
_recentLocationRegistry pushBack [_position, serverTime];
_manager set ["recentLocationRegistry", _recentLocationRegistry];
};
true
}]
];
publicVariable "KillHvtMissionGeneratorBaseClass";
HvtMissionGenerator = createHashMapObject [KillHvtMissionGeneratorBaseClass];
publicVariable "HvtMissionGenerator";

View File

@ -1,21 +0,0 @@
# Mission Manager Functions
Mission manager functions coordinate dynamic mission generation for `forge_pmc_simulator.Tanoa`.
## Registered Functions
- `forge_pmc_fnc_missionManager` is postInit and starts the dynamic mission manager after setup settings are applied.
- `forge_pmc_fnc_persistentCadMissionManager` starts the persistent CAD-oriented mission dispatcher.
- `forge_pmc_fnc_requestMissionTask` lets CAD dispatchers request a specific generator type on demand.
- `forge_pmc_fnc_updateEnemyCountFromActivePlayers` updates the active-player scaling multiplier used by enemy spawns.
## Helper Scripts
- `adminActivatePersistentCad.sqf` is an admin/server-console helper that calls `forge_pmc_fnc_persistentCadMissionManager`.
## Runtime Notes
`forge_pmc_fnc_missionManager` waits for `forge_pmc_missionSettingsApplied` before it creates generator objects. If no UI settings are submitted within the fallback window, it applies mission param defaults and starts generation.
Generated tasks are registered through `forge_server_task_fnc_startTask` with source `mission_manager`, so they appear in the normal Forge task/CAD lifecycle.
Dispatcher-requested tasks use the same generator objects and active mission registry as timer-generated tasks. The request path respects the configured mission cap and updates the generation timestamp so the timer does not immediately create another task after a manual request.
The current CAD integration intentionally calls the mission-directory function `forge_pmc_fnc_requestMissionTask`. That keeps simulator-specific generators and settings owned by `forge_pmc_simulator.Tanoa` while the framework path is still being proven out. If this becomes a reusable framework feature, the server CAD/task layer should grow a framework-owned request interface and delegate to mission-provided generator registrations instead of calling this mission function by name.

View File

@ -1,20 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Admin/server-console helper that activates the persistent CAD mission
* dispatcher once per server session.
*
* Arguments:
* None
*
* Return Value:
* N/A
*
* Public: No
*/
if !(isServer) exitWith {};
if (missionNamespace getVariable ["forge_persistentCadDispatcherEnabled", false]) exitWith {};
missionNamespace setVariable ["forge_persistentCadDispatcherEnabled", true, true];
[] call forge_pmc_fnc_persistentCadMissionManager;

View File

@ -1,255 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Mission-side dynamic mission manager. Waits for startup settings,
* creates mission generator objects, and periodically starts generated
* Forge tasks through forge_server_task_fnc_startTask.
*
* Arguments:
* None
*
* Return Value:
* N/A
*
* Public: No
*/
if !(isServer) exitWith {};
// Startup settings are applied by the setup UI. If the UI is cancelled or
// never opens, fall back to mission params after a short grace period.
if !(missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) exitWith {
if !(missionNamespace getVariable ["forge_pmc_missionManagerStartupPending", false]) then {
missionNamespace setVariable ["forge_pmc_missionManagerStartupPending", true, true];
[] spawn {
waitUntil {
sleep 1;
(missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) || { time > 180 }
};
if !(missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) then {
[] call forge_pmc_fnc_setupMenu_applySettings;
};
missionNamespace setVariable ["forge_pmc_missionManagerStartupPending", false, true];
[] call forge_pmc_fnc_missionManager;
};
};
};
if (missionNamespace getVariable ["forge_pmc_missionManagerStarted", false]) exitWith {};
missionNamespace setVariable ["forge_pmc_missionManagerStarted", true, true];
["INFO", "Mission manager startup requested."] call forge_server_common_fnc_log;
// Defuse tasks complete through the task store when ACE reports a successful
// explosive defuse against an object tagged with the generated task id.
if !(missionNamespace getVariable ["forge_pmc_defuseAceHandlerRegistered", false]) then {
["ace_explosives_defuse", {
private _taskID = "";
{
if (_x isEqualType objNull && { !isNull _x }) then {
_taskID = _x getVariable ["assignedTask", ""];
if (_taskID isNotEqualTo "") exitWith {};
};
} forEach _this;
if (_taskID isEqualTo "") exitWith {};
if !(isNil "forge_server_task_TaskStore") then {
forge_server_task_TaskStore call ["incrementDefuseCount", [_taskID]];
};
}] call CBA_fnc_addEventHandler;
missionNamespace setVariable ["forge_pmc_defuseAceHandlerRegistered", true, true];
};
// Compile each generator before the manager object builds its weighted list.
[] call forge_pmc_fnc_attackMissionGenerator;
[] call forge_pmc_fnc_defendMissionGenerator;
[] call forge_pmc_fnc_defuseMissionGenerator;
[] call forge_pmc_fnc_deliveryMissionGenerator;
[] call forge_pmc_fnc_destroyMissionGenerator;
[] call forge_pmc_fnc_hostageMissionGenerator;
[] call forge_pmc_fnc_hvtMissionGenerator;
[] call forge_pmc_fnc_captureHvtMissionGenerator;
MissionManagerBaseClass = compileFinal createHashMapFromArray [
["#type", "MissionManagerBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap];
private _maxConcurrent = _settings getOrDefault ["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")];
private _missionInterval = _settings getOrDefault ["missionInterval", getNumber (_missionConfig >> "missionInterval")];
_self set ["missionConfig", _missionConfig];
_self set ["maxConcurrentMissions", _maxConcurrent];
_self set ["missionInterval", _missionInterval];
_self set ["lastMissionGenerationAt", -1e10];
_self set ["recentLocationRegistry", []];
_self set ["activeMissionRegistry", createHashMap];
private _generators = [];
{
_x params ["_type", "_variableName"];
if (isNil _variableName) then {
["WARNING", format ["Mission manager skipped unavailable generator '%1' (%2).", _type, _variableName]] call forge_server_common_fnc_log;
continue;
};
private _generator = missionNamespace getVariable [_variableName, createHashMap];
if (_generator isEqualTo createHashMap) then {
["WARNING", format ["Mission manager skipped invalid generator '%1' (%2).", _type, _variableName]] call forge_server_common_fnc_log;
continue;
};
_generators pushBack [_type, _generator];
} forEach [
["attack", "AttackMissionGenerator"],
["defend", "DefendMissionGenerator"],
["defuse", "DefuseMissionGenerator"],
["delivery", "DeliveryMissionGenerator"],
["destroy", "DestroyMissionGenerator"],
["hostage", "HostageMissionGenerator"],
["hvtkill", "HvtMissionGenerator"],
["hvtcapture", "CaptureHvtMissionGenerator"]
];
_self set ["generators", _generators];
}],
["getMissionInterval", compileFinal {
private _interval = _self getOrDefault ["missionInterval", 300];
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _maxConcurrent = _self getOrDefault ["maxConcurrentMissions", 1];
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getActiveMissionIds", compileFinal {
keys (_self getOrDefault ["activeMissionRegistry", createHashMap])
}],
["getGeneratorByType", compileFinal {
params [["_generatorType", "", [""]]];
private _result = createHashMap;
{
if ((_x param [0, "", [""]]) isEqualTo _generatorType) exitWith {
_result = _x param [1, createHashMap, [createHashMap]];
};
} forEach (_self getOrDefault ["generators", []]);
_result
}],
["selectGeneratorType", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _weightsConfig = _missionConfig >> "MissionWeights";
private _weighted = [];
private _totalWeight = 0;
{
private _generatorType = _x param [0, "", [""]];
private _weight = getNumber (_weightsConfig >> _generatorType);
if (_weight <= 0) then { _weight = 1; };
_totalWeight = _totalWeight + _weight;
_weighted pushBack [_generatorType, _totalWeight];
} forEach (_self getOrDefault ["generators", []]);
if (_weighted isEqualTo [] || { _totalWeight <= 0 }) exitWith { "" };
private _roll = random _totalWeight;
private _selected = (_weighted select 0) param [0, "", [""]];
{
if (_roll <= (_x param [1, 0, [0]])) exitWith {
_selected = _x param [0, "", [""]];
};
} forEach _weighted;
_selected
}],
["startRandomMission", compileFinal {
private _generatorType = _self call ["selectGeneratorType", []];
if (_generatorType isEqualTo "") exitWith { "" };
private _generator = _self call ["getGeneratorByType", [_generatorType]];
if (_generator isEqualTo createHashMap) exitWith { "" };
private _taskID = _generator call ["startMission", [_self]];
if (_taskID isEqualTo "") exitWith {
["WARNING", format ["Mission manager failed to start '%1' generated mission.", _generatorType]] call forge_server_common_fnc_log;
""
};
["INFO", format ["Mission manager started %1 generated mission %2.", _generatorType, _taskID]] call forge_server_common_fnc_log;
_taskID
}],
["completeMission", compileFinal {
params [["_taskID", "", [""]]];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
private _generatorType = _missionRecord getOrDefault ["generatorType", ""];
private _generator = _self call ["getGeneratorByType", [_generatorType]];
if !(_generator isEqualTo createHashMap) then {
_generator call ["completeMission", [_self, _taskID]];
} else {
_activeMissionRegistry deleteAt _taskID;
_self set ["activeMissionRegistry", _activeMissionRegistry];
};
true
}]
];
MissionManager = createHashMapObject [MissionManagerBaseClass];
publicVariable "MissionManager";
["INFO", format [
"Mission manager initialized. Interval=%1s, MaxConcurrent=%2",
MissionManager call ["getMissionInterval", []],
MissionManager call ["getMaxConcurrentMissions", []]
]] call forge_server_common_fnc_log;
private _missionManagerLoop = {
if (isNil "forge_server_task_TaskStore") exitWith {
["WARNING", "Mission manager waiting for forge_server_task_TaskStore."] call forge_server_common_fnc_log;
};
// Drop completed/failed task ids so concurrency is based on live missions.
{
private _status = forge_server_task_TaskStore call ["getTaskStatus", [_x]];
private _hasCatalogEntry = forge_server_task_TaskStore call ["hasTaskCatalogEntry", [_x]];
if (_status in ["succeeded", "failed"] || { _status isEqualTo "" && { !_hasCatalogEntry } }) then {
MissionManager call ["completeMission", [_x]];
forge_server_task_TaskStore call ["clearTaskStatus", [_x]];
};
} forEach (MissionManager call ["getActiveMissionIds", []]);
if (count (MissionManager call ["getActiveMissionIds", []]) >= (MissionManager call ["getMaxConcurrentMissions", []])) exitWith {};
// The PFH interval is the wake-up cadence; this timestamp prevents a task
// from being created too quickly if the interval is changed at runtime.
private _now = diag_tickTime;
private _interval = MissionManager call ["getMissionInterval", []];
private _lastMissionGenerationAt = MissionManager getOrDefault ["lastMissionGenerationAt", -1e10];
if ((_now - _lastMissionGenerationAt) < _interval) exitWith {};
MissionManager set ["lastMissionGenerationAt", _now];
private _taskID = MissionManager call ["startRandomMission", []];
if (_taskID isEqualTo "") then {
["WARNING", "Mission manager did not start a generated mission this cycle."] call forge_server_common_fnc_log;
};
};
[
{
params ["_args"];
_args params ["_loop"];
[] call _loop;
},
MissionManager call ["getMissionInterval", []],
[_missionManagerLoop]
] call CBA_fnc_addPerFrameHandler;

View File

@ -1,149 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Persistent CAD-oriented mission dispatcher. Creates CAD-assignable Forge
* tasks by randomly selecting a mission generator and running startMission.
*
* Arguments:
* None
*
* Return Value:
* N/A
*
* Public: No
*/
private _makeGenerator = {
params ["_generatorType"];
switch (_generatorType) do {
case "attack": { [] call forge_pmc_fnc_attackMissionGenerator };
case "defend": { [] call forge_pmc_fnc_defendMissionGenerator };
case "defuse": { [] call forge_pmc_fnc_defuseMissionGenerator };
case "delivery": { [] call forge_pmc_fnc_deliveryMissionGenerator };
case "destroy": { [] call forge_pmc_fnc_destroyMissionGenerator };
case "hostage": { [] call forge_pmc_fnc_hostageMissionGenerator };
case "hvt": { [] call forge_pmc_fnc_hvtMissionGenerator };
case "hvtkill": { [] call forge_pmc_fnc_hvtMissionGenerator };
case "hvtcapture": { [] call forge_pmc_fnc_captureHvtMissionGenerator };
default { createHashMap };
};
};
// Ensure generator symbols exist by compiling generator scripts if needed.
private _ensureGeneratorsLoaded = {
// compileFinal preprocess keeps it in mission namespace
[] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_attackMissionGenerator.sqf";
[] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_defendMissionGenerator.sqf";
[] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_defuseMissionGenerator.sqf";
[] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_deliveryMissionGenerator.sqf";
[] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_destroyMissionGenerator.sqf";
[] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_hostageMissionGenerator.sqf";
[] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_hvtMissionGenerator.sqf";
[] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_captureHvtMissionGenerator.sqf";
};
// Load generator scripts once.
call _ensureGeneratorsLoaded;
private _getGeneratorTypes = {
["attack", "defend", "defuse", "delivery", "destroy", "hostage", "hvtkill", "hvtcapture"]
};
PersistentCadMissionManagerBaseClass = compileFinal createHashMapFromArray [
["#type", "PersistentCadMissionManagerBaseClass"],
["#create", compileFinal {
params ["_self"];
private _missionConfig = missionConfigFile >> "CfgMissions";
_self set ["missionConfig", _missionConfig];
_self set ["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")];
_self set ["missionInterval", getNumber (_missionConfig >> "missionInterval")];
_self set ["activeMissionRegistry", createHashMap];
}],
["getMissionInterval", compileFinal {
private _interval = _self getOrDefault ["missionInterval", 300];
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _maxConcurrent = _self getOrDefault ["maxConcurrentMissions", 1];
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getActiveMissionIds", compileFinal {
keys (_self getOrDefault ["activeMissionRegistry", createHashMap])
}],
["_pickGeneratorType", compileFinal {
selectRandom (call _getGeneratorTypes)
}],
["startRandomGeneratorMission", compileFinal {
private _generatorType = _self call ["_pickGeneratorType", []];
private _generator = [_generatorType] call _makeGenerator;
if (_generator isEqualTo createHashMap) exitWith { "" };
// Generators themselves call forge_server_task_fnc_startTask.
private _taskID = _generator call ["startMission", [_self]];
if (_taskID isEqualTo "") exitWith { "" };
_taskID
}],
["completeMission", compileFinal {
params ["_taskID"];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
private _generatorType = _missionRecord getOrDefault ["generatorType", ""];
private _generator = [_generatorType] call _makeGenerator;
if !(_generator isEqualTo createHashMap) then {
_generator call ["completeMission", [_self, _taskID]];
} else {
_activeMissionRegistry deleteAt _taskID;
_self set ["activeMissionRegistry", _activeMissionRegistry];
};
true
}]
];
PersistentCadMissionManager = createHashMapObject [PersistentCadMissionManagerBaseClass];
PublicVariable "PersistentCadMissionManager";
private _loop = {
// Cleanup completed/failed missions
{
private _taskID = _x;
private _status = forge_server_task_TaskStore call ["getTaskStatus", [_taskID]];
private _hasCatalogEntry = forge_server_task_TaskStore call ["hasTaskCatalogEntry", [_taskID]];
if (_status in ["succeeded", "failed"] || { _status isEqualTo "" && { !_hasCatalogEntry } }) then {
PersistentCadMissionManager call ["completeMission", [_taskID]];
forge_server_task_TaskStore call ["clearTaskStatus", [_taskID]];
};
} forEach (PersistentCadMissionManager call ["getActiveMissionIds", []]);
if ((count (PersistentCadMissionManager call ["getActiveMissionIds", []])) >= (PersistentCadMissionManager call ["getMaxConcurrentMissions", []])) exitWith {};
private _taskID = PersistentCadMissionManager call ["startRandomGeneratorMission", []];
if (_taskID isEqualTo "") exitWith {
["WARNING", "Persistent CAD mission dispatcher failed to start a random mission."] call forge_server_common_fnc_log;
};
["INFO", format ["Persistent CAD mission dispatcher started task %1", _taskID]] call forge_server_common_fnc_log;
};
if (isServer) then {
[
{ call _loop; },
PersistentCadMissionManager call ["getMissionInterval", []],
[]
] call cba_fnc_addPerFrameHandler;
};

View File

@ -1,134 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPakeha
* Handles dispatcher-requested mission generation. This is the server-side
* mission-owned entry point used by CAD; CAD requests intent and the mission
* manager still owns generator selection, active mission tracking, and task
* lifecycle cleanup.
*
* Framework note:
* This function deliberately lives in the mission directory for now because
* these generators and setup settings are mission-specific. If on-demand CAD
* generation becomes framework-owned, replace the direct CAD call to this
* function with a framework request interface that delegates to registered
* mission generators.
*
* Arguments:
* 0: Generator type <STRING>
* 1: Request metadata <HASHMAP> (Default: createHashMap)
* 2: Requesting player UID <STRING> (Default: "")
*
* Return Value:
* Request result with success, message, taskID, and taskType keys <HASHMAP>
*
* Public: No
*/
if !(isServer) exitWith {
createHashMapFromArray [
["success", false],
["message", "Generated task requests must run on the server."]
]
};
params [
["_requestedType", "", [""]],
["_metadata", createHashMap, [createHashMap]],
["_requesterUid", "", [""]]
];
private _result = createHashMapFromArray [
["success", false],
["message", "Generated task request failed."],
["taskID", ""],
["taskType", _requestedType]
];
private _typeAliases = createHashMapFromArray [
["attack", "attack"],
["defend", "defend"],
["defense", "defend"],
["delivery", "delivery"],
["deliver", "delivery"],
["destroy", "destroy"],
["defuse", "defuse"],
["hostage", "hostage"],
["hvt", "hvtkill"],
["hvtkill", "hvtkill"],
["killhvt", "hvtkill"],
["kill_hvt", "hvtkill"],
["hvtcapture", "hvtcapture"],
["capturehvt", "hvtcapture"],
["capture_hvt", "hvtcapture"]
];
private _generatorType = _typeAliases getOrDefault [toLowerANSI _requestedType, ""];
if (_generatorType isEqualTo "") exitWith {
_result set ["message", format ["Unknown generated task type: %1", _requestedType]];
_result
};
_result set ["taskType", _generatorType];
if !(missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) then {
[] call forge_pmc_fnc_setupMenu_applySettings;
};
if (isNil "MissionManager") then {
[] call forge_pmc_fnc_missionManager;
};
if (isNil "MissionManager") exitWith {
_result set ["message", "Mission manager is not ready yet."];
_result
};
if (isNil "forge_server_task_TaskStore") exitWith {
_result set ["message", "Task store is not ready yet."];
_result
};
// Keep the active registry accurate before enforcing the mission cap.
{
private _status = forge_server_task_TaskStore call ["getTaskStatus", [_x]];
private _hasCatalogEntry = forge_server_task_TaskStore call ["hasTaskCatalogEntry", [_x]];
if (_status in ["succeeded", "failed"] || { _status isEqualTo "" && { !_hasCatalogEntry } }) then {
MissionManager call ["completeMission", [_x]];
forge_server_task_TaskStore call ["clearTaskStatus", [_x]];
};
} forEach (MissionManager call ["getActiveMissionIds", []]);
private _activeCount = count (MissionManager call ["getActiveMissionIds", []]);
private _maxConcurrent = MissionManager call ["getMaxConcurrentMissions", []];
if (_activeCount >= _maxConcurrent) exitWith {
_result set ["message", format [
"Mission cap reached (%1/%2 active). Close or complete a task before requesting another.",
_activeCount,
_maxConcurrent
]];
_result
};
private _generator = MissionManager call ["getGeneratorByType", [_generatorType]];
if (_generator isEqualTo createHashMap) exitWith {
_result set ["message", format ["Generated task type is unavailable: %1", _generatorType]];
_result
};
private _taskID = _generator call ["startMission", [MissionManager]];
if (_taskID isEqualTo "") exitWith {
_result set ["message", format ["Mission generator failed to start task type: %1", _generatorType]];
_result
};
MissionManager set ["lastMissionGenerationAt", diag_tickTime];
["INFO", format [
"Dispatcher %1 requested generated %2 mission %3.",
_requesterUid,
_generatorType,
_taskID
]] call forge_server_common_fnc_log;
_result set ["success", true];
_result set ["message", format ["Generated %1 task %2.", _generatorType, _taskID]];
_result set ["taskID", _taskID];
_result

View File

@ -1,55 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Calculates enemy spawn scaling from active player count and stores the
* result in missionNamespace for mission generators.
*
* Arguments:
* None
*
* Return Value:
* Enemy count multiplier <NUMBER>
*
* Public: No
*/
if !(isServer) exitWith { 1 };
private _table = missionNamespace getVariable [
"forge_pmc_enemyCountMultiplierTable",
[
[1, 2, 0.75],
[3, 6, 1.0],
[7, 10, 1.25],
[11, 19, 1.5]
]
];
private _minMultiplier = missionNamespace getVariable ["forge_pmc_enemyCountMultiplierMin", 0.5];
private _maxMultiplier = missionNamespace getVariable ["forge_pmc_enemyCountMultiplierMax", 2.0];
private _activeCount = {
(isPlayer _x) && { alive _x }
} count allPlayers;
private _activeCountSafe = _activeCount max 1;
private _multiplier = 1;
{
_x params ["_min", "_max", "_value"];
if (_activeCountSafe >= _min && { _activeCountSafe <= _max }) exitWith {
_multiplier = _value;
};
} forEach _table;
_multiplier = (_multiplier max _minMultiplier) min _maxMultiplier;
missionNamespace setVariable ["forge_pmc_activePlayerCount", _activeCountSafe, true];
missionNamespace setVariable ["forge_pmc_enemyCountMultiplier", _multiplier, true];
["INFO", format [
"Mission enemy scaling updated. ActivePlayers=%1, Multiplier=%2",
_activeCountSafe,
_multiplier
]] call forge_server_common_fnc_log;
_multiplier

View File

@ -1,20 +0,0 @@
# Mission Setup Functions
Mission setup functions own startup configuration for `forge_pmc_simulator.Tanoa`.
## Registered Functions
- `forge_pmc_fnc_openMissionSetupUI` opens the browser-control startup UI.
- `forge_pmc_fnc_handleMissionSetupUIEvents` handles UI events from `A3API.SendAlert`.
- `forge_pmc_fnc_setupMenu_applySettings` applies UI overrides or mission parameter defaults into `forge_pmc_missionSettings`.
## Startup Flow
The CEO client opens the setup UI from `initPlayerLocal.sqf` when `forge_pmc_missionSettingsApplied` is not set. Other players do not receive the startup setup UI.
Selecting **Start Mission** sends UI values to the server and applies them. Selecting **Cancel** applies the existing Arma mission params/defaults immediately.
## Shared State
- `forge_pmc_missionSettings`: HashMap of applied settings.
- `forge_pmc_missionSettingsApplied`: Boolean gate used by the mission manager.
- `ENEMY_FACTION_STR` and `ENEMY_SIDE`: selected faction and resolved Arma side.
Reusable faction and setting lookup helpers live in `functions\helpers`.

View File

@ -1,114 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Handles JSON events sent by the PMC mission setup browser UI through
* A3API.SendAlert.
*
* Arguments:
* 0: Browser control <CONTROL>
* 1: Whether the event came from a confirm dialog <BOOL>
* 2: JSON event payload <STRING>
*
* Return Value:
* Event handled <BOOL>
*
* Public: No
*/
params [
["_control", controlNull, [controlNull]],
["_isConfirmDialog", false, [false]],
["_message", "", [""]]
];
if (_message isEqualTo "") exitWith { false };
private _alert = fromJSON _message;
if !(_alert isEqualType createHashMap) exitWith { false };
private _event = _alert getOrDefault ["event", ""];
private _data = _alert getOrDefault ["data", createHashMap];
// Keep all browser responses on the existing framework bridge so the mission
// setup UI behaves like other HTML-backed Forge displays.
private _send = {
params ["_eventName", "_payload"];
if (isNull _control) exitWith { false };
private _json = toJSON createHashMapFromArray [
["event", _eventName],
["data", _payload]
];
_control ctrlWebBrowserAction ["ExecJS", format ["MissionSetupBridge.receive(%1)", _json]];
true
};
switch (_event) do {
case "missionSetup::ready": {
private _missionConfig = missionConfigFile >> "CfgMissions";
private _attackConfig = _missionConfig >> "MissionTypes" >> "Attack";
private _factions = [];
{
_x params ["_faction", "_display", "_value"];
_factions pushBack createHashMapFromArray [
["faction", _faction],
["display", _display],
["value", _value]
];
} forEach ([] call forge_pmc_fnc_getEnemyFactionOptions);
private _defaultFaction = "IND_G_F";
private _hasDefaultFaction = false;
{
if ((_x getOrDefault ["faction", ""]) isEqualTo _defaultFaction) exitWith {
_hasDefaultFaction = true;
};
} forEach _factions;
if (!_hasDefaultFaction && { _factions isNotEqualTo [] }) then {
_defaultFaction = (_factions select 0) getOrDefault ["faction", _defaultFaction];
};
private _payload = createHashMapFromArray [
["factions", _factions],
["settings", createHashMapFromArray [
["enemyFaction", _defaultFaction],
["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")],
["missionInterval", getNumber (_missionConfig >> "missionInterval")],
["moneyMin", (getArray (_attackConfig >> "Rewards" >> "money")) param [0, 25000]],
["moneyMax", (getArray (_attackConfig >> "Rewards" >> "money")) param [1, 60000]],
["reputationMin", (getArray (_attackConfig >> "Rewards" >> "reputation")) param [0, 6]],
["reputationMax", (getArray (_attackConfig >> "Rewards" >> "reputation")) param [1, 14]],
["penaltyMin", (getArray (_attackConfig >> "penalty")) param [0, -8]],
["penaltyMax", (getArray (_attackConfig >> "penalty")) param [1, -3]],
["timeLimitMin", (getArray (_attackConfig >> "timeLimit")) param [0, 900]],
["timeLimitMax", (getArray (_attackConfig >> "timeLimit")) param [1, 1800]]
]]
];
["missionSetup::hydrate", _payload] call _send;
};
case "missionSetup::apply": {
if !(_data isEqualType createHashMap) exitWith {
["missionSetup::error", createHashMapFromArray [["message", "Invalid mission setup payload."]]] call _send;
};
[_data] remoteExecCall ["forge_pmc_fnc_setupMenu_applySettings", 2];
closeDialog 1;
};
case "missionSetup::cancel": {
// Cancelling still applies mission params/defaults so postInit startup
// can continue without waiting for explicit UI confirmation.
[] remoteExecCall ["forge_pmc_fnc_setupMenu_applySettings", 2];
closeDialog 1;
};
case "missionSetup::close": {
closeDialog 1;
};
default {
["missionSetup::error", createHashMapFromArray [["message", format ["Unhandled setup event: %1", _event]]]] call _send;
};
};
true

View File

@ -1,54 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Opens the PMC simulator startup setup UI and wires browser events into
* forge_pmc_fnc_handleMissionSetupUIEvents.
*
* Arguments:
* None
*
* Return Value:
* UI opened <BOOL>
*
* Public: No
*/
if !(hasInterface) exitWith { false };
if (missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) exitWith { false };
private _ceoUnit = missionNamespace getVariable ["ceo", objNull];
private _isCeoSlot =
!(isNull _ceoUnit && { player isEqualTo _ceoUnit }) ||
{ toLowerANSI (vehicleVarName player) isEqualTo "ceo" };
if !(_isCeoSlot) exitWith { false };
private _display = createDialog ["RscPmcMissionSetup", true];
if (isNull _display) exitWith { false };
private _control = _display displayCtrl 92011;
if (isNull _control) exitWith {
closeDialog 1;
false
};
_control ctrlAddEventHandler ["JSDialog", {
params ["_control", "_isConfirmDialog", "_message"];
[_control, _isConfirmDialog, _message] call forge_pmc_fnc_handleMissionSetupUIEvents;
}];
_control ctrlWebBrowserAction ["LoadFile", "ui\_site\index.html"];
[] spawn {
waitUntil {
sleep 0.25;
isNull (uiNamespace getVariable ["RscPmcMissionSetup", displayNull]) ||
{ missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false] }
};
private _display = uiNamespace getVariable ["RscPmcMissionSetup", displayNull];
if !(isNull _display) then {
closeDialog 1;
};
};
true

View File

@ -1,139 +0,0 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Applies startup UI overrides or Arma mission parameter defaults into
* missionNamespace for server-side generators and managers.
*
* Arguments:
* 0: UI override settings <HASHMAP> (Default: createHashMap)
*
* Return Value:
* Settings applied <BOOL>
*
* Public: No
*/
if !(isServer) exitWith {};
params [
["_overrides", createHashMap, [createHashMap]]
];
private _settings = createHashMap;
// Always allow menu-style overrides for this mission.
_settings set ["useMenuSettings", true];
// Mission parameter IDs are exported as variables in the missionNamespace.
// We defensively read them via missionNamespace so the function also works
// when called before mission parameter evaluation.
private _paramOrDefault = {
params ["_varName", "_default"];
if (_varName in _overrides) exitWith {
_overrides getOrDefault [_varName, _default]
};
private _v = missionNamespace getVariable [_varName, _default];
_v
};
// Numeric settings may come from either the browser payload or mission params.
private _maxConcurrent = ["maxConcurrentMissions", 3] call _paramOrDefault;
private _interval = ["missionInterval", 300] call _paramOrDefault;
private _moneyMin = ["moneyMin", 25000] call _paramOrDefault;
private _moneyMax = ["moneyMax", 60000] call _paramOrDefault;
private _repMin = ["reputationMin", 6] call _paramOrDefault;
private _repMax = ["reputationMax", 14] call _paramOrDefault;
private _penMin = ["penaltyMin", -8] call _paramOrDefault;
private _penMax = ["penaltyMax", -3] call _paramOrDefault;
private _timeMin = ["timeLimitMin", 900] call _paramOrDefault;
private _timeMax = ["timeLimitMax", 1800] call _paramOrDefault;
// Enemy faction selection falls back to Params::enemyFaction when the setup UI
// is closed without pressing Start Mission.
private _enemyFactionParam = ["enemyFaction", 6] call BIS_fnc_getParamValue;
private _enemyFaction = _overrides getOrDefault ["enemyFaction", ""];
private _fallbackEnemyFaction = "IND_G_F";
private _factionOptions = [] call forge_pmc_fnc_getEnemyFactionOptions;
private _hasFallbackFaction = false;
{
_x params ["_optionFaction", "_display", "_value"];
if (_optionFaction isEqualTo _fallbackEnemyFaction) exitWith {
_hasFallbackFaction = true;
};
} forEach _factionOptions;
if (!_hasFallbackFaction && { _factionOptions isNotEqualTo [] }) then {
_fallbackEnemyFaction = (_factionOptions select 0) param [0, _fallbackEnemyFaction];
};
if (_enemyFaction isEqualTo "") then {
if (_enemyFactionParam isEqualTo -1) then {
_enemyFactionParam = 6;
};
_enemyFaction = [_enemyFactionParam, _fallbackEnemyFaction] call forge_pmc_fnc_resolveEnemyFactionParam;
} else {
_enemyFactionParam = _enemyFaction;
};
// Keep a copy of the resolved faction classname for spawning/gear systems.
missionNamespace setVariable ["ENEMY_FACTION_STR", _enemyFaction];
// Normalize ranges so generator code can safely assume min <= max.
_moneyMin = _moneyMin max 0;
_moneyMax = _moneyMax max _moneyMin;
_repMin = _repMin max -100000;
_repMax = _repMax max _repMin;
_penMin = _penMin min 0;
_penMax = _penMax min 0;
if (_penMax < _penMin) then { _penMax = _penMin; };
_timeMin = _timeMin max 1;
_timeMax = _timeMax max _timeMin;
// Publish the normalized settings map used by mission generators.
_settings set ["maxConcurrentMissions", _maxConcurrent];
_settings set ["missionInterval", _interval];
_settings set ["moneyMin", _moneyMin];
_settings set ["moneyMax", _moneyMax];
_settings set ["reputationMin", _repMin];
_settings set ["reputationMax", _repMax];
_settings set ["penaltyMin", _penMin];
_settings set ["penaltyMax", _penMax];
_settings set ["timeLimitMin", _timeMin];
_settings set ["timeLimitMax", _timeMax];
_settings set ["enemyFaction", _enemyFaction];
missionNamespace setVariable ["forge_pmc_missionSettings", _settings, true];
missionNamespace setVariable ["forge_pmc_missionSettingsApplied", true, true];
// Export a simple readiness flag for any scripts that expect settings to exist.
missionNamespace setVariable ["forge_pmc_missionSettings_getterReady", true, true];
// Convert faction classname to Arma side.
private _side = [_enemyFaction, east] call forge_pmc_fnc_getEnemyFactionSide;
diag_log format [
"[FORGE:MissionSettings] Enemy faction param=%1 resolved=%2 side=%3",
_enemyFactionParam,
_enemyFaction,
_side
];
ENEMY_SIDE = _side;
missionNamespace setVariable ["ENEMY_FACTION_STR", _enemyFaction, true];
publicVariable "ENEMY_SIDE";
true;

View File

@ -1,143 +0,0 @@
# Actor Usage Guide
The actor module stores persistent player character data: identity, loadout,
position, direction, stance, contact fields, state, holster status, rank, and
organization.
## Storage Model
Actor data is persisted through SurrealDB by the server extension.
```json
{
"uid": "76561198000000000",
"name": "Player Name",
"loadout": {},
"position": [1234.5, 6789.0, 0.0],
"direction": 90.0,
"stance": "STAND",
"email": "0160000000@spearnet.mil",
"phone_number": "0160000000",
"state": "HEALTHY",
"holster": true,
"rank": null,
"organization": "default"
}
```
Rules validated by the Rust service:
- `uid` is authoritative from the command argument and must be a 17-digit Steam
UID.
- `name` is optional, but cannot be empty when set and cannot exceed 50
characters.
- `position` must be three finite numbers when set.
- `direction` must be in the `0.0 <= direction < 360.0` range.
- `email` must contain `@` and end with `.mil` when set.
- `phone_number` must start with `0160` and be 10 digits when set.
- Empty `phone_number`, `email`, or `organization` fields are filled on create.
## Commands
All commands are called on the `actor` group.
| Command | Arguments | Returns |
| --- | --- | --- |
| `actor:get` | `uid` | Actor JSON. If no actor exists, returns a default actor but does not persist it. |
| `actor:create` | `uid`, `actor_json` | Persisted actor JSON. |
| `actor:update` | `uid`, `patch_json` | Updated actor JSON. |
| `actor:exists` | `uid` | `true` or `false`. |
| `actor:delete` | `uid` | `OK`. |
## Create an Actor
The `uid` field in the JSON is overwritten with the command UID.
```sqf
private _actor = createHashMapFromArray [
["uid", getPlayerUID player],
["name", name player],
["loadout", getUnitLoadout player],
["position", getPosATL player],
["direction", getDir player],
["stance", stance player],
["email", ""],
["phone_number", ""],
["state", "HEALTHY"],
["holster", true],
["organization", "default"]
];
private _result = "forge_server" callExtension ["actor:create", [
getPlayerUID player,
toJSON _actor
]];
```
## New Player Bootstrap
The server actor store treats a player with no persisted actor as a new player.
After `actor:create` succeeds, the actor store runs onboarding once for that UID:
- Initializes the player's phone state.
- Sends a Field Commander email from `field_commander` with the `Job Orientation`
subject and the generated phone number and email address.
- Sends two Field Commander text messages with the first-day instructions.
- Initializes the player's bank account if needed and adds `$2,000` to the bank
balance.
This bootstrap is tied to persistent actor creation, not hot-state hydration, so
returning players and repaired partial actor records do not receive the welcome
messages or starting money again.
## Update an Actor
`actor:update` accepts a JSON object containing only fields to change.
```sqf
private _patch = createHashMapFromArray [
["position", getPosATL player],
["direction", getDir player],
["stance", stance player],
["loadout", getUnitLoadout player]
];
private _result = "forge_server" callExtension ["actor:update", [
getPlayerUID player,
toJSON _patch
]];
```
Supported patch fields are `name`, `position`, `direction`, `stance`, `email`,
`phone_number`, `state`, `holster`, `rank`, `organization`, and `loadout`.
`uid` is ignored.
## Hot State
The `actor:hot:*` commands keep a runtime copy of actor data and write it back
only when `actor:hot:save` runs.
| Command | Arguments | Returns |
| --- | --- | --- |
| `actor:hot:init` | `uid` | Actor JSON from durable storage. |
| `actor:hot:get` | `uid` | Actor JSON. |
| `actor:hot:keys` | none | JSON array of hot actor UIDs. |
| `actor:hot:override` | `uid`, `actor_json` | Actor JSON. |
| `actor:hot:save` | `uid` | Current hot actor JSON and async durable save. |
| `actor:hot:remove` | `uid` | `OK`. |
Use hot state for frequently updated session data such as position and loadout.
Use durable commands for account creation and administrative changes.
## Error Handling
```sqf
private _result = "forge_server" callExtension ["actor:get", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Actor error: %1", _payload];
};
private _actor = fromJSON _payload;
```

View File

@ -1,570 +0,0 @@
# AI Prompt: Creating Arma 3 UI Windows (Dialogs) - Complete Guide
This document provides a comprehensive prompt for creating UI windows (dialogs) for Arma 3 missions.
---
## Table of Contents
1. [Core Concepts](#core-concepts)
2. [Dialog File Structure](#dialog-file-structure)
3. [Base Control Classes](#base-control-classes)
4. [Opening Dialogs via Scripts](#opening-dialogs-via-scripts)
5. [Interacting with Controls](#interacting-with-controls)
6. [Complete Example: Creating a New Store Dialog](#complete-example-creating-a-new-store-dialog)
7. [Integration Steps](#integration-steps)
---
## Core Concepts
### What Are Dialogs?
Dialogs in Arma 3 are UI windows defined in `*.hpp` files using a class-based syntax. They are defined using config classes that inherit from base control classes.
### Key Properties
- **idd**: Dialog ID (must be unique, e.g., 420, 6969, 9290)
- **movingEnable**: Allows dragging the window (true/false)
- **enableSimulation**: Allows simulation when open (true/false)
### Position System
Arma 3 uses a screen-relative coordinate system:
- `safezoneW` = safe zone width
- `safezoneH` = safe zone height
- `safezoneX` = safe zone X offset
- `safezoneY` = safe zone Y offset
Formula: `x = (position * safezoneW) + safezoneX`
Formula: `y = (position * safezoneH) + safezoneY`
Example: `x = 0.5 * safezoneW + safezoneX` places control at 50% screen width
---
## Dialog File Structure
### Basic Dialog Template
```cpp
/*
Dialog Name - Version by Author
[License/Credits]
################################## LET US BEGIN #################################### */
class Dialog_ClassName {
idd = UNIQUE_ID;
movingEnable = true;
enableSimulation = true;
class Controls {
////////////////////////////////////////////////////////
// GUI EDITOR OUTPUT START
////////////////////////////////////////////////////////
// Add your controls here
////////////////////////////////////////////////////////
// GUI EDITOR OUTPUT END
////////////////////////////////////////////////////////
};
};
```
---
## Base Control Classes
### 1. RscText (Static Text)
```cpp
class MyText: RscText {
idc = 1000;
text = "Label Text";
x = 0.5 * safezoneW + safezoneX;
y = 0.5 * safezoneH + safezoneY;
w = 0.1 * safezoneW;
h = 0.033 * safezoneH;
};
```
### 2. RscStructuredText (Rich Text with HTML-like formatting)
```cpp
class MyStructuredText: RscStructuredText {
idc = 1100;
x = 0.5 * safezoneW + safezoneX;
y = 0.5 * safezoneH + safezoneY;
w = 0.1 * safezoneW;
h = 0.033 * safezoneH;
colorBackground[] = {0,0,0,0}; // Optional background
};
```
Set text dynamically:
```cpp
_ChildControl ctrlSetStructuredText parseText format ["Value: %1", myValue];
```
### 3. RscPicture
```cpp
class MyPicture: RscPicture {
idc = 1200;
text = "images\myimage.paa";
x = 0.5 * safezoneW + safezoneX;
y = 0.5 * safezoneH + safezoneY;
w = 0.2 * safezoneW;
h = 0.15 * safezoneH;
};
```
### 4. RscButton
```cpp
class MyButton: RscButton {
onButtonClick = "[] call myFunction;";
idc = 1600;
text = "Click Me";
x = 0.5 * safezoneW + safezoneX;
y = 0.5 * safezoneH + safezoneY;
w = 0.1 * safezoneW;
h = 0.033 * safezoneH;
tooltip = "Tooltip text";
};
```
### 5. RscEdit (Input Field)
```cpp
class MyEdit: RscEdit {
idc = 1400;
x = 0.5 * safezoneW + safezoneX;
y = 0.5 * safezoneH + safezoneY;
w = 0.1 * safezoneW;
h = 0.033 * safezoneH;
tooltip = "Enter value here";
};
```
Get value:
```cpp
_value = ctrlText findDisplay DIALOG_ID displayCtrl CONTROL_ID;
```
### 6. RscListbox
```cpp
class MyListbox: RscListBox {
onLBDblClick = "_this spawn myFunction;";
idc = 1500;
x = 0.5 * safezoneW + safezoneX;
y = 0.5 * safezoneH + safezoneY;
w = 0.3 * safezoneW;
h = 0.4 * safezoneH;
};
```
Populate dynamically:
```cpp
lbClear CONTROL_ID;
{
lbAdd [CONTROL_ID, _x select 0];
} forEach myArray;
```
Get selection:
```cpp
_selectedIndex = lbCurSel CONTROL_ID;
_selectedValue = myArray select _selectedIndex;
```
### 7. RscCombo (Dropdown)
```cpp
class MyCombo: RscCombo {
onLBSelChanged = "_this spawn myFunction;";
idc = 2100;
x = 0.5 * safezoneW + safezoneX;
y = 0.5 * safezoneH + safezoneY;
w = 0.15 * safezoneW;
h = 0.033 * safezoneH;
};
```
### 8. RscFrame (Window Border/Title)
```cpp
class MyFrame: RscFrame {
Moving = 1; // Allow moving with this frame
idc = 1800;
text = "Window Title";
x = 0.3 * safezoneW + safezoneX;
y = 0.3 * safezoneH + safezoneY;
w = 0.4 * safezoneW;
h = 0.4 * safezoneH;
};
```
### 9. RscClrButton (Colored/Image Button)
```cpp
class MyImageButton: RscClrButton {
onButtonClick = "[] call myFunction;";
idc = 2000;
x = 0.5 * safezoneW + safezoneX;
y = 0.5 * safezoneH + safezoneY;
w = 0.2 * safezoneW;
h = 0.1 * safezoneH;
tooltip = "Button tooltip";
};
```
---
## Opening Dialogs via Scripts
### Method 1: Using CreateDialog (Simple)
```cpp
// In a script file (.sqf)
_handle = CreateDialog "Dialog_ClassName";
```
### Method 2: Using execVM
```cpp
// In a script file (.sqf)
execVM "scripts\myDialogScript.sqf";
```
The script then calls CreateDialog.
### Example: Triggering from an Action
```cpp
// Add to object's init line
this addAction ["Open Menu", "execVM 'scripts\myDialogScript.sqf'"];
```
---
## Interacting with Controls
### Finding the Display
```cpp
disableSerialization;
_disp = findDisplay DIALOG_ID;
```
### Getting a Control
```cpp
_ctrl = _disp displayCtrl CONTROL_ID;
```
### Setting Text
```cpp
_ctrl ctrlSetText "New Text";
```
### Getting Text
```cpp
_value = ctrlText CONTROL_ID;
```
### Setting Structured Text
```cpp
_ctrl ctrlSetStructuredText parseText format ["Value: %1", myValue];
```
### Setting Focus
```cpp
ctrlSetFocus _ctrl;
```
### Hiding/Showing Controls
```cpp
_ctrl ctrlShow false; // Hide
_ctrl ctrlShow true; // Show
```
### Enabling/Disabling Controls
```cpp
_ctrl ctrlEnable false; // Disable
_ctrl ctrlEnable true; // Enable
```
---
## Complete Example: Creating a New Store Dialog
### Step 1: Create the Dialog Definition File
File: `dialogs\MyNewStore.hpp`
```cpp
/*
My New Store GUI V 1.0 by [Your Name]
License:
[Your License]
################################## LET US BEGIN #################################### */
class A3M_MyNewStore {
idd = 5000; // Use a unique ID!
movingEnable = true;
enableSimulation = true;
class Controls {
////////////////////////////////////////////////////////
// GUI EDITOR OUTPUT START
////////////////////////////////////////////////////////
class MyStore_Frame: RscFrame {
Moving = 1;
idc = 1800;
text = "My New Store";
x = 0.3 * safezoneW + safezoneX;
y = 0.3 * safezoneH + safezoneY;
w = 0.4 * safezoneW;
h = 0.4 * safezoneH;
};
class MyStore_ExitButton: RscButton {
onButtonClick = "closeDialog 0;";
idc = 1600;
text = "Exit";
x = 0.65 * safezoneW + safezoneX;
y = 0.65 * safezoneH + safezoneY;
w = 0.04 * safezoneW;
h = 0.033 * safezoneH;
};
class MyStore_CategoryTitle: RscText {
idc = 1000;
text = "Categories:";
x = 0.32 * safezoneW + safezoneX;
y = 0.32 * safezoneH + safezoneY;
w = 0.08 * safezoneW;
h = 0.033 * safezoneH;
};
class MyStore_Btn_Items: RscButton {
onButtonClick = "[] call MyStore_fnc_loadItems;";
idc = 1610;
text = "Browse Items";
x = 0.32 * safezoneW + safezoneX;
y = 0.36 * safezoneH + safezoneY;
w = 0.1 * safezoneW;
h = 0.033 * safezoneH;
};
class MyStore_ItemList: RscListBox {
onLBDblClick = "_this spawn MyStore_fnc_handleSelect;";
idc = 1500;
x = 0.32 * safezoneW + safezoneX;
y = 0.41 * safezoneH + safezoneY;
w = 0.36 * safezoneW;
h = 0.2 * safezoneH;
};
class MyStore_InfoTitle: RscText {
idc = 1001;
text = "Information:";
x = 0.5 * safezoneW + safezoneX;
y = 0.32 * safezoneH + safezoneY;
w = 0.1 * safezoneW;
h = 0.033 * safezoneH;
};
class MyStore_InfoDisplay: RscStructuredText {
idc = 1100;
x = 0.5 * safezoneW + safezoneX;
y = 0.36 * safezoneH + safezoneY;
w = 0.18 * safezoneW;
h = 0.1 * safezoneH;
colorBackground[] = {0,0,0,0.3};
};
class MyStore_Btn_Purchase: RscButton {
onButtonClick = "[] call MyStore_fnc_purchaseItem;";
idc = 1620;
text = "Purchase";
x = 0.6 * safezoneW + safezoneX;
y = 0.65 * safezoneW + safezoneW;
w = 0.08 * safezoneW;
h = 0.033 * safezoneH;
};
class MyStore_BalanceTitle: RscText {
idc = 1002;
text = "Balance:";
x = 0.32 * safezoneW + safezoneX;
y = 0.65 * safezoneH + safezoneY;
w = 0.06 * safezoneW;
h = 0.033 * safezoneH;
};
class MyStore_BalanceDisplay: RscStructuredText {
idc = 1101;
x = 0.38 * safezoneW + safezoneX;
y = 0.65 * safezoneH + safezoneY;
w = 0.1 * safezoneW;
h = 0.033 * safezoneH;
};
////////////////////////////////////////////////////////
// GUI EDITOR OUTPUT END
////////////////////////////////////////////////////////
};
};
```
### Step 2: Create the Handler Script
File: `scripts\MyNewStore.sqf`
```cpp
/*
My New Store Script by [Your Name]
################################## LET US BEGIN #################################### */
// Open the dialog
_handle = CreateDialog "A3M_MyNewStore";
// Define available items
myStoreItems = [
["Item Name 1", 100],
["Item Name 2", 200],
["Item Name 3", 300]
];
// Update balance display
MyStore_UpdateBalance = {
disableSerialization;
_disp = findDisplay 5000;
if (str(_disp) != "no display") then {
_ctrl = _disp displayCtrl 1101;
_balance = player getVariable "myMoney";
_ctrl ctrlSetStructuredText parseText format ["$%1", _balance];
};
};
[] call MyStore_UpdateBalance;
// Load items into listbox
MyStore_fnc_loadItems = {
lbClear 1500;
{
lbAdd [1500, format ["%1 - $%2", _x select 0, _x select 1]];
} forEach myStoreItems;
};
// Handle selection
MyStore_fnc_handleSelect = {
_index = _this select 1;
_item = myStoreItems select _index;
_disp = findDisplay 5000;
_ctrl = _disp displayCtrl 1100;
_ctrl ctrlSetStructuredText parseText format [
"Selected: %1<br/>Price: $%2",
_item select 0,
_item select 1
];
};
// Purchase item
MyStore_fnc_purchaseItem = {
_index = lbCurSel 1500;
if (_index == -1) exitWith { hint "Select an item first!"; };
_item = myStoreItems select _index;
_price = _item select 1;
_money = player getVariable "myMoney";
if (_money < _price) exitWith { hint "Not enough money!"; };
player setVariable ["myMoney", _money - _price];
hint format ["Purchased %1 for $%2!", _item select 0, _price];
[] call MyStore_UpdateBalance;
};
```
### Step 3: Add to description.ext
```cpp
#include "dialogs\MyNewStore.hpp"
```
### Step 4: Add Open Action (Optional)
Add to an object's init:
```cpp
this addAction ["Open Store", "execVM 'scripts\MyNewStore.sqf'"];
```
---
## Integration Steps
### 1. Create Dialog File (.hpp)
Create the dialog class definition in the `dialogs/` folder.
### 2. Include in description.ext
Add `#include "dialogs\YourDialog.hpp"` to the description.ext file.
### 3. Register Controls Used
If using custom control IDs, ensure they don't conflict with existing ones. Common ranges:
- 1000-1099: Text/Static controls
- 1100-1199: Structured text
- 1200-1299: Pictures
- 1400-1499: Edit fields
- 1500-1599: Listboxes
- 1600-1699: Buttons
- 1800-1899: Frames
- 2000-2999: Custom controls
### 4. Create Handler Script
Create an `.sqf` script file in the `scripts/` folder to handle the dialog logic.
### 5. Test Thoroughly
- Test opening/closing the dialog
- Test all buttons and interactions
- Test list population
- Test form validation
- Test error messages
---
## Best Practices
1. **Unique IDD**: Always use a unique dialog ID (idd)
2. **Organized Layout**: Use a consistent grid layout
3. **Clear Labels**: Always label information displays
4. **Exit Button**: Always include an exit/close button
5. **Form Validation**: Validate inputs before processing
6. **User Feedback**: Use hints to provide feedback
7. **Responsive Design**: Test on different aspect ratios
8. **Mod Support**: Consider adding mod compatibility checks

View File

@ -1,190 +0,0 @@
# Bank Usage Guide
The bank module stores player account balances, earnings, PINs, and transaction
strings. The hot-state API also owns the active banking workflows used by the
UI: deposit, withdraw, transfer, checkout charge, PIN validation, and PIN
changes.
## Storage Model
Bank data is persisted through SurrealDB by the server extension.
```json
{
"uid": "76561198000000000",
"name": "Player Name",
"bank": 1000.0,
"cash": 250.0,
"earnings": 0.0,
"pin": 1234,
"transactions": []
}
```
Rules validated by the Rust service:
- `uid` is authoritative from the command argument.
- `name` cannot be empty.
- `bank` and `cash` cannot be negative.
- `pin` must be a four-digit number.
- Durable `bank:get` requires an existing bank account.
## Durable Commands
| Command | Arguments | Returns |
| --- | --- | --- |
| `bank:create` | `uid`, `bank_json` | Persisted bank JSON. |
| `bank:get` | `uid` | Bank JSON. |
| `bank:update` | `uid`, `patch_json` | Updated bank JSON. |
| `bank:exists` | `uid` | `true` or `false`. |
| `bank:delete` | `uid` | `OK`. |
## Create an Account
The `uid` field in the JSON is overwritten with the command UID.
```sqf
private _account = createHashMapFromArray [
["uid", getPlayerUID player],
["name", name player],
["bank", 0],
["cash", 0],
["earnings", 0],
["pin", 1234],
["transactions", []]
];
private _result = "forge_server" callExtension ["bank:create", [
getPlayerUID player,
toJSON _account
]];
```
## Hot-State Commands
| Command | Arguments | Returns |
| --- | --- | --- |
| `bank:hot:init` | `uid` | Bank JSON loaded into hot state. |
| `bank:hot:get` | `uid` | Bank JSON. |
| `bank:hot:override` | `uid`, `bank_json` | Bank JSON. |
| `bank:hot:patch` | `uid`, `patch_json` | `{ account, patch }`. |
| `bank:hot:deposit` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
| `bank:hot:withdraw` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
| `bank:hot:deposit_earnings` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
| `bank:hot:transfer` | `source_uid`, `target_uid`, `amount`, `context_json` | Transfer result JSON. |
| `bank:hot:charge_checkout` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
| `bank:hot:validate_pin` | `uid`, `pin`, `context_json` | `{}` on success. |
| `bank:hot:change_pin` | `uid`, `current_pin`, `new_pin`, `context_json` | `{ account, patch }`. |
| `bank:hot:save` | `uid` | Current hot bank JSON and async durable save. |
| `bank:hot:remove` | `uid` | `OK`. |
Use hot-state commands for UI workflows. They return patch objects so the UI can
update only changed fields.
## Deposit and Withdraw
ATM sessions require `atmAuthorized: true`. Full bank sessions can set
`mode: "bank"`.
```sqf
private _context = createHashMapFromArray [
["mode", "atm"],
["atmAuthorized", true]
];
private _deposit = "forge_server" callExtension ["bank:hot:deposit", [
getPlayerUID player,
"100",
toJSON _context
]];
private _withdraw = "forge_server" callExtension ["bank:hot:withdraw", [
getPlayerUID player,
"50",
toJSON _context
]];
```
## Transfer
Transfers are only available from the full bank interface. `fromField` can be
`bank` or `cash`.
```sqf
private _context = createHashMapFromArray [
["mode", "bank"],
["atmAuthorized", false],
["fromField", "bank"]
];
private _result = "forge_server" callExtension ["bank:hot:transfer", [
getPlayerUID player,
_targetUid,
"250",
toJSON _context
]];
```
## Checkout Charge
Checkout charging supports `sourceField: "cash"` or `sourceField: "bank"`.
Set `commit` to `false` to preview the patch without saving.
```sqf
private _context = createHashMapFromArray [
["sourceField", "bank"],
["commit", true]
];
private _result = "forge_server" callExtension ["bank:hot:charge_checkout", [
getPlayerUID player,
"125",
toJSON _context
]];
```
## PIN Validation
PIN entry is only valid in ATM mode.
```sqf
private _context = createHashMapFromArray [["mode", "atm"]];
private _result = "forge_server" callExtension ["bank:hot:validate_pin", [
getPlayerUID player,
"1234",
toJSON _context
]];
```
## PIN Changes
PIN changes require the current PIN and a different four-digit new PIN. The
command is only valid from the full bank interface.
```sqf
private _context = createHashMapFromArray [
["mode", "bank"],
["atmAuthorized", false]
];
private _result = "forge_server" callExtension ["bank:hot:change_pin", [
getPlayerUID player,
"1234",
"5678",
toJSON _context
]];
```
## Error Handling
```sqf
private _result = "forge_server" callExtension ["bank:hot:get", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Bank error: %1", _payload];
};
private _bank = fromJSON _payload;
```

View File

@ -1,191 +0,0 @@
# CAD Usage Guide
The CAD module stores transient operational state for dispatch activity,
assignments, dispatch orders, support requests, group profiles, grouped views,
and hydrated UI payloads. CAD state is in-memory and follows the active server
or mission lifecycle.
## Data Model
Most CAD records are flexible JSON objects. The service normalizes important
IDs and returns structured mutation results for higher-level workflows.
Common generated IDs:
- Orders: `cad-order:<sequence>`
- Requests: `cad-request:<sequence>`
- Assignments usually share a task ID or order ID.
## Commands
### Activity
| Command | Arguments | Returns |
| --- | --- | --- |
| `cad:activity:append` | `activity_json` | `OK`. |
| `cad:activity:recent` | `limit` | Recent activity array JSON. |
### Assignments
| Command | Arguments | Returns |
| --- | --- | --- |
| `cad:assignments:list` | none | Assignment array JSON. |
| `cad:assignments:assign` | `entry_id`, `assignment_json` | Assignment mutation result JSON. |
| `cad:assignments:acknowledge` | `entry_id`, `patch_json` | Assignment mutation result JSON. |
| `cad:assignments:decline` | `entry_id`, `patch_json` | Assignment mutation result JSON and removes assignment. |
| `cad:assignments:upsert` | `entry_id`, `assignment_json` | `OK`. |
| `cad:assignments:delete` | `entry_id` | `OK`. |
### Orders
| Command | Arguments | Returns |
| --- | --- | --- |
| `cad:orders:list` | none | Order array JSON. |
| `cad:orders:create` | `order_seed_json` | Dispatch order mutation result JSON. |
| `cad:orders:create_from_context` | `context_json` | Dispatch order mutation result JSON. |
| `cad:orders:close` | `entry_id` | Dispatch order mutation result JSON and removes order/assignment. |
| `cad:orders:upsert` | `entry_id`, `order_json` | `OK`. |
| `cad:orders:delete` | `entry_id` | `OK`. |
### Requests
| Command | Arguments | Returns |
| --- | --- | --- |
| `cad:requests:list` | none | Request array JSON. |
| `cad:requests:submit` | `request_json` | Request mutation result JSON. |
| `cad:requests:submit_from_context` | `context_json` | Request mutation result JSON. |
| `cad:requests:close` | `entry_id` | Request mutation result JSON and removes request. |
| `cad:requests:upsert` | `entry_id`, `request_json` | `OK`. |
| `cad:requests:delete` | `entry_id` | `OK`. |
### Profiles and Views
| Command | Arguments | Returns |
| --- | --- | --- |
| `cad:profiles:list` | none | Profile array JSON. |
| `cad:profiles:update_from_context` | `context_json` | Profile mutation result JSON. |
| `cad:profiles:upsert` | `entry_id`, `profile_json` | `OK`. |
| `cad:profiles:delete` | `entry_id` | `OK`. |
| `cad:groups:build` | `groups_seed_json` | Group array JSON. |
| `cad:view:hydrate` | `hydrate_seed_json` | Hydrated CAD payload JSON. |
## Submit a Support Request
```sqf
private _fields = createHashMapFromArray [
["pickup_location", "Grid 123456"],
["precedence", "urgent"],
["security", "secure"]
];
private _context = createHashMapFromArray [
["type", "medevac_9line"],
["fields", _fields],
["groupId", "alpha"],
["groupCallsign", "Alpha 1-1"],
["submittedByUid", getPlayerUID player],
["submittedByName", name player],
["priority", "emergency"],
["position", getPosATL player],
["createdAt", diag_tickTime]
];
private _result = "forge_server" callExtension ["cad:requests:submit_from_context", [
toJSON _context
]];
```
Supported priority values are `routine`, `priority`, and `emergency`. Unknown
values normalize to `priority`.
## Create a Dispatch Order
```sqf
private _context = createHashMapFromArray [
["assigneeGroupId", "bravo"],
["assigneeGroupCallsign", "Bravo 1-1"],
["targetGroupId", "alpha"],
["targetGroupCallsign", "Alpha 1-1"],
["targetPosition", getPosATL player],
["createdByUid", getPlayerUID player],
["createdByName", name player],
["requestId", "cad-request:1"],
["requestType", "logreq"],
["requestTitle", "LOGREQ | Alpha 1-1"],
["requestSummary", "Ammo resupply requested"],
["requestFields", createHashMap],
["note", "Support Alpha 1-1 at current position."],
["priority", "priority"],
["createdAt", diag_tickTime]
];
private _result = "forge_server" callExtension ["cad:orders:create_from_context", [
toJSON _context
]];
```
## Assignment Workflow
Task contracts have two separate phases. Dispatch assignment reserves a
contract for a group and sets the CAD assignment state to `assigned`, but it
does not accept or start the task. The assigned group leader must acknowledge
the assignment before task ownership is bound and task logic starts. If the
leader declines, the CAD assignment is removed and the contract returns to the
open board. Task status follows the same lifecycle: `available` on creation,
`assigned` after dispatch assignment, and `active` after acknowledgement.
```sqf
private _assignment = createHashMapFromArray [
["groupId", "bravo"],
["assigneeGroupCallsign", "Bravo 1-1"],
["assignedByUid", getPlayerUID player],
["assignedByName", name player],
["assignedAt", diag_tickTime],
["state", "assigned"]
];
"forge_server" callExtension ["cad:assignments:assign", [
"task-123",
toJSON _assignment
]];
private _ack = createHashMapFromArray [
["state", "acknowledged"],
["acknowledgedByUid", getPlayerUID player],
["acknowledgedAt", diag_tickTime]
];
"forge_server" callExtension ["cad:assignments:acknowledge", [
"task-123",
toJSON _ack
]];
```
## Hydrate the CAD UI
```sqf
private _session = createHashMapFromArray [
["uid", getPlayerUID player],
["orgId", "default"],
["isDispatcher", true],
["groupId", "alpha"],
["isLeader", true]
];
private _seed = createHashMapFromArray [
["groups", _liveGroups],
["activeTasks", _activeTasks],
["session", _session]
];
private _result = "forge_server" callExtension ["cad:view:hydrate", [toJSON _seed]];
```
## Error Handling
```sqf
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["CAD error: %1", _payload];
};
```

View File

@ -1,98 +0,0 @@
# Client Actor Usage Guide
The client actor addon owns the player interaction menu and client-side actor
repository. It is the main launcher for nearby player actions and other Forge
client UIs.
## Open the Actor Menu
```sqf
call forge_client_actor_fnc_openUI;
```
The actor menu opens `RscActorMenu`, loads `ui/_site/index.html`, and routes
browser alerts through `forge_client_actor_fnc_handleUIEvents`.
## Repository
`forge_client_actor_fnc_initRepository` creates `GVAR(ActorRepository)`.
The repository:
- requests actor initialization from the server
- saves actor state through the server actor addon
- caches client-visible actor fields
- applies position, direction, stance, rank, and loadout on JIP sync when the
relevant settings allow it
- provides nearby interaction actions to the browser UI
Initialize actor state through the repository:
```sqf
GVAR(ActorRepository) call ["init", []];
```
Save actor state through the server:
```sqf
GVAR(ActorRepository) call ["save", [true]];
```
## Nearby Actions
The menu asks for nearby actions with:
```text
actor::get::actions
```
The repository scans objects within 5 meters and returns actions based on
mission object variables:
| Variable | Action |
| --- | --- |
| `isStore` | store |
| `isAtm` | ATM |
| `isBank` | bank |
| `isGarage` | garage |
| `garageType` | garage subtype |
| `isLocker` | virtual arsenal action when VA is enabled |
| `deviceType` | device action placeholder |
| nearby player unit | player interaction placeholder |
The response is pushed into the browser with `updateAvailableActions(...)`.
## Browser Events
| Event | Client behavior |
| --- | --- |
| `actor::get::actions` | Refresh nearby actions. |
| `actor::close::menu` | Close actor menu. |
| `actor::open::atm` | Open bank UI in ATM mode. |
| `actor::open::bank` | Open bank UI in bank mode. |
| `actor::open::cad` | Open CAD UI. |
| `actor::open::garage` | Open garage UI. |
| `actor::open::vgarage` | Open virtual garage. |
| `actor::open::org` | Open organization UI. |
| `actor::open::vlocker` | Open ACE arsenal on `FORGE_Locker_Box`. |
| `actor::open::phone` | Open phone UI. |
| `actor::open::store` | Open store UI. |
Device and player interaction events currently display placeholder feedback.
## Authoritative State
Actor persistence is server-owned. The client repository requests and displays
actor data, but actor creation, durable updates, and hot-state behavior are
handled by the server actor addon and extension.
## Related Guides
- [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md)
- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md)
- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md)
- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md)
- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md)
- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)

View File

@ -1,89 +0,0 @@
# Client Bank Usage Guide
The client bank addon opens the bank and ATM browser UI, forwards banking
requests to the server bank addon, and pushes account updates back into the
browser.
## Open Bank UI
Open full bank mode:
```sqf
call forge_client_bank_fnc_openUI;
```
Open ATM mode:
```sqf
[true] call forge_client_bank_fnc_openUI;
```
The open function creates `RscBank`, sets the bridge mode to `bank` or `atm`,
loads `ui/_site/index.html`, and routes browser events through
`forge_client_bank_fnc_handleUIEvents`.
## Bridge and Repository
`forge_client_bank_fnc_initRepository` tracks account load and cached account
state.
`forge_client_bank_fnc_initUIBridge` owns:
- active browser control tracking
- bank/ATM mode
- browser ready handling
- account hydrate and sync responses
- deposit, withdrawal, transfer, earnings deposit, credit repayment, PIN
validation, and PIN change requests
- browser notice delivery
## Browser Events
| Event | Client behavior |
| --- | --- |
| `bank::ready` | Mark browser ready and request hydrate from the server. |
| `bank::refresh` | Request fresh bank hydrate data. |
| `bank::deposit::request` | Forward deposit amount to the server. |
| `bank::withdraw::request` | Forward withdrawal amount to the server. |
| `bank::transfer::request` | Forward target, source field, and amount. |
| `bank::depositEarnings::request` | Request earnings deposit. |
| `bank::repayCreditLine::request` | Request credit-line repayment. |
| `bank::pin::request` | Forward PIN validation request. |
| `bank::pin::change::request` | Forward current and new PIN values for a PIN change. |
| `bank::close` | Dispose bridge screen state and close the display. |
## Browser Response Events
The bridge sends:
| Event | Purpose |
| --- | --- |
| `bank::hydrate` | Full session/account payload. |
| `bank::sync` | Account patch or sync data. |
| `bank::notice` | UI-visible notice payload. |
## Request Flow
Example deposit flow:
1. Browser sends `bank::deposit::request` with an `amount`.
2. Client bridge calls the server bank request event.
3. Server bank addon validates the request and calls bank hot-state logic.
4. Server response is caught by the client post-init event handlers.
5. Client bridge sends `bank::sync` or `bank::notice` back to the browser.
## Authoritative State
Balances, PIN authorization, transfers, checkout charges, credit lines, and
persistence are server-owned. The client should only display account data and
request mutations through server events.
PIN changes are available from the full bank UI only. The browser validates the
current, new, and confirmation fields, but the server extension remains
authoritative and persists the updated PIN.
## Related Guides
- [Bank Usage Guide](./BANK_USAGE_GUIDE.md)
- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)
- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)

View File

@ -1,111 +0,0 @@
# Client CAD Usage Guide
The client CAD addon provides the map and dispatch UI for groups, active
tasks, task assignment, dispatch orders, support requests, and task
acknowledge/decline workflows.
## Open CAD UI
```sqf
call forge_client_cad_fnc_openUI;
```
The CAD UI opens `RscMapUI` and loads separate browser controls for:
- top bar
- bottom bar
- side panel
- dispatcher board
The native Arma map remains part of the same display.
## Repository and Bridge
`forge_client_cad_fnc_initRepository` caches the hydrated CAD payload,
selected mode, dispatch view, session data, groups, tasks, requests, and
assignments.
`forge_client_cad_fnc_initUIBridge` owns:
- ready state for side panel, top bar, and dispatcher board
- operations vs dispatch mode
- board vs map dispatch view
- hydrate requests
- task assignment, acknowledge, and decline requests
- dispatch order create/close requests
- support request submit/close requests
- group status, role, and profile requests
- map focus actions
## Map Focus Behavior
CAD list entries can drive the native map position without duplicating map
logic in the browser UI. In operations mode, assigned or accepted task cards,
roster member cards, and support request cards send focus events. In dispatch
map mode, group, contract, and support request cards use the same focus path.
Task and support request focus uses the stored record position. Roster member
focus uses the member position included in the hydrated group roster.
## Browser Events
| Event | Client behavior |
| --- | --- |
| `cad::topbar::ready` | Mark top bar ready and push top bar state. |
| `cad::ready` | Mark side panel ready and request hydrate. |
| `cad::dispatcher::ready` | Mark dispatcher board ready and push hydrate data. |
| `cad::mode::set` | Switch between operations and dispatch mode. |
| `cad::dispatchView::set` | Switch dispatch board/map view. |
| `cad::refresh` | Request fresh CAD hydrate data. |
| `cad::tasks::assign` | Assign a task to a group. |
| `cad::tasks::acknowledge` | Acknowledge assigned task. |
| `cad::tasks::decline` | Decline assigned task. |
| `cad::dispatchOrder::create` | Create dispatch order. |
| `cad::dispatchOrder::close` | Close dispatch order. |
| `cad::supportRequest::submit` | Submit support request. |
| `cad::supportRequest::close` | Close support request. |
| `cad::groups::status` | Update group status. |
| `cad::groups::role` | Update group role. |
| `cad::groups::profile` | Update status and role together. |
| `cad::groups::focus` | Center map on a group. |
| `cad::members::focus` | Center map on a group member. |
| `cad::tasks::focus` | Center map on a task. |
| `cad::requests::focus` | Center map on a support request. |
| `map::zoomIn` | Zoom native map in. |
| `map::zoomOut` | Zoom native map out. |
| `map::search` | Placeholder status update. |
| `map::close` | Dispose bridge state and close the display. |
## Response Events
The bridge pushes:
| Event | Purpose |
| --- | --- |
| `cad::hydrate` | Full hydrated CAD payload to the side panel. |
| `cad::assignment::response` | Task assignment/acknowledge/decline result. |
| `cad::group::response` | Group status/role/profile result. |
| `cad::request::response` | Support request result. |
Dispatcher board controls also receive direct `ExecJS` status and hydrate
calls.
## Task Compatibility
CAD task visibility depends on server-side task catalog entries. Tasks created
through Eden Forge task modules or `forge_task_fnc_startTask` are the
normal CAD-compatible task sources because they register task catalog data.
Direct handler or task-function calls only work with CAD when the task catalog
entry already exists.
## Authorization Notes
Only dispatcher sessions can enter dispatch mode. If the hydrated session is
not a dispatcher, the bridge forces the UI back to operations mode.
## Related Guides
- [CAD Usage Guide](./CAD_USAGE_GUIDE.md)
- [Task Usage Guide](./TASK_USAGE_GUIDE.md)
- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)

View File

@ -1,92 +0,0 @@
# Client Common Usage Guide
The client `common` addon contains shared browser UI bridge declarations and
common client-side browser integration patterns.
## Purpose
Use `forge_client_common` when a browser-backed feature UI needs reusable
screen lifecycle behavior:
- active browser control tracking
- browser ready state
- pending event queues
- `ExecJS` payload delivery
- shared bridge object inheritance through `createHashMapObject`
Feature addons still own their app-specific events and server RPC mapping.
## Shared Bridge
Initialize the bridge declarations with:
```sqf
private _webUIDeclarations = call forge_client_common_fnc_initWebUIBridge;
private _bridgeDeclaration = _webUIDeclarations get "bridgeDeclaration";
```
Feature bridges can inherit from the shared declaration:
```sqf
GVAR(MyUIBridgeBaseClass) = compileFinal createHashMapFromArray [
["#base", _bridgeDeclaration],
["#type", "MyUIBridgeBaseClass"],
["handleReady", compileFinal {
params [["_control", controlNull, [controlNull]]];
_self call ["setActiveBrowserControl", [_control]];
_self call ["sendEvent", ["myAddon::hydrate", createHashMap, _control]];
}]
];
```
## Event Delivery
`sendEvent` builds this payload:
```json
{
"event": "myAddon::event",
"data": {}
}
```
If the browser control is missing or not ready, the payload is queued on the
screen object. When the screen marks ready, `flushPendingEvents` delivers the
queue.
## Screen Lifecycle
The shared screen object tracks:
| Field | Purpose |
| --- | --- |
| `control` | Active browser control. |
| `readyState` | Whether the browser app has sent its ready event. |
| `pendingEvents` | Outbound events waiting for a ready browser. |
Call `handleClose` or `dispose` when a display closes so stale controls and
queued events are cleared.
## Current Consumers
The common bridge pattern is used by the newer bank, CAD, garage, and
organization client bridges. Store currently keeps its own bridge object and
browser bridge function names.
## Usage Rules
- Keep bridge inheritance in feature addons thin and explicit.
- Keep shared code generic; do not add bank, CAD, org, or store-specific logic
to `common`.
- Prefer namespaced events such as `garage::sync`.
- Send hash maps or arrays that can be safely serialized with `toJSON`.
- Avoid direct extension calls from the client bridge; send CBA server events.
## Related Guides
- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md)
- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md)
- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md)

View File

@ -1,114 +0,0 @@
# Client Garage Usage Guide
The client garage addon provides player vehicle storage UI, vehicle
store/retrieve actions, selected nearby vehicle service requests, vehicle
context building, and the virtual garage view.
## Open Garage UI
```sqf
call forge_client_garage_fnc_openUI;
```
The garage UI opens `RscGarage`, loads `ui/_site/index.html`, and routes
browser events through `forge_client_garage_fnc_handleUIEvents`.
## Open Virtual Garage
```sqf
call forge_client_garage_fnc_openVG;
```
The virtual garage resolves the active interaction object near the player,
discovers nearby `garage*` markers placed in Eden, chooses the matching spawn
lane for the selected vehicle type, opens the BIS garage interface, and
restricts the available vehicle lists from the virtual garage repository. When
the BIS garage closes, only the vehicle selected in that virtual garage session
is finalized and spawned onto the resolved lane.
## Client Services
| Service | Purpose |
| --- | --- |
| `GarageRepository` | Player garage view state. |
| `VGRepository` | Virtual garage unlock view state. |
| `GarageHelperService` | Vehicle names, hit points, and payload helpers. |
| `GarageContextService` | Nearby/current vehicle context. |
| `GaragePayloadService` | Browser hydrate payload construction. |
| `GarageActionService` | Store/retrieve request handling and selected nearby vehicle refuel/repair request forwarding. |
| `GarageUIBridge` | Browser ready, hydrate, and sync delivery. |
## Browser Events
| Event | Client behavior |
| --- | --- |
| `garage::ready` | Mark browser ready and send `garage::hydrate`. |
| `garage::refresh` | Send current garage payload as `garage::sync`. |
| `garage::vehicle::retrieve::request` | Forward retrieve request through the action service. |
| `garage::vehicle::store::request` | Forward store request through the action service. |
| `garage::vehicle::refuel::request` | Forward selected nearby vehicle refuel request to the server economy service. |
| `garage::vehicle::repair::request` | Forward selected nearby vehicle repair request to the server economy service. |
| `garage::close` | Dispose bridge screen state and close the display. |
## Browser Response Events
| Event | Purpose |
| --- | --- |
| `garage::hydrate` | Initial vehicle and session payload. |
| `garage::sync` | Refreshed vehicle payload. |
| `garage::service::success` | Browser notice for accepted refuel/rearm/repair requests. |
| `garage::service::failure` | Browser notice for rejected refuel/rearm/repair requests. |
Server action responses are handled by the action service and notification
flow.
## Vehicle Service
The selected vehicle detail panel includes refuel, rearm, and repair actions for nearby
world vehicles. Stored records must be retrieved first because server economy
services operate on live vehicle objects, not stored garage records.
Refuel requests use the server economy `RefuelService` event. Rearm requests
use `RearmService`. Repair requests use `RepairService`. These services are
billed by the server economy addon through organization funds.
## Mission Setup
Garage interactions are normally surfaced through the actor menu when nearby
objects have garage variables such as:
```sqf
_object setVariable ["isGarage", true, true];
_object setVariable ["garageType", "cars", true];
```
When using the server garage auto-init flow, editor-placed objects whose
variable names contain `garage` are marked as garage interaction points and
their `garageType` can be inferred from the name.
Virtual garage spawn lanes are resolved from empty markers placed in Eden. The
marker name should contain `garage` and one of the six supported category names:
`cars`, `armor`, `helis`, `planes`, `naval`, or `other`. Markers are matched to
the nearby interaction object by proximity, and names that include the garage
object's variable name are preferred when multiple garages exist.
Vehicle spawning is strict by category. If the active garage site does not have
a matching local marker for the vehicle category being retrieved or spawned from
the virtual garage, the request is blocked and the player is shown a message.
Nearby world vehicles are not used as virtual garage spawn candidates. They are
only checked to determine whether the resolved spawn position is blocked. If
any vehicle is within 5 meters of the spawn marker when the virtual garage is
opened, the session is blocked and the player is shown a warning.
## Authoritative State
The client gathers vehicle context and sends store/retrieve requests. Stored
vehicle state, validation, spawning, removal, and persistence are owned by the
server garage addon and extension.
## Related Guides
- [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md)
- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md)
- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md)

View File

@ -1,87 +0,0 @@
# Client Locker Usage Guide
The client locker addon manages personal locker display state, local locker
container behavior, and virtual arsenal unlock state.
## Repositories
`forge_client_locker_fnc_initRepository` creates `GVAR(LockerRepository)`.
`forge_client_locker_fnc_initVARepository` creates `GVAR(VARepository)`.
Initialize locker state:
```sqf
GVAR(LockerRepository) call ["init", []];
GVAR(VARepository) call ["init", []];
```
## Locker Container Flow
The repository searches mission namespace variables whose names contain
`locker` and refer to objects. For each server/mission locker object, it creates
a local `Box_NATO_Equip_F` at the same position and attaches container event
handlers.
On container open:
- the local container is cleared
- cached locker items are inserted into the container
- over-capacity warnings are emitted when the item count is above 25
On container close:
- cargo, nested container items, and weapon attachments are read back
- the new locker map is sent to the server with the override request
- the local repository cache is updated
## Virtual Arsenal Flow
The virtual arsenal repository creates a local `FORGE_Locker_Box` and requests
virtual arsenal unlocks from the server.
As sync data arrives, it applies unlocks through ACE Arsenal:
| Data key | Client behavior |
| --- | --- |
| `items` | Add virtual items. |
| `weapons` | Add virtual weapons. |
| `magazines` | Add virtual magazines. |
| `backpacks` | Add virtual backpacks. |
The actor menu opens the virtual locker with:
```sqf
[FORGE_Locker_Box, player, false] spawn ace_arsenal_fnc_openBox;
```
## Server Events
The client repository sends requests for:
- locker initialization
- locker save
- locker override after container close
- virtual arsenal initialization
- virtual arsenal save
The server locker addon and extension own the saved locker and virtual arsenal
state.
## Mission Setup
Mission locker objects must be placed into `missionNamespace` with a variable
name containing `locker`. The client creates local interactive containers from
those authoritative mission objects.
Example:
```sqf
missionNamespace setVariable ["forge_locker_alpha", _lockerObject, true];
```
## Related Guides
- [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md)
- [Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md)
- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md)

View File

@ -1,48 +0,0 @@
# Client Main Usage Guide
The client `main` addon provides the shared mod identity, version metadata,
CBA settings, and macro foundation used by the Forge client addons.
## Purpose
Use `forge_client_main` as the foundation dependency for client addons that
need Forge macros, function naming, settings, or mod-level configuration.
Feature logic should stay in the owning addon. `main` should remain limited to
shared client configuration and compile infrastructure.
## Key Files
| File | Purpose |
| --- | --- |
| `script_mod.hpp` | Client mod identity. |
| `script_version.hpp` | Client mod version values. |
| `script_macros.hpp` | Shared client macros. |
| `CfgSettings.hpp` | Client CBA settings. |
| `config.cpp` | Addon config and mod wiring. |
## Dependency Pattern
Feature addons normally depend on `forge_client_main` in their `config.cpp`.
```cpp
class forge_client_example {
requiredAddons[] = {
"forge_client_main"
};
};
```
## Usage Notes
- Put domain UI, repositories, and event handling in feature addons.
- Put reusable browser bridge behavior in `forge_client_common`.
- Put server-only behavior in `arma/server/addons`.
- Keep settings in `CfgSettings.hpp` when they apply to the client mod as a
whole or to a client feature toggle.
## Related Guides
- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md)
- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)
- [Development Guide](./DEVELOPMENT_GUIDE.md)

View File

@ -1,74 +0,0 @@
# Client Notifications Usage Guide
The client notifications addon owns the notification HUD, notification sound,
and local notification service used by Forge client and server modules.
## Runtime Behavior
The notification display is created during client initialization. The browser
HUD sends:
```text
notifications::ready
```
When that event is received, `NotificationService` initializes and sends a
startup notification.
## Create a Notification
Use the notification service when available:
```sqf
GVAR(NotificationService) call ["create", [
"success",
"Title",
"Notification text.",
4000
]];
```
Arguments:
| Argument | Purpose |
| --- | --- |
| `_type` | Notification type, such as `success`, `info`, `warning`, or `error`. |
| `_title` | Notification title. |
| `_content` | Notification body text. |
| `_duration` | Display duration in milliseconds. |
The service dispatches a browser `forge:notify` custom event.
## CBA Event Surface
Other addons can use the client notification event:
```sqf
["forge_client_notifications_recieveNotification", [
"warning",
"Garage",
"Vehicle spawn position is blocked.",
3000
]] call CBA_fnc_localEvent;
```
The event payload is:
```sqf
[_type, _title, _content, _duration]
```
## Usage Rules
- Use the shared notification service instead of opening separate transient
browser UIs.
- Keep server-driven player feedback short and actionable.
- Treat notification state as transient client UI state.
- Do not use notifications as the only record of durable domain changes.
## Related Guides
- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md)
- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)

View File

@ -1,131 +0,0 @@
# Client Organization Usage Guide
The client organization addon provides the organization portal UI and browser
bridge for login, registration, membership, invites, credit lines, leave and
disband flows, assets, fleet, and treasury display.
Organization registration requires $50,000 in personal funds. The server org
addon enforces and charges the fee; the browser only displays the requirement
and submits the registration request.
## Open Organization UI
```sqf
call forge_client_org_fnc_openUI;
```
The UI opens `RscOrg`, loads `ui/_site/index.html`, and routes browser alerts
through `forge_client_org_fnc_handleUIEvents`.
## Repository and Bridge
`forge_client_org_fnc_initRepository` caches organization portal state.
`forge_client_org_fnc_initUIBridge` owns:
- active browser control tracking
- portal hydrate requests
- create/login response routing
- leave and disband requests
- credit-line assignment requests
- payroll and treasury transfer requests
- invite, accept invite, and decline invite requests
- targeted browser response events
## Browser Events
| Event | Client behavior |
| --- | --- |
| `org::ready` | Mark browser ready and request `org::sync`. |
| `org::login::request` | Request portal hydrate as `org::login::success`. |
| `org::create::request` | Validate org name and request creation on server. |
| `org::disband::request` | Request disband on server. |
| `org::leave::request` | Request leave on server. |
| `org::credit::request` | Request credit-line assignment. |
| `org::payroll::request` | Request payroll payout from the organization treasury. |
| `org::transfer::request` | Request treasury transfer to a member. |
| `org::invite::request` | Request member invite. |
| `org::invite::accept` | Accept invite by org ID. |
| `org::invite::decline` | Decline invite by org ID. |
| `org::close` | Close the display. |
## Browser Response Events
| Event | Purpose |
| --- | --- |
| `org::sync` | Full portal sync payload. |
| `org::login::success` | Login hydrate payload. |
| `org::create::success` | Creation hydrate payload. |
| `org::create::failure` | Creation validation or server failure. |
| `org::disband::success` | Requester disband success. |
| `org::disband::failure` | Disband failure. |
| `org::portal::revoked` | Portal state revoked by someone else's disband action. |
| `org::leave::success` | Leave success. |
| `org::leave::failure` | Leave failure. |
| `org::credit::success` | Credit-line request success. |
| `org::credit::failure` | Credit-line request failure. |
| `org::member::creditUpdated` | Targeted member credit-line patch. |
| `org::invite::success` | Invite success. |
| `org::invite::failure` | Invite failure. |
| `org::invite::decision::success` | Invite accept/decline success. |
| `org::invite::decision::failure` | Invite accept/decline failure. |
## Request Examples
Create organization request payload:
```json
{
"orgName": "Example Logistics"
}
```
Credit-line request payload:
```json
{
"memberUid": "76561198000000000",
"memberName": "Player Name",
"amount": 2500
}
```
Payroll request payload:
```json
{
"amount": 1000
}
```
Treasury transfer request payload:
```json
{
"memberUid": "76561198000000000",
"memberName": "Player Name",
"amount": 1000
}
```
Invite request payload:
```json
{
"targetUid": "76561198000000000",
"targetName": "Player Name"
}
```
## Authoritative State
Organization funds, reputation, membership, invites, credit lines, assets,
fleet, and persistence are server-owned. The client portal only displays and
requests changes.
## Related Guides
- [Organization Usage Guide](./ORG_USAGE_GUIDE.md)
- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)
- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)

View File

@ -1,108 +0,0 @@
# Client Phone Usage Guide
The client phone addon provides the in-game phone UI for contacts, SMS
messages, email, and local utility apps such as notes, calendar events, world
clocks, and alarms.
## Open Phone UI
```sqf
call forge_client_phone_fnc_openUI;
```
The phone UI creates `RscPhone`, loads `ui/_site/index.html`, and routes
browser alerts through `forge_client_phone_fnc_handleUIEvents`.
## State Ownership
Contacts, messages, and emails are server-owned and requested through the
server phone addon.
Local utility app state is stored in `profileNamespace`:
- notes
- calendar events
- world clocks
- alarms
- theme/preferences
## Phone Repository
`forge_client_phone_fnc_initRepository` creates `GVAR(PhoneRepository)`.
The phone repository owns local notes, events, clocks, alarms, and settings
helpers.
Contacts, messages, and emails continue to use server-backed request/response
events.
## Browser Events
### Session and Preferences
| Event | Client behavior |
| --- | --- |
| `phone::get::player` | Send player UID to browser with `setPlayerUid`. |
| `phone::get::theme` | Send saved light/dark theme to browser. |
| `phone::set::theme` | Save theme preference to `profileNamespace`. |
### Contacts
| Event | Client behavior |
| --- | --- |
| `phone::get::contacts` | Load cached contacts and request server refresh. |
| `phone::refresh::contacts` | Request contacts from server. |
| `phone::add::contact` | Add contact by phone number. |
| `phone::add::contact::by::phone` | Add contact by phone number. |
| `phone::add::contact::by::email` | Add contact by email. |
| `phone::remove::contact` | Remove contact by UID. |
### Messages
| Event | Client behavior |
| --- | --- |
| `phone::get::messages` | Request messages from server. |
| `phone::get::message::thread` | Request thread with another UID. |
| `phone::send::message` | Send SMS through server. |
| `phone::mark::message::read` | Mark message read on server. |
| `phone::delete::message` | Delete message on server. |
### Email
| Event | Client behavior |
| --- | --- |
| `phone::get::emails` | Request emails from server. |
| `phone::send::email` | Send email through server. |
| `phone::mark::email::read` | Mark email read on server. |
| `phone::delete::email` | Delete email on server. |
### Local Utility Apps
| Event | Client behavior |
| --- | --- |
| `phone::get::notes` | Load local notes. |
| `phone::save::note` | Save local note. |
| `phone::delete::note` | Delete local note. |
| `phone::get::events` | Load local calendar events. |
| `phone::save::event` | Save local calendar event. |
| `phone::delete::event` | Delete local calendar event. |
| `phone::get::clocks` | Load local world clocks. |
| `phone::save::clock` | Save local world clock. |
| `phone::delete::clock` | Delete local world clock. |
| `phone::get::alarms` | Load local alarms. |
| `phone::save::alarm` | Save local alarm. |
| `phone::delete::alarm` | Delete local alarm. |
| `phone::toggle::alarm` | Toggle local alarm enabled state. |
## Usage Rules
- Send contact, message, and email mutations to the server phone addon.
- Keep local-only utility apps in `profileNamespace` until they are migrated to
server-backed storage.
- Do not treat local phone utility state as shared multiplayer state.
- Validate required UID, phone, email, subject, and message fields before
sending server requests.
## Related Guides
- [Phone Usage Guide](./PHONE_USAGE_GUIDE.md)
- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md)

View File

@ -1,92 +0,0 @@
# Client Store Usage Guide
The client store addon provides the storefront browser UI for catalog browsing,
category hydration, payment source display, cart handling, and checkout
requests.
## Open Store UI
```sqf
call forge_client_store_fnc_openUI;
```
The UI opens `RscStore`, loads `ui/_site/index.html`, and routes browser alerts
through `forge_client_store_fnc_handleUIEvents`.
## Bridge
`forge_client_store_fnc_initUIBridge` owns:
- browser control lookup
- store hydrate requests
- category requests
- checkout requests
- category hydrate/failure responses
- checkout success/failure responses
- store config refresh after successful checkout
Store currently uses its own `StoreUIBridge.receive(...)` browser bridge rather
than the shared `ForgeBridge.receive(...)` delivery used by newer bridges.
## Browser Events
| Event | Client behavior |
| --- | --- |
| `store::ready` | Request store hydrate from the server. |
| `store::category::request` | Request catalog items for a category. |
| `store::checkout::request` | Forward checkout JSON to the server. |
| `store::close` | Close the display. |
## Browser Response Events
| Event | Purpose |
| --- | --- |
| `store::hydrate` | Initial storefront/session/config payload. |
| `store::config::hydrate` | Refreshed payment/source config. |
| `store::category::hydrate` | Category catalog payload. |
| `store::category::failure` | Category request failure. |
| `store::checkout::success` | Checkout success payload. |
| `store::checkout::failure` | Checkout failure payload. |
## Category Requests
Category requests require a non-empty category value.
```json
{
"category": "weapons"
}
```
The client lowercases the category before forwarding it to the server store
addon.
## Checkout Requests
Checkout requests send a serialized checkout payload:
```json
{
"checkoutJson": "{\"items\":[],\"paymentSource\":\"cash\"}"
}
```
The client only forwards the checkout data. The server store addon and
extension validate prices, inventory grants, payment source authorization, and
integration with bank, organization, locker, and garage state.
After a successful checkout, the client asks the server for a fresh store config
payload so payment-source balances and permissions stay current.
## Authoritative State
Catalog data, prices, checkout validation, money movement, organization funds,
credit lines, locker grants, garage grants, and persistence are server-owned.
## Related Guides
- [Store Usage Guide](./STORE_USAGE_GUIDE.md)
- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md)
- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md)
- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)

View File

@ -1,125 +0,0 @@
# Client Usage Guide
Forge Client contains the Arma client-side addons that open player interfaces,
handle browser events, cache client-visible state, and forward authoritative
requests to the server addons.
Use this guide as the entry point for client-side integration. Domain data,
validation, persistence, rewards, ownership, and checkout behavior remain
server-side responsibilities.
## Client Responsibilities
- Open Arma displays and `CT_WEBBROWSER` controls.
- Load browser UI assets from each addon's `ui/_site` folder.
- Receive browser alerts through `JSDialog` handlers.
- Translate browser events into local actions or CBA server events.
- Cache display state in client repositories.
- Push server responses back into browser UIs with `ExecJS`.
- Provide local-only utility state where the feature is intentionally local.
## Authoritative Boundaries
Client repositories are view state. They are useful for rendering, local UI
decisions, and short-lived session behavior, but they should not be treated as
durable state.
Authoritative state lives in:
- server SQF addons for mission and player workflow ownership
- the `forge_server` extension for durable and hot-state domain logic
- SurrealDB where the extension persists durable domain records
## Common Runtime Flow
Most browser-backed client addons follow this shape:
1. The addon creates a display, finds a browser control, and registers a
`JSDialog` event handler.
2. The browser loads an HTML entrypoint from `ui/_site`.
3. The browser sends JSON alerts with an `event` name and `data` payload.
4. `fnc_handleUIEvents.sqf` parses the alert and routes the event.
5. A bridge object or repository sends a CBA server event when server data is
needed.
6. Server responses are caught in `XEH_postInitClient.sqf`.
7. The bridge sends browser update events back through `ExecJS`.
Browser alert payload:
```json
{
"event": "module::action",
"data": {}
}
```
## Open UI Entry Points
| UI | Entry point |
| --- | --- |
| Actor menu | `call forge_client_actor_fnc_openUI;` |
| Bank | `call forge_client_bank_fnc_openUI;` |
| ATM | `[true] call forge_client_bank_fnc_openUI;` |
| CAD | `call forge_client_cad_fnc_openUI;` |
| Garage | `call forge_client_garage_fnc_openUI;` |
| Virtual garage | `call forge_client_garage_fnc_openVG;` |
| Organization portal | `call forge_client_org_fnc_openUI;` |
| Phone | `call forge_client_phone_fnc_openUI;` |
| Store | `call forge_client_store_fnc_openUI;` |
Notifications are normally opened during client initialization and then updated
through the notification event/service.
## Addon Guides
- [Client Main Usage Guide](./CLIENT_MAIN_USAGE_GUIDE.md)
- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)
- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md)
- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md)
- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md)
- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md)
- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md)
- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md)
- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)
## Extension Calls
Client addons should usually call server SQF events, not the `forge_server`
extension directly. The server addon owns validation context and converts the
request into extension commands.
Example:
```sqf
[SRPC(bank,requestDeposit), [getPlayerUID player, 100]] call CFUNC(serverEvent);
```
Direct extension calls from client code bypass server authorization boundaries
and should be avoided.
## Browser Bridge Notes
`forge_client_common_fnc_initWebUIBridge` provides reusable bridge and screen
objects for newer browser UIs. It queues outbound events until a browser screen
is ready, then delivers payloads through:
```sqf
_control ctrlWebBrowserAction ["ExecJS", format ["ForgeBridge.receive(%1)", _json]];
```
Feature addons still own their event names, request payloads, and response
mapping.
## Development Checklist
- Keep feature-specific behavior in the owning addon.
- Send authoritative changes to the server addon.
- Use namespaced browser events such as `bank::deposit::request`.
- Treat `profileNamespace` as local player preference or utility state only.
- Make browser-ready events request the current server state before rendering
stale data.
- Queue or ignore bridge responses when the display is closed.
- Keep mission object setup on the mission/server side and client display logic
on the client side.

View File

@ -1,134 +0,0 @@
# Development Guide
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). 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:
```powershell
cargo fmt --check
cargo check
cargo test
cargo build
cargo clippy --all-targets --all-features -- -D warnings
```
Run this after changing browser UI sources:
```powershell
npm run build:webui
```
Build Arma packages with:
```powershell
.\build-arma.ps1
```
## Module Boundaries
Keep each layer responsible for one kind of work:
| Layer | Owns | Avoid |
| --- | --- | --- |
| `lib/models` | Data structures, serde defaults, validation helpers. | Database calls, SQF-specific context. |
| `lib/repositories` | Repository traits and in-memory stores. | SurrealDB-specific code. |
| `lib/services` | Business rules, workflow orchestration, structured results. | Arma engine calls, extension transport details. |
| `arma/server/extension` | Command parsing, context resolution, SurrealDB implementations, serialization to SQF. | Business rules that belong in services. |
| `arma/server/addons` | Server SQF lifecycle, game-object integration, calls into `forge_server`. | Direct database logic. |
| `arma/client/addons` | Client UI, keybinds, local UI events. | Authoritative persistence. |
## Adding a Domain Module
1. Add the model in `lib/models/src/<module>.rs`.
2. Export the model from `lib/models/src/lib.rs`.
3. Add repository traits in `lib/repositories/src/<module>.rs`.
4. Add in-memory repository support if the service needs tests or hot state.
5. Export the traits from `lib/repositories/src/lib.rs`.
6. Add service logic in `lib/services/src/<module>.rs`.
7. Add focused unit tests for service behavior.
8. Export the service from `lib/services/src/lib.rs`.
9. Add a SurrealDB schema module under `arma/server/extension/src/schema`.
10. Add the concrete storage adapter under `arma/server/extension/src/storage`.
11. Register the storage adapter in `arma/server/extension/src/storage.rs`.
12. Add an extension command group under `arma/server/extension/src/<module>.rs`.
13. Register the command group in `arma/server/extension/src/lib.rs`.
14. Add server addon functions under `arma/server/addons/<module>` if SQF needs a module-level API.
15. Add client addon or browser UI files under `arma/client/addons/<module>` if the module has player-facing UI.
16. Add documentation in `docs/` and module-level READMEs.
## Extension Command Rules
Commands should return one of these forms:
- JSON string for structured results.
- `"true"` or `"false"` for simple existence and boolean operations.
- `"OK"` for successful destructive operations with no response body.
- `"Error: <message>"` for failures.
Prefer stable JSON shapes over ad hoc strings. SQF callers should always check
for the `"Error:"` prefix before parsing JSON.
Example:
```sqf
private _result = "forge_server" callExtension ["actor:get", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Actor request failed: %1", _payload];
};
private _actor = fromJSON _payload;
```
## Persistence Rules
SurrealDB is the durable store. Keep database-specific mapping in the extension
storage adapters, not in services or repository traits.
When changing persisted data:
- Update or add the matching `.surql` schema module.
- Update the concrete storage adapter.
- Preserve existing records when possible through serde defaults or migration
logic.
- Add tests at the service level for behavior, and add storage tests only when
database mapping is the risk.
## Hot-State Rules
Use hot state for data that is read or mutated frequently during a player
session. Hot-state modules usually provide:
- `init` to load durable state into memory.
- `get` to read the runtime copy.
- `override` or focused mutation commands to update the runtime copy.
- `save` to write the runtime copy back to SurrealDB.
- `remove` to evict the runtime copy.
Do not assume hot state is durable until `save` succeeds.
## Web UI Rules
Browser UI source files live under each client addon. Built assets usually land
under that addon's `ui/_site` directory.
Use the existing common bridge in `arma/client/addons/common` when a UI needs
to send events back to SQF. Keep UI state and rendering in JavaScript, and keep
server-authoritative decisions in server SQF or Rust services.
## Documentation Checklist
When adding or changing a module, update:
- `docs/MODULE_REFERENCE.md` for framework-level inventory.
- A module-specific README in the addon directory when SQF or UI usage changes.
- `arma/server/docs/api-reference.md` when extension commands change.
- Existing usage guides when payload shapes or workflows change.

View File

@ -1,95 +0,0 @@
# Economy Usage Guide
The economy server addon owns Arma-world service behavior for fuel, medical,
and repair interactions. It does not own money state. Money mutations go
through extension-backed bank and organization hot state before the world
effect is applied.
## Dependencies
- `forge_server_common` for logging, formatting, and player lookup.
- `forge_server_bank` for personal medical billing.
- `forge_server_org` for organization-funded services and medical fallback
debt.
- `forge_client_actor` and `forge_client_notifications` for targeted client
responses.
## Fuel
Fuel is organization-funded.
When refueling stops, `fnc_initFEconomyStore.sqf` calculates the fuel delta and
cost, charges the player's organization through `OrgStore chargeCheckout`, and
syncs the organization patch to online members. If organization funds cannot
cover the refuel, the vehicle is rolled back to the fuel level it had when the
session started.
Garage UI refuel requests use the server `RefuelService` event. The fuel store
calculates missing fuel from the vehicle config `fuelCapacity`, charges the
player's organization, and fills the vehicle only after the organization charge
succeeds.
## Repair
Repair is organization-funded.
Use the repair service event:
```sqf
[QEGVAR(economy,RepairService), [_target, _unit, _cost]] call CBA_fnc_serverEvent;
```
`_cost` is optional. Passing `-1` uses the configured service repair cost.
The target is only repaired after the organization charge succeeds.
The client garage UI forwards selected nearby vehicle repair requests through
the same event.
## Rearm
Rearm is organization-funded.
Use the rearm service event:
```sqf
[QEGVAR(economy,RearmService), [_target, _unit, _cost]] call CBA_fnc_serverEvent;
```
`_cost` is optional. Passing `-1` uses the configured service rearm cost.
The target is only rearmed after the organization charge succeeds.
`setVehicleAmmo` has global effects, but the ammo is only added to local
turrets, so the service broadcasts the ammo reset after billing succeeds.
The client garage UI forwards selected nearby vehicle rearm requests through
the same event.
## Medical
Medical is player-funded first.
When a heal is requested, `fnc_initMEconomyStore.sqf` uses this billing order:
1. Charge the player's bank balance when it can cover the medical fee.
2. Otherwise charge the player's cash when it can cover the fee.
3. If neither personal balance can cover the fee, charge organization funds.
4. When organization funds cover the fallback charge, record the same amount as
debt on the player's organization credit line.
The heal only completes after one of those charges succeeds. If personal
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.
## Medical Debt Repayment
Medical fallback debt uses the existing organization credit-line repayment
flow. The organization treasury is reduced when the service is rendered, and
the player's credit-line `amount_due` increases by the medical fee. When the
player repays through the bank credit-line repayment action, player bank funds
are moved back into the organization treasury.
## Hot-Cache Boundary
The economy addon should stay server-authoritative for world effects such as
vehicle fuel, vehicle repair, healing, respawn placement, and death inventory
movement. Bank and organization balances should continue to mutate through the
extension-backed hot-cache services.

View File

@ -1,145 +0,0 @@
# Framework Architecture
Forge is organized around domain modules. A domain usually has SQF addon
entry points, Rust models, repository traits, service logic, extension command
handlers, and optional browser UI.
## Runtime Flow
![Architectural Flow Diagram](architecture-flow.svg)
```text
Arma client UI or SQF action
-> client addon bridge
-> server addon function
-> forge_server callExtension command
-> extension command group
-> forge-services domain service
-> forge-repositories trait
-> SurrealDB repository implementation
-> SurrealDB
```
For small payloads, server SQF calls `forge_server` directly through the
extension bridge. For large payloads, `arma/server/addons/extension` stages
request and response chunks through the extension transport module.
## Main Layers
### Client Addons
Client addons live under `arma/client/addons`. They own local player UX,
keybinds, browser UI dialogs, and UI-to-SQF event handling. When a client needs
durable or authoritative state, it routes work to the matching server addon
instead of touching persistence directly.
### Server Addons
Server addons live under `arma/server/addons`. They own server-side SQF
initialization, game-object integration, validation near the Arma runtime, and
calls into the Rust extension. The `extension` addon is the shared bridge for
`callExtension` and transport handling.
### Rust Extension
The server extension lives under `arma/server/extension`. It registers the
`forge_server` command groups, loads configuration, initializes SurrealDB, and
maps SQF command inputs into service calls.
The extension should stay thin:
- Parse and validate command arguments that arrive from SQF.
- Resolve Arma-specific context such as player UID when required.
- Call the matching service.
- Serialize the service result back to JSON or a simple string.
### Shared Rust Crates
The `lib` workspace contains reusable Rust crates:
- `forge-models`: shared domain structs and serialization rules.
- `forge-repositories`: storage-agnostic repository traits and in-memory
implementations used by tests and hot-state services.
- `forge-services`: domain behavior, validation, and mutation workflows.
- `forge-shared`: cross-crate helpers.
### Persistence
Durable storage is SurrealDB. Schema modules live under
`arma/server/extension/src/schema`, and concrete SurrealDB repository
implementations live under `arma/server/extension/src/storage`.
Repository traits stay in `lib/repositories` so service logic remains testable
without a database.
## Hot State
Several domains have `hot` command groups. Hot state keeps a runtime copy of
frequently accessed data in memory, then saves it back to durable storage when
requested. This is useful for player state that changes often during a session.
Typical hot-state flow:
```text
actor:hot:init
actor:hot:get
actor:hot:override
actor:hot:save
actor:hot:remove
```
Use hot state for session workflows. Use normal domain commands for direct
durable CRUD operations.
## Transport Layer
The transport layer exists because Arma extension calls have practical payload
size limits. It provides chunked request and response handling while still
routing to the same domain command groups.
Common direct command:
```sqf
"forge_server" callExtension ["status", []];
```
Common transport path:
```text
server addon fnc_extCall
-> transport:request:append
-> transport:invoke_stored
-> transport:response:get
```
## Configuration
The server extension reads `config.toml` next to the extension DLL. The current
persistence section is:
```toml
[surreal]
endpoint = "127.0.0.1:8000"
namespace = "forge"
database = "main"
username = "root"
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).
Check persistence readiness before issuing commands that require storage:
```sqf
"forge_server" callExtension ["status", []];
"forge_server" callExtension ["surreal:status", []];
```

View File

@ -1,212 +0,0 @@
# Garage Usage Guide
The garage module stores physical player vehicles. Each record keeps the
vehicle classname, generated plate UUID, fuel, overall damage, and detailed hit
point damage.
## Storage Model
Garage data is persisted through SurrealDB by the server extension.
```json
{
"plate-uuid": {
"plate": "plate-uuid",
"classname": "B_Quadbike_01_F",
"fuel": 1.0,
"damage": 0.0,
"hit_points": {
"names": ["hitengine"],
"selections": ["engine_hitpoint"],
"values": [0.0]
}
}
}
```
Rules validated by the Rust service:
- A player garage can contain up to 5 vehicles.
- `garage:add` generates a UUID plate automatically.
- `fuel`, `damage`, and every hit point value must be between `0.0` and `1.0`.
- `hit_points.names`, `hit_points.selections`, and `hit_points.values` must have
the same length.
- `garage:get`, `garage:patch`, and `garage:remove` require an existing garage.
- `garage:add` creates an empty garage automatically when one does not exist.
## Commands
All commands are called on the `garage` group.
| Command | Arguments | Returns |
| --- | --- | --- |
| `garage:create` | `uid` | Empty vehicle map as JSON. |
| `garage:get` | `uid` | Vehicle map as JSON. |
| `garage:add` | `uid`, `vehicle_json` | Updated vehicle map as JSON. |
| `garage:update` | `uid`, `vehicles_json` | Replaced vehicle map as JSON. |
| `garage:patch` | `uid`, `patch_json` | Updated vehicle map as JSON. |
| `garage:remove` | `uid`, `remove_json` | Updated vehicle map as JSON. |
| `garage:delete` | `uid` | `OK`. |
| `garage:exists` | `uid` | `true` or `false`. |
## Error Handling
Every command returns a string payload. Always check for the `Error:` prefix
before parsing JSON.
```sqf
private _result = "forge_server" callExtension ["garage:get", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Garage error: %1", _payload];
};
private _garage = fromJSON _payload;
```
## Add a Vehicle
`garage:add` requires `classname`, `fuel`, `damage`, and `hit_points`.
```sqf
private _hitPointData = getAllHitPointsDamage _vehicle;
private _hitPoints = createHashMapFromArray [
["names", _hitPointData select 0],
["selections", _hitPointData select 1],
["values", _hitPointData select 2]
];
private _vehicleData = createHashMapFromArray [
["classname", typeOf _vehicle],
["fuel", fuel _vehicle],
["damage", damage _vehicle],
["hit_points", _hitPoints]
];
private _result = "forge_server" callExtension ["garage:add", [
getPlayerUID player,
toJSON _vehicleData
]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
hint format ["Failed to store vehicle: %1", _payload];
};
private _garage = fromJSON _payload;
```
The returned value is a hash map keyed by generated plate. To find the newly
stored vehicle, compare returned keys before and after the add, or search by
classname if your workflow guarantees a unique pending vehicle.
```sqf
private _storedPlate = "";
{
private _vehicleRecord = _garage get _x;
if ((_vehicleRecord get "classname") == typeOf _vehicle) then {
_storedPlate = _x;
};
} forEach keys _garage;
```
## Patch a Vehicle
`garage:patch` updates selected fields for one plate. The `plate` field is
required. `fuel`, `damage`, and `hit_points` are optional.
```sqf
private _patch = createHashMapFromArray [
["plate", _vehicle getVariable ["forge_garage_plate", ""]],
["fuel", fuel _vehicle],
["damage", damage _vehicle]
];
private _result = "forge_server" callExtension ["garage:patch", [
getPlayerUID player,
toJSON _patch
]];
```
## Remove a Vehicle
`garage:remove` expects JSON with a `plate` field.
```sqf
private _remove = createHashMapFromArray [
["plate", _plate]
];
private _result = "forge_server" callExtension ["garage:remove", [
getPlayerUID player,
toJSON _remove
]];
```
## Spawn a Stored Vehicle
```sqf
fnc_spawnGarageVehicle = {
params ["_plate"];
private _result = "forge_server" callExtension ["garage:get", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
hint format ["Failed to load garage: %1", _payload];
objNull
};
private _garage = fromJSON _payload;
private _vehicleData = _garage getOrDefault [_plate, createHashMap];
if (_vehicleData isEqualTo createHashMap) exitWith {
hint "Vehicle plate was not found in your garage.";
objNull
};
private _vehicle = (_vehicleData get "classname") createVehicle (player getPos [10, getDir player]);
_vehicle setFuel (_vehicleData getOrDefault ["fuel", 1]);
_vehicle setDamage (_vehicleData getOrDefault ["damage", 0]);
_vehicle setVariable ["forge_garage_plate", _plate, true];
private _hitPoints = _vehicleData getOrDefault ["hit_points", createHashMap];
private _names = _hitPoints getOrDefault ["names", []];
private _values = _hitPoints getOrDefault ["values", []];
{
_vehicle setHitPointDamage [_x, _values select _forEachIndex];
} forEach _names;
private _remove = createHashMapFromArray [["plate", _plate]];
"forge_server" callExtension ["garage:remove", [getPlayerUID player, toJSON _remove]];
_vehicle
};
```
## Hot State
The `garage:hot:*` commands keep a runtime copy of a player's garage and write
it back only when `garage:hot:save` runs.
| Command | Arguments | Returns |
| --- | --- | --- |
| `garage:hot:init` | `uid` | Vehicle map as JSON. |
| `garage:hot:get` | `uid` | Vehicle map as JSON. |
| `garage:hot:override` | `uid`, `vehicles_json` | Vehicle map as JSON. |
| `garage:hot:add` | `uid`, `vehicle_json` | Vehicle map as JSON. |
| `garage:hot:remove_vehicle` | `uid`, `remove_json` | Vehicle map as JSON. |
| `garage:hot:save` | `uid` | Current hot vehicle map as JSON. |
| `garage:hot:remove` | `uid` | `OK`. |
Use hot state for session-heavy vehicle workflows. Use the durable commands for
simple store/retrieve operations.
## Best Practices
- Store the generated plate on spawned vehicles with `setVariable`.
- Use `garage:patch` for frequent fuel and damage syncs.
- Use `garage:update` only when replacing the whole vehicle map intentionally.
- Do not delete the world vehicle until `garage:add` succeeds.
- Treat vehicle maps as hash maps keyed by plate, not arrays.

View File

@ -1,195 +0,0 @@
# ICOM Usage Guide
ICOM is the Forge inter-server communication helper. It lets multiple Arma 3
servers exchange generic JSON events through a central TCP hub instead of
connecting directly to each other.
## Runtime Shape
```text
Arma server SQF
-> forge_server extension icom:* command
-> ICOM client inside the extension
-> forge-icom TCP hub
-> target server extension
-> forge_icom_event CBA server event
```
The ICOM hub lives in `bin/icom`. The Arma server extension integrates with it
through `arma/server/extension/src/icom.rs`.
## Components
| Component | Path | Role |
| --- | --- | --- |
| 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. |
| 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. |
## Build and Run the Hub
Build the release binary:
```powershell
cargo build --release -p forge-icom
```
Run it during development:
```powershell
cargo run -p forge-icom
```
The default bind address is `0.0.0.0:9090`.
## Hub Configuration
Copy `bin/icom/config.example.toml` to `config.toml` beside the `forge-icom`
executable or into the working directory used to launch it.
```toml
[server]
host = "0.0.0.0"
port = 9090
```
Use `127.0.0.1` for same-machine testing. Use `0.0.0.0` when remote Arma
servers need to connect, and secure the port at the firewall or host network
layer.
## Extension Commands
ICOM commands are exposed through the `icom` command group in `forge_server`.
| Command | Arguments | Returns |
| --- | --- | --- |
| `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:broadcast` | `event_name`, `data_json` | `OK` or `ERROR: <reason>`. |
The current extension connects when `icom:connect` is called. Start the ICOM hub
first, then connect each Arma server with a unique `server_id`.
```sqf
private _result = "forge_server" callExtension [
"icom:connect",
["127.0.0.1:9090", "server_1"]
];
diag_log format ["[ICOM] Connect result: %1", _result select 0];
```
## Send an Event
Send a targeted event to one connected server:
```sqf
private _data = createHashMapFromArray [
["coords", [1234, 5678, 0]],
["supplies", ["ammo_box", "medical_supplies"]]
];
"forge_server" callExtension [
"icom:send_event",
["server_2", "supply_drop", toJSON _data]
];
```
Broadcast to every connected server except the sender:
```sqf
private _alert = createHashMapFromArray [
["message", "Server restart in 5 minutes"],
["severity", "warning"]
];
"forge_server" callExtension [
"icom:broadcast",
["global_alert", toJSON _alert]
];
```
## Receive Events
Inbound ICOM events are forwarded to SQF as the CBA server event
`forge_icom_event`.
```sqf
["forge_icom_event", {
params ["_eventName", "_data"];
switch (_eventName) do {
case "supply_drop": {
private _coords = _data getOrDefault ["coords", []];
private _supplies = _data getOrDefault ["supplies", []];
diag_log format ["[ICOM] Supply drop at %1: %2", _coords, _supplies];
};
case "global_alert": {
private _message = _data getOrDefault ["message", ""];
if (_message isNotEqualTo "") then {
[_message] remoteExec ["hint", 0];
};
};
default {
diag_log format ["[ICOM] Unhandled event: %1 | %2", _eventName, _data];
};
};
}] call CBA_fnc_addEventHandler;
```
## Message Protocol
The hub uses newline-delimited JSON. The first message from each client is a
registration payload:
```json
{
"type": "register",
"server_id": "server_1"
}
```
Targeted events use `type: "event"`:
```json
{
"type": "event",
"target_server": "server_2",
"event_name": "supply_drop",
"data": {
"coords": [1234, 5678, 0]
}
}
```
Broadcasts use `type: "broadcast"` and are routed to all connected servers
except the sender.
## Operational Notes
- Server IDs must be unique. If the same ID reconnects, the hub replaces the old
connection.
- Event names are mission/application contracts. ICOM only routes them; it does
not validate gameplay meaning.
- Always send valid JSON in the `data_json` argument.
- `icom:send_event` and `icom:broadcast` return quickly after scheduling async
work in the extension. Check extension and ICOM hub logs for delivery errors.
- Keep event payloads small and stable. Use IDs or compact data where possible.
## Testing
Start the hub:
```powershell
cargo run -p forge-icom
```
Run example clients in separate terminals:
```powershell
cargo run -p forge-icom --example server_1_client
cargo run -p forge-icom --example server_2_client
```
For Arma testing, start the hub, connect the server with `icom:connect`, register
a `forge_icom_event` handler, then send an event from another connected server.

View File

@ -1,203 +0,0 @@
# Locker Usage Guide
The locker module stores physical player inventory items by classname. It is
separate from the virtual arsenal unlock module documented in
[Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md).
## Storage Model
Locker data is persisted through SurrealDB by the server extension.
```json
{
"arifle_MX_F": {
"category": "weapon",
"classname": "arifle_MX_F",
"amount": 1
}
}
```
Rules validated by the Rust service:
- A locker can contain up to 25 unique classnames.
- `category` and `classname` cannot be empty.
- `amount` must be greater than `0`.
- `locker:add` creates an empty locker automatically when one does not exist.
- `locker:get`, `locker:patch`, and `locker:remove` require an existing locker.
- `locker:remove` takes the classname directly, not a JSON object.
## Commands
All commands are called on the `locker` group.
| Command | Arguments | Returns |
| --- | --- | --- |
| `locker:create` | `uid` | Empty item map as JSON. |
| `locker:get` | `uid` | Item map as JSON. |
| `locker:add` | `uid`, `item_json` | Updated item map as JSON. |
| `locker:update` | `uid`, `items_json` | Replaced item map as JSON. |
| `locker:patch` | `uid`, `patch_json` | Updated item map as JSON. |
| `locker:remove` | `uid`, `classname` | Updated item map as JSON. |
| `locker:delete` | `uid` | `OK`. |
| `locker:exists` | `uid` | `true` or `false`. |
## Error Handling
Every command returns a string payload. Always check for the `Error:` prefix
before parsing JSON.
```sqf
private _result = "forge_server" callExtension ["locker:get", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Locker error: %1", _payload];
};
private _locker = fromJSON _payload;
```
## Add an Item
`locker:add` creates or overwrites one classname entry.
```sqf
private _item = createHashMapFromArray [
["category", "weapon"],
["classname", "arifle_MX_F"],
["amount", 1]
];
private _result = "forge_server" callExtension ["locker:add", [
getPlayerUID player,
toJSON _item
]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
hint format ["Failed to store item: %1", _payload];
};
private _locker = fromJSON _payload;
```
## Patch an Amount
`locker:patch` currently patches the `amount` field for an existing classname.
```sqf
private _patch = createHashMapFromArray [
["classname", "arifle_MX_F"],
["amount", 5]
];
private _result = "forge_server" callExtension ["locker:patch", [
getPlayerUID player,
toJSON _patch
]];
```
## Remove an Item
`locker:remove` takes the classname as the second argument.
```sqf
private _result = "forge_server" callExtension ["locker:remove", [
getPlayerUID player,
"arifle_MX_F"
]];
```
## Retrieve an Item
```sqf
fnc_retrieveLockerItem = {
params ["_classname"];
private _result = "forge_server" callExtension ["locker:get", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
hint format ["Failed to load locker: %1", _payload];
false
};
private _locker = fromJSON _payload;
private _item = _locker getOrDefault [_classname, createHashMap];
if (_item isEqualTo createHashMap) exitWith {
hint "Item was not found in your locker.";
false
};
private _amount = _item getOrDefault ["amount", 0];
if (_amount <= 0) exitWith {
hint "Item is out of stock.";
false
};
if !(player canAdd _classname) exitWith {
hint "Not enough inventory space.";
false
};
player addItem _classname;
if (_amount > 1) then {
private _patch = createHashMapFromArray [
["classname", _classname],
["amount", _amount - 1]
];
"forge_server" callExtension ["locker:patch", [getPlayerUID player, toJSON _patch]];
} else {
"forge_server" callExtension ["locker:remove", [getPlayerUID player, _classname]];
};
true
};
```
## Replace the Whole Locker
`locker:update` replaces the whole item map. Use it for explicit bulk syncs,
not single-item changes.
```sqf
private _items = createHashMapFromArray [
["arifle_MX_F", createHashMapFromArray [
["category", "weapon"],
["classname", "arifle_MX_F"],
["amount", 1]
]]
];
private _result = "forge_server" callExtension ["locker:update", [
getPlayerUID player,
toJSON _items
]];
```
## Hot State
The `locker:hot:*` commands keep a runtime copy of a player's locker and write
it back only when `locker:hot:save` runs.
| Command | Arguments | Returns |
| --- | --- | --- |
| `locker:hot:init` | `uid` | Item map as JSON. |
| `locker:hot:get` | `uid` | Item map as JSON. |
| `locker:hot:override` | `uid`, `items_json` | Item map as JSON. |
| `locker:hot:save` | `uid` | Current hot item map as JSON. |
| `locker:hot:remove` | `uid` | `OK`. |
Use hot state for session-heavy locker workflows. Use the durable commands for
simple item deposits and withdrawals.
## Best Practices
- Keep categories normalized, for example `weapon`, `magazine`, `item`, or
`backpack`.
- Use `locker:patch` for quantity changes.
- Use `locker:remove` when quantity reaches zero.
- Treat the locker response as a hash map keyed by classname.
- Check capacity before bulk operations that may exceed 25 unique items.

View File

@ -1,750 +0,0 @@
# Mission Designer Guide
Build playable Forge missions in Eden with the required interaction objects,
garage markers, and CAD-compatible task modules.
This guide focuses on editor placement and mission validation. Framework
internals, extension commands, and persistence details are covered in the
developer-oriented module guides.
## Core Rule
Most Forge systems become available to players through nearby Eden objects.
Place the object, give it the correct variable name in Eden, and the server
initializer marks it with the runtime variable the actor menu scans for.
Players must be within 5 meters of the object for the actor menu to offer the
action.
## Interaction Objects
Use the object's Eden variable name, not its display name. The matching is
case-sensitive in some initializers, so use lower-case names.
![Bank object variable name field](images/eden/bank_obj_var.jpg)
| System | Eden Object Variable Name | Runtime Variable | Player Action | Notes |
| --- | --- | --- | --- | --- |
| Bank | name contains `bank` | `isBank = true` | Full bank UI | Allows full banking workflows, including PIN changes. |
| ATM | name contains `atm` | `isAtm = true` | ATM bank UI | ATM mode requires PIN authorization and does not allow PIN changes. |
| Store | name contains `store` | `isStore = true` | Store UI | Store catalog and checkout behavior are configured server-side. |
| Garage | name contains `garage` | `isGarage = true` | Garage UI and virtual garage | Include a garage category in the name or set `garageType` manually. |
| Locker | name contains `locker` | local `isLocker = true` | Virtual arsenal action | The server hides the editor object; each client creates a local locker at the same position. |
Recommended object names:
```text
atm
bank
store
locker
garage_hq
garage_hq_2
```
The example mission uses short lower-case names. Keep single-use objects simple,
add an index when there may be multiple copies, and include a site label for
garage objects so related spawn markers can share the same prefix.
Avoid using `forge_locker_box` as an editor-placed locker variable name. That
name is reserved by the client-side virtual arsenal box.
## Manual Object Variables
The automatic initializers are the normal path. If a mission script creates
interaction objects dynamically, set the same variables manually:
```sqf
_bankLaptop setVariable ["isBank", true, true];
_atmTerminal setVariable ["isAtm", true, true];
_storeCounter setVariable ["isStore", true, true];
_garageTerminal setVariable ["isGarage", true, true];
_garageTerminal setVariable ["garageType", "cars", true];
```
Supported garage types are:
- `cars`
- `armor`
- `helis`
- `planes`
- `naval`
- `other`
## Garage Markers
Garage interaction objects open the garage UI. Vehicle spawn positions come
from Eden markers.
![Garage object placement](images/eden/garage_obj.jpg)
![Garage object variable name](images/eden/garage_obj_var.jpg)
![Garage category spawn markers](images/eden/garage_spawn_mrkrs.jpg)
![Garage spawn marker variable name](images/eden/garage_spawn_1_mrkr_var.jpg)
Additional garage sites use the same pattern: place another garage interaction
object, give it a `garage` variable name that identifies the site, then place
matching category spawn markers near that garage.
![Second garage object placement](images/eden/garage_obj_2.jpg)
![Second garage object variable name](images/eden/garage_obj_2_var.jpg)
![Second garage site spawn markers](images/eden/garage_spawn_2_mrkrs.jpg)
Create empty markers near each garage site. Marker names must contain `garage`
and one supported garage category:
```text
garage_hq_cars
garage_hq_armor
garage_hq_helis
garage_hq_helis_1
garage_hq_planes
garage_hq_naval
garage_hq_other
```
This convention keeps the site and category visible in the marker name:
`garage_hq_planes` is the planes spawn marker for `garage_hq`, while
`garage_hq_2` can use another nearby set of `garage_hq_*` category markers for
the second HQ garage area. If two garage objects of the same category are close
to each other, include the full object name in the marker prefix, such as
`garage_hq_2_planes`.
Use these rules:
1. Put the marker where the vehicle should spawn.
2. Rotate the marker to control spawn heading.
3. Keep the marker close to the matching garage object.
4. Include the garage object's variable name when multiple garages exist at
different sites.
5. Do not allow parked vehicles to block the marker. If a vehicle is within 5
meters of the spawn position, the virtual garage blocks the session.
Vehicle spawning is strict by category. A garage without a matching category
marker cannot spawn that vehicle category.
## Store Setup
Store objects only unlock the store UI. The actual item catalog, prices,
payment source handling, locker grants, and garage unlocks are server-owned.
![Store object placement](images/eden/store_obj.jpg)
![Store object variable name](images/eden/store_obj_var.jpg)
Minimum Eden setup:
1. Place a terminal, table, NPC, or other object players can stand near.
2. Set its Eden variable name to something containing `store`.
3. Test that the actor menu shows the store action within 5 meters.
## Bank and ATM Setup
Bank and ATM objects intentionally expose different workflows.
![Bank object placement](images/eden/bank_obj.jpg)
![Bank object variable name](images/eden/bank_obj_var.jpg)
![ATM object placement](images/eden/atm_obj.jpg)
![ATM object variable name](images/eden/atm_obj_var.jpg)
Use a `bank` object for the full bank interface:
- account view
- transfers
- earnings deposit
- PIN change
Use an `atm` object for ATM access:
- PIN-gated account access
- ATM-mode banking actions
- no PIN change
Minimum Eden setup:
1. Place one or more bank laptops or terminals with variable names containing
`bank`.
2. Place one or more ATM objects with variable names containing `atm`.
3. Keep the object accessible so players can stand within 5 meters.
## Locker Setup
Locker objects are slightly different from other interaction objects. The
server finds editor-placed objects whose variable names contain `locker`, hides
those global objects, and each client creates a local locker object at the same
position.
![Locker object placement](images/eden/locker_obj.jpg)
![Locker object variable name](images/eden/locker_obj_var.jpg)
Minimum Eden setup:
1. Place a container object where the locker should appear.
2. Set its Eden variable name to something containing `locker`.
3. Do not use `forge_locker_box`.
4. Test that the local locker appears and opens the virtual arsenal action.
## Medical Spawn Setup
The medical economy store discovers up to eleven medical spawn objects by exact
mission namespace variable name:
- `med_spawn`
- `med_spawn_1`
- `med_spawn_2`
- continuing through `med_spawn_10`
These objects are used for medical respawn placement and occupancy checks.
![Medical spawn object placement](images/eden/med_spawn_obj.jpg)
![Medical spawn object variable name](images/eden/med_spawn_obj_var.jpg)
Minimum Eden setup:
1. Place an object at each medical respawn position.
2. Set the first object's Eden variable name to `med_spawn`.
3. Set additional medical spawns to `med_spawn_1`, `med_spawn_2`, and so on.
4. Keep each spawn position clear enough for a revived player to occupy.
## CAD Access
The CAD UI is currently opened from the actor menu action path, but there is no
server initializer that marks Eden objects as dedicated CAD terminals. If a
mission needs a CAD terminal object, wire it through mission script or a custom
interaction that calls:
```sqf
[] spawn forge_client_cad_fnc_openUI;
```
Tasks show in CAD only when they are created through a CAD-compatible task
creation path.
## CEO and Dispatch Slots
Forge grants dispatch-board permissions from the player's Eden unit variable
name when that player belongs to the default organization.
Use these exact lower-case variable names:
| Slot | Eden Unit Variable Name | Permissions |
| --- | --- | --- |
| CEO | `ceo` | Can administer the default organization, use default organization funds where supported, and use the CAD dispatch board. |
| Dispatch | `dispatch` | Can use the CAD dispatch board. |
![CEO unit placement](images/eden/ceo_unit.jpg)
![CEO unit variable name](images/eden/ceo_unit_var.jpg)
![Dispatch unit placement](images/eden/dispatch_unit.jpg)
![Dispatch unit variable name](images/eden/dispatch_unit_var.jpg)
The CEO slot is intentionally broader than the dispatch slot. Use it for the
player who should administrate the default organization. Use the dispatch slot
for players who need dispatcher tools without default organization
administration rights.
## Task and CAD Setup
Mission designers should use Forge Eden task modules for CAD-visible work.
Those modules delegate to `forge_task_fnc_startTask`, which creates the
BIS task, registers the Forge task catalog entry, sets active task state, and
dispatches the task handler.
Use the Arma 3 `Create Task` module when you need a standard BIS map task
alongside Forge task handling. Use Forge task modules for CAD-visible task
contracts and runtime task logic.
![Arma 3 Create Task module placement](images/eden/create_task_mod.jpg)
![Arma 3 Create Task module parameters](images/eden/create_task_mod_params.jpg)
![Attack task module placement](images/eden/attack_task_mod.jpg)
![Attack task module parameters](images/eden/attack_task_mod_params.jpg)
![Attack task target sync](images/eden/attack_task_tgts.jpg)
![CAD visible task](images/eden/cad-visible-task.jpg)
CAD-compatible task creation paths:
| Path | CAD Compatible | Use When |
| --- | --- | --- |
| Forge Eden task modules | Yes | Normal mission-designer workflow. |
| `forge_task_fnc_startTask` | Yes | Scripted or generated mission content. |
| Dynamic mission manager attack tasks | Yes | Server-generated attack missions. |
| `forge_task_fnc_handler` directly | Only if catalog and BIS task already exist | Advanced scripted flows. |
| Direct task function calls | No by default | Custom server-owned flows that do not need CAD assignment. |
General task rules:
1. Give every task a unique `TaskID`.
2. Set success and fail limits explicitly.
3. Use area markers for zone fields.
4. Use Forge grouping modules where required.
5. Sync task modules to real world objects, units, vehicles, or grouping
modules.
6. To chain tasks, set `Prerequisite Task IDs` on the dependent task module to
a comma-separated list of task IDs that must succeed first.
7. Reward class fields use comma-separated class names without brackets, such
as `ItemGPS, FirstAidKit`. Existing SQF array strings such as
`["ItemGPS","FirstAidKit"]` still work for older missions.
8. Test that unchained tasks appear in CAD immediately and chained tasks appear
only after their prerequisite tasks succeed.
Task chaining uses only task IDs. The dependent task is still registered during
mission setup, but it stays hidden from CAD, cannot be assigned, and does not
start its task logic until every prerequisite task has completed successfully.
If any prerequisite task fails or never completes, the dependent task remains
locked.
Zone fields that must reference area markers:
![Task marker fields](images/eden/create_task_mod_params.jpg)
| Field | Used By | Marker Requirement |
| --- | --- | --- |
| `DefenseZone` | Defend Task | Rectangle or ellipse area marker. |
| `DeliveryZone` | Delivery Task | Rectangle or ellipse area marker. |
| `ExtZone` | Hostage and HVT capture tasks | Rectangle or ellipse area marker. |
| `CBRNZone` | Hostage CBRN variant | Rectangle or ellipse area marker. |
## Task Module Setup Guides
Use these task sections as the setup guide and capture plan. Save any new
screenshots under `docus/public/images/eden/` with the listed filenames.
### Attack Task
Use `FORGE_Module_Attack` when players need to eliminate hostile units or
vehicles.
Existing screenshots:
- `attack_task_mod.jpg` - Attack task module placement.
- `attack_task_mod_params.jpg` - Attack task module attributes.
- `attack_task_tgts.jpg` - Attack task synced to target units or vehicles.
Setup:
1. Place the enemy units or vehicles.
2. Place `FORGE_Module_Attack`.
3. Set a unique `TaskID`.
4. Set `LimitSuccess` to the number of targets that must be killed.
5. Set `LimitFail` if the mission should fail after too many losses.
6. Set reward funds, rating gain/loss, end-state behavior, and optional
`TimeLimit`.
7. Set `Prerequisite Task IDs` only if this attack task should unlock after
other tasks succeed.
8. Sync the attack module directly to the target units or vehicles.
Validation:
- The task appears in CAD after creation.
- Killing the configured number of targets succeeds the task.
- `TimeLimit` uses seconds; `0` disables the timer.
### Destroy Task
Use `FORGE_Module_Destroy` when players must destroy objects, vehicles, or
units.
![Destroy task module placement](images/eden/destroy_task_mod.jpg)
![Destroy task module parameters](images/eden/destroy_task_mod_params.jpg)
![Destroy task target sync](images/eden/destroy_task_tgts.jpg)
Setup:
1. Place the objects, vehicles, or units that must be destroyed.
2. Place `FORGE_Module_Destroy`.
3. Set a unique `TaskID`.
4. Set `LimitSuccess` to the number of targets that must be destroyed.
5. Set `LimitFail` if the mission should fail after too many protected losses
or failed conditions.
6. Set reward funds, rating gain/loss, end-state behavior, and optional
`TimeLimit`.
7. Set `Prerequisite Task IDs` only if this destroy task should unlock after
other tasks succeed.
8. Sync the destroy module directly to the targets.
Validation:
- The module reads direct syncs only.
- Destroying the configured number of targets succeeds the task.
- `TimeLimit` uses seconds; `0` disables the timer.
### Defuse Task
Use `FORGE_Module_Defuse` when players must defuse explosives while optionally
protecting other entities.
![Defuse task module placement](images/eden/defuse_task_mod.jpg)
![Defuse task module parameters](images/eden/defuse_task_mod_params.jpg)
![Explosive Entities grouping module](images/eden/defuse_explosives_mod.jpg)
![Protected Entities grouping module](images/eden/defuse_protected_mod.jpg)
The Defuse task screenshots show both module placement and the required sync
layout.
Required module layout:
```text
[Defuse Task] --> [Explosive Entities] --> explosive objects
[Defuse Task] --> [Protected Entities] --> protected objects, vehicles, or units
```
Setup:
1. Place the explosive objects that players must defuse.
2. Place `FORGE_Module_Explosives`.
3. Sync each explosive object to `FORGE_Module_Explosives`.
4. Place any objects, vehicles, or units that must survive.
5. Place `FORGE_Module_Protected` when protected entities are part of the task.
6. Sync each protected entity to `FORGE_Module_Protected`.
7. Place `FORGE_Module_Defuse`.
8. Set a unique `TaskID`.
9. Set `LimitSuccess` to the number of explosives that must be defused.
10. Set `LimitFail` to the number of protected entities that can be lost before
failure.
11. Set `TimeLimit` to the IED countdown in seconds.
12. Set reward funds, rating gain/loss, and end-state behavior.
13. Set `Prerequisite Task IDs` only if this defuse task should unlock after
other tasks succeed.
14. Sync `FORGE_Module_Defuse` to `FORGE_Module_Explosives`.
15. Sync `FORGE_Module_Defuse` to `FORGE_Module_Protected` if used.
Validation:
- The defuse task reads grouped entities, not direct object syncs.
- The ACE defuse event resolves the correct IED for the task.
- Defuse `TimeLimit` is the IED countdown and should be greater than `0`.
### Delivery Task
Use `FORGE_Module_Delivery` when players must move cargo objects into a
delivery zone.
![Delivery task module placement](images/eden/delivery_task_mod.jpg)
![Delivery task module parameters](images/eden/delivery_task_mod_params.jpg)
![Cargo Entities grouping module](images/eden/delivery_cargo_mod.jpg)
![Delivery area marker placement](images/eden/delivery_zone_mrkr.jpg)
![Delivery marker name](images/eden/delivery_zone_mrkr_var.jpg)
The Delivery task screenshots show both module placement and the required sync
layout.
Required module layout:
```text
[Delivery Task] --> [Cargo Entities] --> cargo objects
```
Setup:
1. Place the cargo objects.
2. Create a rectangle or ellipse area marker for the delivery zone.
3. Place `FORGE_Module_Cargo`.
4. Sync each cargo object to `FORGE_Module_Cargo`.
5. Place `FORGE_Module_Delivery`.
6. Set a unique `TaskID`.
7. Set `DeliveryZone` to the delivery marker name.
8. Set `LimitSuccess` to the number of cargo objects that must arrive.
9. Set `LimitFail` to the number of cargo objects that can be damaged past the
fail threshold.
10. Set reward funds, rating gain/loss, end-state behavior, and optional
`TimeLimit`.
11. Set `Prerequisite Task IDs` only if this delivery task should unlock after
other tasks succeed.
12. Sync `FORGE_Module_Delivery` to `FORGE_Module_Cargo`.
Validation:
- `DeliveryZone` must be an area marker, not an icon marker.
- The runtime checks cargo with `inArea DeliveryZone`.
- The task succeeds only after the configured cargo count reaches the zone.
### Hostage Task
Use `FORGE_Module_Hostage` when players must rescue hostage units and move them
to an extraction zone.
![Hostage task module placement](images/eden/hostage_task_mod.jpg)
![Hostage task module parameters](images/eden/hostage_task_mod_params.jpg)
![Hostage Entities grouping module](images/eden/hostage_entities_mod.jpg)
![Shooter Entities grouping module](images/eden/hostage_shooters_mod.jpg)
![Hostage extraction area marker placement](images/eden/hostage_ext_zone_mrkr.jpg)
![Hostage extraction marker name](images/eden/hostage_ext_zone_mrkr_var.jpg)
The Hostage task screenshots show both module placement and the required sync
layout.
Required module layout:
```text
[Hostage Task] --> [Hostage Entities] --> hostage units
[Hostage Task] --> [Shooter Entities] --> hostile shooter units
```
Setup:
1. Place the hostage AI units.
2. Place the hostile shooter AI units.
3. Create a rectangle or ellipse area marker for the extraction zone.
4. If using the CBRN variant, create a rectangle or ellipse area marker for
`CBRNZone`.
5. Place `FORGE_Module_Hostages`.
6. Sync the hostage units to `FORGE_Module_Hostages`.
7. Place `FORGE_Module_Shooters`.
8. Sync the shooter units to `FORGE_Module_Shooters`.
9. Place `FORGE_Module_Hostage`.
10. Set a unique `TaskID`.
11. Set `ExtZone` to the extraction marker name.
12. Set `LimitSuccess` to the number of hostages that must be rescued.
13. Set `LimitFail` to the number of hostages that can be lost before failure.
14. Enable `CBRN Attack` or `Execution` when that mission variant is needed.
15. If `CBRN Attack` is enabled, set `CBRNZone`.
16. Set reward funds, rating gain/loss, end-state behavior, and optional
`TimeLimit`.
17. Set `Prerequisite Task IDs` only if this hostage task should unlock after
other tasks succeed.
18. Sync `FORGE_Module_Hostage` to `FORGE_Module_Hostages`.
19. Sync `FORGE_Module_Hostage` to `FORGE_Module_Shooters`.
Validation:
- `ExtZone` and `CBRNZone` must be area markers.
- Hostage and shooter grouping modules should sync to real units only.
- The hostage timer waits until the assigned group leader acknowledges the
task.
### HVT Task
Use `FORGE_Module_HVT` when players must capture or eliminate high-value target
units. The `HVT Task` example below shows an elimination task. The `HVT Task 1`
example shows a capture/extract task.
Eliminate HVT example:
![HVT eliminate task module placement](images/eden/hvt_task_mod.jpg)
![HVT eliminate task module parameters](images/eden/hvt_task_mod_params.jpg)
Capture HVT example:
![HVT capture task module placement](images/eden/hvt_capture_task_mod.jpg)
![HVT capture task module parameters](images/eden/hvt_capture_task_mod_params.jpg)
![HVT capture extraction area marker placement](images/eden/hvt_ext_zone_mrkr.jpg)
![HVT capture extraction marker name](images/eden/hvt_ext_zone_mrkr_var.jpg)
The HVT task screenshots show the direct HVT unit sync for both eliminate and
capture examples.
Setup:
1. Place the HVT unit or units.
2. Place `FORGE_Module_HVT`.
3. Set a unique `TaskID`.
4. For kill/eliminate missions, set `Capture HVT` to `False` and
`Eliminate HVT` to `True`.
5. For capture/extract missions, set `Capture HVT` to `True` and
`Eliminate HVT` to `False`.
6. If using capture mode, create a rectangle or ellipse area marker for the
extraction zone and set `ExtZone` to that marker name.
7. Set `LimitSuccess` to the number of HVTs that must be captured or
eliminated.
8. Set `LimitFail` if the mission should fail after too many HVT deaths in
capture mode.
9. Set reward funds, rating gain/loss, end-state behavior, and optional
`TimeLimit`.
10. Set `Prerequisite Task IDs` only if this HVT task should unlock after other
tasks succeed.
11. Sync the HVT module directly to the HVT unit or units.
Validation:
- Capture mode requires `ExtZone`; elimination mode does not.
- `ExtZone` must be an area marker.
- The HVT timer waits until the assigned group leader acknowledges the task.
### Defend Task
Use `FORGE_Module_Defend` when players must hold an area against spawned enemy
waves.
![Defend task module placement](images/eden/defend_task_mod.jpg)
![Defend task module parameters](images/eden/defend_task_mod_params.jpg)
![Defense area marker placement](images/eden/defend_zone_mrkr.jpg)
![Defense marker name](images/eden/defend_zone_mrkr_var.jpg)
The Defend task screenshots show module placement, marker setup, enemy wave
templates, and the required sync layout.
Setup:
1. Create a rectangle or ellipse area marker for the defense zone.
2. Place `FORGE_Module_Defend`.
3. Set a unique `TaskID`.
4. Set `DefenseZone` to the defense marker name.
5. Set `DefendTime` to how long the area must be held.
6. Set `WaveCount`.
7. Set `WaveCooldown`.
8. Set `MinBlufor` to the minimum number of friendly players or units required
in the zone.
9. Place one or more enemy groups or units to use as wave templates.
10. Sync any unit from each enemy group to the defend module.
11. Set reward funds, rating gain/loss, and end-state behavior.
12. Set `Prerequisite Task IDs` only if this defend task should unlock after
other tasks succeed.
Validation:
- `DefenseZone` must be an area marker.
- Syncing one unit from an enemy group makes the whole group available as a
wave composition.
- If no enemy units are synced, the task falls back to default CSAT infantry
waves.
- The timer, waves, and empty-zone failure checks start after enough BLUFOR
enter the zone.
## Task Module Quick Reference
| Task Module | Sync Target | Required Marker |
| --- | --- | --- |
| `FORGE_Module_Attack` | Target units or vehicles | None |
| `FORGE_Module_Destroy` | Target objects, vehicles, or units | None |
| `FORGE_Module_Defuse` | `FORGE_Module_Explosives`, optionally `FORGE_Module_Protected` | None |
| `FORGE_Module_Delivery` | `FORGE_Module_Cargo` | `DeliveryZone` |
| `FORGE_Module_Hostage` | `FORGE_Module_Hostages` and `FORGE_Module_Shooters` | `ExtZone`, optional `CBRNZone` |
| `FORGE_Module_HVT` | HVT units | `ExtZone` when capture mode is enabled |
| `FORGE_Module_Defend` | Optional enemy units as wave templates | `DefenseZone` |
## Mission Manager Blacklist Markers
The dynamic mission generator avoids rectangle and ellipse area markers whose
marker name or marker text starts with `blklist`.
Use blacklist area markers to keep generated missions out of bases, spawn
areas, training zones, or protected set pieces.
![Blacklist marker placement](images/eden/blacklist_mrkr.jpg)
![Blacklist marker variable name](images/eden/blacklist_mrkr_var.jpg)
Setup:
1. Create a rectangle or ellipse area marker over the area to exclude.
2. Set the marker variable name or marker text to start with `blklist`.
3. Give the marker real size so the generator can test candidate positions
against the area.
## Task Setup Checklist
Before publishing a mission, verify:
- Every task has a unique `TaskID`.
- Every configured marker name exists in Eden.
- Zone markers are area markers, not icon-only markers.
- Grouping modules are synced in the correct direction.
- Success and fail limits match the number of required entities.
- Reward funds and rating changes are intentional.
- Unchained tasks appear in CAD when created.
- Chained tasks remain hidden until all prerequisite task IDs succeed.
- Assigned CAD tasks can be acknowledged, declined, and completed.
## Mission Validation Checklist
Run this checklist in a local multiplayer test:
- Stand within 5 meters of each bank object and verify the full bank action.
- Stand within 5 meters of each ATM and verify ATM mode.
- Confirm PIN changes are only available from the full bank interface.
- Stand near each store object and complete a test checkout.
- Stand near each locker and verify the local locker/arsenal opens.
- Open each garage and retrieve/store a vehicle.
- Open each virtual garage category and confirm the correct spawn marker is
used.
- Block a garage spawn marker with a vehicle and confirm the warning appears.
- Create each mission task and confirm CAD visibility.
- Assign a task in CAD and verify the player flow through completion or failure.
## Eden Screenshot Set
The live docs should include real Eden screenshots for mission designers. When
capturing them, save the images under `docus/public/images/eden/` and use these
filenames so the docs can reference stable assets:
| File | Capture |
| --- | --- |
| `bank_obj.jpg`, `bank_obj_var.jpg` | Bank object placement and variable name. |
| `atm_obj.jpg`, `atm_obj_var.jpg` | ATM object placement and variable name. |
| `store_obj.jpg`, `store_obj_var.jpg` | Store object placement and variable name. |
| `locker_obj.jpg`, `locker_obj_var.jpg` | Locker container placement and variable name. |
| `garage_obj.jpg`, `garage_obj_var.jpg` | Garage interaction object placement and variable name. |
| `garage_spawn_mrkrs.jpg`, `garage_spawn_1_mrkr_var.jpg` | Garage category spawn markers and marker variable naming. |
| `garage_obj_2.jpg`, `garage_obj_2_var.jpg`, `garage_spawn_2_mrkrs.jpg` | Additional garage site placement, variable name, and spawn markers. |
| `med_spawn_obj.jpg`, `med_spawn_obj_var.jpg` | Medical spawn object placement and variable name. |
| `ceo_unit.jpg`, `ceo_unit_var.jpg` | CEO playable unit placement and variable name. |
| `dispatch_unit.jpg`, `dispatch_unit_var.jpg` | Dispatch playable unit placement and variable name. |
| `blacklist_mrkr.jpg`, `blacklist_mrkr_var.jpg` | Mission-manager blacklist marker placement and marker variable naming. |
| `create_task_mod.jpg`, `create_task_mod_params.jpg` | Arma 3 Create Task module placement and parameters. |
| `attack_task_mod.jpg`, `attack_task_mod_params.jpg`, `attack_task_tgts.jpg` | Attack task module placement, parameters, and target sync. |
| `destroy_task_mod.jpg`, `destroy_task_mod_params.jpg`, `destroy_task_tgts.jpg` | Destroy task module placement, parameters, and target sync. |
| `defuse_task_mod.jpg`, `defuse_task_mod_params.jpg` | Defuse task module placement and parameters. |
| `defuse_explosives_mod.jpg`, `defuse_protected_mod.jpg` | Defuse grouping modules for explosive and protected entities. |
| `delivery_task_mod.jpg`, `delivery_task_mod_params.jpg`, `delivery_cargo_mod.jpg` | Delivery task module, parameters, and Cargo Entities grouping module. |
| `delivery_zone_mrkr.jpg`, `delivery_zone_mrkr_var.jpg` | Delivery area marker placement and marker name. |
| `hostage_task_mod.jpg`, `hostage_task_mod_params.jpg` | Hostage task module placement and parameters. |
| `hostage_entities_mod.jpg`, `hostage_shooters_mod.jpg` | Hostage grouping modules for hostage and shooter units. |
| `hostage_ext_zone_mrkr.jpg`, `hostage_ext_zone_mrkr_var.jpg` | Hostage extraction marker placement and marker name. |
| Hostage CBRN marker | Use the same extraction-marker capture pattern if a separate CBRN screenshot is ever needed. |
| `hvt_task_mod.jpg`, `hvt_task_mod_params.jpg` | HVT eliminate task module placement and parameters. |
| `hvt_capture_task_mod.jpg`, `hvt_capture_task_mod_params.jpg` | HVT capture task module placement and parameters. |
| `hvt_ext_zone_mrkr.jpg`, `hvt_ext_zone_mrkr_var.jpg` | HVT capture extraction marker placement and marker name. |
| `defend_task_mod.jpg`, `defend_task_mod_params.jpg` | Defend task module placement, parameters, wave templates, and sync. |
| `defend_zone_mrkr.jpg`, `defend_zone_mrkr_var.jpg` | Defense area marker placement and marker name. |
| `cad-visible-task.jpg` | In-game CAD showing a task created from the Eden module. |
Use screenshots that show the Eden left-side entity list, the selected object's
attributes panel, and the map placement where possible. Crop only enough to
remove unrelated mission content.
## Related Guides
- [Task Usage Guide](./TASK_USAGE_GUIDE.md)
- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md)
- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md)
- [Store Usage Guide](./STORE_USAGE_GUIDE.md)
- [Bank Usage Guide](./BANK_USAGE_GUIDE.md)
- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md)

View File

@ -1,221 +0,0 @@
# Module Reference
This reference lists the main Forge modules and where each layer lives.
## Directory Map
```text
arma/client/addons/ Client-side Arma addons and browser UIs
arma/server/addons/ Server-side Arma addons and extension bridge
arma/server/extension/ Rust arma-rs extension and SurrealDB adapters
bin/icom/ Interprocess communication helper
lib/models/ Shared domain data models
lib/repositories/ Repository traits and in-memory stores
lib/services/ Domain services and workflow logic
lib/shared/ Cross-crate helpers
tools/ Web UI build tooling
docs/ Framework-level documentation
```
## Gameplay Domains
| Domain | Purpose | Client addon | Server addon | Service/model layer | Extension group |
| --- | --- | --- | --- | --- | --- |
| Actor | Player identity, loadout, position, status, contact identifiers, and persistent character data. | `arma/client/addons/actor` | `arma/server/addons/actor` | `lib/models/src/actor.rs`, `lib/services/src/actor.rs` | `actor:*` |
| Bank | Player accounts, cash/bank balances, PIN validation and changes, transfers, checkout charging, and transaction context. | `arma/client/addons/bank` | `arma/server/addons/bank` | `lib/models/src/bank.rs`, `lib/services/src/bank.rs` | `bank:*`, `bank:hot:*` |
| CAD | Dispatch requests, assignments, orders, activity stream, profiles, groups, and hydrated dispatcher views. | `arma/client/addons/cad` | `arma/server/addons/cad` | `lib/models/src/cad.rs`, `lib/services/src/cad.rs` | `cad:*` |
| Garage | Player vehicle storage with plate IDs, fuel, damage, and hit point state. | `arma/client/addons/garage` | `arma/server/addons/garage` | `lib/models/src/garage.rs`, `lib/services/src/garage.rs` | `garage:*`, `garage:hot:*` |
| Locker | Player item storage keyed by classname with category and amount. | `arma/client/addons/locker` | `arma/server/addons/locker` | `lib/models/src/locker.rs`, `lib/services/src/locker.rs` | `locker:*`, `locker:hot:*` |
| Organization | Player organizations, membership, treasury, credit lines, shared assets, and fleet data. | `arma/client/addons/org` | `arma/server/addons/org` | `lib/models/src/org.rs`, `lib/services/src/org.rs` | `org:*`, `org:hot:*` |
| Phone | Contacts, messages, and email state. | `arma/client/addons/phone` | `arma/server/addons/phone` | `lib/models/src/phone.rs`, `lib/services/src/phone.rs` | `phone:*` |
| Store | Storefront entity setup, catalog hydration, checkout workflows, and checkout charging integration. | `arma/client/addons/store` | `arma/server/addons/store` | `lib/models/src/store.rs`, `lib/services/src/store.rs` | `store:checkout` |
| Task | Server-owned mission/task flows, catalog, ownership, status, participant tracking, rewards, and defuse counters. | none | `arma/server/addons/task` | `lib/models/src/task.rs`, `lib/services/src/task.rs` | `task:*` |
| Owned Garage | Organization or owner-scoped vehicle unlock storage. | via garage/org UI | server extension only | `lib/models/src/v_garage.rs`, `lib/services/src/v_garage.rs` | `owned:garage:*` |
| Owned Locker | Organization or owner-scoped arsenal unlock storage. | via locker/org UI | server extension only | `lib/models/src/v_locker.rs`, `lib/services/src/v_locker.rs` | `owned:locker:*` |
Server and extension guides:
[Actor](./ACTOR_USAGE_GUIDE.md),
[Bank](./BANK_USAGE_GUIDE.md),
[CAD](./CAD_USAGE_GUIDE.md),
[Economy](./ECONOMY_USAGE_GUIDE.md),
[Garage](./GARAGE_USAGE_GUIDE.md),
[Locker](./LOCKER_USAGE_GUIDE.md),
[Organization](./ORG_USAGE_GUIDE.md),
[Owned Storage](./OWNED_STORAGE_USAGE_GUIDE.md),
[Phone](./PHONE_USAGE_GUIDE.md),
[Store](./STORE_USAGE_GUIDE.md),
[Task](./TASK_USAGE_GUIDE.md).
Client guides:
[Client Overview](./CLIENT_USAGE_GUIDE.md),
[Main](./CLIENT_MAIN_USAGE_GUIDE.md),
[Common](./CLIENT_COMMON_USAGE_GUIDE.md),
[Actor](./CLIENT_ACTOR_USAGE_GUIDE.md),
[Bank](./CLIENT_BANK_USAGE_GUIDE.md),
[CAD](./CLIENT_CAD_USAGE_GUIDE.md),
[Garage](./CLIENT_GARAGE_USAGE_GUIDE.md),
[Locker](./CLIENT_LOCKER_USAGE_GUIDE.md),
[Notifications](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md),
[Organization](./CLIENT_ORG_USAGE_GUIDE.md),
[Phone](./CLIENT_PHONE_USAGE_GUIDE.md),
[Store](./CLIENT_STORE_USAGE_GUIDE.md).
## Infrastructure Modules
| Module | Purpose | Location |
| --- | --- | --- |
| `common` | Shared SQF helpers, base stores, utility functions, and shared UI bridge pieces. | `arma/client/addons/common`, `arma/server/addons/common` |
| `extension` | Server SQF bridge around `forge_server` extension calls and chunked transport. | `arma/server/addons/extension` |
| `main` | Mod-level configuration, pre-init wiring, and server/client startup glue. | `arma/client/addons/main`, `arma/server/addons/main` |
| `economy` | Server-side fuel, medical, and service economy helpers. Fuel and repair charge organization hot state; medical charges player bank/cash first, then organization funds with repayable member debt when personal funds cannot cover the bill. | `arma/server/addons/economy` |
| `notifications` | Client notification UI, sounds, and UI event handling. | `arma/client/addons/notifications` |
| `icom` | Rust helper for interprocess communication and event broadcasting. | `bin/icom`, `arma/server/extension/src/icom.rs` |
| `terrain` | Extension-side terrain export helper. | `arma/server/extension/src/terrain.rs` |
| `transport` | Chunked request/response handling for large extension payloads. | `arma/server/extension/src/transport.rs` |
| `surreal` | SurrealDB connection lifecycle and status reporting. | `arma/server/extension/src/surreal.rs` |
## Extension Command Groups
Commands are invoked with:
```sqf
"forge_server" callExtension ["group:command", [_arg1, _arg2]];
```
Nested groups use additional `:` separators, for example
`bank:hot:deposit`.
### Core
| Command | Purpose |
| --- | --- |
| `version` | Return the extension version string. |
| `status` | Return SurrealDB connection state. |
| `surreal:status` | Return SurrealDB connection state directly from the Surreal module. |
### Actor
| Command | Purpose |
| --- | --- |
| `actor:get` | Fetch actor data for a resolved player UID. |
| `actor:create` | Create actor data from JSON. |
| `actor:update` | Apply actor JSON updates. |
| `actor:exists` | Return `true` or `false`. |
| `actor:delete` | Delete actor data. |
| `actor:hot:init`, `actor:hot:get`, `actor:hot:keys`, `actor:hot:override`, `actor:hot:save`, `actor:hot:remove` | Manage actor hot state. |
See [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md) for examples.
### Bank
| Command | Purpose |
| --- | --- |
| `bank:get`, `bank:create`, `bank:update`, `bank:exists`, `bank:delete` | Durable bank CRUD. |
| `bank:hot:init`, `bank:hot:get`, `bank:hot:override`, `bank:hot:patch`, `bank:hot:save`, `bank:hot:remove` | Manage bank hot state. |
| `bank:hot:deposit`, `bank:hot:withdraw`, `bank:hot:deposit_earnings`, `bank:hot:transfer` | Mutate hot bank balances with operation context. |
| `bank:hot:charge_checkout` | Charge a checkout against hot bank state. |
| `bank:hot:validate_pin`, `bank:hot:change_pin` | Validate and update PINs for bank operations. |
See [Bank Usage Guide](./BANK_USAGE_GUIDE.md) for examples.
### Garage
| Command | Purpose |
| --- | --- |
| `garage:create`, `garage:get`, `garage:add`, `garage:update`, `garage:patch`, `garage:remove`, `garage:delete`, `garage:exists` | Durable player garage operations. |
| `garage:hot:init`, `garage:hot:get`, `garage:hot:override`, `garage:hot:add`, `garage:hot:remove_vehicle`, `garage:hot:save`, `garage:hot:remove` | Manage player garage hot state. |
See [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md) for examples.
### Locker
| Command | Purpose |
| --- | --- |
| `locker:create`, `locker:get`, `locker:add`, `locker:update`, `locker:patch`, `locker:remove`, `locker:delete`, `locker:exists` | Durable player locker operations. |
| `locker:hot:init`, `locker:hot:get`, `locker:hot:override`, `locker:hot:save`, `locker:hot:remove` | Manage player locker hot state. |
See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples.
### Organization
| Command | Purpose |
| --- | --- |
| `org:get`, `org:create`, `org:update`, `org:exists`, `org:delete` | Durable organization CRUD. |
| `org:assets:get`, `org:assets:update` | Manage organization assets. |
| `org:fleet:get`, `org:fleet:update` | Manage organization fleet entries. |
| `org:members:get`, `org:members:add`, `org:members:remove` | Manage organization membership. |
| `org:hot:*` | Runtime organization workflows including registration, invites, credit lines, checkout charging, assets, fleet, leave, disband, save, and remove. |
See [Org Usage Guide](./ORG_USAGE_GUIDE.md) for examples.
### Phone
| Command | Purpose |
| --- | --- |
| `phone:init` | Initialize phone state for a UID. |
| `phone:contacts:list`, `phone:contacts:add`, `phone:contacts:remove` | Manage contacts. |
| `phone:messages:list`, `phone:messages:thread`, `phone:messages:send`, `phone:messages:mark_read`, `phone:messages:delete` | Manage messages. |
| `phone:emails:list`, `phone:emails:send`, `phone:emails:mark_read`, `phone:emails:delete` | Manage emails. |
| `phone:remove` | Remove phone state for a UID. |
See [Phone Usage Guide](./PHONE_USAGE_GUIDE.md) for examples.
### CAD
| Command Group | Purpose |
| --- | --- |
| `cad:activity:append`, `cad:activity:recent` | Append and read recent activity. |
| `cad:assignments:list`, `cad:assignments:assign`, `cad:assignments:acknowledge`, `cad:assignments:decline`, `cad:assignments:upsert`, `cad:assignments:delete` | Manage dispatch assignments. |
| `cad:orders:list`, `cad:orders:create`, `cad:orders:create_from_context`, `cad:orders:close`, `cad:orders:upsert`, `cad:orders:delete` | Manage orders. |
| `cad:requests:list`, `cad:requests:submit`, `cad:requests:submit_from_context`, `cad:requests:close`, `cad:requests:upsert`, `cad:requests:delete` | Manage requests. |
| `cad:profiles:list`, `cad:profiles:update_from_context`, `cad:profiles:upsert`, `cad:profiles:delete` | Manage profiles. |
| `cad:groups:build` | Build grouped CAD state. |
| `cad:view:hydrate` | Build the dispatcher view model. |
See [CAD Usage Guide](./CAD_USAGE_GUIDE.md) for examples.
### Task
| Command Group | Purpose |
| --- | --- |
| `task:reset` | Reset task state. |
| `task:catalog:active`, `task:catalog:get`, `task:catalog:upsert`, `task:catalog:delete` | Manage task catalog entries. |
| `task:ownership:bind`, `task:ownership:release`, `task:ownership:accept`, `task:ownership:reward_context` | Manage task ownership and rewards. |
| `task:status:set`, `task:status:get`, `task:status:clear` | Manage task status. |
| `task:defuse:increment`, `task:defuse:get` | Manage defuse counters. |
| `task:clear` | Clear task state. |
See [Task Usage Guide](./TASK_USAGE_GUIDE.md) for examples.
### Owned Storage
| Command Group | Purpose |
| --- | --- |
| `owned:garage:create`, `owned:garage:fetch`, `owned:garage:get`, `owned:garage:add`, `owned:garage:remove`, `owned:garage:delete`, `owned:garage:exists` | Owner-scoped vehicle storage. |
| `owned:garage:hot:*` | Owner-scoped vehicle hot state. |
| `owned:locker:create`, `owned:locker:fetch`, `owned:locker:get`, `owned:locker:add`, `owned:locker:remove`, `owned:locker:delete`, `owned:locker:exists` | Owner-scoped item storage. |
| `owned:locker:hot:*` | Owner-scoped item hot state. |
See [Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md) for examples.
### Other Extension Groups
| Command Group | Purpose |
| --- | --- |
| `store:checkout` | Run store checkout behavior. |
| `icom:connect`, `icom:broadcast`, `icom:send_event` | ICOM connection and event forwarding. See [ICOM Usage Guide](./ICOM_USAGE_GUIDE.md). |
| `terrain:exportSVG` | Export terrain data as SVG. |
| `transport:invoke`, `transport:invoke_stored` | Invoke commands through transport. |
| `transport:request:append`, `transport:request:clear` | Manage stored request chunks. |
| `transport:response:get`, `transport:response:clear` | Manage stored response chunks. |
## Rust Crates
| Crate | Role |
| --- | --- |
| `forge-models` | Domain models and validation. Keep these serializable and free of persistence details. |
| `forge-repositories` | Repository traits and in-memory implementations. Keep these storage-agnostic. |
| `forge-services` | Business rules and workflows. Depend on repository traits, not concrete databases. |
| `forge-shared` | Cross-crate helpers. Keep dependencies light. |
| `forge-server` | Arma extension crate. Owns command registration, SurrealDB runtime wiring, and concrete storage adapters. |
| `forge-icom` | ICom helper binary and client library. |

View File

@ -1,239 +0,0 @@
# Organization Usage Guide
The organization module stores organization records, members, assets, fleet
entries, and credit lines. Durable commands manage persisted records directly.
Hot-state commands support the active organization UI workflows.
## Storage Model
Core organization:
```json
{
"id": "default",
"owner": "server",
"name": "Default Organization",
"funds": 0.0,
"reputation": 0,
"credit_lines": {}
}
```
Hot organization:
```json
{
"id": "default",
"owner": "server",
"name": "Default Organization",
"funds": 0.0,
"reputation": 0,
"credit_lines": {},
"assets": {},
"fleet": {},
"members": {},
"pending_invites": {}
}
```
Rules validated by the Rust service:
- `id` must be non-empty and contain only alphanumeric characters or `_`.
- `owner` must be `server` or a 17-digit Steam UID.
- `name` cannot be empty, cannot exceed 100 characters, and cannot contain
control characters.
- `funds`, reputation, and credit line amounts cannot be negative.
- Player registration is rejected when the player already belongs to a
non-default organization.
- Player registration through the server org addon requires a $50,000 personal
funds registration fee. The fee is charged from the player's bank balance
first, then on-hand cash if needed.
## Durable Commands
| Command | Arguments | Returns |
| --- | --- | --- |
| `org:create` | `org_id`, `org_json` | Organization JSON. |
| `org:get` | `org_id` | Organization JSON. |
| `org:update` | `org_id`, `patch_json` | Updated organization JSON. |
| `org:exists` | `org_id` | `true` or `false`. |
| `org:delete` | `org_id` | `OK`. |
| `org:assets:get` | `org_id` | Asset map JSON. |
| `org:assets:update` | `org_id`, `assets_json` | Updated asset map JSON. |
| `org:fleet:get` | `org_id` | Fleet map JSON. |
| `org:fleet:update` | `org_id`, `fleet_json` | Updated fleet map JSON. |
| `org:members:get` | `org_id` | Member array JSON. |
| `org:members:add` | `org_id`, `member_uid` | `OK`. |
| `org:members:remove` | `org_id`, `member_uid` | `OK`. |
## Create an Organization
The command key is authoritative for `id`.
```sqf
private _org = createHashMapFromArray [
["id", _orgId],
["owner", getPlayerUID player],
["name", "Spearnet Logistics"],
["funds", 0],
["reputation", 0],
["credit_lines", createHashMap]
];
private _result = "forge_server" callExtension ["org:create", [
_orgId,
toJSON _org
]];
```
## Update Organization Funds
```sqf
private _patch = createHashMapFromArray [
["funds", 5000],
["reputation", 10]
];
private _result = "forge_server" callExtension ["org:update", [
_orgId,
toJSON _patch
]];
```
Supported durable patch fields are `id`, `owner`, `name`, `funds`,
`reputation`, and `credit_lines`.
## Assets and Fleet
Assets are grouped by category, then classname.
```sqf
private _assets = createHashMapFromArray [
["ammo", createHashMapFromArray [
["ACE_30Rnd_65x39_caseless_mag", createHashMapFromArray [
["classname", "ACE_30Rnd_65x39_caseless_mag"],
["type", "ammo"],
["quantity", 20]
]]
]]
];
"forge_server" callExtension ["org:assets:update", [_orgId, toJSON _assets]];
```
Fleet is keyed by an internal fleet entry ID.
```sqf
private _fleet = createHashMapFromArray [
["B_Truck_01_transport_F_0", createHashMapFromArray [
["classname", "B_Truck_01_transport_F"],
["name", "Transport Truck"],
["type", "cars"],
["status", "Ready"],
["damage", "0%"]
]]
];
"forge_server" callExtension ["org:fleet:update", [_orgId, toJSON _fleet]];
```
## Hot-State Commands
| Command | Arguments | Returns |
| --- | --- | --- |
| `org:hot:init` | `org_id` | Hot organization JSON. |
| `org:hot:get` | `org_id` | Hot organization JSON. |
| `org:hot:override` | `org_id`, `hot_org_json` | Hot organization JSON. |
| `org:hot:ensure_member` | `context_json` | Hot organization JSON. |
| `org:hot:member_invites` | `member_uid` | Invite array JSON. |
| `org:hot:register` | `context_json` | Register result JSON. |
| `org:hot:invite_member` | `context_json` | Invite result JSON. |
| `org:hot:accept_invite` | `context_json` | Invite decision result JSON. |
| `org:hot:decline_invite` | `context_json` | Invite decision result JSON. |
| `org:hot:assign_credit_line` | `context_json` | Mutation result JSON. |
| `org:hot:repay_credit_line` | `context_json` | Repayment result JSON. |
| `org:hot:charge_checkout` | `context_json` | Mutation result JSON. |
| `org:hot:add_assets` | `context_json`, `assets_json` | Mutation result JSON. |
| `org:hot:add_fleet` | `context_json`, `fleet_json` | Mutation result JSON. |
| `org:hot:leave` | `context_json` | Leave result JSON. |
| `org:hot:disband` | `context_json` | Disband result JSON. |
| `org:hot:save` | `org_id` | Current hot organization JSON and async durable save. |
| `org:hot:remove` | `org_id` | `OK`. |
## Register from UI Context
The server-side `forge_server_org` registration flow charges the $50,000
registration fee before completing organization creation. If the organization
service rejects the registration, the bank charge is refunded.
```sqf
private _context = createHashMapFromArray [
["requesterUid", getPlayerUID player],
["requesterName", name player],
["orgId", _orgId],
["orgName", "Spearnet Logistics"],
["existingOrgId", "default"]
];
private _result = "forge_server" callExtension ["org:hot:register", [toJSON _context]];
```
## Invite and Accept
```sqf
private _invite = createHashMapFromArray [
["requesterUid", getPlayerUID player],
["requesterName", name player],
["orgId", _orgId],
["requesterIsDefaultOrgCeo", false],
["targetUid", _targetUid],
["targetName", _targetName],
["targetOrgId", "default"]
];
"forge_server" callExtension ["org:hot:invite_member", [toJSON _invite]];
private _decision = createHashMapFromArray [
["requesterUid", _targetUid],
["requesterName", _targetName],
["orgId", _orgId],
["existingOrgId", "default"]
];
"forge_server" callExtension ["org:hot:accept_invite", [toJSON _decision]];
```
## Credit Line Checkout
```sqf
private _credit = createHashMapFromArray [
["requesterUid", getPlayerUID player],
["orgId", _orgId],
["requesterIsDefaultOrgCeo", false],
["memberUid", _memberUid],
["memberName", _memberName],
["amount", 1000]
];
"forge_server" callExtension ["org:hot:assign_credit_line", [toJSON _credit]];
private _charge = createHashMapFromArray [
["requesterUid", _memberUid],
["orgId", _orgId],
["requesterIsDefaultOrgCeo", false],
["source", "credit_line"],
["amount", 250],
["commit", true]
];
"forge_server" callExtension ["org:hot:charge_checkout", [toJSON _charge]];
```
## Error Handling
```sqf
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Organization error: %1", _payload];
};
```

View File

@ -1,158 +0,0 @@
# Owned Storage Usage Guide
Owned storage covers the `owned:locker` and `owned:garage` extension command
groups. These modules store unlock lists rather than physical item or vehicle
instances.
Use these modules for virtual arsenal and virtual garage unlocks. Use
[Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) and
[Garage Usage Guide](./GARAGE_USAGE_GUIDE.md) for physical inventory and stored
vehicle instances.
## Owned Locker Model
```json
{
"items": ["FirstAidKit"],
"weapons": ["arifle_MX_F"],
"magazines": ["30Rnd_65x39_caseless_black_mag"],
"backpacks": ["B_AssaultPack_rgr"]
}
```
Supported owned locker categories:
- `items`
- `weapons`
- `magazines`
- `backpacks`
New owned lockers are created with default unlocks from the Rust model.
## Owned Garage Model
```json
{
"cars": ["B_Quadbike_01_F"],
"armor": [],
"helis": [],
"planes": [],
"naval": [],
"other": []
}
```
Supported owned garage categories:
- `cars`
- `armor`
- `helis`
- `planes`
- `naval`
- `other`
The durable `owned:garage:remove` command currently accepts `heli` for the
helicopter category. Add, get, and hot remove accept `helis`.
New owned garages are created with default unlocks from the Rust model.
## Owned Locker Commands
| Command | Arguments | Returns |
| --- | --- | --- |
| `owned:locker:create` | `uid` | Full owned locker JSON. |
| `owned:locker:fetch` | `uid` | Full owned locker JSON. |
| `owned:locker:get` | `uid`, `category` | Category classname array JSON. |
| `owned:locker:add` | `uid`, `category`, `classnames_json` | Updated category array JSON. |
| `owned:locker:remove` | `uid`, `category`, `classname` | Updated category array JSON. |
| `owned:locker:delete` | `uid` | `OK`. |
| `owned:locker:exists` | `uid` | `true` or `false`. |
## Owned Garage Commands
| Command | Arguments | Returns |
| --- | --- | --- |
| `owned:garage:create` | `uid` | Full owned garage JSON. |
| `owned:garage:fetch` | `uid` | Full owned garage JSON. |
| `owned:garage:get` | `uid`, `category` | Category classname array JSON. |
| `owned:garage:add` | `uid`, `category`, `classnames_json` | Updated category array JSON. |
| `owned:garage:remove` | `uid`, `category`, `classname` | Updated category array JSON. |
| `owned:garage:delete` | `uid` | `OK`. |
| `owned:garage:exists` | `uid` | `true` or `false`. |
## Add Virtual Arsenal Unlocks
```sqf
private _classes = ["arifle_MX_F", "hgun_P07_F"];
private _result = "forge_server" callExtension ["owned:locker:add", [
getPlayerUID player,
"weapons",
toJSON _classes
]];
```
## Add Virtual Garage Unlocks
```sqf
private _classes = ["B_Quadbike_01_F", "B_MRAP_01_F"];
private _result = "forge_server" callExtension ["owned:garage:add", [
getPlayerUID player,
"cars",
toJSON _classes
]];
```
## Remove an Unlock
```sqf
"forge_server" callExtension ["owned:locker:remove", [
getPlayerUID player,
"weapons",
"arifle_MX_F"
]];
"forge_server" callExtension ["owned:garage:remove", [
getPlayerUID player,
"cars",
"B_Quadbike_01_F"
]];
```
## Hot-State Commands
Both owned storage modules support hot state.
Owned locker:
| Command | Arguments | Returns |
| --- | --- | --- |
| `owned:locker:hot:init` | `uid` | Full owned locker JSON. |
| `owned:locker:hot:fetch` | `uid` | Full owned locker JSON. |
| `owned:locker:hot:get` | `uid`, `category` | Category array JSON. |
| `owned:locker:hot:override` | `uid`, `locker_json` | Full owned locker JSON. |
| `owned:locker:hot:save` | `uid` | Current hot owned locker JSON and async durable save. |
| `owned:locker:hot:remove` | `uid` | `OK`. |
Owned garage:
| Command | Arguments | Returns |
| --- | --- | --- |
| `owned:garage:hot:init` | `uid` | Full owned garage JSON. |
| `owned:garage:hot:fetch` | `uid` | Full owned garage JSON. |
| `owned:garage:hot:get` | `uid`, `category` | Category array JSON. |
| `owned:garage:hot:override` | `uid`, `garage_json` | Full owned garage JSON. |
| `owned:garage:hot:add` | `uid`, `category`, `classnames_json` | Updated category array JSON. |
| `owned:garage:hot:remove_item` | `uid`, `category`, `classname` | Updated category array JSON. |
| `owned:garage:hot:save` | `uid` | Current hot owned garage JSON and async durable save. |
| `owned:garage:hot:remove` | `uid` | `OK`. |
## Error Handling
```sqf
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Owned storage error: %1", _payload];
};
```

View File

@ -1,136 +0,0 @@
# Phone Usage Guide
The phone module stores contacts, messages, and emails for each UID. It is a
server-extension state module backed by SurrealDB.
## Storage Model
```json
{
"contacts": ["76561198000000000", "field_commander"],
"messages": [
{
"id": "phone-message:sender:receiver:1",
"from": "sender",
"to": "receiver",
"message": "Text body",
"timestamp": 123.45,
"read": false
}
],
"emails": [
{
"id": "phone-email:sender:receiver:2",
"from": "sender",
"to": "receiver",
"subject": "Subject",
"body": "Email body",
"timestamp": 123.45,
"read": false
}
]
}
```
Rules validated by the Rust service:
- UID arguments cannot be empty.
- Message and email bodies cannot be empty.
- Empty email subjects become `No subject`.
- Player messages and emails cannot target `field_commander`.
- `field_commander` can send messages or emails to players.
- Deleting a message or email removes it only from the requesting UID's index.
## Commands
| Command | Arguments | Returns |
| --- | --- | --- |
| `phone:init` | `uid` | Full phone payload. |
| `phone:contacts:list` | `uid` | Contact UID array. |
| `phone:contacts:add` | `uid`, `contact_uid` | `true` or `false`. |
| `phone:contacts:remove` | `uid`, `contact_uid` | `true` or `false`. |
| `phone:messages:list` | `uid` | Message array. |
| `phone:messages:thread` | `uid`, `other_uid` | Message array for both participants. |
| `phone:messages:send` | `from_uid`, `to_uid`, `message`, `timestamp` | Message JSON. |
| `phone:messages:mark_read` | `uid`, `message_id` | `true` or `false`. |
| `phone:messages:delete` | `uid`, `message_id` | `true` or `false`. |
| `phone:emails:list` | `uid` | Email array. |
| `phone:emails:send` | `from_uid`, `to_uid`, `subject`, `body`, `timestamp` | Email JSON. |
| `phone:emails:mark_read` | `uid`, `email_id` | `true` or `false`. |
| `phone:emails:delete` | `uid`, `email_id` | `true` or `false`. |
| `phone:remove` | `uid` | `OK`. |
## Initialize Phone State
`phone:init` creates phone state if needed and seeds self-contact plus
`field_commander`.
```sqf
private _result = "forge_server" callExtension ["phone:init", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Phone init failed: %1", _payload];
};
private _phone = fromJSON _payload;
```
## Send a Message
```sqf
private _timestamp = str diag_tickTime;
private _result = "forge_server" callExtension ["phone:messages:send", [
getPlayerUID player,
_targetUid,
"Move to checkpoint Alpha.",
_timestamp
]];
```
## Read a Conversation
```sqf
private _result = "forge_server" callExtension ["phone:messages:thread", [
getPlayerUID player,
_otherUid
]];
private _messages = fromJSON (_result select 0);
```
## Send an Email
```sqf
private _result = "forge_server" callExtension ["phone:emails:send", [
getPlayerUID player,
_targetUid,
"Supply Request",
"Requesting resupply at grid 123456.",
str diag_tickTime
]];
```
## Mark and Delete Records
```sqf
"forge_server" callExtension ["phone:messages:mark_read", [
getPlayerUID player,
_messageId
]];
"forge_server" callExtension ["phone:emails:delete", [
getPlayerUID player,
_emailId
]];
```
## Error Handling
```sqf
private _payload = (_result select 0);
if (_payload find "Error:" == 0) then {
systemChat format ["Phone error: %1", _payload];
};
```

View File

@ -1,318 +0,0 @@
# Player Guide
Use this guide as the player-facing overview for Forge systems. It explains
what players interact with during normal missions, how task assignment works,
and what persistent storage limits apply.
## Opening Forge Interactions
Most Forge actions are opened from the actor interaction menu while standing
near a configured mission object.
![Custom interaction menu](images/player/interaction_menu.jpg)
Press `Tab` by default to open the custom interaction menu. Server settings or
local keybind changes may use a different key.
Known current behavior: after closing the custom interaction menu, players may
need to press `Tab` twice before it opens again. Treat this as a temporary
workaround until the interaction menu focus behavior is investigated further.
Players usually need to be within 5 meters of an interaction object such as a
bank terminal, ATM, store counter, garage terminal, or locker.
## CAD and Tasks
CAD is the main task and dispatch system. It is used for mission contracts,
group status, support requests, dispatch orders, and task assignment.
![CAD operations task board](images/player/cad_ops_board.jpg)
Player workflow:
1. Open CAD from the available interaction path.
2. Review available or assigned tasks.
3. If a dispatcher assigns a task to your group, the group leader must
acknowledge or decline it.
4. Once acknowledged, the task becomes active for the assigned group.
5. Complete the task objective shown by CAD, map task state, and mission
instructions.
Map focus behavior:
- Click an assigned or accepted task in the operations task board to center the
map on that task.
- Click a roster member to center the map on that player.
- Click a support request to center the map on the request location.
- Dispatch map mode supports the same focus behavior for groups, contracts,
and support requests.
Dispatch workflow:
![CAD dispatch board](images/player/cad_dispatch_board.jpg)
1. Open CAD with a dispatcher-enabled slot or permission.
2. Use dispatch mode to review groups, open contracts, assigned contracts, and
support requests.
3. Assign available contracts to active groups.
4. Send dispatch orders or close completed orders as needed.
5. Track group status and recent CAD activity.
Dispatch access:
- The CEO slot can administer the default organization and use CAD dispatch
permissions.
- The Dispatch slot grants CAD dispatch permissions without default
organization administration rights.
- Players who are the CEO or owner of their own organization also receive CAD
dispatch permissions.
Important task behavior:
- CAD assignment reserves a task for a group.
- The task starts after the assigned group leader acknowledges it.
- If the leader declines, the task returns to the open contract board.
- Some task timers wait for group-leader acknowledgment before counting down.
## Phone
The phone provides contacts, messages, email, and local utility apps.
![Phone home screen](images/player/phone_home.jpg)
### Contacts
Use Contacts to keep track of other players by phone number or email address.
Adding contacts makes it easier to start messages and emails without manually
entering recipient details every time.
![Phone contacts screen](images/player/phone_contacts.jpg)
### Messages
Messages are short player-to-player conversations.
![Phone messages screen](images/player/phone_messages.jpg)
Use Messages to:
- start or continue a conversation with a contact
- read incoming messages
- mark messages as read
- delete messages you no longer need
### Email
Email is used for longer player-to-player communication.
![Phone email screen](images/player/phone_email.jpg)
Use Email to:
- send a subject and body to another player
- read incoming mail
- mark email as read
- delete old email
### Local Phone Apps
Notes, calendar events, clocks, alarms, and theme preferences are local utility
features. They are saved for the local player profile and should not be treated
as shared multiplayer data.
## Bank and ATM
Bank and ATM access are separate.
Use a bank object for full banking:
![Bank app](images/player/bank_app.jpg)
- view account information
- transfer funds
- deposit earnings
- change PIN
Use an ATM for limited account access:
![ATM PIN screen](images/player/atm_app_pin.jpg)
![ATM home screen](images/player/atm_app_home.jpg)
- PIN-gated account actions
- ATM banking workflows
- no PIN changes
If a PIN prompt appears, enter the correct PIN before attempting account
actions.
## Organizations
Players start in the default organization. A player can create a player-owned
organization only if they have `$50,000` available for the registration fee.
Organization access depends on the player's role.
![Organization home screen](images/player/org_home.jpg)
![Organization registration screen](images/player/org_registration.jpg)
Default organization:
- The `ceo` slot can administer the default organization.
- The `dispatch` slot receives CAD dispatch permissions, but does not receive
default organization administration rights.
Player-owned organizations:
![Organization dashboard](images/player/org_dashboard.jpg)
![Organization treasury screen](images/player/org_treasury.jpg)
- The player who created the organization is its owner or CEO.
- The owner can administer the organization, including treasury and roster
actions exposed by the organization interface.
- Organization owners can invite players, manage members, assign credit lines,
transfer funds or run payroll when funds are available, and disband the
organization.
- Organization owners can use organization funds for supported store purchases.
- Members may receive assigned credit lines, accept or decline organization
invites, and leave the organization.
- The organization CEO or owner cannot leave their own organization directly.
They must disband the organization if they want to leave it.
Organization actions are server-authoritative. If an organization action fails,
check that the player has the correct role, the player or organization has
enough funds, and the target player is eligible for the action.
## Store
Stores sell unlocks and equipment through the configured server-side catalog.
![Store catalog](images/player/store_catalog.jpg)
Store purchases may grant:
- items or equipment added to the locker
- matching gear unlocks in the virtual arsenal
- vehicle unlocks in the virtual garage
- other mission-configured rewards
Store purchases are server-authoritative. If a purchase succeeds, the relevant
bank, locker, virtual arsenal, virtual garage, or organization state updates
from the server.
![Store checkout result](images/player/store_checkout.jpg)
Vehicle purchases unlock the vehicle in the virtual garage. They do not place a
physical vehicle into the player's 5-slot garage. Use the virtual garage to
spawn an unlocked vehicle, and use the garage to store or retrieve live world
vehicles.
## Locker and Virtual Arsenal
The locker is personal item storage.
![Locker storage](images/player/locker.jpg)
Locker rules:
- Up to 25 items can be stored.
- The locker saves when the locker container is closed.
- Over-capacity storage can warn or fail depending on server handling.
The virtual arsenal is locked down. Players only see gear they have been
granted or have unlocked through systems such as the store. The virtual arsenal
is not intended to expose the full unrestricted Arma arsenal.
![Virtual arsenal unlocks](images/player/virtual_arsenal.jpg)
## Garage and Virtual Garage
The garage stores physical player vehicles that have been saved from the world.
![Garage dashboard](images/player/garage.jpg)
Garage rules:
- Up to 5 vehicles can be stored.
- Stored vehicles can be retrieved from a garage interaction point.
- Retrieved vehicles become live world vehicles again.
- Vehicle service actions operate on live nearby vehicles, not vehicles that
are still stored.
The virtual garage is locked down. Players only see vehicles they have been
granted or have unlocked through systems such as the store. Virtual garage
unlocks are separate from the 5 physical vehicle slots in the garage. The
virtual garage uses mission-configured spawn lanes, and spawning may be blocked
if the spawn position is occupied.
![Virtual garage unlocks](images/player/virtual_garage.jpg)
## Economy Services
Economy services are server-controlled. Charges must succeed before the world
effect is applied.
![Garage service controls](images/player/garage.jpg)
### Medical
Medical services are player-funded first.
![Medical respawn screen](images/player/medical_respawn.jpg)
Billing order:
1. Player bank balance.
2. Player cash.
3. Organization funds, when allowed by the server.
4. Organization credit-line debt for the player when organization fallback is
used.
Medical respawn placement uses mission-configured medical spawn objects.
### Refuel
Refuel service is organization-funded. If the organization cannot cover the
cost, the vehicle is not refueled or the fuel level is rolled back.
Refuel is available from the garage app dashboard shown above.
### Repair
Repair service is organization-funded. The repair is only applied after the
organization charge succeeds.
Repair is available from the garage app dashboard shown above.
### Rearm
If the mission exposes rearm service through the economy or support workflow,
expect it to follow the same server-authoritative pattern: the service request
must be accepted and billed before equipment or vehicle state changes are
applied.
Rearm is available from the garage app dashboard shown above.
## Common Player Checks
If a system does not appear or does not work:
- Move closer to the interaction object.
- Confirm you are using the correct object type, such as ATM vs bank.
- Confirm your group leader has acknowledged an assigned CAD task.
- Confirm the needed store unlock has been purchased before checking VA or VG.
- Confirm the garage spawn point is clear before using the virtual garage.
- Confirm your player, cash, bank, or organization funds can cover the service.
## Related Guides
- [Mission Designer Guide](./MISSION_DESIGNER_GUIDE.md)
- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md)
- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md)
- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md)
- [Organization Usage Guide](./ORG_USAGE_GUIDE.md)
- [Store Usage Guide](./STORE_USAGE_GUIDE.md)
- [Economy Usage Guide](./ECONOMY_USAGE_GUIDE.md)

View File

@ -1,77 +0,0 @@
# Forge Documentation
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,
Rust services, repositories, and SurrealDB fit together.
- [Module Reference](./MODULE_REFERENCE.md): module inventory for gameplay
domains, extension command groups, client addons, server addons, and Rust
crates.
- [Development Guide](./DEVELOPMENT_GUIDE.md): how to add or change a module
without breaking the framework boundaries.
- [Mission Designer Guide](./MISSION_DESIGNER_GUIDE.md): how to place Eden
objects, garage markers, and CAD-compatible task modules for playable
missions.
- [Player Guide](./PLAYER_GUIDE.md): how players use CAD, phone, bank, store,
locker, garage, and economy services during missions.
- [SurrealDB Setup](./surrealdb-setup.md): where to get SurrealDB or
Surrealist and how to connect Forge to it for local or live use.
## Server and Extension Usage Guides
- [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md)
- [Bank Usage Guide](./BANK_USAGE_GUIDE.md)
- [CAD Usage Guide](./CAD_USAGE_GUIDE.md)
- [Economy Usage Guide](./ECONOMY_USAGE_GUIDE.md)
- [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md)
- [ICOM Usage Guide](./ICOM_USAGE_GUIDE.md)
- [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md)
- [Organization Usage Guide](./ORG_USAGE_GUIDE.md)
- [Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md)
- [Phone Usage Guide](./PHONE_USAGE_GUIDE.md)
- [Store Usage Guide](./STORE_USAGE_GUIDE.md)
- [Task Usage Guide](./TASK_USAGE_GUIDE.md)
## Client Usage Guides
- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md)
- [Client Main Usage Guide](./CLIENT_MAIN_USAGE_GUIDE.md)
- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)
- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md)
- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md)
- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md)
- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md)
- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md)
- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md)
- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)
## Related Documentation
- [Server Extension Docs](../arma/server/docs/README.md)
- [Server Extension API Reference](../arma/server/docs/api-reference.md)
- [Server Extension Usage Examples](../arma/server/docs/usage-examples.md)
- [Client Addon Docs](../arma/client/docs/README.md)
- [Shared Rust Libraries](../lib/README.md)
- [Repository Crate](../lib/repositories/README.md)
- [Service Crate](../lib/services/README.md)
- [Model Crate](../lib/models/README.md)

View File

@ -1,151 +0,0 @@
# Store Usage Guide
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.
## Server SQF Module
The server addon uses two long-lived module objects:
- `StorefrontStore` is the storefront workflow facade. It builds hydrate
payloads, validates checkout requests, calls the Rust `store:checkout`
command, syncs UI patches, and asks related module stores to save hot state.
- `StoreCatalogService` scans configured item and vehicle categories, builds
catalog responses, resolves checkout entries, and calculates authoritative
prices.
Editor-placed store entities are initialized by `fnc_initStore` during store
post-init. The initializer matches non-null mission namespace objects whose
variable names contain `store` and sets `isStore = true`, following the same
pattern used by garage entities.
## Checkout Model
`store:checkout` accepts one JSON context.
```json
{
"requesterUid": "76561198000000000",
"requesterName": "Player Name",
"orgId": "default",
"requesterIsDefaultOrgCeo": false,
"paymentMethod": "bank",
"items": [
{
"classname": "arifle_MX_F",
"category": "weapon",
"priceValue": 500,
"quantity": 1
}
],
"vehicles": [
{
"classname": "B_Quadbike_01_F",
"category": "cars",
"priceValue": 1500
}
]
}
```
Rules validated by the Rust service:
- `requesterUid` is required.
- At least one item or vehicle is required.
- The checkout total must be greater than zero.
- Item categories must be `item`, `attachment`, `weapon`, `magazine`, or
`backpack`.
- Vehicle categories must be `cars`, `armor`, `helis`, `planes`, `naval`, or
`other`.
- Payment method must be `cash`, `bank`, `org_funds`, or `credit_line`.
- Player locker capacity cannot exceed 25 unique items after checkout.
- Organization funds can only be charged by the org owner or the default org
CEO flag.
## Command
| Command | Arguments | Returns |
| --- | --- | --- |
| `store:checkout` | `checkout_json` | Checkout result JSON. |
## Result Model
```json
{
"chargedTotal": 2000.0,
"paymentMethod": "bank",
"message": "Checkout completed. $2,000 charged, 1 locker grant(s), 1 vehicle unlock(s).",
"lockerGranted": [],
"vehicleGranted": [],
"lockerPatch": {},
"vaPatch": {},
"vgaragePatch": {},
"bankPatch": {},
"orgPatch": {},
"orgTargetUids": []
}
```
Patch fields are intended for UI updates after checkout. The service commits
all grants and payment changes together, and attempts rollback if a later write
fails.
## Player Bank Checkout
```sqf
private _item = createHashMapFromArray [
["classname", "arifle_MX_F"],
["category", "weapon"],
["priceValue", 500],
["quantity", 1]
];
private _checkout = createHashMapFromArray [
["requesterUid", getPlayerUID player],
["requesterName", name player],
["orgId", "default"],
["requesterIsDefaultOrgCeo", false],
["paymentMethod", "bank"],
["items", [_item]],
["vehicles", []]
];
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
```
## Organization Funds Checkout
When `paymentMethod` is `org_funds`, vehicles are also added to the
organization fleet patch.
```sqf
private _vehicle = createHashMapFromArray [
["classname", "B_Quadbike_01_F"],
["category", "cars"],
["priceValue", 1500]
];
private _checkout = createHashMapFromArray [
["requesterUid", getPlayerUID player],
["requesterName", name player],
["orgId", _orgId],
["requesterIsDefaultOrgCeo", false],
["paymentMethod", "org_funds"],
["items", []],
["vehicles", [_vehicle]]
];
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
```
## Error Handling
```sqf
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
hint format ["Checkout failed: %1", _payload];
};
private _checkoutResult = fromJSON _payload;
```

View File

@ -1,140 +0,0 @@
# SurrealDB Setup
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
Use this path if you are building Forge, running a local test server, or
hosting the live Arma server.
Official SurrealDB resources:
- [SurrealDB install page](https://surrealdb.com/install)
- [SurrealDB CLI `start` reference](https://surrealdb.com/docs/surrealdb/cli/start)
Forge also includes helper scripts under `arma/server/surrealdb`:
```powershell
cd arma/server/surrealdb
.\UpdateMe.bat
.\RunMe.bat
```
On Linux or macOS:
```bash
cd arma/server/surrealdb
./setup.sh
./run.sh
```
Install SurrealDB with the official method for your platform:
```powershell
# Windows
iwr https://windows.surrealdb.com -useb | iex
```
```bash
# macOS
brew install surrealdb/tap/surreal
```
```bash
# Linux
curl -sSf https://install.surrealdb.com | sh
```
For Forge, start a persistent local database instead of the default in-memory
mode:
```powershell
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
server, set a real password and keep `config.toml` aligned.
Then copy `arma/server/extension/config.example.toml` to `config.toml` next to
`forge_server_x64.dll` and keep the values aligned with the database you
started:
```toml
[surreal]
endpoint = "127.0.0.1:8000"
namespace = "forge"
database = "main"
username = "root"
password = "root"
connect_timeout_ms = 5000
```
Before starting the game server, confirm SurrealDB is still running. After
launching the Arma server:
1. Let the extension connect and apply the Forge schema modules.
2. Verify the connection state:
```sqf
"forge_server" callExtension ["status", []];
"forge_server" callExtension ["surreal:status", []];
```
If you change the endpoint, namespace, database, username, or password in
SurrealDB, change the same values in Forge's `config.toml`.
### Mission Designer or Community Manager/Leader
Use this path if you mostly need to inspect, query, or adjust data for a test
or live server and you are not changing Forge source code.
Official SurrealDB resources:
- [Surrealist installation](https://surrealdb.com/docs/surrealist/installation)
- [Surrealist web app](https://app.surrealdb.com)
- [Surrealist local database serving](https://surrealdb.com/docs/surrealist/concepts/local-database-serving)
Recommended approach:
1. Install **Surrealist Desktop**. It is the better fit for Forge because the
official docs note that the web app can be limited when connecting to
`localhost` or non-HTTPS endpoints.
2. Connect Surrealist to the same database Forge uses.
3. Use the values from the server's `config.toml`:
```text
Endpoint: http://127.0.0.1:8000
Namespace: forge
Database: main
Username: root
Password: root
```
If you need your own local sandbox instead of connecting to an existing Forge
server, install SurrealDB first and follow the developer/server-operator path
above. Surrealist Desktop can also launch a local database for you after the
`surreal` executable is installed and available on your `PATH`.

View File

@ -1,585 +0,0 @@
# Task Usage Guide
The task module stores transient mission task metadata for active server or
mission lifecycle workflows. SQF still owns Arma-only runtime state such as
objects and participants.
The server addon at `arma/server/addons/task` also owns task execution:
creating BIS tasks, registering task entities, tracking participants, binding
task ownership, applying player/org rewards, and clearing task state when a
task completes.
Runtime dependencies:
- `forge_server_extension`
- `forge_server_common`
- `forge_server_actor`
- `forge_server_bank`
- `forge_server_org`
- `forge_client_notifications`
## Data Model
Catalog entries are flexible JSON objects. The service normalizes these fields
when a catalog entry is inserted or ownership changes:
- `taskId`
- `taskID`
- `accepted`
- `requesterUid`
- `orgID`
- `prerequisiteTaskIds`
Ownership context:
```json
{
"requesterUid": "76561198000000000",
"orgId": "default"
}
```
## Commands
| Command | Arguments | Returns |
| --- | --- | --- |
| `task:reset` | none | `true`. |
| `task:catalog:active` | none | Active catalog entry array JSON. |
| `task:catalog:get` | `task_id` | Catalog entry JSON or `null`. |
| `task:catalog:upsert` | `task_id`, `entry_json` | Stored catalog entry JSON. |
| `task:catalog:delete` | `task_id` | `true`. |
| `task:ownership:bind` | `task_id`, `ownership_json` | Ownership mutation result JSON. |
| `task:ownership:release` | `task_id` | Ownership mutation result JSON. |
| `task:ownership:accept` | `task_id`, `ownership_json` | Ownership mutation result JSON. |
| `task:ownership:reward_context` | `task_id` | Reward context JSON. |
| `task:status:set` | `task_id`, `status` | `true`. |
| `task:status:get` | `task_id` | Status string JSON. |
| `task:status:clear` | `task_id` | `true`. |
| `task:defuse:increment` | `task_id` | New counter value JSON. |
| `task:defuse:get` | `task_id` | Counter value JSON. |
| `task:clear` | `task_id` | `true`. |
## Upsert a Catalog Entry
```sqf
private _entry = createHashMapFromArray [
["title", "Destroy Cache"],
["description", "Destroy the enemy supply cache."],
["reward", 1500]
];
private _result = "forge_server" callExtension ["task:catalog:upsert", [
"task-cache-1",
toJSON _entry
]];
```
## Mark a Task Active
```sqf
"forge_server" callExtension ["task:status:set", [
"task-cache-1",
"active"
]];
private _active = "forge_server" callExtension ["task:catalog:active", []];
```
Completed statuses `succeeded` and `failed` are also stored as completed status
fallbacks. Clearing status removes active and completed state.
## Accept a Task
```sqf
private _ownership = createHashMapFromArray [
["requesterUid", getPlayerUID player],
["orgId", "default"]
];
private _result = "forge_server" callExtension ["task:ownership:accept", [
"task-cache-1",
toJSON _ownership
]];
```
`task:ownership:accept` fails if the task is not active or another requester
already accepted it.
## Rewards
```sqf
private _result = "forge_server" callExtension ["task:ownership:reward_context", [
"task-cache-1"
]];
private _context = fromJSON (_result select 0);
```
The reward context contains `requesterUid` and `orgId`.
## Server Task Flows
The task addon provides these server-owned task flows:
- `attack`
- `defend`
- `defuse`
- `delivery`
- `destroy`
- `hostage`
- `hvt`
Mission designers can create tasks in four ways:
- Eden modules for editor-authored tasks.
- `forge_task_fnc_startTask` for script-authored tasks.
- `forge_task_fnc_handler` for pre-registered entities with reputation
gating and ownership binding. This path expects the BIS task and catalog
entry to already exist if map-task and CAD visibility are required.
- Direct task function calls for server-owned or mission-authored flows that
intentionally fall back to the `default` org. This path expects the BIS task
to already exist if map-task visibility is required.
The dynamic mission manager can also generate attack tasks from config. That is
system-generated content rather than a hand-authored task creation path.
## CAD Compatibility
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
have a catalog entry and a task status of `available`, `assigned`, or `active`
before CAD can show it.
CAD assignment only reserves a task for a group. The task is accepted and task
logic starts after the assigned group leader acknowledges the assignment. If
the leader declines, the CAD assignment is removed and the task returns to the
open contract board.
CAD-compatible creation paths:
- Eden modules: compatible because they delegate to
`forge_task_fnc_startTask`.
- `forge_task_fnc_startTask`: compatible because it registers the
catalog entry, creates the BIS task, and dispatches through the handler.
- Dynamic mission manager attack tasks: compatible because the mission manager
uses `forge_task_fnc_startTask`.
Limited or incompatible paths:
- `forge_task_fnc_handler`: only compatible if a catalog entry was
already registered elsewhere. The handler sets available status and ownership,
but it does not create the BIS task shown in the map task tab or upsert the
catalog entry.
- Direct task function calls: not CAD-compatible by default. They bypass
`startTask` and usually do not register the task catalog entry or active
status that CAD hydrates from. They also only call `BIS_fnc_taskSetState` at
completion/failure; they do not create the BIS task first.
## BIS Map Task Prerequisite
Only the Eden task modules and `forge_task_fnc_startTask` create the BIS
task automatically through `BIS_fnc_taskCreate`.
If a mission uses `forge_task_fnc_handler` directly or calls a task flow
function such as `forge_task_fnc_attack`, the mission must create a BIS
task with the same task ID before the Forge task completes. Otherwise the
success/failure `BIS_fnc_taskSetState` call has no visible map task to update.
That prerequisite can be satisfied with a vanilla Eden task creation module or
a scripted `BIS_fnc_taskCreate` call. `forge_task_fnc_startTask` is the
preferred Forge path because it handles BIS task creation, Forge catalog
registration, entity registration, and handler dispatch together.
## Eden Modules
Eden task modules are the normal designer-facing path. Place the module,
configure its attributes, and sync it to the relevant entities or grouping
modules.
Available task modules:
- `FORGE_Module_Attack`: sync directly to target units or vehicles.
- `FORGE_Module_Destroy`: sync directly to objects, vehicles, or units.
- `FORGE_Module_Defuse`: sync to `FORGE_Module_Explosives` and optionally
`FORGE_Module_Protected`.
- `FORGE_Module_Delivery`: sync to `FORGE_Module_Cargo`; the cargo module syncs
to cargo objects.
- `FORGE_Module_Hostage`: sync to `FORGE_Module_Hostages` and
`FORGE_Module_Shooters`.
- `FORGE_Module_HVT`: sync directly to HVT units.
- `FORGE_Module_Defend`: configure the defense marker and wave settings; sync
enemy units to use their groups as wave templates.
These modules delegate to `forge_task_fnc_startTask`.
Each task module also includes an optional chain field:
- `Prerequisite Task IDs`: comma-separated task IDs that must succeed first.
## Mission Designer Guide
This section is the practical Eden setup guide for mission designers.
### General Rules
Use these rules for every Forge task:
1. Give every task a unique `TaskID`.
2. Use area markers for zone-style fields such as:
- `DefenseZone`
- `DeliveryZone`
- `ExtZone`
- `CBRNZone`
3. Prefer `RECTANGLE` or `ELLIPSE` markers with real size.
4. Set success and fail limits explicitly instead of relying on defaults.
5. If a task uses a timer, the countdown now waits until the assigned group
leader acknowledges the task.
6. Grouping modules such as `Explosive Entities`, `Protected Entities`,
`Cargo`, `Hostages`, and `Shooters` should be synced to real world objects,
not other logic modules.
7. To chain tasks, set `Prerequisite Task IDs` on the dependent task module.
Use comma-separated IDs such as `attack_01, delivery_02`. The dependent
task stays hidden from CAD and cannot be assigned until every listed task
succeeds.
8. Reward class fields accept comma-separated class names without brackets,
such as `ItemGPS, FirstAidKit`. Legacy SQF array strings such as
`["ItemGPS","FirstAidKit"]` are still supported.
### Attack Task
Use `FORGE_Module_Attack` when players need to eliminate hostile units or
vehicles.
Setup:
1. Place the enemy units or vehicles.
2. Place `FORGE_Module_Attack`.
3. Set `TaskID`.
4. Set `LimitSuccess` to the number of targets that must be killed.
5. Set `LimitFail` if you want a fail threshold.
6. Set rewards, rating, and optional `TimeLimit`.
7. Sync the attack module directly to the target units or vehicles.
Notes:
- This module reads its synced entities directly.
- `TimeLimit` uses seconds. `0` means no limit.
### Destroy Task
Use `FORGE_Module_Destroy` when players must destroy objects, vehicles, or
units.
Setup:
1. Place the objects, vehicles, or units that must be destroyed.
2. Place `FORGE_Module_Destroy`.
3. Set `TaskID`.
4. Set `LimitSuccess` to the number of targets that must be destroyed.
5. Set `LimitFail` if the mission should fail after too many losses.
6. Set rewards, rating, and optional `TimeLimit`.
7. Sync the destroy module directly to the targets.
Notes:
- This module reads its synced entities directly.
- `TimeLimit` uses seconds. `0` means no limit.
### Defuse Task
Use `FORGE_Module_Defuse` when players must defuse one or more explosives while
protecting other entities.
Required module layout:
```text
[Defuse Task] --> [Explosive Entities] --> explosive objects
[Defuse Task] --> [Protected Entities] --> protected objects/vehicles/units
```
Setup:
1. Place the explosive objects that players must defuse.
2. Place `FORGE_Module_Explosives`.
3. Sync each explosive object to `FORGE_Module_Explosives`.
4. Place the objects, vehicles, or units that must survive.
5. Place `FORGE_Module_Protected`.
6. Sync each protected entity to `FORGE_Module_Protected`.
7. Place `FORGE_Module_Defuse`.
8. Set `TaskID`.
9. Set `LimitSuccess` to the number of explosives that must be defused.
10. Set `LimitFail` to the number of protected entities that can be lost before failure.
11. Set `TimeLimit` to the IED countdown in seconds. This is per-IED countdown behavior, not a global mission timer.
12. Set rewards, rating, and end-state options.
13. Sync `FORGE_Module_Defuse` to `FORGE_Module_Explosives`.
14. Sync `FORGE_Module_Defuse` to `FORGE_Module_Protected`.
Notes:
- The module reads grouped objects from the `Explosive Entities` and
`Protected Entities` modules, not from direct object syncs.
- Logic objects are filtered out already, so only real explosives and protected
entities are counted.
- The ACE defuse event is wired to the task system and resolves IEDs back to
the correct task.
### Delivery Task
Use `FORGE_Module_Delivery` when players must move cargo into a delivery zone.
Required module layout:
```text
[Delivery Task] --> [Cargo] --> cargo objects
```
Setup:
1. Place the cargo objects.
2. Create an area marker for the delivery zone.
3. Place `FORGE_Module_Cargo`.
4. Sync each cargo object to `FORGE_Module_Cargo`.
5. Place `FORGE_Module_Delivery`.
6. Set `TaskID`.
7. Set `DeliveryZone` to the marker name.
8. Set `LimitSuccess` to the number of cargo objects that must arrive.
9. Set `LimitFail` to the number of cargo objects that can be damaged past the fail threshold.
10. Set rewards, rating, and optional `TimeLimit`.
11. Sync `FORGE_Module_Delivery` to `FORGE_Module_Cargo`.
Notes:
- The runtime checks `inArea DeliveryZone`, so the zone must be an area marker.
### Hostage Task
Use `FORGE_Module_Hostage` when players must rescue hostages and move them to
an extraction zone.
Required module layout:
```text
[Hostage Task] --> [Hostage Entities] --> hostage units
[Hostage Task] --> [Shooter Entities] --> hostile shooter units
```
Setup:
1. Place the hostage AI units.
2. Place the hostile shooter AI units.
3. Create an area marker for the extraction zone.
4. If using the CBRN variant, create an area marker for the `CBRNZone`.
5. Place `FORGE_Module_Hostages`.
6. Sync the hostage units to `FORGE_Module_Hostages`.
7. Place `FORGE_Module_Shooters`.
8. Sync the shooter units to `FORGE_Module_Shooters`.
9. Place `FORGE_Module_Hostage`.
10. Set `TaskID`.
11. Set `ExtZone` to the extraction marker name.
12. Set `LimitSuccess` to the number of hostages that must be rescued.
13. Set `LimitFail` to the number of hostages that can be lost before failure.
14. Set `Execution` or `CBRN` as needed for the mission variant.
15. If `CBRN` is enabled, set `CBRNZone`.
16. Set rewards, rating, and optional `TimeLimit`.
17. Sync `FORGE_Module_Hostage` to `FORGE_Module_Hostages`.
18. Sync `FORGE_Module_Hostage` to `FORGE_Module_Shooters`.
Notes:
- Hostages and shooters are filtered to real units only.
- Hostages are protected immediately on task registration to avoid startup race conditions.
- The hostage timer now waits until the assigned group leader acknowledges the
task before counting down.
- `ExtZone` is checked with `inArea`, so it must be an area marker.
### HVT Task
Use `FORGE_Module_HVT` when players must capture or eliminate a high-value
target.
Setup:
1. Place the HVT unit or units.
2. If using capture mode, create an area marker for the extraction zone.
3. Place `FORGE_Module_HVT`.
4. Set `TaskID`.
5. Set `CaptureHVT` as needed:
- enabled for capture/extract
- disabled for kill/eliminate
6. If using capture mode, set `ExtZone` to the extraction marker name.
7. Set `LimitSuccess` to the number of HVTs that must be captured or eliminated.
8. Set `LimitFail` if the mission should fail after too many HVT deaths in capture mode.
9. Set rewards, rating, and optional `TimeLimit`.
10. Sync the HVT module directly to the HVT unit or units.
Notes:
- Capture mode uses `ExtZone` with `inArea`, so use an area marker.
- Elimination mode does not require an extraction zone.
- The HVT timer now waits until the assigned group leader acknowledges the task
before counting down.
### Defend Task
Use `FORGE_Module_Defend` when players must hold an area against spawned enemy
waves.
Setup:
1. Create an area marker for the defense zone.
2. Place `FORGE_Module_Defend`.
3. Set `TaskID`.
4. Set `DefenseZone` to the defense marker name.
5. Set `DefendTime` to how long the area must be held.
6. Set `WaveCount`.
7. Set `WaveCooldown`.
8. Set `MinBlufor` to the minimum number of friendlies required in the zone.
9. Place one or more enemy groups or units to use as wave templates.
10. Sync any unit from each enemy group to the defend module.
11. Set rewards, rating, and end-state options.
Notes:
- Synced enemy units are treated as templates. Syncing one unit from a group
makes the whole group available as a wave composition.
- If no enemy units are synced, the defend task falls back to default CSAT
infantry waves.
- The defend task waits for the required number of BLUFOR to enter the zone
before the timer, waves, and empty-zone failure checks begin.
- `DefenseZone` must be an area marker.
### Quick Reference
Use direct syncs:
- `Attack Task` -> target units/vehicles
- `Destroy Task` -> target objects/vehicles/units
- `HVT Task` -> HVT units
Use grouping modules:
- `Defuse Task` -> `Explosive Entities`, `Protected Entities`
- `Delivery Task` -> `Cargo`
- `Hostage Task` -> `Hostage Entities`, `Shooter Entities`
Use area markers:
- `DefenseZone`
- `DeliveryZone`
- `ExtZone`
- `CBRNZone`
## Scripted Start Task
Use `forge_task_fnc_startTask` when creating tasks from modules,
mission scripts, or generated mission-manager content. It registers task
entities, creates the BIS task, stores the catalog entry, then dispatches
through `forge_task_fnc_handler`.
```sqf
[
"attack",
"compound_attack_01",
getPosATL leader1,
"Attack: East Compound",
"Eliminate all hostile forces.",
createHashMapFromArray [["targets", [unit1, unit2, unit3]]],
createHashMapFromArray [
["limitFail", 0],
["limitSuccess", 3],
["prerequisiteTaskIds", ["recon_01"]],
["funds", 50000],
["ratingFail", -10],
["ratingSuccess", 20],
["timeLimit", 900]
],
0,
getPlayerUID player,
"script"
] call forge_task_fnc_startTask;
```
## Chained Tasks
Use `prerequisiteTaskIds` when a task should stay hidden until one or more
other tasks succeed. The task is still registered during mission setup, but it
is stored with `locked` status, filtered out of CAD, blocked from assignment,
and its task logic does not start until every prerequisite task has completed
with `succeeded`.
```sqf
[
"delivery",
"supply_delivery_02",
getMarkerPos "delivery_zone_02",
"Deliver Medical Supplies",
"Move the cargo into the marked delivery area.",
createHashMapFromArray [["cargo", [cargoBox1, cargoBox2]]],
createHashMapFromArray [
["deliveryZone", "delivery_zone_02"],
["limitSuccess", 2],
["prerequisiteTaskIds", ["compound_attack_01"]],
["funds", 30000]
]
] call forge_task_fnc_startTask;
```
Notes:
- `prerequisiteTaskIds` accepts either a string or an array of task ID strings.
- All prerequisite tasks must succeed before the chained task unlocks.
- If a prerequisite fails or never completes, the chained task remains locked.
## Handler Calls
Use `forge_task_fnc_handler` directly when the task entities are already
registered and you want reputation gating plus ownership binding. Create the
BIS task and catalog entry separately if this task should appear in the map
task tab or CAD:
```sqf
[
"delivery",
["delivery_1", 1, 3, "delivery_zone", 250000, -75, 300, false, false, 900],
250,
getPlayerUID player
] call forge_task_fnc_handler;
```
## Direct Task Calls
Direct task function calls still work for mission-authored or server-owned
tasks, but they do not provide a requester UID. Ownership falls back to the
`default` org. Create the BIS task separately if this task should appear in the
map task tab.
## Timer Semantics
Task time limits use `0` for no limit:
- attack `timeLimit`
- destroy `timeLimit`
- delivery `timeLimit`
- hostage `timeLimit`
- HVT `timeLimit`
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.
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
to `300` seconds.
## Defuse Counter
```sqf
"forge_server" callExtension ["task:defuse:increment", ["task-cache-1"]];
private _count = "forge_server" callExtension ["task:defuse:get", ["task-cache-1"]];
```
## Error Handling
```sqf
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Task error: %1", _payload];
};
```

View File

@ -1,140 +0,0 @@
# SurrealDB Setup
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
Use this path if you are building Forge, running a local test server, or
hosting the live Arma server.
Official SurrealDB resources:
- [SurrealDB install page](https://surrealdb.com/install)
- [SurrealDB CLI `start` reference](https://surrealdb.com/docs/surrealdb/cli/start)
Forge also includes helper scripts under `arma/server/surrealdb`:
```powershell
cd arma/server/surrealdb
.\UpdateMe.bat
.\RunMe.bat
```
On Linux or macOS:
```bash
cd arma/server/surrealdb
./setup.sh
./run.sh
```
Install SurrealDB with the official method for your platform:
```powershell
# Windows
iwr https://windows.surrealdb.com -useb | iex
```
```bash
# macOS
brew install surrealdb/tap/surreal
```
```bash
# Linux
curl -sSf https://install.surrealdb.com | sh
```
For Forge, start a persistent local database instead of the default in-memory
mode:
```powershell
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
server, set a real password and keep `config.toml` aligned.
Then copy `arma/server/extension/config.example.toml` to `config.toml` next to
`forge_server_x64.dll` and keep the values aligned with the database you
started:
```toml
[surreal]
endpoint = "127.0.0.1:8000"
namespace = "forge"
database = "main"
username = "root"
password = "root"
connect_timeout_ms = 5000
```
Before starting the game server, confirm SurrealDB is still running. After
launching the Arma server:
1. Let the extension connect and apply the Forge schema modules.
2. Verify the connection state:
```sqf
"forge_server" callExtension ["status", []];
"forge_server" callExtension ["surreal:status", []];
```
If you change the endpoint, namespace, database, username, or password in
SurrealDB, change the same values in Forge's `config.toml`.
### Mission Designer or Community Manager/Leader
Use this path if you mostly need to inspect, query, or adjust data for a test
or live server and you are not changing Forge source code.
Official SurrealDB resources:
- [Surrealist installation](https://surrealdb.com/docs/surrealist/installation)
- [Surrealist web app](https://app.surrealdb.com)
- [Surrealist local database serving](https://surrealdb.com/docs/surrealist/concepts/local-database-serving)
Recommended approach:
1. Install **Surrealist Desktop**. It is the better fit for Forge because the
official docs note that the web app can be limited when connecting to
`localhost` or non-HTTPS endpoints.
2. Connect Surrealist to the same database Forge uses.
3. Use the values from the server's `config.toml`:
```text
Endpoint: http://127.0.0.1:8000
Namespace: forge
Database: main
Username: root
Password: root
```
If you need your own local sandbox instead of connecting to an existing Forge
server, install SurrealDB first and follow the developer/server-operator path
above. Surrealist Desktop can also launch a local database for you after the
`surreal` executable is installed and available on your `PATH`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 941 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Some files were not shown because too many files have changed in this diff Show More