Integrate mission generators into framework

This commit is contained in:
Jacob Schmidt 2026-05-26 21:12:04 -05:00
parent 1454a29de9
commit 9c2a09eed9
32 changed files with 4080 additions and 254 deletions

View File

@ -30,6 +30,7 @@ GVAR(CADRepository) = createHashMapObject [[
_self set ["requests", []];
_self set ["assignments", []];
_self set ["activity", []];
_self set ["generatedTaskTypes", []];
_self set ["session", createHashMap];
_self set ["mode", "operations"];
_self set ["dispatchView", "board"];
@ -41,6 +42,7 @@ GVAR(CADRepository) = createHashMapObject [[
["requests", +(_self getOrDefault ["requests", []])],
["assignments", +(_self getOrDefault ["assignments", []])],
["activity", +(_self getOrDefault ["activity", []])],
["generatedTaskTypes", +(_self getOrDefault ["generatedTaskTypes", []])],
["session", +(_self getOrDefault ["session", createHashMap])],
["mode", _self getOrDefault ["mode", "operations"]],
["dispatchView", _self getOrDefault ["dispatchView", "board"]]
@ -72,6 +74,7 @@ GVAR(CADRepository) = createHashMapObject [[
_self set ["requests", +(_payload getOrDefault ["requests", []])];
_self set ["assignments", +(_payload getOrDefault ["assignments", []])];
_self set ["activity", +(_payload getOrDefault ["activity", []])];
_self set ["generatedTaskTypes", +(_payload getOrDefault ["generatedTaskTypes", []])];
_self set ["session", +(_payload getOrDefault ["session", createHashMap])];
true
}],

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,16 @@
const dispatcherFormatters = window.cadDispatcherFormatters || {};
const dispatcherModals = window.cadDispatcherModals || {};
const dispatcherRender = window.cadDispatcherRender || {};
const defaultTaskTypes = [
{ value: "attack", label: "Attack" },
{ value: "defend", label: "Defend" },
{ value: "delivery", label: "Delivery" },
{ value: "destroy", label: "Destroy" },
{ value: "defuse", label: "Defuse" },
{ value: "hostage", label: "Hostage" },
{ value: "hvtkill", label: "Kill HVT" },
{ value: "hvtcapture", label: "Capture HVT" },
];
window.cadDispatcher = {
contracts: [],
@ -11,16 +21,7 @@ window.cadDispatcher = {
editingGroupId: "",
viewingRequestId: "",
convertingRequestId: "",
taskTypes: [
{ value: "attack", label: "Attack" },
{ value: "defend", label: "Defend" },
{ value: "delivery", label: "Delivery" },
{ value: "destroy", label: "Destroy" },
{ value: "defuse", label: "Defuse" },
{ value: "hostage", label: "Hostage" },
{ value: "hvtkill", label: "Kill HVT" },
{ value: "hvtcapture", label: "Capture HVT" },
],
taskTypes: defaultTaskTypes.slice(),
statuses: [
"available",
"en_route",
@ -133,6 +134,14 @@ window.cadDispatcher = {
this.requests = Array.isArray(payload.requests) ? payload.requests : [];
this.groups = Array.isArray(payload.groups) ? payload.groups : [];
this.activity = Array.isArray(payload.activity) ? payload.activity : [];
if (Array.isArray(payload.generatedTaskTypes)) {
this.taskTypes = payload.generatedTaskTypes
.map((entry) => ({
value: String(entry?.value || "").trim(),
label: String(entry?.label || entry?.value || "").trim(),
}))
.filter((entry) => entry.value);
}
this.session =
payload.session && typeof payload.session === "object"
? payload.session
@ -223,6 +232,14 @@ window.cadDispatcher = {
this.closeOrderModal();
},
requestGeneratedTask() {
if (!this.taskTypes.length) {
this.setStatus(
"Generated task requests are disabled by server settings.",
"error",
);
return;
}
const taskType = document.getElementById(
"dispatcherTaskTypeSelect",
).value;

View File

@ -18,6 +18,21 @@ window.cadDispatcherModals = {
return;
}
const saveButton = document.getElementById(
"dispatcherTaskModalSaveBtn",
);
const hasTaskTypes = this.taskTypes.length > 0;
taskTypeSelect.disabled = !hasTaskTypes;
if (saveButton) {
saveButton.disabled = !hasTaskTypes;
}
if (!hasTaskTypes) {
taskTypeSelect.innerHTML =
'<option value="">No generated tasks available</option>';
return;
}
taskTypeSelect.innerHTML = this.buildTaskTypeOptions(
taskTypeSelect.value || this.taskTypes[0]?.value || "",
);

View File

@ -91,20 +91,23 @@ call FUNC(registerEventListeners);
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
};
if (isNil "forge_pmc_fnc_requestMissionTask") exitWith {
_result set ["message", "This mission does not expose dispatcher-generated tasks."];
if !(isNil QEFUNC(task,requestMissionTask)) then {
_result = [_taskType, _metadata, _uid] call EFUNC(task,requestMissionTask);
} else {
if (isNil "forge_pmc_fnc_requestMissionTask") exitWith {
_result set ["message", "This mission does not expose dispatcher-generated tasks."];
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
};
_result = [_taskType, _metadata, _uid] call forge_pmc_fnc_requestMissionTask;
};
if !(_result getOrDefault ["success", false]) exitWith {
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
};
// Temporary mission-owned integration point. This keeps simulator-specific
// generator logic in the mission until CAD/task grows a framework-level
// on-demand generation interface.
_result = [_taskType, _metadata, _uid] call forge_pmc_fnc_requestMissionTask;
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
if (_result getOrDefault ["success", false]) then {
[CRPC(cad,invalidateCadState), []] call CFUNC(globalEvent);
};
[CRPC(cad,invalidateCadState), []] call CFUNC(globalEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestSubmitCadSupportRequest), {

View File

@ -299,6 +299,16 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
private _permissionService = _self get "PermissionService";
private _groupRepository = _self get "GroupRepository";
private _generatedTaskTypes = [];
if (missionNamespace getVariable [QEGVAR(task,enableGenerator), false]) then {
if (isNil QEGVAR(task,MissionManager) && { !(isNil QEFUNC(task,missionManager)) }) then {
call EFUNC(task,missionManager);
};
if !(isNil QEGVAR(task,MissionManager)) then {
_generatedTaskTypes = EGVAR(task,MissionManager) call ["getGeneratedTaskTypes", []];
};
};
private _groupID = _groupRepository call ["getPlayerGroupId", [_uid]];
private _session = createHashMapFromArray [
@ -311,6 +321,7 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
private _seed = createHashMapFromArray [
["groups", _groupRepository call ["buildGroups", []]],
["activeTasks", EGVAR(task,TaskStore) call ["getActiveTaskCatalog", []]],
["generatedTaskTypes", _generatedTaskTypes],
["session", _session]
];
private _emptyPayload = createHashMapFromArray [
@ -319,6 +330,7 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
["requests", []],
["assignments", []],
["activity", []],
["generatedTaskTypes", _generatedTaskTypes],
["session", _session]
];
private _persistenceService = _self getOrDefault ["PersistenceService", createHashMap];
@ -330,7 +342,9 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
private _hydrateResult = _persistenceService call ["buildHydratePayload", [_seed]];
if (_hydrateResult getOrDefault ["success", false]) exitWith {
_hydrateResult getOrDefault ["data", createHashMap]
private _data = _hydrateResult getOrDefault ["data", createHashMap];
_data set ["generatedTaskTypes", _generatedTaskTypes];
_data
};
["WARNING", "CAD hydrate failed in the extension; returning seed-only payload."] call EFUNC(common,log);

View File

@ -1,167 +1,68 @@
// TODO: Move to mission template and provide documentation
/*
* 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:
* - Arma mission params/defaults provide the startup setup UI defaults.
* - If the setup UI is cancelled, those same 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 {
// Global settings
// Maximum number of generated missions allowed to be active at once.
maxConcurrentMissions = 3;
missionInterval = 300; // 5 minutes between mission generation
// Mission type weights
// 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;
hvt = 0.15;
hvtkill = 0.15;
hvtcapture = 0.15;
defuse = 0.15;
delivery = 0.1;
destroy = 0.2;
};
// Mission locations
class Locations {
class CityOne {
position[] = {1000, 1000, 0};
type = "city";
radius = 300;
suitable[] = {"attack", "defend", "hostage"};
};
class MilitaryBase {
position[] = {2000, 2000, 0};
type = "military";
radius = 500;
suitable[] = {"hvt", "defend", "attack"};
};
class Industrial {
position[] = {3000, 3000, 0};
type = "industrial";
radius = 200;
suitable[] = {"delivery", "defuse"};
};
};
// AI Groups configuration
class AIGroups {
class Infantry {
side = "EAST";
class Units {
class Unit0 {
vehicle = "O_Soldier_TL_F";
rank = "SERGEANT";
position[] = {0, 0, 0};
};
class Unit1 {
vehicle = "O_Soldier_AR_F";
rank = "CORPORAL";
position[] = {5, -5, 0};
};
class Unit2 {
vehicle = "O_Soldier_LAT_F";
rank = "PRIVATE";
position[] = {-5, -5, 0};
};
};
suitable[] = {"attack", "defend", "hostage"};
};
class Assault {
side = "EAST";
class Units {
class Unit0 {
vehicle = "O_Soldier_SL_F";
rank = "SERGEANT";
position[] = {0, 0, 0};
};
class Unit1 {
vehicle = "O_Soldier_GL_F";
rank = "CORPORAL";
position[] = {4, -3, 0};
};
class Unit2 {
vehicle = "O_Soldier_AR_F";
rank = "CORPORAL";
position[] = {-4, -3, 0};
};
class Unit3 {
vehicle = "O_medic_F";
rank = "PRIVATE";
position[] = {7, -6, 0};
};
};
suitable[] = {"attack", "defend"};
};
class MotorizedPatrol {
side = "EAST";
class Units {
class Unit0 {
vehicle = "O_Soldier_TL_F";
rank = "SERGEANT";
position[] = {0, 0, 0};
};
class Unit1 {
vehicle = "O_Soldier_LAT_F";
rank = "CORPORAL";
position[] = {5, -4, 0};
};
class Unit2 {
vehicle = "O_Soldier_F";
rank = "PRIVATE";
position[] = {-5, -4, 0};
};
class Unit3 {
vehicle = "O_Soldier_A_F";
rank = "PRIVATE";
position[] = {8, -7, 0};
};
};
suitable[] = {"attack", "defend"};
};
class SpecOps {
side = "EAST";
class Units {
class Unit0 {
vehicle = "O_recon_TL_F";
rank = "SERGEANT";
position[] = {0, 0, 0};
};
class Unit1 {
vehicle = "O_recon_M_F";
rank = "CORPORAL";
position[] = {5, -5, 0};
};
};
suitable[] = {"hvt", "hostage"};
};
class ReconRaid {
side = "EAST";
class Units {
class Unit0 {
vehicle = "O_recon_TL_F";
rank = "SERGEANT";
position[] = {0, 0, 0};
};
class Unit1 {
vehicle = "O_recon_M_F";
rank = "CORPORAL";
position[] = {4, -4, 0};
};
class Unit2 {
vehicle = "O_recon_LAT_F";
rank = "CORPORAL";
position[] = {-4, -4, 0};
};
class Unit3 {
vehicle = "O_recon_medic_F";
rank = "PRIVATE";
position[] = {7, -7, 0};
};
};
suitable[] = {"attack", "hvt", "hostage"};
};
};
// TODO: Continue to refine mission types and their specific settings
// Mission type specific settings
/*
* 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[]: numeric min/max reputation penalty on failure. UI settings
* may express these as min/max reputation hits, then the helper sorts the
* numeric roll range before generators use it.
* - timeLimit[]: min/max task time limit in seconds.
*/
class MissionTypes {
// Search-and-destroy infantry engagement.
class Attack {
minUnits = 4;
maxUnits = 8;
patrolRadius = 200;
class Rewards {
money[] = {25000, 60000};
reputation[] = {6, 14};
@ -172,13 +73,16 @@ class CfgMissions {
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-8, -3};
timeLimit[] = {900, 1800}; // 15-30 minutes
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};
@ -190,13 +94,15 @@ class CfgMissions {
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-12, -4};
timeLimit[] = {1800, 3600}; // 30-60 minutes
timeLimit[] = {300, 1800};
};
// Rescue a hostage from a generated hostile site.
class Hostage {
// Candidate hostage classnames by broad source category.
class Hostages {
civilian[] = {"C_man_1", "C_man_polo_1_F"};
military[] = {"B_Pilot_F", "B_officer_F"};
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};
@ -208,14 +114,17 @@ class CfgMissions {
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-16, -6};
timeLimit[] = {600, 900}; // 10-15 minutes
timeLimit[] = {600, 900};
};
class HVT {
// 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};
@ -227,15 +136,40 @@ class CfgMissions {
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-14, -5};
timeLimit[] = {900, 1800}; // 15-30 minutes
timeLimit[] = {900, 1800};
};
class Defuse {
class Devices {
small[] = {"DemoCharge_Remote_Mag"};
large[] = {"SatchelCharge_Remote_Mag"};
// 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"};
};
maxDevices = 3;
// 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};
@ -246,13 +180,15 @@ class CfgMissions {
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-9, -3};
timeLimit[] = {600, 900}; // 10-15 minutes
timeLimit[] = {600, 900};
};
// Deliver cargo or vehicles between generated locations.
class Delivery {
// Candidate delivery objects grouped by cargo type.
class Cargo {
supplies[] = {"Land_CargoBox_V1_F"};
vehicles[] = {"B_MRAP_01_F", "B_Truck_01_transport_F"};
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[] = {};
};
class Rewards {
money[] = {10000, 30000};
@ -264,7 +200,26 @@ class CfgMissions {
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-6, -2};
timeLimit[] = {900, 1800}; // 15-30 minutes
timeLimit[] = {0, 0};
};
// Destroy generated infrastructure targets.
class Destroy {
// Candidate destructible target classnames.
class Bomb {
building[] = {"Land_Radar_F", "Land_Radar_Small_F", "Land_MobileRadar_01_radar_F", "Land_MobileRadar_01_generator_F", "Land_Communication_F", "Land_spp_Tower_F", "Land_TTowerSmall_1_F", "Land_TTowerSmall_2_F", "Land_TTowerBig_1_F", "Land_TTowerBig_2_F"};
};
class Rewards {
money[] = {10000, 30000};
reputation[] = {3, 8};
equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
};
penalty[] = {-6, -2};
timeLimit[] = {900, 1800};
};
};
};

View File

@ -95,7 +95,8 @@ Mission designers can create tasks in four ways:
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
The dynamic mission manager can also generate attack, defend, defuse, delivery,
destroy, hostage, HVT kill, and HVT capture tasks from config. That is
system-generated content rather than a hand-authored task creation path.
### CAD Compatibility
@ -110,7 +111,7 @@ CAD-compatible creation paths:
- Eden modules: compatible because they delegate to `fnc_startTask.sqf`
- `fnc_startTask.sqf`: compatible because it registers the catalog entry,
creates the BIS task, and dispatches through `fnc_handler.sqf`
- dynamic mission manager attack tasks: compatible because the mission manager
- dynamic mission manager tasks: compatible because the mission manager
uses `fnc_startTask.sqf`
Limited or incompatible paths:
@ -244,7 +245,8 @@ Task module emits the following events to the event bus:
- `task.notification.requested` - participant notifications pending dispatch
## Notes
- the dynamic mission manager in `fnc_missionManager.sqf` is initialized during task post-init; mission generation only runs when the `forge_task_enableGenerator` CBA setting is enabled
- the dynamic mission manager in `fnc_missionManager.sqf` is initialized during task post-init; timer-based mission generation only runs when the `forge_task_enableGenerator` CBA setting is enabled
- CAD can request a specific generated mission type through `fnc_requestMissionTask.sqf`
- it starts server-owned tasks through `fnc_handler.sqf` and binds them to the `default` org
- task lifecycle for the mission manager is tracked through `TaskStore` status entries
- task backend state is intentionally transient and resets with the active server/mission lifecycle

View File

@ -14,15 +14,26 @@ PREP(makeObject);
PREP(makeShooter);
PREP(makeTarget);
PREP(missionManager);
PREP(requestMissionTask);
PREP(initTaskStore);
PREP_SUBDIR(generators,attackMissionGenerator);
PREP_SUBDIR(generators,captureHvtMissionGenerator);
PREP_SUBDIR(generators,defendMissionGenerator);
PREP_SUBDIR(generators,defuseMissionGenerator);
PREP_SUBDIR(generators,deliveryMissionGenerator);
PREP_SUBDIR(generators,destroyMissionGenerator);
PREP_SUBDIR(generators,hostageMissionGenerator);
PREP_SUBDIR(generators,hvtMissionGenerator);
PREP_SUBDIR(helpers,getEnemyFactionUnitPool);
PREP_SUBDIR(helpers,getMissionSettingRange);
PREP_SUBDIR(helpers,handleTaskRewards);
PREP_SUBDIR(helpers,parseTaskChainAttributes);
PREP_SUBDIR(helpers,parseRewards);
PREP_SUBDIR(helpers,spawnEnemyWave);
PREP_SUBDIR(helpers,startTask);
PREP_SUBDIR(helpers,updateEnemyCountFromActivePlayers);
PREP_SUBDIR(modules,attackModule);
PREP_SUBDIR(modules,cargoModule);

View File

@ -19,6 +19,13 @@
if !(isServer) exitWith { false };
if !(isNil QGVAR(MissionManagerPFH)) exitWith { false };
if (isNil QGVAR(AttackMissionGeneratorBaseClass)) then { call FUNC(attackMissionGenerator); };
if (isNil QGVAR(DefendMissionGeneratorBaseClass)) then { call FUNC(defendMissionGenerator); };
if (isNil QGVAR(DefuseMissionGeneratorBaseClass)) then { call FUNC(defuseMissionGenerator); };
if (isNil QGVAR(DeliveryMissionGeneratorBaseClass)) then { call FUNC(deliveryMissionGenerator); };
if (isNil QGVAR(DestroyMissionGeneratorBaseClass)) then { call FUNC(destroyMissionGenerator); };
if (isNil QGVAR(HostageMissionGeneratorBaseClass)) then { call FUNC(hostageMissionGenerator); };
if (isNil QGVAR(KillHvtMissionGeneratorBaseClass)) then { call FUNC(hvtMissionGenerator); };
if (isNil QGVAR(CaptureHvtMissionGeneratorBaseClass)) then { call FUNC(captureHvtMissionGenerator); };
#pragma hemtt ignore_variables ["_self"]
GVAR(MissionManagerBaseClass) = compileFinal createHashMapFromArray [
@ -27,11 +34,55 @@ GVAR(MissionManagerBaseClass) = compileFinal createHashMapFromArray [
_self set ["lastMissionGenerationAt", -1e10];
_self set ["recentLocationRegistry", []];
_self set ["activeMissionRegistry", createHashMap];
_self set ["generators", [createHashMapObject [GVAR(AttackMissionGeneratorBaseClass)]]];
_self set ["generators", [
["attack", createHashMapObject [GVAR(AttackMissionGeneratorBaseClass)]],
["defend", createHashMapObject [GVAR(DefendMissionGeneratorBaseClass)]],
["defuse", createHashMapObject [GVAR(DefuseMissionGeneratorBaseClass)]],
["delivery", createHashMapObject [GVAR(DeliveryMissionGeneratorBaseClass)]],
["destroy", createHashMapObject [GVAR(DestroyMissionGeneratorBaseClass)]],
["hostage", createHashMapObject [GVAR(HostageMissionGeneratorBaseClass)]],
["hvtkill", createHashMapObject [GVAR(KillHvtMissionGeneratorBaseClass)]],
["hvtcapture", createHashMapObject [GVAR(CaptureHvtMissionGeneratorBaseClass)]]
]];
}],
["getGenerators", compileFinal {
(_self getOrDefault ["generators", []]) apply { _x param [1, createHashMap, [createHashMap]] }
}],
["getGeneratorEntries", compileFinal {
_self getOrDefault ["generators", []]
}],
["getGeneratorByType", compileFinal {
params [["_generatorType", "", [""]]];
private _result = createHashMap;
{
if ((_x param [0, "", [""]]) isEqualTo _generatorType) exitWith {
_result = _x param [1, createHashMap, [createHashMap]];
};
} forEach (_self call ["getGeneratorEntries", []]);
_result
}],
["getGeneratedTaskTypes", compileFinal {
private _labels = createHashMapFromArray [
["attack", "Attack"],
["defend", "Defend"],
["defuse", "Defuse"],
["delivery", "Delivery"],
["destroy", "Destroy"],
["hostage", "Hostage"],
["hvtkill", "Kill HVT"],
["hvtcapture", "Capture HVT"]
];
(_self call ["getGeneratorEntries", []]) apply {
private _generatorType = _x param [0, "", [""]];
createHashMapFromArray [
["value", _generatorType],
["label", _labels getOrDefault [_generatorType, _generatorType]]
]
}
}],
["getActiveMissionIds", compileFinal {
private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
keys _activeMissionRegistry
@ -119,15 +170,44 @@ GVAR(MissionManagerBaseClass) = compileFinal createHashMapFromArray [
""
};
private _startedTaskID = "";
private _missionConfig = missionConfigFile >> "CfgMissions";
if !(isClass _missionConfig) then {
_missionConfig = configFile >> "CfgMissions";
};
private _weightsConfig = _missionConfig >> "MissionWeights";
private _weighted = [];
private _totalWeight = 0;
{
private _taskID = _x call ["startMission", [_self]];
if (_taskID isNotEqualTo "") exitWith {
_startedTaskID = _taskID;
};
} forEach (_self call ["getGenerators", []]);
private _generatorType = _x param [0, "", [""]];
private _generator = _x param [1, createHashMap, [createHashMap]];
if (_generatorType isEqualTo "" || { _generator isEqualTo createHashMap }) then { continue; };
_startedTaskID
private _weight = getNumber (_weightsConfig >> _generatorType);
if (_weight <= 0) then { _weight = 1; };
_totalWeight = _totalWeight + _weight;
_weighted pushBack [_generatorType, _generator, _totalWeight];
} forEach (_self call ["getGeneratorEntries", []]);
if (_weighted isEqualTo [] || { _totalWeight <= 0 }) exitWith { "" };
private _roll = random _totalWeight;
private _selected = _weighted select 0;
{
if (_roll <= (_x param [2, 0, [0]])) exitWith {
_selected = _x;
};
} forEach _weighted;
private _generatorType = _selected param [0, "", [""]];
private _generator = _selected param [1, createHashMap, [createHashMap]];
private _taskID = _generator call ["startMission", [_self]];
if (_taskID isEqualTo "") exitWith {
["WARNING", format ["Mission manager failed to start '%1' generated mission.", _generatorType]] call EFUNC(common,log);
""
};
_taskID
}]
];
@ -154,6 +234,8 @@ if (isNil QGVAR(MissionManagerTaskEventTokens)) then {
if (GVAR(enableGenerator)) then {
GVAR(MissionManagerPFH) = [{
if !(GVAR(enableGenerator)) exitWith {};
GVAR(MissionManager) call ["cleanupCompletedMissions", []];
private _now = diag_tickTime;

View File

@ -0,0 +1,120 @@
#include "..\script_component.hpp"
/*
* Author: IDSolutions
* Framework-owned on-demand dynamic mission request entry point for CAD and
* other server-side dispatchers.
*
* Arguments:
* 0: Generator type <STRING>
* 1: Request metadata <HASHMAP> (Default: createHashMap)
* 2: Requesting player UID <STRING> (Default: "")
*
* Return Value:
* Request result with success, message, taskID, and taskType keys <HASHMAP>
*
* Public: No
*/
if !(isServer) exitWith {
createHashMapFromArray [
["success", false],
["message", "Generated task requests must run on the server."]
]
};
params [
["_requestedType", "", [""]],
["_metadata", createHashMap, [createHashMap]],
["_requesterUid", "", [""]]
];
private _result = createHashMapFromArray [
["success", false],
["message", "Generated task request failed."],
["taskID", ""],
["taskType", _requestedType]
];
if !(GVAR(enableGenerator)) exitWith {
_result set ["message", "Generated task requests are disabled by server settings."];
_result
};
private _typeAliases = createHashMapFromArray [
["attack", "attack"],
["defend", "defend"],
["defense", "defend"],
["delivery", "delivery"],
["deliver", "delivery"],
["destroy", "destroy"],
["defuse", "defuse"],
["hostage", "hostage"],
["hvt", "hvtkill"],
["hvtkill", "hvtkill"],
["killhvt", "hvtkill"],
["kill_hvt", "hvtkill"],
["hvtcapture", "hvtcapture"],
["capturehvt", "hvtcapture"],
["capture_hvt", "hvtcapture"]
];
private _generatorType = _typeAliases getOrDefault [toLowerANSI _requestedType, ""];
if (_generatorType isEqualTo "") exitWith {
_result set ["message", format ["Unknown generated task type: %1", _requestedType]];
_result
};
_result set ["taskType", _generatorType];
if (isNil QGVAR(TaskStore)) exitWith {
_result set ["message", "Task store is not ready yet."];
_result
};
if (isNil QGVAR(MissionManager)) then {
call FUNC(missionManager);
};
if (isNil QGVAR(MissionManager)) exitWith {
_result set ["message", "Mission manager is not ready yet."];
_result
};
GVAR(MissionManager) call ["cleanupCompletedMissions", []];
private _activeCount = count (GVAR(MissionManager) call ["getActiveMissionIds", []]);
private _maxConcurrent = GVAR(MissionManager) call ["getMaxConcurrentMissions", []];
if (_activeCount >= _maxConcurrent) exitWith {
_result set ["message", format [
"Mission cap reached (%1/%2 active). Close or complete a task before requesting another.",
_activeCount,
_maxConcurrent
]];
_result
};
private _generator = GVAR(MissionManager) call ["getGeneratorByType", [_generatorType]];
if (_generator isEqualTo createHashMap) exitWith {
_result set ["message", format ["Generated task type is unavailable: %1", _generatorType]];
_result
};
private _taskID = _generator call ["startMission", [GVAR(MissionManager)]];
if (_taskID isEqualTo "") exitWith {
_result set ["message", format ["Mission generator failed to start task type: %1", _generatorType]];
_result
};
GVAR(MissionManager) set ["lastMissionGenerationAt", diag_tickTime];
["INFO", format [
"Dispatcher %1 requested generated %2 mission %3.",
_requesterUid,
_generatorType,
_taskID
]] call EFUNC(common,log);
_result set ["success", true];
_result set ["message", format ["Generated %1 task %2.", _generatorType, _taskID]];
_result set ["taskID", _taskID];
_result

View File

@ -1,17 +1,17 @@
#include "..\script_component.hpp"
/*
* Author: IDSolutions
* Attack mission generator used by the dynamic mission manager.
* 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:
* None
*
* Example:
* [] call forge_server_task_fnc_attackMissionGenerator
* N/A. Defines GVAR(AttackMissionGeneratorBaseClass) in missionNamespace.
*
* Public: No
*/
@ -21,6 +21,9 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
["#type", "AttackMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
if !(isClass _missionConfig) then {
_missionConfig = configFile >> "CfgMissions";
};
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
@ -85,6 +88,7 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
@ -104,7 +108,7 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
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;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(findSafePos);
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
@ -134,7 +138,7 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
};
} forEach _blkListMarkers;
if (!_inBlkList) then {
if !(_inBlkList) then {
_taskPos = _candidate;
};
};
@ -161,35 +165,42 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
};
} forEach ("true" configClasses _aiGroupsConfig);
if (_groups isEqualTo []) exitWith {
["WARNING", "Attack mission generator: no AI group configs are suitable for attack missions."] call EFUNC(common,log);
grpNull
};
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
private _sideText = str _side;
private _group = createGroup _side;
[] call FUNC(updateEnemyCountFromActivePlayers);
private _groupConfig = selectRandom _groups;
private _side = getText (_groupConfig >> "side");
private _group = createGroup (call compile _side);
private _minUnits = getNumber (_attackConfig >> "minUnits");
private _maxUnits = getNumber (_attackConfig >> "maxUnits");
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
private _minUnitsBase = getNumber (_attackConfig >> "minUnits");
private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits");
private _patrolRadius = getNumber (_attackConfig >> "patrolRadius");
if (_minUnits <= 0) then { _minUnits = 4; };
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
if (_minUnitsBase <= 0) then { _minUnitsBase = 4; };
if (_maxUnitsBase < _minUnitsBase) then { _maxUnitsBase = _minUnitsBase; };
if (_patrolRadius <= 0) then { _patrolRadius = 200; };
private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1];
private _unitPool = [];
{
if ((getText (_x >> "side")) isNotEqualTo _side) then { continue; };
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 FUNC(getEnemyFactionUnitPool);
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
{
_unitPool pushBack createHashMapFromArray [
["vehicle", getText (_x >> "vehicle")],
["rank", getText (_x >> "rank")],
["position", getArray (_x >> "position")]
];
} forEach ("true" configClasses (_x >> "Units"));
} forEach _groups;
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 EFUNC(common,log);
@ -301,10 +312,10 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
};
private _taskID = format ["task_attack_%1", round (diag_tickTime * 1000)];
private _rewardRange = getArray (_attackConfig >> "Rewards" >> "money");
private _reputationRange = getArray (_attackConfig >> "Rewards" >> "reputation");
private _penaltyRange = getArray (_attackConfig >> "penalty");
private _timeRange = getArray (_attackConfig >> "timeLimit");
private _rewardRange = [_attackConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [25000, 60000]] call FUNC(getMissionSettingRange);
private _reputationRange = [_attackConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [6, 14]] call FUNC(getMissionSettingRange);
private _penaltyRange = [_attackConfig, ["penalty"], "penaltyMin", "penaltyMax", [-8, -3]] call FUNC(getMissionSettingRange);
private _timeRange = [_attackConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call FUNC(getMissionSettingRange);
private _rewards = _self call ["rollRewards"];
private _fundsReward = _rewardRange call BFUNC(randomNum);
private _reputationReward = _reputationRange call BFUNC(randomNum);

View File

@ -0,0 +1,446 @@
#include "..\script_component.hpp"
/*
* 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 GVAR(CaptureHvtMissionGeneratorBaseClass) in missionNamespace.
*
* Public: No
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(CaptureHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
["#type", "CaptureHvtMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
if !(isClass _missionConfig) then {
_missionConfig = configFile >> "CfgMissions";
};
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["hvtConfig", (_missionConfig >> "MissionTypes" >> "HVTCapture")];
_self set ["generatorType", "hvtcapture"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "hvtcapture"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "hvtcapture") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(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 EFUNC(common,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 FUNC(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 FUNC(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 BFUNC(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 EFUNC(common,log);
private _taskID = format ["task_capture_hvt_%1", round (diag_tickTime * 1000)];
private _rewardRange = [_hvtConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [50000, 120000]] call FUNC(getMissionSettingRange);
private _reputationRange = [_hvtConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [10, 22]] call FUNC(getMissionSettingRange);
private _penaltyRange = [_hvtConfig, ["penalty"], "penaltyMin", "penaltyMax", [-14, -5]] call FUNC(getMissionSettingRange);
private _timeRange = [_hvtConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call FUNC(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 BFUNC(randomNum);
private _reputationReward = _reputationRange call BFUNC(randomNum);
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
private _timeLimit = _timeRange call BFUNC(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 BFUNC(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 BFUNC(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 setMarkerShapeLocal "ELLIPSE";
_extZone setMarkerSizeLocal [160, 160];
_extZone setMarkerTextLocal format ["HVT Extraction Zone %1", _grid];
_extZone setMarkerAlphaLocal 0.5;
_extZone setMarkerBrushLocal "DiagGrid";
_extZone setMarkerColor "ColorOrange";
private _success = [
"hvt",
_taskID,
_position,
format ["HVT: Grid %1", _grid],
format ["Capture the high-value target near grid %1.", _grid],
createHashMapFromArray [["hvts", [_hvtTarget]]],
createHashMapFromArray [
["limitFail", 0],
["limitSuccess", 1],
["extractionZone", _extZone],
["captureHvt", true],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"]
],
0,
"",
"mission_manager"
] call FUNC(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
}]
];

View File

@ -0,0 +1,386 @@
#include "..\script_component.hpp"
/*
* 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 GVAR(DefendMissionGeneratorBaseClass) in missionNamespace.
*
* Public: No
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(DefendMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
["#type", "DefendMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
if !(isClass _missionConfig) then {
_missionConfig = configFile >> "CfgMissions";
};
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["defendConfig", (_missionConfig >> "MissionTypes" >> "Defend")];
_self set ["generatorType", "defend"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "defend"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "defend") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(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 EFUNC(common,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 FUNC(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 FUNC(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 FUNC(getMissionSettingRange);
private _reputationRange = [_defendConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [8, 18]] call FUNC(getMissionSettingRange);
private _penaltyRange = [_defendConfig, ["penalty"], "penaltyMin", "penaltyMax", [-12, -4]] call FUNC(getMissionSettingRange);
private _timeRange = [_defendConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [300, 1800]] call FUNC(getMissionSettingRange);
private _rewards = _self call ["rollRewards"];
private _enemyTemplates = _self call ["buildDefendTemplateGroups", [_position]];
if (_enemyTemplates isEqualTo []) exitWith { "" };
private _fundsReward = _rewardRange call BFUNC(randomNum);
private _reputationReward = _reputationRange call BFUNC(randomNum);
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
private _timeLimit = _timeRange call BFUNC(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 setMarkerShapeLocal "ELLIPSE";
_defenseZone setMarkerSizeLocal [25, 25];
_defenseZone setMarkerTextLocal format ["Defense Zone %1", _grid];
_defenseZone setMarkerAlphaLocal 0.5;
_defenseZone setMarkerBrushLocal "DiagGrid";
_defenseZone setMarkerColor "ColorOrange";
private _success = [
"defend",
_taskID,
_position,
format ["Defend: Grid %1", _grid],
format ["Hold the area in and around grid %1.", _grid],
createHashMapFromArray [],
createHashMapFromArray [
["limitFail", 0],
["limitSuccess", _limitSuccess],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"],
["defenseZone", _defenseZone],
["defendTime", _timeLimit],
["waveCount", _limitSuccess],
["waveCooldown", _waveCooldown],
["minBlufor", _minBlufor],
["enemyTemplates", _enemyTemplates]
],
0,
"",
"mission_manager"
] call FUNC(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
}]
];

View File

@ -0,0 +1,513 @@
#include "..\script_component.hpp"
/*
* 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 GVAR(DefuseMissionGeneratorBaseClass) in missionNamespace.
*
* Public: No
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(DefuseMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
["#type", "DefuseMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
if !(isClass _missionConfig) then {
_missionConfig = configFile >> "CfgMissions";
};
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
_self set ["defuseConfig", (_missionConfig >> "MissionTypes" >> "Defuse")];
_self set ["generatorType", "defuse"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "defuse"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "defuse") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(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 EFUNC(common,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 FUNC(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 FUNC(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 EFUNC(common,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 BFUNC(taskPatrol);
["INFO", format [
"Defuse mission generator: spawned attack group. Side=%1, Units=%2, PatrolRadius=%3, Position=%4",
_side,
count (units _group),
_patrolRadius,
_position
]] call EFUNC(common,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 isNotEqualTo []) 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 EFUNC(common,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 EFUNC(common,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 EFUNC(common,log);
deleteGroup _group;
""
};
private _taskID = format ["task_defuse_%1", round (diag_tickTime * 1000)];
private _rewardRange = [_defuseConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [20000, 50000]] call FUNC(getMissionSettingRange);
private _reputationRange = [_defuseConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [5, 12]] call FUNC(getMissionSettingRange);
private _penaltyRange = [_defuseConfig, ["penalty"], "penaltyMin", "penaltyMax", [-9, -3]] call FUNC(getMissionSettingRange);
private _timeRange = [_defuseConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [600, 900]] call FUNC(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 BFUNC(randomNum);
private _reputationReward = _reputationRange call BFUNC(randomNum);
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
private _timeLimit = _timeRange call BFUNC(randomNum);
private _iedTimer = 300;
private _targetCount = count _devices;
private _defuseZone = format ["forge_defuse_zone_%1", _taskID];
createMarker [_defuseZone, _position];
_defuseZone setMarkerShapeLocal "ELLIPSE";
_defuseZone setMarkerSizeLocal [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 FUNC(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
}]
];

View File

@ -0,0 +1,378 @@
#include "..\script_component.hpp"
/*
* 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 GVAR(DeliveryMissionGeneratorBaseClass) in missionNamespace.
*
* Public: No
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(DeliveryMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
["#type", "DeliveryMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
if !(isClass _missionConfig) then {
_missionConfig = configFile >> "CfgMissions";
};
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["deliveryConfig", (_missionConfig >> "MissionTypes" >> "Delivery")];
_self set ["generatorType", "delivery"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "delivery"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "delivery") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(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 EFUNC(common,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]
]
}],
["getCargoPickupPosition", compileFinal {
params [["_fallbackPosition", [0, 0, 0], [[]]]];
if ("CargoSpawn" in allMapMarkers) exitWith { getMarkerPos "CargoSpawn" };
private _cargoSpawn = missionNamespace getVariable ["CargoSpawn", objNull];
if (_cargoSpawn isEqualType "" && { _cargoSpawn in allMapMarkers }) exitWith { getMarkerPos _cargoSpawn };
if (_cargoSpawn isEqualType objNull && { !(isNull _cargoSpawn) }) exitWith { getPosATL _cargoSpawn };
if ("ExtZone" in allMapMarkers) exitWith { getMarkerPos "ExtZone" };
private _extZone = missionNamespace getVariable ["ExtZone", objNull];
if (_extZone isEqualType "" && { _extZone in allMapMarkers }) exitWith { getMarkerPos _extZone };
if (_extZone isEqualType objNull && { !(isNull _extZone) }) exitWith { getPosATL _extZone };
_fallbackPosition
}],
["spawnDeliveryCargo", compileFinal {
params [["_pickupPosition", [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 { [] };
for "_i" from 1 to _cargoCount do {
private _cargoClass = selectRandom _cargoPool;
private _spawnPos = _pickupPosition vectorAdd [(random 12 - 6), (random 12 - 6), 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 _pickupPos = _self call ["getCargoPickupPosition", [_position]];
private _grid = mapGridPosition _pickupPos;
private _taskID = format ["task_delivery_%1", round (diag_tickTime * 1000)];
private _pickupMarker = format ["forge_delivery_pickup_%1", _taskID];
private _deliveryZone = format ["forge_delivery_zone_%1", _taskID];
private _dropoffMarker = format ["forge_delivery_dropoff_%1", _taskID];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _deliveryPos = [0, 0, 0];
private _attempt = 0;
private _deliverySearchRadius = (_worldSize / 2 - 1000) max 500;
while { _attempt < 80 && { _deliveryPos isEqualTo [0, 0, 0] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, 0, _deliverySearchRadius, 10, 0, 0.3, 0] call BFUNC(findSafePos);
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if ((_candidate distance2D _pickupPos) < 1200) then { continue; };
_candidate set [2, 0];
_deliveryPos = _candidate;
};
if (_deliveryPos isEqualTo [0, 0, 0]) then {
_deliveryPos = [_pickupPos, 1200, 2500, 10, 0, 0.3, 0] call BFUNC(findSafePos);
};
if (_deliveryPos isEqualTo [0, 0, 0]) then {
_deliveryPos = _pickupPos vectorAdd [1500, 0, 0];
};
private _deliveryGrid = mapGridPosition _deliveryPos;
private _rewardRange = [_deliveryConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [10000, 30000]] call FUNC(getMissionSettingRange);
private _reputationRange = [_deliveryConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [3, 8]] call FUNC(getMissionSettingRange);
private _penaltyRange = [_deliveryConfig, ["penalty"], "penaltyMin", "penaltyMax", [-6, -2]] call FUNC(getMissionSettingRange);
private _rewards = _self call ["rollRewards"];
private _cargoObjects = _self call ["spawnDeliveryCargo", [_pickupPos]];
if (_cargoObjects isEqualTo []) exitWith { "" };
createMarker [_pickupMarker, _pickupPos];
_pickupMarker setMarkerTypeLocal "hd_pickup";
_pickupMarker setMarkerColorLocal "ColorBLUFOR";
_pickupMarker setMarkerText format ["Pickup %1", _grid];
createMarker [_deliveryZone, _deliveryPos];
_deliveryZone setMarkerShapeLocal "ELLIPSE";
_deliveryZone setMarkerSizeLocal [25, 25];
_deliveryZone setMarkerTextLocal format ["Delivery Zone %1", _deliveryGrid];
_deliveryZone setMarkerAlphaLocal 0.5;
_deliveryZone setMarkerBrushLocal "DiagGrid";
_deliveryZone setMarkerColor "ColorOrange";
createMarker [_dropoffMarker, _deliveryPos];
_dropoffMarker setMarkerTypeLocal "hd_end";
_dropoffMarker setMarkerColorLocal "ColorBLUFOR";
_dropoffMarker setMarkerText format ["Drop-off %1", _deliveryGrid];
private _fundsReward = _rewardRange call BFUNC(randomNum);
private _reputationReward = _reputationRange call BFUNC(randomNum);
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
private _timeLimit = 0;
private _cargoCount = count _cargoObjects;
private _success = [
"delivery",
_taskID,
_pickupPos,
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 FUNC(startTask);
if !(_success) exitWith {
deleteMarker _pickupMarker;
deleteMarker _deliveryZone;
deleteMarker _dropoffMarker;
""
};
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
_activeMissionRegistry set [_taskID, createHashMapFromArray [
["generatorType", _self call ["getGeneratorType", []]],
["position", _pickupPos],
["markers", [_pickupMarker, _deliveryZone, _dropoffMarker]],
["startedAt", serverTime]
]];
_manager set ["activeMissionRegistry", _activeMissionRegistry];
_taskID
}],
["completeMission", compileFinal {
params [
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
["_taskID", "", [""]]
];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
private _position = _missionRecord getOrDefault ["position", []];
private _markers = _missionRecord getOrDefault ["markers", []];
{
if (_x isEqualType "" && { _x in allMapMarkers }) then {
deleteMarker _x;
};
} forEach _markers;
_activeMissionRegistry deleteAt _taskID;
_manager set ["activeMissionRegistry", _activeMissionRegistry];
if (_position isEqualType [] && { count _position >= 2 }) then {
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
_recentLocationRegistry pushBack [_position, serverTime];
_manager set ["recentLocationRegistry", _recentLocationRegistry];
};
true
}]
];

View File

@ -0,0 +1,469 @@
#include "..\script_component.hpp"
/*
* 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 GVAR(DestroyMissionGeneratorBaseClass) in missionNamespace.
*
* Public: No
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(DestroyMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
["#type", "DestroyMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
if !(isClass _missionConfig) then {
_missionConfig = configFile >> "CfgMissions";
};
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
_self set ["destroyConfig", (_missionConfig >> "MissionTypes" >> "Destroy")];
_self set ["generatorType", "destroy"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "destroy"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "destroy") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(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 EFUNC(common,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 FUNC(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 FUNC(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 EFUNC(common,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 BFUNC(taskDefend);
} else {
[_group, _position, _patrolRadius] call BFUNC(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 EFUNC(common,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 EFUNC(common,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 EFUNC(common,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 EFUNC(common,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 EFUNC(common,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 FUNC(getMissionSettingRange);
private _reputationRange = [_destroyConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [3, 8]] call FUNC(getMissionSettingRange);
private _penaltyRange = [_destroyConfig, ["penalty"], "penaltyMin", "penaltyMax", [-6, -2]] call FUNC(getMissionSettingRange);
private _timeRange = [_destroyConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call FUNC(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 BFUNC(randomNum);
private _reputationReward = _reputationRange call BFUNC(randomNum);
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
private _timeLimit = _timeRange call BFUNC(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 FUNC(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
}]
];

View File

@ -0,0 +1,653 @@
#include "..\script_component.hpp"
/*
* 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 GVAR(HostageMissionGeneratorBaseClass) in missionNamespace.
*
* Public: No
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(HostageMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
["#type", "HostageMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
if !(isClass _missionConfig) then {
_missionConfig = configFile >> "CfgMissions";
};
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
_self set ["hostageConfig", (_missionConfig >> "MissionTypes" >> "Hostage")];
_self set ["generatorType", "hostage"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "hostage"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "hostage") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(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 EFUNC(common,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 FUNC(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 FUNC(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 EFUNC(common,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 BFUNC(taskPatrol);
["INFO", format [
"Hostage mission generator: spawned attack group. Side=%1, Units=%2, PatrolRadius=%3, Position=%4",
_side,
count (units _group),
_patrolRadius,
_position
]] call EFUNC(common,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 FUNC(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 EFUNC(common,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 EFUNC(common,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 EFUNC(common,log);
deleteGroup _group;
""
};
private _taskID = format ["task_hostage_%1", round (diag_tickTime * 1000)];
private _rewardRange = [_hostageConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [60000, 140000]] call FUNC(getMissionSettingRange);
private _reputationRange = [_hostageConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [12, 25]] call FUNC(getMissionSettingRange);
private _penaltyRange = [_hostageConfig, ["penalty"], "penaltyMin", "penaltyMax", [-16, -6]] call FUNC(getMissionSettingRange);
private _timeRange = [_hostageConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [600, 900]] call FUNC(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 BFUNC(randomNum);
private _reputationReward = _reputationRange call BFUNC(randomNum);
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
private _timeLimit = _timeRange call BFUNC(randomNum);
private _extZone = format ["forge_hostage_ext_zone_%1", _taskID];
// Choose extraction marker position:
// 1) Prefer editor-placed marker containing "ExtZone".
// 2) Else, pick a safe point inside a marker containing "blklist".
// 3) Else, pick a safe point anywhere on the map at least 2km away from task position.
private _extPos = [0, 0, 0];
private _extZoneMarkers = allMapMarkers select {
(toLowerANSI (markerText _x) find "extzone") == 0
|| { (toLowerANSI _x find "extzone") == 0 }
};
if (_extZoneMarkers isNotEqualTo []) then {
private _mPos = getMarkerPos (selectRandom _extZoneMarkers);
// Put marker on ground.
private _ground = +_mPos;
private _safe = [_ground, 0, 30, 3, 0, 0.3, 0] call BFUNC(findSafePos);
if (_safe isNotEqualTo [0, 0, 0]) then {
_ground = _safe;
};
_ground set [2, 0];
_extPos = _ground;
} else {
// Collect blklist-like markers (rectangle/ellipse) that already exist.
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
if (_blkListMarkers isNotEqualTo []) then {
private _selectedBlk = selectRandom _blkListMarkers;
private _attempt = 0;
private _maxAttempts = 60;
private _found = false;
while { _attempt < _maxAttempts && { !_found } } do {
_attempt = _attempt + 1;
private _markerSize = getMarkerSize _selectedBlk;
private _markerRadius = ((_markerSize param [0, 250, [0]]) max (_markerSize param [1, 250, [0]])) max 250;
private _candidate = [getMarkerPos _selectedBlk, 0, _markerRadius, 3, 0, 0.3, 0] call BFUNC(findSafePos);
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if !(_candidate inArea _selectedBlk) then { continue; };
// Ensure it's on land.
private _try = +_candidate;
_try set [2, 0];
_extPos = _try;
_found = true;
};
};
if (_extPos isEqualTo [0, 0, 0]) then {
// Fallback: anywhere on map, at least 2km from task location.
private _taskPos2D = +_position;
_taskPos2D set [2, 0];
private _worldMin = 0;
private _worldMax = worldSize;
private _attempt = 0;
private _maxAttempts = 80;
private _found = false;
while { _attempt < _maxAttempts && { !_found } } do {
_attempt = _attempt + 1;
private _randX = _worldMin + random (_worldMax - _worldMin);
private _randY = _worldMin + random (_worldMax - _worldMin);
private _probe = [_randX, _randY, 0];
if ((_probe distance2D _taskPos2D) < 2000) then { continue; };
private _safe = [_probe, 0, 500, 3, 0, 0.3, 0] call BFUNC(findSafePos);
if (_safe isEqualTo [0, 0, 0]) then { continue; };
if ((_safe distance2D _taskPos2D) < 2000) then { continue; };
_safe set [2, 0];
_extPos = _safe;
_found = true;
};
// Absolute last resort.
if (_extPos isEqualTo [0, 0, 0]) then {
private _fallback = _position vectorAdd [2500, 0, 0];
_fallback set [2, 0];
_extPos = _fallback;
};
};
};
createMarker [_extZone, _extPos];
_extZone setMarkerShapeLocal "ELLIPSE";
_extZone setMarkerSizeLocal [25, 25];
_extZone setMarkerTextLocal format ["Hostage Extraction %1", _grid];
_extZone setMarkerAlphaLocal 0.5;
_extZone setMarkerBrushLocal "DiagGrid";
_extZone setMarkerColor "ColorOrange";
private _hostageCount = count _hostageUnits;
private _limitFail = 1;
private _success = [
"hostage",
_taskID,
_position,
format ["Hostage: Grid %1", _grid],
format ["Rescue hostages operating near grid %1.", _grid],
createHashMapFromArray [["hostages", _hostageUnits], ["shooters", _shooterUnits]],
createHashMapFromArray [
["limitFail", _limitFail],
["limitSuccess", _hostageCount],
["extractionZone", _extZone],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"],
["execution", true],
["cbrn", false]
],
0,
"",
"mission_manager"
] call FUNC(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
}]
];

View File

@ -0,0 +1,377 @@
#include "..\script_component.hpp"
/*
* 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 GVAR(KillHvtMissionGeneratorBaseClass) in missionNamespace.
*
* Public: No
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(KillHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
["#type", "KillHvtMissionGeneratorBaseClass"],
["#create", compileFinal {
private _missionConfig = missionConfigFile >> "CfgMissions";
if !(isClass _missionConfig) then {
_missionConfig = configFile >> "CfgMissions";
};
_self set ["missionConfig", _missionConfig];
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
_self set ["hvtConfig", (_missionConfig >> "MissionTypes" >> "HVTKill")];
_self set ["generatorType", "hvtkill"];
}],
["getGeneratorType", compileFinal {
_self getOrDefault ["generatorType", "hvtkill"]
}],
["getMissionInterval", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _interval = getNumber (_missionConfig >> "missionInterval");
if (_interval <= 0) then { _interval = 300; };
_interval
}],
["getMaxConcurrentMissions", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
_maxConcurrent
}],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
if (_cooldown <= 0) then { _cooldown = 900; };
_cooldown
}],
["pruneRecentLocations", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
private _now = serverTime;
_recentLocationRegistry = _recentLocationRegistry select {
private _usedAt = _x param [1, -1, [0]];
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
};
_manager set ["recentLocationRegistry", _recentLocationRegistry];
_recentLocationRegistry
}],
["getActiveMissionPositions", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _positions = [];
{
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "hvtkill") then { continue; };
private _position = _y getOrDefault ["position", []];
if (_position isEqualType [] && { count _position >= 2 }) then {
_positions pushBack _position;
};
} forEach _activeMissionRegistry;
_positions
}],
["selectLocation", compileFinal {
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
private _worldSize = worldSize;
private _center = [_worldSize / 2, _worldSize / 2, 0];
private _safeDist = 800;
private _playerPos = _center;
private _minEdgeDist = _safeDist + 200;
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
_blkListMarkers = _blkListMarkers select {
(
(toLowerANSI _x find "blklist") == 0
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
)
&& { getMarkerPos _x distance2D [0, 0] > 0 }
};
private _taskPos = [];
private _attempt = 0;
private _maxAttempts = 50;
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
_attempt = _attempt + 1;
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(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 EFUNC(common,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 FUNC(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 FUNC(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 BFUNC(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 EFUNC(common,log);
private _taskID = format ["task_kill_hvt_%1", round (diag_tickTime * 1000)];
private _rewardRange = [_hvtConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [50000, 120000]] call FUNC(getMissionSettingRange);
private _reputationRange = [_hvtConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [10, 22]] call FUNC(getMissionSettingRange);
private _penaltyRange = [_hvtConfig, ["penalty"], "penaltyMin", "penaltyMax", [-14, -5]] call FUNC(getMissionSettingRange);
private _timeRange = [_hvtConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call FUNC(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 BFUNC(randomNum);
private _reputationReward = _reputationRange call BFUNC(randomNum);
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
private _timeLimit = _timeRange call BFUNC(randomNum);
private _success = [
"hvt",
_taskID,
_position,
format ["HVT: Grid %1", _grid],
format ["Eliminate a high-value target near grid %1.", _grid],
createHashMapFromArray [["hvts", [_hvtTarget]]],
createHashMapFromArray [
["limitFail", 0],
["limitSuccess", 1],
["captureHvt", false],
["funds", _fundsReward],
["ratingFail", _reputationPenalty],
["ratingSuccess", _reputationReward],
["endSuccess", false],
["endFail", false],
["timeLimit", _timeLimit],
["equipment", _rewards get "equipment"],
["supplies", _rewards get "supplies"],
["weapons", _rewards get "weapons"],
["vehicles", _rewards get "vehicles"],
["special", _rewards get "special"]
],
0,
"",
"mission_manager"
] call FUNC(startTask);
if !(_success) exitWith { "" };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
_activeMissionRegistry set [_taskID, createHashMapFromArray [
["generatorType", _self call ["getGeneratorType", []]],
["position", _position],
["startedAt", serverTime]
]];
_manager set ["activeMissionRegistry", _activeMissionRegistry];
_taskID
}],
["completeMission", compileFinal {
params [
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
["_taskID", "", [""]]
];
if (_taskID isEqualTo "") exitWith { false };
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
private _position = _missionRecord getOrDefault ["position", []];
private _markers = _missionRecord getOrDefault ["markers", []];
{
if (_x isEqualType "" && { _x in allMapMarkers }) then {
deleteMarker _x;
};
} forEach _markers;
_activeMissionRegistry deleteAt _taskID;
_manager set ["activeMissionRegistry", _activeMissionRegistry];
if (_position isEqualType [] && { count _position >= 2 }) then {
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
_recentLocationRegistry pushBack [_position, serverTime];
_manager set ["recentLocationRegistry", _recentLocationRegistry];
};
true
}]
];

View File

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

View File

@ -0,0 +1,54 @@
#include "..\script_component.hpp"
/*
* 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:
* Numeric [min, max] range <ARRAY>. Reversed input is sorted so reputation-hit
* fields can use -5 / -25 semantics while generators still receive [-25, -5].
*
* 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 {
private _swap = _min;
_min = _max;
_max = _swap;
};
[_min, _max]

View File

@ -0,0 +1,57 @@
#include "..\script_component.hpp"
/*
* 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 EFUNC(common,log);
_multiplier

View File

@ -69,6 +69,26 @@ Common generated IDs:
| `cad:groups:build` | `groups_seed_json` | Group array JSON. |
| `cad:view:hydrate` | `hydrate_seed_json` | Hydrated CAD payload JSON. |
## Generated Mission Requests
Dispatchers can request framework-generated mission tasks from the CAD
dispatcher board. The server hydrates the available generated task types from
the task mission manager as `generatedTaskTypes`; the client uses that hydrated
list for the dropdown.
Generated mission requests are controlled by the server CBA setting
`forge_task_enableGenerator`:
- Enabled: CAD receives the generated task type list and dispatchers can request
a specific generator type.
- Disabled: CAD receives an empty generated task type list, the task request UI
is disabled, and server-side request handling rejects any manual request.
The framework-owned request entry point is
`forge_server_task_fnc_requestMissionTask`. Server CAD calls that first and only
falls back to a mission-local `forge_pmc_fnc_requestMissionTask` when the
framework entry point is unavailable.
## Submit a Support Request
```sqf
@ -175,6 +195,7 @@ private _session = createHashMapFromArray [
private _seed = createHashMapFromArray [
["groups", _liveGroups],
["activeTasks", _activeTasks],
["generatedTaskTypes", _generatedTaskTypes],
["session", _session]
];

View File

@ -22,8 +22,8 @@ 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.
selected mode, dispatch view, session data, groups, tasks, requests,
assignments, and server-provided generated task types.
`forge_client_cad_fnc_initUIBridge` owns:
@ -57,6 +57,7 @@ focus uses the member position included in the hydrated group roster.
| `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::generatedTask::request` | Request a server-generated mission task from the selected generator type. |
| `cad::tasks::assign` | Assign a task to a group. |
| `cad::tasks::acknowledge` | Acknowledge assigned task. |
| `cad::tasks::decline` | Decline assigned task. |
@ -99,6 +100,13 @@ 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.
The dispatcher-generated task dropdown is hydrated from the server
`generatedTaskTypes` payload. The UI has a built-in fallback list for loading or
older payload compatibility, but any hydrate payload that includes
`generatedTaskTypes` replaces that fallback. An empty hydrated list disables the
request control, which is how `forge_task_enableGenerator = false` is surfaced
client-side.
## Authorization Notes
Only dispatcher sessions can enter dispatch mode. If the hydrated session is

View File

@ -728,6 +728,21 @@ Validation:
## Mission Manager Blacklist Markers
Generated missions are configured through mission-local `CfgMissions.hpp`.
Place it in the mission folder and include it from `description.ext` so mission
designers own task weights, reward ranges, time limits, HVT/hostage class
pools, defuse device pools, delivery cargo, destroy targets, and the
`locationReuseCooldown`.
If a mission does not define `CfgMissions`, the task addon falls back to the
framework copy at `arma/server/addons/task/CfgMissions.hpp`. Treat the
framework file as the default schema and mission copies as the tuning layer.
The generated mission system supports `attack`, `defend`, `defuse`,
`delivery`, `destroy`, `hostage`, `hvtkill`, and `hvtcapture`. The
`forge_task_enableGenerator` CBA setting gates both timer-based generation and
CAD dispatcher-requested generation.
The dynamic mission generator avoids rectangle and ellipse area markers whose
marker name or marker text starts with `blklist`.

View File

@ -140,9 +140,47 @@ Mission designers can create tasks in four ways:
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
The dynamic mission manager can also generate attack, defend, defuse, delivery,
destroy, hostage, HVT kill, and HVT capture tasks from config. That is
system-generated content rather than a hand-authored task creation path.
## Generated Mission Configuration
Mission designers should define `class CfgMissions` in the mission folder, such
as `arma/missions/<missionName>/CfgMissions.hpp`, and include it from
`description.ext`. The framework also ships
`arma/server/addons/task/CfgMissions.hpp` as a fallback schema for missions that
do not provide their own config.
The generator lookup order is:
1. `missionConfigFile >> "CfgMissions"`
2. `configFile >> "CfgMissions"`
Mission config therefore wins. Use mission-local `CfgMissions.hpp` for task
weights, reward ranges, time limits, object pools, HVT/hostage classes, defuse
device pools, delivery cargo pools, destroy targets, and location reuse
cooldown. Keep the framework copy in sync with the expected schema so fallback
generation still works.
Generated mission types currently exposed by the framework are:
| Type | CAD Value | Base Task Flow |
| --- | --- | --- |
| Attack | `attack` | `attack` |
| Defend | `defend` | `defend` |
| Defuse | `defuse` | `defuse` |
| Delivery | `delivery` | `delivery` |
| Destroy | `destroy` | `destroy` |
| Hostage | `hostage` | `hostage` |
| Kill HVT | `hvtkill` | `hvt` |
| Capture HVT | `hvtcapture` | `hvt` |
The server CBA setting `forge_task_enableGenerator` is the single runtime gate
for generated missions. When disabled, timer-based generation does not run, CAD
hydrates no generated task types, and manual dispatcher requests are rejected
server-side.
## CAD Compatibility
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
@ -159,7 +197,7 @@ CAD-compatible creation paths:
`forge_server_task_fnc_startTask`.
- `forge_server_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
- Dynamic mission manager tasks: compatible because the mission manager
uses `forge_server_task_fnc_startTask`.
Limited or incompatible paths:

View File

@ -150,8 +150,7 @@ Minimum Eden setup:
Transport nodes are generic paid travel points. They can represent ferries,
airports, bus stops, teleport terminals, or any other mission transport system.
The framework owns the menu, billing, cargo scan, and movement logic. The
mission only needs placed objects and optional arrival markers.
The framework owns the menu, billing, cargo scan, and movement logic.
![Eden transport location one](images/eden/transport_loc_1.jpg)
@ -171,7 +170,7 @@ transport_2
transport_10
```
Place optional arrival markers with matching suffixes:
Place arrival markers with matching suffixes:
```text
transport_arrival
@ -729,6 +728,21 @@ Validation:
## Mission Manager Blacklist Markers
Generated missions are configured through mission-local `CfgMissions.hpp`.
Place it in the mission folder and include it from `description.ext` so mission
designers own task weights, reward ranges, time limits, HVT/hostage class
pools, defuse device pools, delivery cargo, destroy targets, and the
`locationReuseCooldown`.
If a mission does not define `CfgMissions`, the task addon falls back to the
framework copy at `arma/server/addons/task/CfgMissions.hpp`. Treat the
framework file as the default schema and mission copies as the tuning layer.
The generated mission system supports `attack`, `defend`, `defuse`,
`delivery`, `destroy`, `hostage`, `hvtkill`, and `hvtcapture`. The
`forge_task_enableGenerator` CBA setting gates both timer-based generation and
CAD dispatcher-requested generation.
The dynamic mission generator avoids rectangle and ellipse area markers whose
marker name or marker text starts with `blklist`.

View File

@ -226,7 +226,7 @@ Player workflow:
1. Stand near a transport point.
2. Open the actor interaction menu.
3. Select Transport.
4. Select a destination from the transport submenu, or select Close to return
4. Select a destination from the transport submenu, or select Back to return
to the default interaction menu.
![Transport destination submenu](images/player/transport_destination_menu.jpg)

View File

@ -139,9 +139,47 @@ Mission designers can create tasks in four ways:
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
The dynamic mission manager can also generate attack, defend, defuse, delivery,
destroy, hostage, HVT kill, and HVT capture tasks from config. That is
system-generated content rather than a hand-authored task creation path.
## Generated Mission Configuration
Mission designers should define `class CfgMissions` in the mission folder, such
as `arma/missions/<missionName>/CfgMissions.hpp`, and include it from
`description.ext`. The framework also ships
`arma/server/addons/task/CfgMissions.hpp` as a fallback schema for missions that
do not provide their own config.
The generator lookup order is:
1. `missionConfigFile >> "CfgMissions"`
2. `configFile >> "CfgMissions"`
Mission config therefore wins. Use mission-local `CfgMissions.hpp` for task
weights, reward ranges, time limits, object pools, HVT/hostage classes, defuse
device pools, delivery cargo pools, destroy targets, and location reuse
cooldown. Keep the framework copy in sync with the expected schema so fallback
generation still works.
Generated mission types currently exposed by the framework are:
| Type | CAD Value | Base Task Flow |
| --- | --- | --- |
| Attack | `attack` | `attack` |
| Defend | `defend` | `defend` |
| Defuse | `defuse` | `defuse` |
| Delivery | `delivery` | `delivery` |
| Destroy | `destroy` | `destroy` |
| Hostage | `hostage` | `hostage` |
| Kill HVT | `hvtkill` | `hvt` |
| Capture HVT | `hvtcapture` | `hvt` |
The server CBA setting `forge_task_enableGenerator` is the single runtime gate
for generated missions. When disabled, timer-based generation does not run, CAD
hydrates no generated task types, and manual dispatcher requests are rejected
server-side.
## CAD Compatibility
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
@ -158,7 +196,7 @@ CAD-compatible creation paths:
`forge_server_task_fnc_startTask`.
- `forge_server_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
- Dynamic mission manager tasks: compatible because the mission manager
uses `forge_server_task_fnc_startTask`.
Limited or incompatible paths:

View File

@ -1,6 +1,6 @@
---
title: "Transport Service Guide"
description: "The transport service provides paid point-to-point travel for players and nearby vehicles or passengers. It is framework-owned: missions only need placed transport objects and optional arrival markers with the expected variable names."
description: "The transport service provides paid point-to-point travel for players and nearby vehicles or passengers. It is framework-owned: missions only need placed transport objects and arrival markers with the expected variable names."
---
## Mission Contract
@ -134,9 +134,3 @@ These screenshots show the default transport setup and player workflow:
![Player transport destination submenu](images/player/transport_destination_menu.jpg)
![Player transport completion notification](images/player/transport_complete.jpg)
## Mission-Side Code Requirement
No mission-side transport service, addAction script, or server event bridge is
required. The framework handles menu discovery, destination selection, pricing,
billing, cargo movement, and EventBus notifications.

View File

@ -67,6 +67,26 @@ Common generated IDs:
| `cad:groups:build` | `groups_seed_json` | Group array JSON. |
| `cad:view:hydrate` | `hydrate_seed_json` | Hydrated CAD payload JSON. |
## Generated Mission Requests
Dispatchers can request framework-generated mission tasks from the CAD
dispatcher board. The server hydrates the available generated task types from
the task mission manager as `generatedTaskTypes`; the client uses that hydrated
list for the dropdown.
Generated mission requests are controlled by the server CBA setting
`forge_task_enableGenerator`:
- Enabled: CAD receives the generated task type list and dispatchers can request
a specific generator type.
- Disabled: CAD receives an empty generated task type list, the task request UI
is disabled, and server-side request handling rejects any manual request.
The framework-owned request entry point is
`forge_server_task_fnc_requestMissionTask`. Server CAD calls that first and only
falls back to a mission-local `forge_pmc_fnc_requestMissionTask` when the
framework entry point is unavailable.
## Submit a Support Request
```sqf
@ -173,6 +193,7 @@ private _session = createHashMapFromArray [
private _seed = createHashMapFromArray [
["groups", _liveGroups],
["activeTasks", _activeTasks],
["generatedTaskTypes", _generatedTaskTypes],
["session", _session]
];

View File

@ -21,8 +21,8 @@ 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.
selected mode, dispatch view, session data, groups, tasks, requests,
assignments, and server-provided generated task types.
`forge_client_cad_fnc_initUIBridge` owns:
@ -56,6 +56,7 @@ focus uses the member position included in the hydrated group roster.
| `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::generatedTask::request` | Request a server-generated mission task from the selected generator type. |
| `cad::tasks::assign` | Assign a task to a group. |
| `cad::tasks::acknowledge` | Acknowledge assigned task. |
| `cad::tasks::decline` | Decline assigned task. |
@ -98,6 +99,13 @@ 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.
The dispatcher-generated task dropdown is hydrated from the server
`generatedTaskTypes` payload. The UI has a built-in fallback list for loading or
older payload compatibility, but any hydrate payload that includes
`generatedTaskTypes` replaces that fallback. An empty hydrated list disables the
request control, which is how `forge_task_enableGenerator = false` is surfaced
client-side.
## Authorization Notes
Only dispatcher sessions can enter dispatch mode. If the hydrated session is