Discover enemy factions dynamically from loaded mods

- Scan CfgFactionClasses/CfgVehicles at runtime for selectable enemy factions
- Add CfgFactionUnitMap overrides and keep legacy params as fallback
This commit is contained in:
Jacob Schmidt 2026-05-23 01:23:14 -05:00
parent 256ce0d5af
commit 8582e6c5e5
27 changed files with 322 additions and 151 deletions

View File

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

View File

@ -1,19 +1,22 @@
/*
* Optional faction-to-unit template map.
* Optional faction-to-unit override 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.
* - forge_pmc_fnc_getEnemyFactionOptions treats a mapped faction as selectable
* when at least one mapped vehicle exists.
* - forge_pmc_fnc_getEnemyFactionUnitPool checks this map first.
* - If a selected faction has a class here, the listed Units are used as the
* deterministic spawn pool for generated mission enemies.
* - If no class exists here, the helper falls back to CfgVehicles traversal for
* units whose faction and side match the selected faction.
*
* To enable this map, wire it into forge_pmc_fnc_getEnemyFactionUnitPool or the
* generator spawn path before falling back to config traversal.
* Most mod factions do not need an entry here. Add a class only when a faction
* needs a curated or corrected spawn pool.
*/
class CfgFactionUnitMap {
/*
* Mapping key should match the selected faction classname from
* CfgEnemyFactions.Options[].faction, such as "IND_G_F".
* CfgFactionClasses, such as "IND_G_F".
*/
class IND_G_F {
/*

View File

@ -1,11 +1,18 @@
/*
* Mission lobby fallback params.
*
* The startup setup UI now discovers selectable factions dynamically from the
* active modset. Params remain intentionally static because Arma evaluates
* them before mission runtime scripts can scan loaded factions. If the setup UI
* is cancelled or never opened, these values provide the default fallback.
*/
class Params {
class enemyFaction {
title = "Enemy Faction";
values[] = {0,1,2,3,4,5,6,7,8,9,10,11,12};
values[] = {0,1,2,3,4,5,6,7,8,9,10};
texts[] = {
"CSAT",
"CSAT (Pacific)",
"Viper",
"Spetnaz",
"SFIA (OPFOR)",
"Tura (OPFOR)",
@ -14,10 +21,9 @@ class Params {
"LDF",
"Syndikat",
"Looters",
"SFIA (Independent)",
"Tura (Independent)"
};
default = 7;
default = 6;
};
class maxConcurrentMissions {

View File

@ -1 +0,0 @@
#include "CfgFunctions.h"

View File

@ -24,6 +24,7 @@ corpseManagerMode = 0;
#include "CfgParams.hpp"
#include "CfgEnemyFactions.hpp"
#include "CfgFactionUnitMap.hpp"
#include "CfgMissions.hpp"
#include "CfgFunctions.hpp"
#include "ui\baseControls.hpp"

View File

@ -4,7 +4,7 @@ Helper functions provide reusable lookups and conversions for the PMC simulator
## 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_getEnemyFactionOptions` scans the active modset for selectable OPFOR/Independent factions and applies `CfgEnemyFactions` filters/labels.
- `forge_pmc_fnc_resolveEnemyFactionParam` converts a mission parameter value into a faction classname.
- `forge_pmc_fnc_getEnemyFactionSide` resolves a faction classname to an Arma side.
- `forge_pmc_fnc_getEnemyFactionUnitPool` builds the unit pool used by generated enemy spawns.
@ -12,4 +12,8 @@ Helper functions provide reusable lookups and conversions for the PMC simulator
- `forge_pmc_fnc_getEnemyFactionListboxSelection` and `forge_pmc_fnc_populateEnemyFactionListbox` support faction picker UI/listbox flows.
## Notes
The mission setup UI is populated dynamically from loaded `CfgFactionClasses` and `CfgVehicles`. `CfgEnemyFactions` only controls allowed sides, denylisted factions, and friendly labels/order for known factions.
`forge_pmc_fnc_getEnemyFactionUnitPool` checks `CfgFactionUnitMap` first. Add a faction class there when the automatic `CfgVehicles` faction lookup is too broad, too sparse, or needs a curated unit pool.
These functions are registered under the `forge_pmc` tag, so their public names do not include the folder name. Moving a helper within this folder should not change callers as long as `CfgFunctions.hpp` remains updated.

View File

@ -1,8 +1,8 @@
/*
* 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.
* This is a runtime helper only. The setup UI uses
* forge_pmc_fnc_getEnemyFactionOptions so it can filter to spawnable factions.
*
* Arguments:
* 0: Exclude BLUFOR/WEST factions <BOOL> (Default: true)

View File

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

View File

@ -25,10 +25,15 @@ if (_f isEqualTo "") exitWith { _fallbackSide };
private _side = _fallbackSide;
private _cfgFaction = configFile >> "CfgFactionClasses" >> _enemyFaction;
if (isClass _cfgFaction) then {
private _sideNumber = getNumber (_cfgFaction >> "side");
private _hasSideNumber = isNumber (_cfgFaction >> "side");
private _sideNumber = if (_hasSideNumber) then { getNumber (_cfgFaction >> "side") } else { -1 };
private _sideText = toUpperANSI getText (_cfgFaction >> "side");
if (_sideNumber > 0 || { _sideText isEqualTo "0" }) then {
if (_hasSideNumber || { _sideText in ["0", "1", "2"] }) then {
if (!_hasSideNumber) then {
_sideNumber = parseNumber _sideText;
};
switch (_sideNumber) do {
case 0: { _side = east; };
case 2: { _side = resistance; };

View File

@ -6,6 +6,8 @@
* Arguments:
* 0: Faction classname <STRING> (Default: ENEMY_FACTION_STR or "IND_G_F")
* 1: Fallback side <SIDE> (Default: ENEMY_SIDE or east)
* 2: Allow side-default fallback units when no faction units exist <BOOL>
* (Default: true)
*
* Return Value:
* Unit definitions with vehicle, rank, and position keys <ARRAY>
@ -15,7 +17,8 @@
params [
["_faction", missionNamespace getVariable ["ENEMY_FACTION_STR", "IND_G_F"], [""]],
["_fallbackSide", missionNamespace getVariable ["ENEMY_SIDE", east], [east]]
["_fallbackSide", missionNamespace getVariable ["ENEMY_SIDE", east], [east]],
["_allowSideFallback", true, [false]]
];
if (_faction isEqualTo "") then {
@ -25,33 +28,56 @@ if (_faction isEqualTo "") then {
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; };
// Check CfgFactionUnitMap first for explicit faction unit definitions
private _factionMapConfig = missionConfigFile >> "CfgFactionUnitMap" >> _faction;
if (isClass _factionMapConfig) then {
{
private _vehicle = getText (_x >> "vehicle");
if (_vehicle isEqualTo "" || { !(isClass (configFile >> "CfgVehicles" >> _vehicle)) }) then {
continue;
};
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"));
_pool pushBack createHashMapFromArray [
["vehicle", _vehicle],
["rank", getText (_x >> "rank")],
["position", getArray (_x >> "position")]
];
} forEach ("true" configClasses (_factionMapConfig >> "Units"));
};
// Fall back to config traversal if no explicit mapping exists.
if (_pool isEqualTo []) then {
private _factionFallback = _faction;
{
if (getNumber (_x >> "scope") < 2) then { continue; };
private _unitFaction = getText (_x >> "faction");
if ((_unitFaction isNotEqualTo _faction) && (_unitFaction isNotEqualTo _factionFallback)) then { continue; };
if (getNumber (_x >> "side") isNotEqualTo _sideNumber) then { continue; };
if !(configName _x isKindOf "CAManBase") then { continue; };
private _className = configName _x;
private _upperClassName = toUpperANSI _className;
private _rank = "PRIVATE";
if (
(_upperClassName find "_SL_" >= 0)
|| { _upperClassName find "_TL_" >= 0 }
|| { _upperClassName find "OFFICER" >= 0 }
|| { _upperClassName find "COMMANDER" >= 0 }
) then {
_rank = "SERGEANT";
};
_pool pushBack createHashMapFromArray [
["vehicle", _className],
["rank", _rank],
["position", [0, 0, 0]]
];
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
};
if (_pool isEqualTo [] && { _allowSideFallback }) then {
private _fallbackUnits = switch (_fallbackSide) do {
case east: { ["O_Soldier_SL_F", "O_Soldier_TL_F", "O_Soldier_F", "O_Soldier_AR_F", "O_Soldier_GL_F", "O_medic_F"] };
case resistance: { ["I_G_Soldier_SL_F", "I_G_Soldier_TL_F", "I_G_Soldier_F", "I_G_Soldier_AR_F", "I_G_medic_F"] };

View File

@ -1,6 +1,7 @@
/*
* Author: IDSolutions, Blackbox AI, MrPākehā
* Populates a listbox or combo control with CfgEnemyFactions options.
* Populates a listbox or combo control with dynamically discovered enemy
* faction options.
*
* Arguments:
* 0: Listbox/combo control or display <CONTROL|DISPLAY>

View File

@ -4,7 +4,7 @@
* enemy faction classname.
*
* Arguments:
* 0: Param value from Params::enemyFaction <NUMBER|STRING> (Default: 7)
* 0: Param value from Params::enemyFaction <NUMBER|STRING> (Default: 6)
* 1: Fallback faction classname <STRING> (Default: "IND_G_F")
*
* Return Value:
@ -14,7 +14,7 @@
*/
params [
["_value", 7, [0, ""]],
["_value", 6, [0, ""]],
["_fallback", "IND_G_F", [""]]
];

View File

@ -131,7 +131,7 @@ AttackMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
} forEach _blkListMarkers;
if (!_inBlkList) then {
if !(_inBlkList) then {
_taskPos = _candidate;
};
};

View File

@ -131,7 +131,7 @@ CaptureHvtMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
} forEach _blkListMarkers;
if (!_inBlkList) then {
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
@ -152,7 +152,7 @@ CaptureHvtMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
private _buildingPositions = [];
if (!isNull _building) then {
if !(isNull _building) then {
for "_i" from 0 to 100 do {
private _buildingPos = _building buildingPos _i;
if (_buildingPos isEqualTo [0, 0, 0]) exitWith {};
@ -214,7 +214,7 @@ CaptureHvtMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
selectRandom _buildingPositions
};
private _escort = _group createUnit [_escortClass, _escortPos, [], 0, "NONE"];
if (!isNull _escort) then {
if !(isNull _escort) then {
_escort setRank (_escortDef getOrDefault ["rank", "PRIVATE"]);
_escortUnits pushBack _escort;
};

View File

@ -131,7 +131,7 @@ DefendMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
} forEach _blkListMarkers;
if (!_inBlkList) then {
if !(_inBlkList) then {
_taskPos = _candidate;
};
};

View File

@ -132,7 +132,7 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
} forEach _blkListMarkers;
if (!_inBlkList) then {
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
@ -297,7 +297,7 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
if (_nearBuildings isNotEqualTo []) then {
// prefer the closest building that actually contains the position
{
if (!isNull _x && { _position inArea _x }) exitWith {
if !(isNull _x && { _position inArea _x }) exitWith {
_building = _x;
};
} forEach _nearBuildings;
@ -313,14 +313,14 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
};
if (!isNull _building) then {
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 isEqualType [])) then { continue; };
if ((_candidate vectorDistance _position) <= 60) exitWith {
_buildingPos = _candidate;
};
@ -328,7 +328,7 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
private _protectedPos = [0,0,0];
if (!(_buildingPos isEqualTo [])) then {
if !((_buildingPos isEqualTo [])) then {
_protectedPos = _buildingPos;
} else {
// Outdoor fallback: keep previous behavior
@ -337,7 +337,7 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
private _protectedObject = createVehicle [_protectedClass, _protectedPos, [], 0, "NONE"];
private _protectedObjects = [];
if (!isNull _protectedObject) then {
if !(isNull _protectedObject) then {
_protectedObjects pushBack _protectedObject;
};
@ -356,7 +356,7 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
private _devicePos = _protectedPos vectorAdd _deviceOffset;
private _deviceObject = createVehicle [_deviceClass, _devicePos, [], 0, "NONE"];
if (!isNull _deviceObject) then {
if !(isNull _deviceObject) then {
_devices pushBack _deviceObject;
};
};

View File

@ -131,7 +131,7 @@ DeliveryMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
} forEach _blkListMarkers;
if (!_inBlkList) then {
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
@ -193,22 +193,22 @@ DeliveryMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
if (_cargoPool isEqualTo []) exitWith { [] };
private _cargoSpawnObj = objNull;
if (!isNil "cargoSpawn") then { _cargoSpawnObj = cargoSpawn; };
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 !(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 {
if !(isNull _cargoSpawnObj) then {
private _basePos = getPosATL _cargoSpawnObj;
_spawnPos = _basePos vectorAdd [(random 12 - 6), (random 12 - 6), 0];
} else {
if (!isNull _extZoneObj) then {
if !(isNull _extZoneObj) then {
private _basePos = getPosATL _extZoneObj;
_spawnPos = _basePos vectorAdd [(random 12 - 6), (random 12 - 6), 0];
} else {
@ -217,7 +217,7 @@ DeliveryMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
private _cargoObject = createVehicle [_cargoClass, _spawnPos, [], 0, "NONE"];
if (!isNull _cargoObject) then {
if !(isNull _cargoObject) then {
_cargoObjects pushBack _cargoObject;
};
};

View File

@ -132,7 +132,7 @@ DestroyMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
} forEach _blkListMarkers;
if (!_inBlkList) then {
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
@ -349,13 +349,13 @@ DestroyMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
_position
]] call forge_server_common_fnc_log;
if (!isNull _defendGroup) then {
if !(isNull _defendGroup) then {
deleteGroup _defendGroup;
};
};
private _spawnedGroups = [_group];
if (!isNull _defendGroup && { units _defendGroup isNotEqualTo [] }) then {
if !(isNull _defendGroup && { units _defendGroup isNotEqualTo [] }) then {
_spawnedGroups pushBack _defendGroup;
};
@ -442,7 +442,7 @@ DestroyMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
private _position = _missionRecord getOrDefault ["position", []];
private _groups = _missionRecord getOrDefault ["groups", []];
{
if (!isNull _x) then {
if !(isNull _x) then {
{ deleteVehicle _x; } forEach (units _x);
deleteGroup _x;
};

View File

@ -132,7 +132,7 @@ HostageMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
} forEach _blkListMarkers;
if (!_inBlkList) then {
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
@ -155,7 +155,7 @@ HostageMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
private _buildingPositions = [];
if (!isNull _building) then {
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;
@ -282,7 +282,7 @@ HostageMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
private _hostageClass = selectRandom _hostageClasses;
private _hostagePos = [0,0,0];
if (!_useBuildingPositions) then {
if !(_useBuildingPositions) then {
private _bp = selectRandom _buildingPositions;
_hostagePos = _bp;
} else {
@ -290,7 +290,7 @@ HostageMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
private _hostage = _hostageGroup createUnit [_hostageClass, _hostagePos, [], 0, "NONE"];
if (!isNull _hostage) then {
if !(isNull _hostage) then {
_hostage setCaptive true;
_hostages pushBack _hostage;
};
@ -369,7 +369,7 @@ HostageMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
_unitOffset set [1, (_unitOffset # 1) + (random 10 - 5)];
private _shooter = _group createUnit [_unitClass, _shootBasePos vectorAdd _unitOffset, [], 0, "NONE"];
if (!isNull _shooter) then {
if !(isNull _shooter) then {
_shooter setRank (_x getOrDefault ["rank", "PRIVATE"]);
_shooters pushBack _shooter;
};
@ -483,7 +483,7 @@ HostageMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
// Put marker on ground.
private _ground = +_mPos;
private _safe = [_ground, 30, 0] call BIS_fnc_findSafePos;
if (!(_safe isEqualTo [0, 0, 0])) then {
if !((_safe isEqualTo [0, 0, 0])) then {
_ground = _safe;
};
_ground set [2, 0];

View File

@ -131,7 +131,7 @@ KillHvtMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
} forEach _blkListMarkers;
if (!_inBlkList) then {
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
@ -152,7 +152,7 @@ KillHvtMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
};
private _buildingPositions = [];
if (!isNull _building) then {
if !(isNull _building) then {
for "_i" from 0 to 100 do {
private _buildingPos = _building buildingPos _i;
if (_buildingPos isEqualTo [0, 0, 0]) exitWith {};
@ -214,7 +214,7 @@ KillHvtMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
selectRandom _buildingPositions
};
private _escort = _group createUnit [_escortClass, _escortPos, [], 0, "NONE"];
if (!isNull _escort) then {
if !(isNull _escort) then {
_escort setRank (_escortDef getOrDefault ["rank", "PRIVATE"]);
_escortUnits pushBack _escort;
};

View File

@ -55,7 +55,7 @@ if !(missionNamespace getVariable ["forge_pmc_defuseAceHandlerRegistered", false
} forEach _this;
if (_taskID isEqualTo "") exitWith {};
if (!isNil "forge_server_task_TaskStore") then {
if !(isNil "forge_server_task_TaskStore") then {
forge_server_task_TaskStore call ["incrementDefuseCount", [_taskID]];
};
}] call CBA_fnc_addEventHandler;

View File

@ -12,7 +12,7 @@
* Public: No
*/
if (!isServer) exitWith { 1 };
if !(isServer) exitWith { 1 };
private _table = missionNamespace getVariable [
"forge_pmc_enemyCountMultiplierTable",

View File

@ -8,7 +8,7 @@ Mission setup functions own startup configuration for `forge_pmc_simulator.Tanoa
- `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.
The CEO client opens the setup UI from `initPlayerLocal.sqf` when `forge_pmc_missionSettingsApplied` is not set. Other players do not receive the startup setup UI.
Selecting **Start Mission** sends UI values to the server and applies them. Selecting **Cancel** applies the existing Arma mission params/defaults immediately.

View File

@ -58,10 +58,22 @@ switch (_event) do {
];
} forEach ([] call forge_pmc_fnc_getEnemyFactionOptions);
private _defaultFaction = "IND_G_F";
private _hasDefaultFaction = false;
{
if ((_x getOrDefault ["faction", ""]) isEqualTo _defaultFaction) exitWith {
_hasDefaultFaction = true;
};
} forEach _factions;
if (!_hasDefaultFaction && { _factions isNotEqualTo [] }) then {
_defaultFaction = (_factions select 0) getOrDefault ["faction", _defaultFaction];
};
private _payload = createHashMapFromArray [
["factions", _factions],
["settings", createHashMapFromArray [
["enemyFaction", "IND_G_F"],
["enemyFaction", _defaultFaction],
["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")],
["missionInterval", getNumber (_missionConfig >> "missionInterval")],
["moneyMin", (getArray (_attackConfig >> "Rewards" >> "money")) param [0, 25000]],

View File

@ -12,15 +12,15 @@
* Public: No
*/
if (!hasInterface) exitWith { false };
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 }) ||
!(isNull _ceoUnit && { player isEqualTo _ceoUnit }) ||
{ toLowerANSI (vehicleVarName player) isEqualTo "ceo" };
if (!_isCeoSlot) exitWith { false };
if !(_isCeoSlot) exitWith { false };
private _display = createDialog ["RscPmcMissionSetup", true];
if (isNull _display) exitWith { false };
@ -46,7 +46,7 @@ _control ctrlWebBrowserAction ["LoadFile", "ui\_site\index.html"];
};
private _display = uiNamespace getVariable ["RscPmcMissionSetup", displayNull];
if (!isNull _display) then {
if !(isNull _display) then {
closeDialog 1;
};
};

View File

@ -12,7 +12,7 @@
* Public: No
*/
if (!isServer) exitWith {};
if !(isServer) exitWith {};
params [
["_overrides", createHashMap, [createHashMap]]
@ -56,13 +56,27 @@ 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 _enemyFactionParam = ["enemyFaction", 6] call BIS_fnc_getParamValue;
private _enemyFaction = _overrides getOrDefault ["enemyFaction", ""];
private _fallbackEnemyFaction = "IND_G_F";
private _factionOptions = [] call forge_pmc_fnc_getEnemyFactionOptions;
private _hasFallbackFaction = false;
{
_x params ["_optionFaction", "_display", "_value"];
if (_optionFaction isEqualTo _fallbackEnemyFaction) exitWith {
_hasFallbackFaction = true;
};
} forEach _factionOptions;
if (!_hasFallbackFaction && { _factionOptions isNotEqualTo [] }) then {
_fallbackEnemyFaction = (_factionOptions select 0) param [0, _fallbackEnemyFaction];
};
if (_enemyFaction isEqualTo "") then {
if (_enemyFactionParam isEqualTo -1) then {
_enemyFactionParam = 7;
_enemyFactionParam = 6;
};
_enemyFaction = [_enemyFactionParam, "IND_G_F"] call forge_pmc_fnc_resolveEnemyFactionParam;
_enemyFaction = [_enemyFactionParam, _fallbackEnemyFaction] call forge_pmc_fnc_resolveEnemyFactionParam;
} else {
_enemyFactionParam = _enemyFaction;
};

View File

@ -4,7 +4,7 @@
private _ceoUnit = missionNamespace getVariable ["ceo", objNull];
private _isCeoSlot =
(!isNull _ceoUnit && { player isEqualTo _ceoUnit }) ||
!(isNull _ceoUnit && { player isEqualTo _ceoUnit }) ||
{ toLowerANSI (vehicleVarName player) isEqualTo "ceo" };
if (_isCeoSlot && { !(missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) }) then {