Add PMC simulator mission pack and setup flow
- Add mission config, params, and function registrations - Implement faction helpers and mission generators - Include setup docs, CBA settings, and mission scaffolding
1
.gitignore
vendored
@ -36,4 +36,3 @@ Thumbs.db
|
||||
arma/ui/map-viewer/
|
||||
arma/server/surrealdb/forge.db/
|
||||
promo/
|
||||
arma/forge_pmc_simulator.Tanoa/
|
||||
|
||||
41
arma/forge_pmc_simulator.Tanoa/CfgEnemyFactions.hpp
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Enemy faction options for the PMC simulator setup flow.
|
||||
*
|
||||
* Consumers:
|
||||
* - forge_pmc_fnc_getEnemyFactionOptions reads this config for the startup UI.
|
||||
* - forge_pmc_fnc_resolveEnemyFactionParam maps mission param values back to
|
||||
* faction classnames during fallback/default setup.
|
||||
* - Mission generators use ENEMY_FACTION_STR and ENEMY_SIDE after setup has
|
||||
* applied the selected option.
|
||||
*
|
||||
* Keep this list aligned with the server modpack. Classnames that are not
|
||||
* present in CfgFactionClasses may still appear in the UI, but downstream unit
|
||||
* pool generation may fall back or fail to find units.
|
||||
*
|
||||
* BLUFOR/WEST factions are intentionally omitted because generated missions use
|
||||
* these options as opposing forces.
|
||||
*/
|
||||
class CfgEnemyFactions {
|
||||
/*
|
||||
* Option format:
|
||||
* - class name: stable config entry name, normally matching the faction.
|
||||
* - value: legacy mission param numeric value.
|
||||
* - faction: CfgFactionClasses classname used for spawning.
|
||||
* - display: user-facing label shown in the setup UI.
|
||||
*/
|
||||
class Options {
|
||||
class OPF_F { value = 0; faction = "OPF_F"; display = "CSAT"; };
|
||||
class OPF_T_F { value = 1; faction = "OPF_T_F"; display = "CSAT (Pacific)"; };
|
||||
class OPF_V_F { value = 2; faction = "OPF_V_F"; display = "Viper"; };
|
||||
class OPF_R_F { value = 3; faction = "OPF_R_F"; display = "Spetnaz"; };
|
||||
class OPF_SFIA_lxWS { value = 4; faction = "OPF_SFIA_lxWS"; display = "SFIA"; };
|
||||
class OPF_TURA_lxWS { value = 5; faction = "OPF_TURA_lxWS"; display = "Tura"; };
|
||||
class IND_F { value = 6; faction = "IND_F"; display = "AAF"; };
|
||||
class IND_G_F { value = 7; faction = "IND_G_F"; display = "FIA"; };
|
||||
class IND_E_F { value = 8; faction = "IND_E_F"; display = "LDF"; };
|
||||
class IND_C_F { value = 9; faction = "IND_C_F"; display = "Syndikat"; };
|
||||
class IND_L_F { value = 10; faction = "IND_L_F"; display = "Looters"; };
|
||||
class IND_SFIA_lxWS { value = 11; faction = "IND_SFIA_lxWS"; display = "SFIA"; };
|
||||
class IND_TURA_lxWS { value = 12; faction = "IND_TURA_lxWS"; display = "Tura"; };
|
||||
};
|
||||
};
|
||||
31
arma/forge_pmc_simulator.Tanoa/CfgFactionUnitMap.hpp
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Optional faction-to-unit template map.
|
||||
*
|
||||
* Current behavior:
|
||||
* - Enemy unit pools are primarily built from CfgGroups/CfgVehicles through
|
||||
* forge_pmc_fnc_getEnemyFactionUnitPool.
|
||||
* - This config is a template for deterministic per-faction unit pools if the
|
||||
* automatic faction lookup is not specific enough for a server/modpack.
|
||||
*
|
||||
* To enable this map, wire it into forge_pmc_fnc_getEnemyFactionUnitPool or the
|
||||
* generator spawn path before falling back to config traversal.
|
||||
*/
|
||||
class CfgFactionUnitMap {
|
||||
/*
|
||||
* Mapping key should match the selected faction classname from
|
||||
* CfgEnemyFactions.Options[].faction, 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}; };
|
||||
};
|
||||
};
|
||||
};
|
||||
45
arma/forge_pmc_simulator.Tanoa/CfgFunctions.hpp
Normal file
@ -0,0 +1,45 @@
|
||||
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 updateEnemyCountFromActivePlayers {};
|
||||
};
|
||||
|
||||
class missionGenerators {
|
||||
file = "functions\missionGenerators";
|
||||
class attackMissionGenerator {};
|
||||
class defendMissionGenerator {};
|
||||
class destroyMissionGenerator {};
|
||||
class deliveryMissionGenerator {};
|
||||
class defuseMissionGenerator {};
|
||||
class hostageMissionGenerator {};
|
||||
class hvtMissionGenerator {};
|
||||
class captureHvtMissionGenerator {};
|
||||
};
|
||||
};
|
||||
};
|
||||
223
arma/forge_pmc_simulator.Tanoa/CfgMissions.hpp
Normal file
@ -0,0 +1,223 @@
|
||||
/*
|
||||
* 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};
|
||||
};
|
||||
};
|
||||
};
|
||||
82
arma/forge_pmc_simulator.Tanoa/CfgParams.hpp
Normal file
@ -0,0 +1,82 @@
|
||||
class Params {
|
||||
class enemyFaction {
|
||||
title = "Enemy Faction";
|
||||
values[] = {0,1,2,3,4,5,6,7,8,9,10,11,12};
|
||||
texts[] = {
|
||||
"CSAT",
|
||||
"CSAT (Pacific)",
|
||||
"Viper",
|
||||
"Spetnaz",
|
||||
"SFIA (OPFOR)",
|
||||
"Tura (OPFOR)",
|
||||
"AAF",
|
||||
"FIA",
|
||||
"LDF",
|
||||
"Syndikat",
|
||||
"Looters",
|
||||
"SFIA (Independent)",
|
||||
"Tura (Independent)"
|
||||
};
|
||||
default = 7;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
};
|
||||
136
arma/forge_pmc_simulator.Tanoa/TODO.md
Normal file
@ -0,0 +1,136 @@
|
||||
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
|
||||
5
arma/forge_pmc_simulator.Tanoa/cba_settings.sqf
Normal file
@ -0,0 +1,5 @@
|
||||
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;
|
||||
1
arma/forge_pmc_simulator.Tanoa/config.h
Normal file
@ -0,0 +1 @@
|
||||
#include "CfgFunctions.h"
|
||||
41
arma/forge_pmc_simulator.Tanoa/description.ext
Normal file
@ -0,0 +1,41 @@
|
||||
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 "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;
|
||||
};
|
||||
};
|
||||
};
|
||||
15
arma/forge_pmc_simulator.Tanoa/functions/helpers/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# 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` reads configured faction options from `CfgEnemyFactions`.
|
||||
- `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
|
||||
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.
|
||||
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Returns candidate enemy faction classnames available to the mission.
|
||||
* This is a runtime helper only; description.ext mission params still use
|
||||
* the static CfgEnemyFactions list.
|
||||
*
|
||||
* 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
|
||||
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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
|
||||
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Reads enemy faction options from missionConfigFile >> CfgEnemyFactions
|
||||
* >> Options for setup UI hydration and mission param fallback resolution.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Enemy faction options as [factionClassname, displayName, paramValue] <ARRAY>
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
private _optionsConfig = missionConfigFile >> "CfgEnemyFactions" >> "Options";
|
||||
private _options = [];
|
||||
|
||||
if (isClass _optionsConfig) then {
|
||||
{
|
||||
private _configName = configName _x;
|
||||
private _value = getNumber (_x >> "value");
|
||||
private _faction = getText (_x >> "faction");
|
||||
private _display = getText (_x >> "display");
|
||||
|
||||
if (_faction isEqualTo "") then {
|
||||
_faction = _configName;
|
||||
};
|
||||
|
||||
if (!isClass (configFile >> "CfgFactionClasses" >> _faction) && {
|
||||
isClass (configFile >> "CfgFactionClasses" >> _configName)
|
||||
}) then {
|
||||
_faction = _configName;
|
||||
};
|
||||
|
||||
if (_display isEqualTo "") then {
|
||||
_display = _faction;
|
||||
};
|
||||
|
||||
if (_faction isNotEqualTo "") then {
|
||||
_options pushBack [_faction, _display, _value];
|
||||
};
|
||||
} forEach ("true" configClasses _optionsConfig);
|
||||
};
|
||||
|
||||
if (_options isEqualTo []) then {
|
||||
_options = [
|
||||
["OPF_F", "CSAT", 0],
|
||||
["IND_G_F", "FIA", 7]
|
||||
];
|
||||
};
|
||||
|
||||
_options
|
||||
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 _sideNumber = getNumber (_cfgFaction >> "side");
|
||||
private _sideText = toUpperANSI getText (_cfgFaction >> "side");
|
||||
|
||||
if (_sideNumber > 0 || { _sideText isEqualTo "0" }) then {
|
||||
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
|
||||
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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)
|
||||
*
|
||||
* 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]]
|
||||
];
|
||||
|
||||
if (_faction isEqualTo "") then {
|
||||
_faction = "IND_G_F";
|
||||
};
|
||||
|
||||
private _pool = [];
|
||||
private _sideNumber = [_fallbackSide] call BIS_fnc_sideID;
|
||||
|
||||
{
|
||||
if (getNumber (_x >> "scope") < 2) then { continue; };
|
||||
if (getText (_x >> "faction") isNotEqualTo _faction) 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 []) 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
|
||||
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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]
|
||||
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Populates a listbox or combo control with CfgEnemyFactions 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
|
||||
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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: 7)
|
||||
* 1: Fallback faction classname <STRING> (Default: "IND_G_F")
|
||||
*
|
||||
* Return Value:
|
||||
* Faction classname <STRING>
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
params [
|
||||
["_value", 7, [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
|
||||
@ -0,0 +1,29 @@
|
||||
# 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.
|
||||
|
||||
@ -0,0 +1,413 @@
|
||||
/*
|
||||
* 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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 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";
|
||||
@ -0,0 +1,440 @@
|
||||
/*
|
||||
* 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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 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];
|
||||
|
||||
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";
|
||||
@ -0,0 +1,380 @@
|
||||
/*
|
||||
* 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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 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];
|
||||
|
||||
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";
|
||||
@ -0,0 +1,510 @@
|
||||
/*
|
||||
* 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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 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";
|
||||
@ -0,0 +1,359 @@
|
||||
/*
|
||||
* 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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 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 _deliveryZone = format ["forge_delivery_zone_%1", _taskID];
|
||||
private _worldSize = worldSize;
|
||||
private _center = [_worldSize / 2, _worldSize / 2, 0];
|
||||
private _deliveryPos = [0, 0, 0];
|
||||
private _attempt = 0;
|
||||
while { _attempt < 80 && { _deliveryPos isEqualTo [0, 0, 0] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _candidate = [_center, 0, _worldSize / 2 - 1000, 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 [_deliveryZone, _deliveryPos];
|
||||
_deliveryZone setMarkerShape "ELLIPSE";
|
||||
_deliveryZone setMarkerSize [25, 25];
|
||||
_deliveryZone setMarkerText format ["Delivery Zone %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 _deliveryZone;
|
||||
""
|
||||
};
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
_activeMissionRegistry set [_taskID, createHashMapFromArray [
|
||||
["generatorType", _self call ["getGeneratorType", []]],
|
||||
["position", _position],
|
||||
["markers", [_deliveryZone]],
|
||||
["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";
|
||||
@ -0,0 +1,466 @@
|
||||
/*
|
||||
* 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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 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";
|
||||
@ -0,0 +1,644 @@
|
||||
/*
|
||||
* 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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 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, 30, 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 _candidate = [_position, 2000, 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, 2000, 0] call BIS_fnc_findSafePos;
|
||||
if (_safe isEqualTo [0, 0, 0]) 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];
|
||||
|
||||
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";
|
||||
@ -0,0 +1,440 @@
|
||||
/*
|
||||
* 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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 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 _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];
|
||||
|
||||
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],
|
||||
["extractionZone", _extZone],
|
||||
["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 {
|
||||
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 "KillHvtMissionGeneratorBaseClass";
|
||||
|
||||
HvtMissionGenerator = createHashMapObject [KillHvtMissionGeneratorBaseClass];
|
||||
publicVariable "HvtMissionGenerator";
|
||||
@ -0,0 +1,16 @@
|
||||
# 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_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.
|
||||
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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;
|
||||
@ -0,0 +1,255 @@
|
||||
/*
|
||||
* 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;
|
||||
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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;
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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
|
||||
@ -0,0 +1,20 @@
|
||||
# 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 client opens the setup UI from `initPlayerLocal.sqf` when `forge_pmc_missionSettingsApplied` is not set.
|
||||
|
||||
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`.
|
||||
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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 _payload = createHashMapFromArray [
|
||||
["factions", _factions],
|
||||
["settings", createHashMapFromArray [
|
||||
["enemyFaction", "IND_G_F"],
|
||||
["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
|
||||
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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
|
||||
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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", 7] call BIS_fnc_getParamValue;
|
||||
private _enemyFaction = _overrides getOrDefault ["enemyFaction", ""];
|
||||
if (_enemyFaction isEqualTo "") then {
|
||||
if (_enemyFactionParam isEqualTo -1) then {
|
||||
_enemyFactionParam = 7;
|
||||
};
|
||||
_enemyFaction = [_enemyFactionParam, "IND_G_F"] 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;
|
||||
143
arma/forge_pmc_simulator.Tanoa/guides/ACTOR_USAGE_GUIDE.md
Normal file
@ -0,0 +1,143 @@
|
||||
# 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;
|
||||
```
|
||||
570
arma/forge_pmc_simulator.Tanoa/guides/AI_UI_Prompt_Guide.md
Normal file
@ -0,0 +1,570 @@
|
||||
# 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
|
||||
190
arma/forge_pmc_simulator.Tanoa/guides/BANK_USAGE_GUIDE.md
Normal file
@ -0,0 +1,190 @@
|
||||
# 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;
|
||||
```
|
||||
191
arma/forge_pmc_simulator.Tanoa/guides/CAD_USAGE_GUIDE.md
Normal file
@ -0,0 +1,191 @@
|
||||
# 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];
|
||||
};
|
||||
```
|
||||
@ -0,0 +1,98 @@
|
||||
# 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)
|
||||
@ -0,0 +1,89 @@
|
||||
# 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)
|
||||
111
arma/forge_pmc_simulator.Tanoa/guides/CLIENT_CAD_USAGE_GUIDE.md
Normal file
@ -0,0 +1,111 @@
|
||||
# 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)
|
||||
@ -0,0 +1,92 @@
|
||||
# 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)
|
||||
@ -0,0 +1,114 @@
|
||||
# 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)
|
||||
@ -0,0 +1,87 @@
|
||||
# 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)
|
||||
@ -0,0 +1,48 @@
|
||||
# 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)
|
||||
@ -0,0 +1,74 @@
|
||||
# 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)
|
||||
131
arma/forge_pmc_simulator.Tanoa/guides/CLIENT_ORG_USAGE_GUIDE.md
Normal file
@ -0,0 +1,131 @@
|
||||
# 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)
|
||||
@ -0,0 +1,108 @@
|
||||
# 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)
|
||||
@ -0,0 +1,92 @@
|
||||
# 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)
|
||||
125
arma/forge_pmc_simulator.Tanoa/guides/CLIENT_USAGE_GUIDE.md
Normal file
@ -0,0 +1,125 @@
|
||||
# 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.
|
||||
134
arma/forge_pmc_simulator.Tanoa/guides/DEVELOPMENT_GUIDE.md
Normal file
@ -0,0 +1,134 @@
|
||||
# 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.
|
||||
95
arma/forge_pmc_simulator.Tanoa/guides/ECONOMY_USAGE_GUIDE.md
Normal file
@ -0,0 +1,95 @@
|
||||
# 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.
|
||||
145
arma/forge_pmc_simulator.Tanoa/guides/FRAMEWORK_ARCHITECTURE.md
Normal file
@ -0,0 +1,145 @@
|
||||
# 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
|
||||
|
||||

|
||||
|
||||
```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", []];
|
||||
```
|
||||
212
arma/forge_pmc_simulator.Tanoa/guides/GARAGE_USAGE_GUIDE.md
Normal file
@ -0,0 +1,212 @@
|
||||
# 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.
|
||||
195
arma/forge_pmc_simulator.Tanoa/guides/ICOM_USAGE_GUIDE.md
Normal file
@ -0,0 +1,195 @@
|
||||
# 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.
|
||||
203
arma/forge_pmc_simulator.Tanoa/guides/LOCKER_USAGE_GUIDE.md
Normal file
@ -0,0 +1,203 @@
|
||||
# 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.
|
||||
750
arma/forge_pmc_simulator.Tanoa/guides/MISSION_DESIGNER_GUIDE.md
Normal file
@ -0,0 +1,750 @@
|
||||
# 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.
|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

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

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Capture HVT example:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||
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)
|
||||
221
arma/forge_pmc_simulator.Tanoa/guides/MODULE_REFERENCE.md
Normal file
@ -0,0 +1,221 @@
|
||||
# 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. |
|
||||
239
arma/forge_pmc_simulator.Tanoa/guides/ORG_USAGE_GUIDE.md
Normal file
@ -0,0 +1,239 @@
|
||||
# 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];
|
||||
};
|
||||
```
|
||||
@ -0,0 +1,158 @@
|
||||
# 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];
|
||||
};
|
||||
```
|
||||
136
arma/forge_pmc_simulator.Tanoa/guides/PHONE_USAGE_GUIDE.md
Normal file
@ -0,0 +1,136 @@
|
||||
# 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];
|
||||
};
|
||||
```
|
||||
318
arma/forge_pmc_simulator.Tanoa/guides/PLAYER_GUIDE.md
Normal file
@ -0,0 +1,318 @@
|
||||
# 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.
|
||||
|
||||

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

|
||||
|
||||
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:
|
||||
|
||||

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

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

|
||||
|
||||
### Messages
|
||||
|
||||
Messages are short player-to-player conversations.
|
||||
|
||||

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

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
- view account information
|
||||
- transfer funds
|
||||
- deposit earnings
|
||||
- change PIN
|
||||
|
||||
Use an ATM for limited account access:
|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||

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

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

|
||||
|
||||
## Garage and Virtual Garage
|
||||
|
||||
The garage stores physical player vehicles that have been saved from the world.
|
||||
|
||||

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

|
||||
|
||||
## Economy Services
|
||||
|
||||
Economy services are server-controlled. Charges must succeed before the world
|
||||
effect is applied.
|
||||
|
||||

|
||||
|
||||
### Medical
|
||||
|
||||
Medical services are player-funded first.
|
||||
|
||||

|
||||
|
||||
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)
|
||||
77
arma/forge_pmc_simulator.Tanoa/guides/README.md
Normal file
@ -0,0 +1,77 @@
|
||||
# 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)
|
||||
151
arma/forge_pmc_simulator.Tanoa/guides/STORE_USAGE_GUIDE.md
Normal file
@ -0,0 +1,151 @@
|
||||
# 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;
|
||||
```
|
||||
140
arma/forge_pmc_simulator.Tanoa/guides/SURREALDB_SETUP.md
Normal file
@ -0,0 +1,140 @@
|
||||
# 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`.
|
||||
585
arma/forge_pmc_simulator.Tanoa/guides/TASK_USAGE_GUIDE.md
Normal file
@ -0,0 +1,585 @@
|
||||
# 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];
|
||||
};
|
||||
```
|
||||
140
arma/forge_pmc_simulator.Tanoa/guides/surrealdb-setup.md
Normal file
@ -0,0 +1,140 @@
|
||||
# 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`.
|
||||
BIN
arma/forge_pmc_simulator.Tanoa/img/000.jpg
Normal file
|
After Width: | Height: | Size: 435 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/001.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/002.jpg
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/003.jpg
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/004.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/005.jpg
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/006.jpg
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/007.jpg
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/010.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/011.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/012.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/013.jpg
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/Datasheet_EKM4000.pdf
Normal file
BIN
arma/forge_pmc_simulator.Tanoa/img/StopTutti.jpg
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/a011.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/a012.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/a013.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/a014.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/armi02.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/armi03.jpg
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/eng2.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/eng3.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/lavagna01.jpg
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/lavagna02.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/lavagna03.jpg
Normal file
|
After Width: | Height: | Size: 941 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/lavagna04.jpg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/medical01.jpg
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/medical02.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/medical03.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
arma/forge_pmc_simulator.Tanoa/img/medical04.jpg
Normal file
|
After Width: | Height: | Size: 140 KiB |