Add CAD mission task generation flow

- Wire dispatcher UI events into a new generated task request path
- Add bridge/server handling for generated mission task requests
- Update mission generators and dispatcher UI for task metadata
This commit is contained in:
Jacob Schmidt 2026-05-23 02:10:34 -05:00
parent 8582e6c5e5
commit e688d9426b
22 changed files with 389 additions and 24 deletions

View File

@ -88,6 +88,16 @@ switch (_event) do {
GVAR(CADUIBridge) call ["requestCreateDispatchOrder", [_assigneeGroupID, _targetGroupID, _note, _priority, _request]];
};
case "cad::generatedTask::request": {
private _taskType = "";
private _metadata = createHashMap;
if (_data isEqualType createHashMap) then {
_taskType = _data getOrDefault ["taskType", ""];
_metadata = _data getOrDefault ["metadata", createHashMap];
};
GVAR(CADUIBridge) call ["requestGeneratedMissionTask", [_taskType, _metadata]];
};
case "cad::supportRequest::submit": {
private _type = "";
private _fields = createHashMap;

View File

@ -227,6 +227,17 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [
[SRPC(cad,requestCreateCadDispatchOrder), [getPlayerUID player, _assigneeGroupID, _targetGroupID, _note, _priority, _request]] call CFUNC(serverEvent);
true
}],
["requestGeneratedMissionTask", compileFinal {
params [
["_taskType", "", [""]],
["_metadata", createHashMap, [createHashMap]]
];
if (_taskType isEqualTo "") exitWith { false };
[SRPC(cad,requestGenerateCadMissionTask), [getPlayerUID player, _taskType, _metadata]] call CFUNC(serverEvent);
true
}],
["requestSubmitSupportRequest", compileFinal {
params [
["_type", "", [""]],

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -49,15 +49,24 @@
<section class="dispatch-panel dispatch-panel-open">
<div class="dispatch-panel-header">
<h3>Available Contracts</h3>
<button
id="dispatcherCreateOrderBtn"
type="button"
class="dispatch-icon-btn"
aria-label="Create dispatch order"
title="Create dispatch order"
>
+
</button>
<div class="dispatch-panel-actions">
<button
id="dispatcherRequestTaskBtn"
type="button"
class="dispatch-btn dispatch-btn-compact"
>
Request Task
</button>
<button
id="dispatcherCreateOrderBtn"
type="button"
class="dispatch-icon-btn"
aria-label="Create dispatch order"
title="Create dispatch order"
>
+
</button>
</div>
</div>
<div
id="dispatcherOpenContracts"
@ -244,6 +253,51 @@
</div>
</div>
<div id="dispatcherTaskModal" class="dispatch-modal is-hidden">
<div class="dispatch-modal-backdrop"></div>
<div
class="dispatch-modal-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="dispatcherTaskModalTitle"
>
<div class="dispatch-modal-header">
<div>
<p class="dispatch-kicker">Mission Generator</p>
<h3 id="dispatcherTaskModalTitle">Request Task</h3>
</div>
<button
id="dispatcherTaskModalCloseBtn"
class="dispatch-icon-btn"
type="button"
aria-label="Close task request"
>
x
</button>
</div>
<div class="dispatch-modal-body">
<div class="dispatch-modal-fields">
<label class="dispatch-field">
<span>Task Type</span>
<select
id="dispatcherTaskTypeSelect"
class="dispatch-select"
></select>
</label>
</div>
</div>
<div class="dispatch-modal-actions">
<button
id="dispatcherTaskModalSaveBtn"
type="button"
class="dispatch-btn"
>
Generate Task
</button>
</div>
</div>
</div>
<div id="dispatcherRequestModal" class="dispatch-modal is-hidden">
<div class="dispatch-modal-backdrop"></div>
<div

View File

@ -73,6 +73,15 @@ window.cadDispatcherFormatters = {
})
.join("");
},
buildTaskTypeOptions(selectedTaskType) {
return this.taskTypes
.map((taskType) => {
const value = taskType.value || "";
const selected = value === selectedTaskType ? "selected" : "";
return `<option value="${value}" ${selected}>${taskType.label || value}</option>`;
})
.join("");
},
formatRequestFieldLabel(fieldID) {
return (fieldID || "field")
.replaceAll("_", " ")

View File

@ -11,6 +11,16 @@ 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" },
],
statuses: [
"available",
"en_route",
@ -24,6 +34,12 @@ window.cadDispatcher = {
...dispatcherModals,
...dispatcherRender,
init() {
document
.getElementById("dispatcherRequestTaskBtn")
.addEventListener("click", () => {
this.openTaskModal();
});
document
.getElementById("dispatcherCreateOrderBtn")
.addEventListener("click", () => {
@ -66,6 +82,24 @@ window.cadDispatcher = {
this.closeOrderModal();
});
document
.getElementById("dispatcherTaskModalCloseBtn")
.addEventListener("click", () => {
this.closeTaskModal();
});
document
.getElementById("dispatcherTaskModalSaveBtn")
.addEventListener("click", () => {
this.requestGeneratedTask();
});
document
.querySelector("#dispatcherTaskModal .dispatch-modal-backdrop")
.addEventListener("click", () => {
this.closeTaskModal();
});
document
.getElementById("dispatcherRequestModalCloseBtn")
.addEventListener("click", () => {
@ -188,6 +222,24 @@ window.cadDispatcher = {
this.closeOrderModal();
},
requestGeneratedTask() {
const taskType = document.getElementById(
"dispatcherTaskTypeSelect",
).value;
if (!taskType) {
this.setStatus(
"Select a task type before requesting a task.",
"error",
);
return;
}
this.setStatus("Requesting generated task...", "info");
window.mapUI.sendEvent("cad::generatedTask::request", {
taskType: taskType,
});
this.closeTaskModal();
},
assignTask(taskID) {
const selector = document.getElementById(
`dispatcher-assign-group-${taskID}`,

View File

@ -1,4 +1,27 @@
window.cadDispatcherModals = {
openTaskModal() {
this.populateTaskModal();
document
.getElementById("dispatcherTaskModal")
.classList.remove("is-hidden");
},
closeTaskModal() {
document
.getElementById("dispatcherTaskModal")
.classList.add("is-hidden");
},
populateTaskModal() {
const taskTypeSelect = document.getElementById(
"dispatcherTaskTypeSelect",
);
if (!taskTypeSelect) {
return;
}
taskTypeSelect.innerHTML = this.buildTaskTypeOptions(
taskTypeSelect.value || this.taskTypes[0]?.value || "",
);
},
openOrderModal() {
this.convertingRequestId = "";
this.populateOrderModal();

View File

@ -34,6 +34,12 @@ body {
gap: 16px;
}
.dispatch-panel-actions {
display: flex;
align-items: center;
gap: 8px;
}
.dispatch-kicker {
margin: 0 0 4px;
color: var(--accent);
@ -63,6 +69,12 @@ body {
cursor: pointer;
}
.dispatch-btn-compact {
padding: 8px 10px;
min-height: 32px;
font-size: 12px;
}
.dispatch-btn-secondary {
background: rgba(53, 40, 39, 0.92);
}

View File

@ -27,6 +27,7 @@ class CfgFunctions {
postInit = 1;
};
class persistentCadMissionManager {};
class requestMissionTask {};
class updateEnemyCountFromActivePlayers {};
};

View File

@ -82,6 +82,7 @@ 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]];
@ -101,7 +102,7 @@ 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 BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };

View File

@ -82,6 +82,7 @@ CaptureHvtMissionGeneratorBaseClass = 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]];
@ -101,7 +102,7 @@ CaptureHvtMissionGeneratorBaseClass = 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 BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };

View File

@ -82,6 +82,7 @@ DefendMissionGeneratorBaseClass = 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]];
@ -101,7 +102,7 @@ DefendMissionGeneratorBaseClass = 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 BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };

View File

@ -83,6 +83,7 @@ DefuseMissionGeneratorBaseClass = 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]];
@ -102,7 +103,7 @@ DefuseMissionGeneratorBaseClass = 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 BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };

View File

@ -82,6 +82,7 @@ DeliveryMissionGeneratorBaseClass = 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]];
@ -101,7 +102,7 @@ DeliveryMissionGeneratorBaseClass = 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 BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
@ -242,9 +243,10 @@ DeliveryMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
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, _worldSize / 2 - 1000, 10, 0, 0.3, 0] call BIS_fnc_findSafePos;
private _candidate = [_center, 0, _deliverySearchRadius, 10, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if ((_candidate distance2D _position) < 1200) then { continue; };
_candidate set [2, 0];

View File

@ -83,6 +83,7 @@ DestroyMissionGeneratorBaseClass = 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]];
@ -102,7 +103,7 @@ DestroyMissionGeneratorBaseClass = 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 BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };

View File

@ -83,6 +83,7 @@ HostageMissionGeneratorBaseClass = 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]];
@ -102,7 +103,7 @@ HostageMissionGeneratorBaseClass = 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 BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
@ -482,7 +483,7 @@ HostageMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
private _mPos = getMarkerPos (selectRandom _extZoneMarkers);
// Put marker on ground.
private _ground = +_mPos;
private _safe = [_ground, 30, 0] call BIS_fnc_findSafePos;
private _safe = [_ground, 0, 30, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if !((_safe isEqualTo [0, 0, 0])) then {
_ground = _safe;
};
@ -507,7 +508,9 @@ HostageMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
private _found = false;
while { _attempt < _maxAttempts && { !_found } } do {
_attempt = _attempt + 1;
private _candidate = [_position, 2000, 0] call BIS_fnc_findSafePos;
private _markerSize = getMarkerSize _selectedBlk;
private _markerRadius = ((_markerSize param [0, 250, [0]]) max (_markerSize param [1, 250, [0]])) max 250;
private _candidate = [getMarkerPos _selectedBlk, 0, _markerRadius, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if !(_candidate inArea _selectedBlk) then { continue; };
// Ensure it's on land.
@ -535,8 +538,9 @@ HostageMissionGeneratorBaseClass = compileFinal createHashMapFromArray [
private _randY = _worldMin + random (_worldMax - _worldMin);
private _probe = [_randX, _randY, 0];
if ((_probe distance2D _taskPos2D) < 2000) then { continue; };
private _safe = [_probe, 2000, 0] call BIS_fnc_findSafePos;
private _safe = [_probe, 0, 500, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
if (_safe isEqualTo [0, 0, 0]) then { continue; };
if ((_safe distance2D _taskPos2D) < 2000) then { continue; };
_safe set [2, 0];
_extPos = _safe;
_found = true;

View File

@ -82,6 +82,7 @@ KillHvtMissionGeneratorBaseClass = 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]];
@ -101,7 +102,7 @@ KillHvtMissionGeneratorBaseClass = 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 BIS_fnc_findSafePos;
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
if (_candidate distance2D _playerPos < _safeDist) then { continue; };

View File

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

View File

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

View File

@ -65,6 +65,48 @@ call FUNC(registerEventListeners);
]];
}] call CFUNC(addEventHandler);
[QGVAR(requestGenerateCadMissionTask), {
params [
["_uid", "", [""]],
["_taskType", "", [""]],
["_metadata", createHashMap, [createHashMap]]
];
private _player = GVAR(CadStore) call ["resolveRequestPlayer", [_uid, "Invalid CAD generated task payload."]];
if (_player isEqualTo objNull) exitWith {};
private _result = createHashMapFromArray [
["success", false],
["message", "Generated task request failed."]
];
if (_taskType isEqualTo "") exitWith {
_result set ["message", "Select a task type before requesting a generated task."];
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
};
private _permissionService = GVAR(CadStore) getOrDefault ["PermissionService", createHashMap];
if (_permissionService isEqualTo createHashMap || { !(_permissionService call ["canDispatch", [_uid]]) }) exitWith {
_result set ["message", "Only dispatchers can request generated tasks."];
[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."];
[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);
};
}] call CFUNC(addEventHandler);
[QGVAR(requestSubmitCadSupportRequest), {
params [
["_uid", "", [""]],