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: * Consumers:
* - forge_pmc_fnc_getEnemyFactionOptions reads this config for the startup UI. * - forge_pmc_fnc_getEnemyFactionOptions scans loaded CfgFactionClasses and
* - forge_pmc_fnc_resolveEnemyFactionParam maps mission param values back to * CfgVehicles at runtime, then uses this config to filter and polish the UI.
* faction classnames during fallback/default setup. * - 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 * - Mission generators use ENEMY_FACTION_STR and ENEMY_SIDE after setup has
* applied the selected option. * applied the selected option.
* *
* Keep this list aligned with the server modpack. Classnames that are not * This config is intentionally not the full faction list. Any loaded mod
* present in CfgFactionClasses may still appear in the UI, but downstream unit * faction on an allowed side can appear automatically if it has spawnable
* pool generation may fall back or fail to find units. * infantry or a CfgFactionUnitMap override.
*
* BLUFOR/WEST factions are intentionally omitted because generated missions use
* these options as opposing forces.
*/ */
class CfgEnemyFactions { class CfgEnemyFactions {
/* /*
* Option format: * Arma side IDs allowed as generated enemy factions:
* - class name: stable config entry name, normally matching the faction. * - 0: EAST / OPFOR
* - value: legacy mission param numeric value. * - 2: RESISTANCE / Independent
* - faction: CfgFactionClasses classname used for spawning. *
* - display: user-facing label shown in the setup UI. * WEST / BLUFOR is intentionally excluded because generated missions use
* these options as opposing forces.
*/ */
class Options { sides[] = {0, 2};
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"; }; * Factions that should never be offered even if present in the active
class OPF_R_F { value = 3; faction = "OPF_R_F"; display = "Spetnaz"; }; * modset. Keep this for factions that are unsuitable for PMC contracts.
class OPF_SFIA_lxWS { value = 4; faction = "OPF_SFIA_lxWS"; display = "SFIA"; }; */
class OPF_TURA_lxWS { value = 5; faction = "OPF_TURA_lxWS"; display = "Tura"; }; denylist[] = {
class IND_F { value = 6; faction = "IND_F"; display = "AAF"; }; "IND_UN_lxWS"
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"; }; * Optional display/order/value metadata for known factions.
class IND_SFIA_lxWS { value = 11; faction = "IND_SFIA_lxWS"; display = "SFIA"; }; *
class IND_TURA_lxWS { value = 12; faction = "IND_TURA_lxWS"; display = "Tura"; }; * - 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: * Current behavior:
* - Enemy unit pools are primarily built from CfgGroups/CfgVehicles through * - forge_pmc_fnc_getEnemyFactionOptions treats a mapped faction as selectable
* forge_pmc_fnc_getEnemyFactionUnitPool. * when at least one mapped vehicle exists.
* - This config is a template for deterministic per-faction unit pools if the * - forge_pmc_fnc_getEnemyFactionUnitPool checks this map first.
* automatic faction lookup is not specific enough for a server/modpack. * - 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 * Most mod factions do not need an entry here. Add a class only when a faction
* generator spawn path before falling back to config traversal. * needs a curated or corrected spawn pool.
*/ */
class CfgFactionUnitMap { class CfgFactionUnitMap {
/* /*
* Mapping key should match the selected faction classname from * 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 { 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 Params {
class enemyFaction { class enemyFaction {
title = "Enemy Faction"; 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[] = { texts[] = {
"CSAT", "CSAT",
"CSAT (Pacific)", "CSAT (Pacific)",
"Viper",
"Spetnaz", "Spetnaz",
"SFIA (OPFOR)", "SFIA (OPFOR)",
"Tura (OPFOR)", "Tura (OPFOR)",
@ -14,10 +21,9 @@ class Params {
"LDF", "LDF",
"Syndikat", "Syndikat",
"Looters", "Looters",
"SFIA (Independent)",
"Tura (Independent)" "Tura (Independent)"
}; };
default = 7; default = 6;
}; };
class maxConcurrentMissions { class maxConcurrentMissions {

View File

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

View File

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

View File

@ -4,7 +4,7 @@ Helper functions provide reusable lookups and conversions for the PMC simulator
## Registered Functions ## Registered Functions
- `forge_pmc_fnc_getAllEnemyFactions` returns available non-BLUFOR faction classnames. - `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_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_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_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. - `forge_pmc_fnc_getEnemyFactionListboxSelection` and `forge_pmc_fnc_populateEnemyFactionListbox` support faction picker UI/listbox flows.
## Notes ## 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. 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ā * Author: IDSolutions, Blackbox AI, MrPākehā
* Returns candidate enemy faction classnames available to the mission. * Returns candidate enemy faction classnames available to the mission.
* This is a runtime helper only; description.ext mission params still use * This is a runtime helper only. The setup UI uses
* the static CfgEnemyFactions list. * forge_pmc_fnc_getEnemyFactionOptions so it can filter to spawnable factions.
* *
* Arguments: * Arguments:
* 0: Exclude BLUFOR/WEST factions <BOOL> (Default: true) * 0: Exclude BLUFOR/WEST factions <BOOL> (Default: true)

View File

@ -1,7 +1,8 @@
/* /*
* Author: IDSolutions, Blackbox AI, MrPākehā * Author: IDSolutions, Blackbox AI, MrPakeha
* Reads enemy faction options from missionConfigFile >> CfgEnemyFactions * Builds setup UI faction options from the active modset. The helper scans
* >> Options for setup UI hydration and mission param fallback resolution. * loaded faction classes and only returns allowed OPFOR/Independent factions
* that can provide infantry through CfgVehicles or CfgFactionUnitMap.
* *
* Arguments: * Arguments:
* None * None
@ -12,40 +13,122 @@
* Public: No * Public: No
*/ */
private _optionsConfig = missionConfigFile >> "CfgEnemyFactions" >> "Options"; private _config = missionConfigFile >> "CfgEnemyFactions";
private _options = []; private _allowedSides = getArray (_config >> "sides");
if (_allowedSides isEqualTo []) then {
if (isClass _optionsConfig) then { _allowedSides = [0, 2];
{
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 _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 { if (_options isEqualTo []) then {
_options = [ _options = [
["OPF_F", "CSAT", 0], ["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 _side = _fallbackSide;
private _cfgFaction = configFile >> "CfgFactionClasses" >> _enemyFaction; private _cfgFaction = configFile >> "CfgFactionClasses" >> _enemyFaction;
if (isClass _cfgFaction) then { 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"); 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 { switch (_sideNumber) do {
case 0: { _side = east; }; case 0: { _side = east; };
case 2: { _side = resistance; }; case 2: { _side = resistance; };

View File

@ -6,6 +6,8 @@
* Arguments: * Arguments:
* 0: Faction classname <STRING> (Default: ENEMY_FACTION_STR or "IND_G_F") * 0: Faction classname <STRING> (Default: ENEMY_FACTION_STR or "IND_G_F")
* 1: Fallback side <SIDE> (Default: ENEMY_SIDE or east) * 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: * Return Value:
* Unit definitions with vehicle, rank, and position keys <ARRAY> * Unit definitions with vehicle, rank, and position keys <ARRAY>
@ -15,7 +17,8 @@
params [ params [
["_faction", missionNamespace getVariable ["ENEMY_FACTION_STR", "IND_G_F"], [""]], ["_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 { if (_faction isEqualTo "") then {
@ -25,33 +28,56 @@ if (_faction isEqualTo "") then {
private _pool = []; private _pool = [];
private _sideNumber = [_fallbackSide] call BIS_fnc_sideID; private _sideNumber = [_fallbackSide] call BIS_fnc_sideID;
{ // Check CfgFactionUnitMap first for explicit faction unit definitions
if (getNumber (_x >> "scope") < 2) then { continue; }; private _factionMapConfig = missionConfigFile >> "CfgFactionUnitMap" >> _faction;
if (getText (_x >> "faction") isNotEqualTo _faction) then { continue; }; if (isClass _factionMapConfig) then {
if (getNumber (_x >> "side") isNotEqualTo _sideNumber) then { continue; }; {
if !(configName _x isKindOf "CAManBase") then { continue; }; private _vehicle = getText (_x >> "vehicle");
if (_vehicle isEqualTo "" || { !(isClass (configFile >> "CfgVehicles" >> _vehicle)) }) then {
continue;
};
private _className = configName _x; _pool pushBack createHashMapFromArray [
private _upperClassName = toUpperANSI _className; ["vehicle", _vehicle],
private _rank = "PRIVATE"; ["rank", getText (_x >> "rank")],
["position", getArray (_x >> "position")]
if ( ];
(_upperClassName find "_SL_" >= 0) } forEach ("true" configClasses (_factionMapConfig >> "Units"));
|| { _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"));
// Fall back to config traversal if no explicit mapping exists.
if (_pool isEqualTo []) then { 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 { 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 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"] }; 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ā * 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: * Arguments:
* 0: Listbox/combo control or display <CONTROL|DISPLAY> * 0: Listbox/combo control or display <CONTROL|DISPLAY>

View File

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

View File

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

View File

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

View File

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

View File

@ -132,7 +132,7 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
}; };
} forEach _blkListMarkers; } forEach _blkListMarkers;
if (!_inBlkList) then { if !(_inBlkList) then {
_taskPos = _candidate; _taskPos = _candidate;
}; };
}; };
@ -297,7 +297,7 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
if (_nearBuildings isNotEqualTo []) then { if (_nearBuildings isNotEqualTo []) then {
// prefer the closest building that actually contains the position // 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; _building = _x;
}; };
} forEach _nearBuildings; } forEach _nearBuildings;
@ -313,14 +313,14 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
}; };
}; };
if (!isNull _building) then { if !(isNull _building) then {
for "_i" from 1 to _buildingSpawnAttempts do { for "_i" from 1 to _buildingSpawnAttempts do {
private _posIndex = floor random 1000; private _posIndex = floor random 1000;
private _candidate = _building buildingPos _posIndex; private _candidate = _building buildingPos _posIndex;
// buildingPos returns [0,0,0] for invalid positions // buildingPos returns [0,0,0] for invalid positions
if (_candidate isEqualTo [0, 0, 0]) then { continue; }; if (_candidate isEqualTo [0, 0, 0]) then { continue; };
// ensure candidate is still inside the building footprint // ensure candidate is still inside the building footprint
if (!(_candidate isEqualType [])) then { continue; }; if !((_candidate isEqualType [])) then { continue; };
if ((_candidate vectorDistance _position) <= 60) exitWith { if ((_candidate vectorDistance _position) <= 60) exitWith {
_buildingPos = _candidate; _buildingPos = _candidate;
}; };
@ -328,7 +328,7 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
}; };
private _protectedPos = [0,0,0]; private _protectedPos = [0,0,0];
if (!(_buildingPos isEqualTo [])) then { if !((_buildingPos isEqualTo [])) then {
_protectedPos = _buildingPos; _protectedPos = _buildingPos;
} else { } else {
// Outdoor fallback: keep previous behavior // Outdoor fallback: keep previous behavior
@ -337,7 +337,7 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
private _protectedObject = createVehicle [_protectedClass, _protectedPos, [], 0, "NONE"]; private _protectedObject = createVehicle [_protectedClass, _protectedPos, [], 0, "NONE"];
private _protectedObjects = []; private _protectedObjects = [];
if (!isNull _protectedObject) then { if !(isNull _protectedObject) then {
_protectedObjects pushBack _protectedObject; _protectedObjects pushBack _protectedObject;
}; };
@ -356,7 +356,7 @@ DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
private _devicePos = _protectedPos vectorAdd _deviceOffset; private _devicePos = _protectedPos vectorAdd _deviceOffset;
private _deviceObject = createVehicle [_deviceClass, _devicePos, [], 0, "NONE"]; private _deviceObject = createVehicle [_deviceClass, _devicePos, [], 0, "NONE"];
if (!isNull _deviceObject) then { if !(isNull _deviceObject) then {
_devices pushBack _deviceObject; _devices pushBack _deviceObject;
}; };
}; };

View File

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

View File

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

View File

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

View File

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

View File

@ -55,7 +55,7 @@ if !(missionNamespace getVariable ["forge_pmc_defuseAceHandlerRegistered", false
} forEach _this; } forEach _this;
if (_taskID isEqualTo "") exitWith {}; 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]]; forge_server_task_TaskStore call ["incrementDefuseCount", [_taskID]];
}; };
}] call CBA_fnc_addEventHandler; }] call CBA_fnc_addEventHandler;

View File

@ -12,7 +12,7 @@
* Public: No * Public: No
*/ */
if (!isServer) exitWith { 1 }; if !(isServer) exitWith { 1 };
private _table = missionNamespace getVariable [ private _table = missionNamespace getVariable [
"forge_pmc_enemyCountMultiplierTable", "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`. - `forge_pmc_fnc_setupMenu_applySettings` applies UI overrides or mission parameter defaults into `forge_pmc_missionSettings`.
## Startup Flow ## 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. 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); } 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 [ private _payload = createHashMapFromArray [
["factions", _factions], ["factions", _factions],
["settings", createHashMapFromArray [ ["settings", createHashMapFromArray [
["enemyFaction", "IND_G_F"], ["enemyFaction", _defaultFaction],
["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")], ["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")],
["missionInterval", getNumber (_missionConfig >> "missionInterval")], ["missionInterval", getNumber (_missionConfig >> "missionInterval")],
["moneyMin", (getArray (_attackConfig >> "Rewards" >> "money")) param [0, 25000]], ["moneyMin", (getArray (_attackConfig >> "Rewards" >> "money")) param [0, 25000]],

View File

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

View File

@ -12,7 +12,7 @@
* Public: No * Public: No
*/ */
if (!isServer) exitWith {}; if !(isServer) exitWith {};
params [ params [
["_overrides", createHashMap, [createHashMap]] ["_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 // Enemy faction selection falls back to Params::enemyFaction when the setup UI
// is closed without pressing Start Mission. // 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 _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 (_enemyFaction isEqualTo "") then {
if (_enemyFactionParam isEqualTo -1) 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 { } else {
_enemyFactionParam = _enemyFaction; _enemyFactionParam = _enemyFaction;
}; };

View File

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