diff --git a/arma/client/addons/cad/functions/fnc_initRepository.sqf b/arma/client/addons/cad/functions/fnc_initRepository.sqf
index 07e535d..e7c4af1 100644
--- a/arma/client/addons/cad/functions/fnc_initRepository.sqf
+++ b/arma/client/addons/cad/functions/fnc_initRepository.sqf
@@ -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
}],
diff --git a/arma/client/addons/cad/ui/_site/cad-dispatcher.js b/arma/client/addons/cad/ui/_site/cad-dispatcher.js
index db9bd0d..183ef78 100644
--- a/arma/client/addons/cad/ui/_site/cad-dispatcher.js
+++ b/arma/client/addons/cad/ui/_site/cad-dispatcher.js
@@ -1 +1 @@
-window.cadDispatcherFormatters={getDangerGroups(){return this.groups.filter(e=>"danger"===(e.status||""))},getSupportAlertRequests(){return this.requests.filter(e=>["medevac_9line","fire_support","air_support"].includes(e.type||""))},buildSupportAlertMessage(){const e=this.getSupportAlertRequests();if(!e.length)return"";return`Support request alert: ${e.map(e=>`${e.groupCallsign||e.groupId||"Unknown Group"} ${this.getRequestTypeLabel(e.type||"request")}`).join(", ")}`},getSortedGroups(){return this.groups.slice().sort((e,t)=>{const s="danger"===(e.status||"")?0:1,n="danger"===(t.status||"")?0:1;if(s!==n)return s-n;const r=e.callsign||e.groupId||"",a=t.callsign||t.groupId||"";return r.localeCompare(a)})},isDispatchOrder:e=>!!e.isDispatchOrder||"dispatch_order"===(e.type||""),formatTypeLabel(e){const t=(e.type||"task").replaceAll("_"," ");return this.isDispatchOrder(e)?"dispatch order":t},getRequestTypeLabel(e){switch(e){case"medevac_9line":return"9-Line MEDEVAC";case"ace_lace":return"ACE/LACE";case"fire_support":return"Fire Support";case"air_support":return"Air Support";case"logreq":return"LOGREQ";default:return(e||"request").replaceAll("_"," ")}},buildGroupOptions(e){return this.getSortedGroups().map(t=>{const s=t.groupId||"";return`${t.callsign||s} `}).join("")},buildTaskTypeOptions(e){return this.taskTypes.map(t=>{const s=t.value||"";return`${t.label||s} `}).join("")},formatRequestFieldLabel:e=>(e||"field").replaceAll("_"," ").replace(/\b\w/g,e=>e.toUpperCase()),formatRequestFieldValue(e){if(Array.isArray(e))return e.join(", ");if(e&&"object"==typeof e)return JSON.stringify(e);return String(e??"").trim()||"Not provided"},buildRequestOrderNote(e){const t=this.getRequestTypeLabel(e.type||"request"),s=e.groupCallsign||e.groupId||"Unknown Group",n=(e.summary||"").trim(),r=e.fields&&"object"==typeof e.fields?Object.entries(e.fields).map(([e,t])=>{const s=this.formatRequestFieldValue(t);return"Not provided"===s?"":`${this.formatRequestFieldLabel(e)} ${s}`}).filter(Boolean):[],a=r.length?r:[n].filter(Boolean);return a.length?`${t} requested by ${s}. ${a.join(" | ")}`:`${t} requested by ${s}.`}},window.cadDispatcherModals={openTaskModal(){this.populateTaskModal(),document.getElementById("dispatcherTaskModal").classList.remove("is-hidden")},closeTaskModal(){document.getElementById("dispatcherTaskModal").classList.add("is-hidden")},populateTaskModal(){const e=document.getElementById("dispatcherTaskTypeSelect");e&&(e.innerHTML=this.buildTaskTypeOptions(e.value||this.taskTypes[0]?.value||""))},openOrderModal(){this.convertingRequestId="",this.populateOrderModal(),document.getElementById("dispatcherOrderModalTitle").textContent="Create Support Order",document.getElementById("dispatcherOrderModal").classList.remove("is-hidden")},closeOrderModal(){this.convertingRequestId="",document.getElementById("dispatcherOrderNoteInput").value="",document.getElementById("dispatcherOrderPrioritySelect").value="priority",document.getElementById("dispatcherOrderModalTitle").textContent="Create Support Order",document.getElementById("dispatcherOrderModal").classList.add("is-hidden")},openRequestModal(e){const t=this.requests.find(t=>t.requestId===e);t&&(this.viewingRequestId=e,this.populateRequestModal(t),document.getElementById("dispatcherRequestModal").classList.remove("is-hidden"))},closeRequestModal(){this.viewingRequestId="",document.getElementById("dispatcherRequestModal").classList.add("is-hidden")},syncRequestModal(){if(!this.viewingRequestId)return;const e=this.requests.find(e=>e.requestId===this.viewingRequestId);e?this.populateRequestModal(e):this.closeRequestModal()},populateRequestModal(e){const t=e.fields&&"object"==typeof e.fields?Object.entries(e.fields):[],s=t.length?t.map(([e,t])=>`\n
\n ${this.formatRequestFieldLabel(e)} \n ${this.formatRequestFieldValue(t)} \n
\n `).join(""):'';document.getElementById("dispatcherRequestTitle").textContent=e.title||e.requestId||"Support Request",document.getElementById("dispatcherRequestPriority").textContent=(e.priority||"priority").replaceAll("_"," "),document.getElementById("dispatcherRequestGroup").textContent=e.groupCallsign||e.groupId||"Unknown",document.getElementById("dispatcherRequestType").textContent=this.getRequestTypeLabel(e.type||"request"),document.getElementById("dispatcherRequestSummary").textContent=e.summary||"No summary provided.",document.getElementById("dispatcherRequestFields").innerHTML=s},convertRequestToOrder(e){const t=this.requests.find(t=>(t.requestId||"")===e);if(!t)return void this.setStatus("Selected request is no longer available.","error");const s=t.groupId||"";if(!s)return void this.setStatus("Selected request has no owning group to target.","error");this.groups.find(e=>(e.groupId||"")===s)?(this.convertingRequestId=e,this.populateOrderModal({selectedAssigneeID:this.getSortedGroups().find(e=>(e.groupId||"")!==s)?.groupId||"",selectedTargetID:s,note:this.buildRequestOrderNote(t),priority:t.priority||"priority"}),document.getElementById("dispatcherOrderModalTitle").textContent="Create Order From Request",document.getElementById("dispatcherOrderModal").classList.remove("is-hidden"),this.setStatus("Preparing dispatch order from request...","info")):this.setStatus("Selected request group is no longer available.","error")},convertViewedRequestToOrder(){if(!this.viewingRequestId)return;const e=this.viewingRequestId;this.closeRequestModal(),this.convertRequestToOrder(e)},populateOrderModal(e={}){const t=this.getSortedGroups(),s=document.getElementById("dispatcherOrderAssigneeSelect"),n=document.getElementById("dispatcherOrderTargetSelect"),r=document.getElementById("dispatcherOrderNoteInput"),a=document.getElementById("dispatcherOrderPrioritySelect");if(!s||!n)return;const d=e.selectedAssigneeID||"",i=e.selectedTargetID||"",o=d||t.find(e=>(e.groupId||"")!==i)?.groupId||t[0]?.groupId||"",c=i||t.find(e=>(e.groupId||"")!==o)?.groupId||t[0]?.groupId||"";s.innerHTML=this.buildGroupOptions(o),n.innerHTML=this.buildGroupOptions(c),r&&(r.value=e.note||""),a&&(a.value=e.priority||"priority")},syncOrderModal(){const e=document.getElementById("dispatcherOrderModal");e&&!e.classList.contains("is-hidden")&&this.populateOrderModal({selectedAssigneeID:document.getElementById("dispatcherOrderAssigneeSelect")?.value||"",selectedTargetID:document.getElementById("dispatcherOrderTargetSelect")?.value||"",note:document.getElementById("dispatcherOrderNoteInput")?.value||"",priority:document.getElementById("dispatcherOrderPrioritySelect")?.value||"priority"})},openGroupModal(e){const t=this.groups.find(t=>t.groupId===e);t&&(this.editingGroupId=e,document.getElementById("dispatcherModalGroupCallsign").textContent=t.callsign||t.groupId||"Unknown",document.getElementById("dispatcherModalGroupLeader").textContent=t.leaderName||"Unknown",document.getElementById("dispatcherModalGroupTask").textContent=t.currentTaskId||"None",document.getElementById("dispatcherModalGroupOrg").textContent=t.orgId||"default",document.getElementById("dispatcherModalRoleSelect").innerHTML=this.roles.map(e=>`${e.replaceAll("_"," ")} `).join(""),document.getElementById("dispatcherModalStatusSelect").innerHTML=this.statuses.map(e=>`${e.replaceAll("_"," ")} `).join(""),document.getElementById("dispatcherGroupModal").classList.remove("is-hidden"))},closeGroupModal(){this.editingGroupId="",document.getElementById("dispatcherGroupModal").classList.add("is-hidden")},syncOpenModal(){if(!this.editingGroupId)return;const e=this.groups.find(e=>e.groupId===this.editingGroupId);e?(document.getElementById("dispatcherModalGroupCallsign").textContent=e.callsign||e.groupId||"Unknown",document.getElementById("dispatcherModalGroupLeader").textContent=e.leaderName||"Unknown",document.getElementById("dispatcherModalGroupTask").textContent=e.currentTaskId||"None",document.getElementById("dispatcherModalGroupOrg").textContent=e.orgId||"default"):this.closeGroupModal()}},window.cadDispatcherRender={updateDangerAlert(){const e=document.getElementById("dispatcherDangerAlert");if(!e)return;const t=this.getDangerGroups();if(!t.length)return e.textContent="",void e.classList.add("is-hidden");const s=t.map(e=>e.callsign||e.groupId||"Unknown Group");e.textContent=`Danger alert active: ${s.join(", ")}`,e.classList.remove("is-hidden")},updateRequestAlert(){const e=document.getElementById("dispatcherRequestAlert");if(!e)return;const t=this.buildSupportAlertMessage();if(!t)return e.textContent="",void e.classList.add("is-hidden");e.textContent=t,e.classList.remove("is-hidden")},buildGroupEditorButton:e=>`\n \n ⚙\n \n `,buildCloseOrderButton:e=>`\n \n Close\n \n `,buildCloseRequestButton:e=>`\n \n Close\n \n `,buildConvertRequestButton:e=>`\n \n Convert to Order\n \n `,renderMetrics(){const e=this.contracts.filter(e=>"unassigned"!==(e.assignmentState||"unassigned")),t=this.contracts.filter(e=>"unassigned"===(e.assignmentState||"unassigned")),s=this.requests.length,n=this.getSupportAlertRequests(),r=this.groups.filter(e=>"danger"===(e.status||""));document.getElementById("metricOpenContracts").textContent=t.length,document.getElementById("metricAssignedContracts").textContent=e.length,document.getElementById("metricActiveGroups").textContent=this.groups.length,document.getElementById("metricOpenRequests").textContent=s,document.getElementById("metricDangerGroups").textContent=r.length;const a=document.getElementById("metricDangerGroupsCard");a&&a.classList.toggle("is-danger",r.length>0);const d=document.getElementById("metricOpenRequestsCard");d&&d.classList.toggle("is-warning",n.length>0)},renderOpenContracts(){const e=document.getElementById("dispatcherOpenContracts"),t=this.contracts.filter(e=>"unassigned"===(e.assignmentState||"unassigned"));if(!t.length)return void(e.innerHTML='');const s=this.buildGroupOptions("");e.innerHTML=t.map(e=>{const t=e.taskId||e.taskID||"",n=Array.isArray(e.position)?e.position:[0,0,0],r=this.groups.find(t=>t.groupId===(e.targetGroupId||""));return`\n \n \n ${e.description||""}
\n \n Unassigned \n ${window.mapUI.formatPosition(n)} \n
\n \n Target: ${r?r.callsign:e.targetGroupCallsign||"None"} \n Priority: ${(e.priority||"priority").replaceAll("_"," ")} \n
\n \n \n Assign to group \n ${s}\n \n Assign \n
\n \n `}).join("")},renderAssignedContracts(){const e=document.getElementById("dispatcherAssignedContracts"),t=this.contracts.filter(e=>"unassigned"!==(e.assignmentState||"unassigned"));t.length?e.innerHTML=t.map(e=>{const t=e.taskId||e.taskID||"",s=this.groups.find(t=>t.groupId===(e.assignedGroupId||"")),n=this.groups.find(t=>t.groupId===(e.targetGroupId||"")),r=this.isDispatchOrder(e);return`\n \n \n ${e.description||""}
\n \n Group: ${s?s.callsign:e.assignedGroupId||"Unknown"} \n Type: ${this.formatTypeLabel(e)} \n
\n \n Target: ${n?n.callsign:e.targetGroupCallsign||"None"} \n Priority: ${(e.priority||"priority").replaceAll("_"," ")} \n
\n ${r?`${this.buildCloseOrderButton(t)}
`:""}\n \n `}).join(""):e.innerHTML=''},renderGroups(){const e=document.getElementById("dispatcherGroups");this.groups.length?e.innerHTML=this.getSortedGroups().map(e=>{const t="danger"===(e.status||"");return`\n \n \n \n Leader: ${e.leaderName||"Unknown"} \n Status: ${e.status||"unknown"} \n
\n \n Org: ${e.orgId||"default"} \n Task: ${e.currentTaskId||"None"} \n
\n \n `}).join(""):e.innerHTML='No active groups available.
'},renderActivity(){const e=document.getElementById("dispatcherActivity"),t=this.requests.length?this.requests.map(e=>`\n \n \n ${e.summary||""}
\n \n Group: ${e.groupCallsign||e.groupId||"Unknown"} \n ${this.getRequestTypeLabel(e.type||"request")} \n
\n \n ${this.buildConvertRequestButton(e.requestId||"")}\n ${this.buildCloseRequestButton(e.requestId||"")}\n
\n \n `).join(""):'No active support requests.
',s=this.activity.length?this.activity.slice().reverse().slice(0,8).map(e=>`\n \n \n ${e.message||""}
\n \n `).join(""):'';e.innerHTML=`\n \n \n ${t}\n
\n \n \n ${s}\n
\n `},render(){this.updateDangerAlert(),this.updateRequestAlert(),this.renderMetrics(),this.renderOpenContracts(),this.renderAssignedContracts(),this.renderGroups(),this.renderActivity()}};const dispatcherFormatters=window.cadDispatcherFormatters||{},dispatcherModals=window.cadDispatcherModals||{},dispatcherRender=window.cadDispatcherRender||{};window.cadDispatcher={contracts:[],requests:[],groups:[],activity:[],session:{},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","on_task","holding","danger","unavailable"],roles:["infantry","recon","armor","air","logistics","support"],...dispatcherFormatters,...dispatcherModals,...dispatcherRender,init(){document.getElementById("dispatcherRequestTaskBtn").addEventListener("click",()=>{this.openTaskModal()}),document.getElementById("dispatcherCreateOrderBtn").addEventListener("click",()=>{this.openOrderModal()}),document.getElementById("dispatcherGroupModalCloseBtn").addEventListener("click",()=>{this.closeGroupModal()}),document.getElementById("dispatcherGroupModalSaveBtn").addEventListener("click",()=>{this.applyGroupUpdates()}),document.querySelector("#dispatcherGroupModal .dispatch-modal-backdrop").addEventListener("click",()=>{this.closeGroupModal()}),document.getElementById("dispatcherOrderModalCloseBtn").addEventListener("click",()=>{this.closeOrderModal()}),document.getElementById("dispatcherOrderModalSaveBtn").addEventListener("click",()=>{this.createDispatchOrder()}),document.querySelector("#dispatcherOrderModal .dispatch-modal-backdrop").addEventListener("click",()=>{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",()=>{this.closeRequestModal()}),document.getElementById("dispatcherRequestModalDoneBtn").addEventListener("click",()=>{this.closeRequestModal()}),document.getElementById("dispatcherRequestConvertBtn").addEventListener("click",()=>{this.convertViewedRequestToOrder()}),document.querySelector("#dispatcherRequestModal .dispatch-modal-backdrop").addEventListener("click",()=>{this.closeRequestModal()}),window.mapUI.sendEvent("cad::dispatcher::ready",{})},receiveHydrate(e){this.contracts=Array.isArray(e.contracts)?e.contracts:[],this.requests=Array.isArray(e.requests)?e.requests:[],this.groups=Array.isArray(e.groups)?e.groups:[],this.activity=Array.isArray(e.activity)?e.activity:[],this.session=e.session&&"object"==typeof e.session?e.session:{};const t=document.getElementById("dispatcherStatusMessage");!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.syncOpenModal(),this.syncOrderModal(),this.syncRequestModal(),this.render()},setStatus(e,t){const s=document.getElementById("dispatcherStatusMessage");s&&(s.textContent=e||"",s.dataset.type=t||"")},createDispatchOrder(){const e=document.getElementById("dispatcherOrderAssigneeSelect").value,t=document.getElementById("dispatcherOrderTargetSelect").value,s=document.getElementById("dispatcherOrderPrioritySelect").value,n=document.getElementById("dispatcherOrderNoteInput").value,r=this.convertingRequestId&&this.requests.find(e=>(e.requestId||"")===this.convertingRequestId)||null;e&&t?e!==t?(this.setStatus(this.convertingRequestId?"Creating dispatch order from request...":"Creating dispatch order...","info"),window.mapUI.sendEvent("cad::dispatchOrder::create",{assigneeGroupID:e,targetGroupID:t,note:n.trim(),priority:s,request:r?{requestId:r.requestId||"",type:r.type||"",title:r.title||"",summary:r.summary||"",fields:r.fields&&"object"==typeof r.fields?r.fields:{}}:{}}),this.closeOrderModal()):this.setStatus("Assignee and target groups must be different.","error"):this.setStatus("Select both an assignee and a target group.","error")},requestGeneratedTask(){const e=document.getElementById("dispatcherTaskTypeSelect").value;e?(this.setStatus("Requesting generated task...","info"),window.mapUI.sendEvent("cad::generatedTask::request",{taskType:e}),this.closeTaskModal()):this.setStatus("Select a task type before requesting a task.","error")},assignTask(e){const t=document.getElementById(`dispatcher-assign-group-${e}`);t&&t.value?(this.setStatus("Submitting assignment...","info"),window.mapUI.sendEvent("cad::tasks::assign",{taskID:e,groupID:t.value,note:""})):this.setStatus("Select a group before assigning a contract.","error")},applyGroupUpdates(){if(!this.editingGroupId)return;const e=this.groups.find(e=>e.groupId===this.editingGroupId);if(!e)return void this.closeGroupModal();const t=document.getElementById("dispatcherModalRoleSelect").value,s=document.getElementById("dispatcherModalStatusSelect").value,n=t&&t!==(e.role||"")?t:"",r=s&&s!==(e.status||"")?s:"";if(!(n||r))return this.setStatus("No group changes to save.","info"),void this.closeGroupModal();this.setStatus("Updating group profile...","info"),window.mapUI.sendEvent("cad::groups::profile",{groupID:this.editingGroupId,role:n,status:r}),this.closeGroupModal()},closeDispatchOrder(e){e&&(this.setStatus("Closing dispatch order...","info"),window.mapUI.sendEvent("cad::dispatchOrder::close",{taskID:e}))},closeSupportRequest(e){e&&(this.setStatus("Closing support request...","info"),window.mapUI.sendEvent("cad::supportRequest::close",{requestID:e}))}},window.cadDispatcher.init();
\ No newline at end of file
+window.cadDispatcherFormatters={getDangerGroups(){return this.groups.filter(e=>"danger"===(e.status||""))},getSupportAlertRequests(){return this.requests.filter(e=>["medevac_9line","fire_support","air_support"].includes(e.type||""))},buildSupportAlertMessage(){const e=this.getSupportAlertRequests();if(!e.length)return"";return`Support request alert: ${e.map(e=>`${e.groupCallsign||e.groupId||"Unknown Group"} ${this.getRequestTypeLabel(e.type||"request")}`).join(", ")}`},getSortedGroups(){return this.groups.slice().sort((e,t)=>{const s="danger"===(e.status||"")?0:1,n="danger"===(t.status||"")?0:1;if(s!==n)return s-n;const r=e.callsign||e.groupId||"",a=t.callsign||t.groupId||"";return r.localeCompare(a)})},isDispatchOrder:e=>!!e.isDispatchOrder||"dispatch_order"===(e.type||""),formatTypeLabel(e){const t=(e.type||"task").replaceAll("_"," ");return this.isDispatchOrder(e)?"dispatch order":t},getRequestTypeLabel(e){switch(e){case"medevac_9line":return"9-Line MEDEVAC";case"ace_lace":return"ACE/LACE";case"fire_support":return"Fire Support";case"air_support":return"Air Support";case"logreq":return"LOGREQ";default:return(e||"request").replaceAll("_"," ")}},buildGroupOptions(e){return this.getSortedGroups().map(t=>{const s=t.groupId||"";return`${t.callsign||s} `}).join("")},buildTaskTypeOptions(e){return this.taskTypes.map(t=>{const s=t.value||"";return`${t.label||s} `}).join("")},formatRequestFieldLabel:e=>(e||"field").replaceAll("_"," ").replace(/\b\w/g,e=>e.toUpperCase()),formatRequestFieldValue(e){if(Array.isArray(e))return e.join(", ");if(e&&"object"==typeof e)return JSON.stringify(e);return String(e??"").trim()||"Not provided"},buildRequestOrderNote(e){const t=this.getRequestTypeLabel(e.type||"request"),s=e.groupCallsign||e.groupId||"Unknown Group",n=(e.summary||"").trim(),r=e.fields&&"object"==typeof e.fields?Object.entries(e.fields).map(([e,t])=>{const s=this.formatRequestFieldValue(t);return"Not provided"===s?"":`${this.formatRequestFieldLabel(e)} ${s}`}).filter(Boolean):[],a=r.length?r:[n].filter(Boolean);return a.length?`${t} requested by ${s}. ${a.join(" | ")}`:`${t} requested by ${s}.`}},window.cadDispatcherModals={openTaskModal(){this.populateTaskModal(),document.getElementById("dispatcherTaskModal").classList.remove("is-hidden")},closeTaskModal(){document.getElementById("dispatcherTaskModal").classList.add("is-hidden")},populateTaskModal(){const e=document.getElementById("dispatcherTaskTypeSelect");if(!e)return;const t=document.getElementById("dispatcherTaskModalSaveBtn"),s=this.taskTypes.length>0;e.disabled=!s,t&&(t.disabled=!s),e.innerHTML=s?this.buildTaskTypeOptions(e.value||this.taskTypes[0]?.value||""):'No generated tasks available '},openOrderModal(){this.convertingRequestId="",this.populateOrderModal(),document.getElementById("dispatcherOrderModalTitle").textContent="Create Support Order",document.getElementById("dispatcherOrderModal").classList.remove("is-hidden")},closeOrderModal(){this.convertingRequestId="",document.getElementById("dispatcherOrderNoteInput").value="",document.getElementById("dispatcherOrderPrioritySelect").value="priority",document.getElementById("dispatcherOrderModalTitle").textContent="Create Support Order",document.getElementById("dispatcherOrderModal").classList.add("is-hidden")},openRequestModal(e){const t=this.requests.find(t=>t.requestId===e);t&&(this.viewingRequestId=e,this.populateRequestModal(t),document.getElementById("dispatcherRequestModal").classList.remove("is-hidden"))},closeRequestModal(){this.viewingRequestId="",document.getElementById("dispatcherRequestModal").classList.add("is-hidden")},syncRequestModal(){if(!this.viewingRequestId)return;const e=this.requests.find(e=>e.requestId===this.viewingRequestId);e?this.populateRequestModal(e):this.closeRequestModal()},populateRequestModal(e){const t=e.fields&&"object"==typeof e.fields?Object.entries(e.fields):[],s=t.length?t.map(([e,t])=>`\n \n ${this.formatRequestFieldLabel(e)} \n ${this.formatRequestFieldValue(t)} \n
\n `).join(""):'';document.getElementById("dispatcherRequestTitle").textContent=e.title||e.requestId||"Support Request",document.getElementById("dispatcherRequestPriority").textContent=(e.priority||"priority").replaceAll("_"," "),document.getElementById("dispatcherRequestGroup").textContent=e.groupCallsign||e.groupId||"Unknown",document.getElementById("dispatcherRequestType").textContent=this.getRequestTypeLabel(e.type||"request"),document.getElementById("dispatcherRequestSummary").textContent=e.summary||"No summary provided.",document.getElementById("dispatcherRequestFields").innerHTML=s},convertRequestToOrder(e){const t=this.requests.find(t=>(t.requestId||"")===e);if(!t)return void this.setStatus("Selected request is no longer available.","error");const s=t.groupId||"";if(!s)return void this.setStatus("Selected request has no owning group to target.","error");this.groups.find(e=>(e.groupId||"")===s)?(this.convertingRequestId=e,this.populateOrderModal({selectedAssigneeID:this.getSortedGroups().find(e=>(e.groupId||"")!==s)?.groupId||"",selectedTargetID:s,note:this.buildRequestOrderNote(t),priority:t.priority||"priority"}),document.getElementById("dispatcherOrderModalTitle").textContent="Create Order From Request",document.getElementById("dispatcherOrderModal").classList.remove("is-hidden"),this.setStatus("Preparing dispatch order from request...","info")):this.setStatus("Selected request group is no longer available.","error")},convertViewedRequestToOrder(){if(!this.viewingRequestId)return;const e=this.viewingRequestId;this.closeRequestModal(),this.convertRequestToOrder(e)},populateOrderModal(e={}){const t=this.getSortedGroups(),s=document.getElementById("dispatcherOrderAssigneeSelect"),n=document.getElementById("dispatcherOrderTargetSelect"),r=document.getElementById("dispatcherOrderNoteInput"),a=document.getElementById("dispatcherOrderPrioritySelect");if(!s||!n)return;const i=e.selectedAssigneeID||"",d=e.selectedTargetID||"",o=i||t.find(e=>(e.groupId||"")!==d)?.groupId||t[0]?.groupId||"",c=d||t.find(e=>(e.groupId||"")!==o)?.groupId||t[0]?.groupId||"";s.innerHTML=this.buildGroupOptions(o),n.innerHTML=this.buildGroupOptions(c),r&&(r.value=e.note||""),a&&(a.value=e.priority||"priority")},syncOrderModal(){const e=document.getElementById("dispatcherOrderModal");e&&!e.classList.contains("is-hidden")&&this.populateOrderModal({selectedAssigneeID:document.getElementById("dispatcherOrderAssigneeSelect")?.value||"",selectedTargetID:document.getElementById("dispatcherOrderTargetSelect")?.value||"",note:document.getElementById("dispatcherOrderNoteInput")?.value||"",priority:document.getElementById("dispatcherOrderPrioritySelect")?.value||"priority"})},openGroupModal(e){const t=this.groups.find(t=>t.groupId===e);t&&(this.editingGroupId=e,document.getElementById("dispatcherModalGroupCallsign").textContent=t.callsign||t.groupId||"Unknown",document.getElementById("dispatcherModalGroupLeader").textContent=t.leaderName||"Unknown",document.getElementById("dispatcherModalGroupTask").textContent=t.currentTaskId||"None",document.getElementById("dispatcherModalGroupOrg").textContent=t.orgId||"default",document.getElementById("dispatcherModalRoleSelect").innerHTML=this.roles.map(e=>`${e.replaceAll("_"," ")} `).join(""),document.getElementById("dispatcherModalStatusSelect").innerHTML=this.statuses.map(e=>`${e.replaceAll("_"," ")} `).join(""),document.getElementById("dispatcherGroupModal").classList.remove("is-hidden"))},closeGroupModal(){this.editingGroupId="",document.getElementById("dispatcherGroupModal").classList.add("is-hidden")},syncOpenModal(){if(!this.editingGroupId)return;const e=this.groups.find(e=>e.groupId===this.editingGroupId);e?(document.getElementById("dispatcherModalGroupCallsign").textContent=e.callsign||e.groupId||"Unknown",document.getElementById("dispatcherModalGroupLeader").textContent=e.leaderName||"Unknown",document.getElementById("dispatcherModalGroupTask").textContent=e.currentTaskId||"None",document.getElementById("dispatcherModalGroupOrg").textContent=e.orgId||"default"):this.closeGroupModal()}},window.cadDispatcherRender={updateDangerAlert(){const e=document.getElementById("dispatcherDangerAlert");if(!e)return;const t=this.getDangerGroups();if(!t.length)return e.textContent="",void e.classList.add("is-hidden");const s=t.map(e=>e.callsign||e.groupId||"Unknown Group");e.textContent=`Danger alert active: ${s.join(", ")}`,e.classList.remove("is-hidden")},updateRequestAlert(){const e=document.getElementById("dispatcherRequestAlert");if(!e)return;const t=this.buildSupportAlertMessage();if(!t)return e.textContent="",void e.classList.add("is-hidden");e.textContent=t,e.classList.remove("is-hidden")},buildGroupEditorButton:e=>`\n \n ⚙\n \n `,buildCloseOrderButton:e=>`\n \n Close\n \n `,buildCloseRequestButton:e=>`\n \n Close\n \n `,buildConvertRequestButton:e=>`\n \n Convert to Order\n \n `,renderMetrics(){const e=this.contracts.filter(e=>"unassigned"!==(e.assignmentState||"unassigned")),t=this.contracts.filter(e=>"unassigned"===(e.assignmentState||"unassigned")),s=this.requests.length,n=this.getSupportAlertRequests(),r=this.groups.filter(e=>"danger"===(e.status||""));document.getElementById("metricOpenContracts").textContent=t.length,document.getElementById("metricAssignedContracts").textContent=e.length,document.getElementById("metricActiveGroups").textContent=this.groups.length,document.getElementById("metricOpenRequests").textContent=s,document.getElementById("metricDangerGroups").textContent=r.length;const a=document.getElementById("metricDangerGroupsCard");a&&a.classList.toggle("is-danger",r.length>0);const i=document.getElementById("metricOpenRequestsCard");i&&i.classList.toggle("is-warning",n.length>0)},renderOpenContracts(){const e=document.getElementById("dispatcherOpenContracts"),t=this.contracts.filter(e=>"unassigned"===(e.assignmentState||"unassigned"));if(!t.length)return void(e.innerHTML='');const s=this.buildGroupOptions("");e.innerHTML=t.map(e=>{const t=e.taskId||e.taskID||"",n=Array.isArray(e.position)?e.position:[0,0,0],r=this.groups.find(t=>t.groupId===(e.targetGroupId||""));return`\n \n \n ${e.description||""}
\n \n Unassigned \n ${window.mapUI.formatPosition(n)} \n
\n \n Target: ${r?r.callsign:e.targetGroupCallsign||"None"} \n Priority: ${(e.priority||"priority").replaceAll("_"," ")} \n
\n \n \n Assign to group \n ${s}\n \n Assign \n
\n \n `}).join("")},renderAssignedContracts(){const e=document.getElementById("dispatcherAssignedContracts"),t=this.contracts.filter(e=>"unassigned"!==(e.assignmentState||"unassigned"));t.length?e.innerHTML=t.map(e=>{const t=e.taskId||e.taskID||"",s=this.groups.find(t=>t.groupId===(e.assignedGroupId||"")),n=this.groups.find(t=>t.groupId===(e.targetGroupId||"")),r=this.isDispatchOrder(e);return`\n \n \n ${e.description||""}
\n \n Group: ${s?s.callsign:e.assignedGroupId||"Unknown"} \n Type: ${this.formatTypeLabel(e)} \n
\n \n Target: ${n?n.callsign:e.targetGroupCallsign||"None"} \n Priority: ${(e.priority||"priority").replaceAll("_"," ")} \n
\n ${r?`${this.buildCloseOrderButton(t)}
`:""}\n \n `}).join(""):e.innerHTML=''},renderGroups(){const e=document.getElementById("dispatcherGroups");this.groups.length?e.innerHTML=this.getSortedGroups().map(e=>{const t="danger"===(e.status||"");return`\n \n \n \n Leader: ${e.leaderName||"Unknown"} \n Status: ${e.status||"unknown"} \n
\n \n Org: ${e.orgId||"default"} \n Task: ${e.currentTaskId||"None"} \n
\n \n `}).join(""):e.innerHTML='No active groups available.
'},renderActivity(){const e=document.getElementById("dispatcherActivity"),t=this.requests.length?this.requests.map(e=>`\n \n \n ${e.summary||""}
\n \n Group: ${e.groupCallsign||e.groupId||"Unknown"} \n ${this.getRequestTypeLabel(e.type||"request")} \n
\n \n ${this.buildConvertRequestButton(e.requestId||"")}\n ${this.buildCloseRequestButton(e.requestId||"")}\n
\n \n `).join(""):'No active support requests.
',s=this.activity.length?this.activity.slice().reverse().slice(0,8).map(e=>`\n \n \n ${e.message||""}
\n \n `).join(""):'';e.innerHTML=`\n \n \n ${t}\n
\n \n \n ${s}\n
\n `},render(){this.updateDangerAlert(),this.updateRequestAlert(),this.renderMetrics(),this.renderOpenContracts(),this.renderAssignedContracts(),this.renderGroups(),this.renderActivity()}};const dispatcherFormatters=window.cadDispatcherFormatters||{},dispatcherModals=window.cadDispatcherModals||{},dispatcherRender=window.cadDispatcherRender||{},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:[],requests:[],groups:[],activity:[],session:{},editingGroupId:"",viewingRequestId:"",convertingRequestId:"",taskTypes:defaultTaskTypes.slice(),statuses:["available","en_route","on_task","holding","danger","unavailable"],roles:["infantry","recon","armor","air","logistics","support"],...dispatcherFormatters,...dispatcherModals,...dispatcherRender,init(){document.getElementById("dispatcherRequestTaskBtn").addEventListener("click",()=>{this.openTaskModal()}),document.getElementById("dispatcherCreateOrderBtn").addEventListener("click",()=>{this.openOrderModal()}),document.getElementById("dispatcherGroupModalCloseBtn").addEventListener("click",()=>{this.closeGroupModal()}),document.getElementById("dispatcherGroupModalSaveBtn").addEventListener("click",()=>{this.applyGroupUpdates()}),document.querySelector("#dispatcherGroupModal .dispatch-modal-backdrop").addEventListener("click",()=>{this.closeGroupModal()}),document.getElementById("dispatcherOrderModalCloseBtn").addEventListener("click",()=>{this.closeOrderModal()}),document.getElementById("dispatcherOrderModalSaveBtn").addEventListener("click",()=>{this.createDispatchOrder()}),document.querySelector("#dispatcherOrderModal .dispatch-modal-backdrop").addEventListener("click",()=>{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",()=>{this.closeRequestModal()}),document.getElementById("dispatcherRequestModalDoneBtn").addEventListener("click",()=>{this.closeRequestModal()}),document.getElementById("dispatcherRequestConvertBtn").addEventListener("click",()=>{this.convertViewedRequestToOrder()}),document.querySelector("#dispatcherRequestModal .dispatch-modal-backdrop").addEventListener("click",()=>{this.closeRequestModal()}),window.mapUI.sendEvent("cad::dispatcher::ready",{})},receiveHydrate(e){this.contracts=Array.isArray(e.contracts)?e.contracts:[],this.requests=Array.isArray(e.requests)?e.requests:[],this.groups=Array.isArray(e.groups)?e.groups:[],this.activity=Array.isArray(e.activity)?e.activity:[],Array.isArray(e.generatedTaskTypes)&&(this.taskTypes=e.generatedTaskTypes.map(e=>({value:String(e?.value||"").trim(),label:String(e?.label||e?.value||"").trim()})).filter(e=>e.value)),this.session=e.session&&"object"==typeof e.session?e.session:{};const t=document.getElementById("dispatcherStatusMessage");!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.syncOpenModal(),this.syncOrderModal(),this.syncRequestModal(),this.render()},setStatus(e,t){const s=document.getElementById("dispatcherStatusMessage");s&&(s.textContent=e||"",s.dataset.type=t||"")},createDispatchOrder(){const e=document.getElementById("dispatcherOrderAssigneeSelect").value,t=document.getElementById("dispatcherOrderTargetSelect").value,s=document.getElementById("dispatcherOrderPrioritySelect").value,n=document.getElementById("dispatcherOrderNoteInput").value,r=this.convertingRequestId&&this.requests.find(e=>(e.requestId||"")===this.convertingRequestId)||null;e&&t?e!==t?(this.setStatus(this.convertingRequestId?"Creating dispatch order from request...":"Creating dispatch order...","info"),window.mapUI.sendEvent("cad::dispatchOrder::create",{assigneeGroupID:e,targetGroupID:t,note:n.trim(),priority:s,request:r?{requestId:r.requestId||"",type:r.type||"",title:r.title||"",summary:r.summary||"",fields:r.fields&&"object"==typeof r.fields?r.fields:{}}:{}}),this.closeOrderModal()):this.setStatus("Assignee and target groups must be different.","error"):this.setStatus("Select both an assignee and a target group.","error")},requestGeneratedTask(){if(!this.taskTypes.length)return void this.setStatus("Generated task requests are disabled by server settings.","error");const e=document.getElementById("dispatcherTaskTypeSelect").value;e?(this.setStatus("Requesting generated task...","info"),window.mapUI.sendEvent("cad::generatedTask::request",{taskType:e}),this.closeTaskModal()):this.setStatus("Select a task type before requesting a task.","error")},assignTask(e){const t=document.getElementById(`dispatcher-assign-group-${e}`);t&&t.value?(this.setStatus("Submitting assignment...","info"),window.mapUI.sendEvent("cad::tasks::assign",{taskID:e,groupID:t.value,note:""})):this.setStatus("Select a group before assigning a contract.","error")},applyGroupUpdates(){if(!this.editingGroupId)return;const e=this.groups.find(e=>e.groupId===this.editingGroupId);if(!e)return void this.closeGroupModal();const t=document.getElementById("dispatcherModalRoleSelect").value,s=document.getElementById("dispatcherModalStatusSelect").value,n=t&&t!==(e.role||"")?t:"",r=s&&s!==(e.status||"")?s:"";if(!(n||r))return this.setStatus("No group changes to save.","info"),void this.closeGroupModal();this.setStatus("Updating group profile...","info"),window.mapUI.sendEvent("cad::groups::profile",{groupID:this.editingGroupId,role:n,status:r}),this.closeGroupModal()},closeDispatchOrder(e){e&&(this.setStatus("Closing dispatch order...","info"),window.mapUI.sendEvent("cad::dispatchOrder::close",{taskID:e}))},closeSupportRequest(e){e&&(this.setStatus("Closing support request...","info"),window.mapUI.sendEvent("cad::supportRequest::close",{requestID:e}))}},window.cadDispatcher.init();
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/src/dispatcher/index.js b/arma/client/addons/cad/ui/src/dispatcher/index.js
index 7331fbd..05b3f35 100644
--- a/arma/client/addons/cad/ui/src/dispatcher/index.js
+++ b/arma/client/addons/cad/ui/src/dispatcher/index.js
@@ -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;
diff --git a/arma/client/addons/cad/ui/src/dispatcher/modals.js b/arma/client/addons/cad/ui/src/dispatcher/modals.js
index ab0393f..e0ba8de 100644
--- a/arma/client/addons/cad/ui/src/dispatcher/modals.js
+++ b/arma/client/addons/cad/ui/src/dispatcher/modals.js
@@ -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 =
+ 'No generated tasks available ';
+ return;
+ }
+
taskTypeSelect.innerHTML = this.buildTaskTypeOptions(
taskTypeSelect.value || this.taskTypes[0]?.value || "",
);
diff --git a/arma/server/addons/cad/XEH_preInit.sqf b/arma/server/addons/cad/XEH_preInit.sqf
index 0f63771..6e561d8 100644
--- a/arma/server/addons/cad/XEH_preInit.sqf
+++ b/arma/server/addons/cad/XEH_preInit.sqf
@@ -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), {
diff --git a/arma/server/addons/cad/functions/fnc_initCadStore.sqf b/arma/server/addons/cad/functions/fnc_initCadStore.sqf
index 0bec8bb..19c0625 100644
--- a/arma/server/addons/cad/functions/fnc_initCadStore.sqf
+++ b/arma/server/addons/cad/functions/fnc_initCadStore.sqf
@@ -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);
diff --git a/arma/server/addons/task/CfgMissions.hpp b/arma/server/addons/task/CfgMissions.hpp
index 38576f5..e109c35 100644
--- a/arma/server/addons/task/CfgMissions.hpp
+++ b/arma/server/addons/task/CfgMissions.hpp
@@ -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.[]: 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};
};
};
};
diff --git a/arma/server/addons/task/README.md b/arma/server/addons/task/README.md
index 31414cc..60762db 100644
--- a/arma/server/addons/task/README.md
+++ b/arma/server/addons/task/README.md
@@ -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
diff --git a/arma/server/addons/task/XEH_PREP.hpp b/arma/server/addons/task/XEH_PREP.hpp
index 426b5e0..7b60740 100644
--- a/arma/server/addons/task/XEH_PREP.hpp
+++ b/arma/server/addons/task/XEH_PREP.hpp
@@ -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);
diff --git a/arma/server/addons/task/functions/fnc_missionManager.sqf b/arma/server/addons/task/functions/fnc_missionManager.sqf
index 72a6e4d..8ed977d 100644
--- a/arma/server/addons/task/functions/fnc_missionManager.sqf
+++ b/arma/server/addons/task/functions/fnc_missionManager.sqf
@@ -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;
diff --git a/arma/server/addons/task/functions/fnc_requestMissionTask.sqf b/arma/server/addons/task/functions/fnc_requestMissionTask.sqf
new file mode 100644
index 0000000..f6ce408
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_requestMissionTask.sqf
@@ -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
+ * 1: Request metadata (Default: createHashMap)
+ * 2: Requesting player UID (Default: "")
+ *
+ * Return Value:
+ * Request result with success, message, taskID, and taskType keys
+ *
+ * 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
diff --git a/arma/server/addons/task/functions/generators/fnc_attackMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_attackMissionGenerator.sqf
index 40f9978..d5fca52 100644
--- a/arma/server/addons/task/functions/generators/fnc_attackMissionGenerator.sqf
+++ b/arma/server/addons/task/functions/generators/fnc_attackMissionGenerator.sqf
@@ -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);
diff --git a/arma/server/addons/task/functions/generators/fnc_captureHvtMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_captureHvtMissionGenerator.sqf
new file mode 100644
index 0000000..bb3eb10
--- /dev/null
+++ b/arma/server/addons/task/functions/generators/fnc_captureHvtMissionGenerator.sqf
@@ -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
+ }]
+];
diff --git a/arma/server/addons/task/functions/generators/fnc_defendMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_defendMissionGenerator.sqf
new file mode 100644
index 0000000..b1b4271
--- /dev/null
+++ b/arma/server/addons/task/functions/generators/fnc_defendMissionGenerator.sqf
@@ -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
+ }]
+];
diff --git a/arma/server/addons/task/functions/generators/fnc_defuseMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_defuseMissionGenerator.sqf
new file mode 100644
index 0000000..77c9870
--- /dev/null
+++ b/arma/server/addons/task/functions/generators/fnc_defuseMissionGenerator.sqf
@@ -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
+ }]
+];
diff --git a/arma/server/addons/task/functions/generators/fnc_deliveryMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_deliveryMissionGenerator.sqf
new file mode 100644
index 0000000..3c12bd7
--- /dev/null
+++ b/arma/server/addons/task/functions/generators/fnc_deliveryMissionGenerator.sqf
@@ -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
+ }]
+];
diff --git a/arma/server/addons/task/functions/generators/fnc_destroyMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_destroyMissionGenerator.sqf
new file mode 100644
index 0000000..1901f9e
--- /dev/null
+++ b/arma/server/addons/task/functions/generators/fnc_destroyMissionGenerator.sqf
@@ -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
+ }]
+];
diff --git a/arma/server/addons/task/functions/generators/fnc_hostageMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_hostageMissionGenerator.sqf
new file mode 100644
index 0000000..5fddc87
--- /dev/null
+++ b/arma/server/addons/task/functions/generators/fnc_hostageMissionGenerator.sqf
@@ -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
+ }]
+];
diff --git a/arma/server/addons/task/functions/generators/fnc_hvtMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_hvtMissionGenerator.sqf
new file mode 100644
index 0000000..fd5d95c
--- /dev/null
+++ b/arma/server/addons/task/functions/generators/fnc_hvtMissionGenerator.sqf
@@ -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
+ }]
+];
diff --git a/arma/server/addons/task/functions/helpers/fnc_getEnemyFactionUnitPool.sqf b/arma/server/addons/task/functions/helpers/fnc_getEnemyFactionUnitPool.sqf
new file mode 100644
index 0000000..f9f2131
--- /dev/null
+++ b/arma/server/addons/task/functions/helpers/fnc_getEnemyFactionUnitPool.sqf
@@ -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 (Default: ENEMY_FACTION_STR or "IND_G_F")
+ * 1: Fallback side (Default: ENEMY_SIDE or east)
+ * 2: Allow side-default fallback units when no faction units exist
+ * (Default: true)
+ *
+ * Return Value:
+ * Unit definitions with vehicle, rank, and position keys
+ *
+ * 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
diff --git a/arma/server/addons/task/functions/helpers/fnc_getMissionSettingRange.sqf b/arma/server/addons/task/functions/helpers/fnc_getMissionSettingRange.sqf
new file mode 100644
index 0000000..ca27e8c
--- /dev/null
+++ b/arma/server/addons/task/functions/helpers/fnc_getMissionSettingRange.sqf
@@ -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
+ * 1: Config path segments to the range array
+ * 2: Mission settings min key
+ * 3: Mission settings max key
+ * 4: Fallback [min, max] range (Default: [0, 0])
+ *
+ * Return Value:
+ * Numeric [min, max] range . 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]
diff --git a/arma/server/addons/task/functions/helpers/fnc_updateEnemyCountFromActivePlayers.sqf b/arma/server/addons/task/functions/helpers/fnc_updateEnemyCountFromActivePlayers.sqf
new file mode 100644
index 0000000..0e50256
--- /dev/null
+++ b/arma/server/addons/task/functions/helpers/fnc_updateEnemyCountFromActivePlayers.sqf
@@ -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
+ *
+ * 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
diff --git a/docs/CAD_USAGE_GUIDE.md b/docs/CAD_USAGE_GUIDE.md
index e8e3663..f6dd7fa 100644
--- a/docs/CAD_USAGE_GUIDE.md
+++ b/docs/CAD_USAGE_GUIDE.md
@@ -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]
];
diff --git a/docs/CLIENT_CAD_USAGE_GUIDE.md b/docs/CLIENT_CAD_USAGE_GUIDE.md
index 1af8a96..8c467f7 100644
--- a/docs/CLIENT_CAD_USAGE_GUIDE.md
+++ b/docs/CLIENT_CAD_USAGE_GUIDE.md
@@ -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
diff --git a/docs/MISSION_DESIGNER_GUIDE.md b/docs/MISSION_DESIGNER_GUIDE.md
index 489eef1..054fef2 100644
--- a/docs/MISSION_DESIGNER_GUIDE.md
+++ b/docs/MISSION_DESIGNER_GUIDE.md
@@ -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`.
diff --git a/docs/TASK_USAGE_GUIDE.md b/docs/TASK_USAGE_GUIDE.md
index e971204..eeb21f2 100644
--- a/docs/TASK_USAGE_GUIDE.md
+++ b/docs/TASK_USAGE_GUIDE.md
@@ -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//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:
diff --git a/docus/content/1.getting-started/5.mission-designer.md b/docus/content/1.getting-started/5.mission-designer.md
index ebd84c4..0d3e84d 100644
--- a/docus/content/1.getting-started/5.mission-designer.md
+++ b/docus/content/1.getting-started/5.mission-designer.md
@@ -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.

@@ -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`.
diff --git a/docus/content/1.getting-started/6.player-guide.md b/docus/content/1.getting-started/6.player-guide.md
index 47ab36d..f6de3ca 100644
--- a/docus/content/1.getting-started/6.player-guide.md
+++ b/docus/content/1.getting-started/6.player-guide.md
@@ -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.

diff --git a/docus/content/3.server-modules/11.task.md b/docus/content/3.server-modules/11.task.md
index f9731f3..b8f0846 100644
--- a/docus/content/3.server-modules/11.task.md
+++ b/docus/content/3.server-modules/11.task.md
@@ -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//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:
diff --git a/docus/content/3.server-modules/12.transport-service.md b/docus/content/3.server-modules/12.transport-service.md
index 0904497..724d98f 100644
--- a/docus/content/3.server-modules/12.transport-service.md
+++ b/docus/content/3.server-modules/12.transport-service.md
@@ -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:


-
-## 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.
diff --git a/docus/content/3.server-modules/3.cad.md b/docus/content/3.server-modules/3.cad.md
index c10f700..c24c49e 100644
--- a/docus/content/3.server-modules/3.cad.md
+++ b/docus/content/3.server-modules/3.cad.md
@@ -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]
];
diff --git a/docus/content/4.client-addons/5.cad.md b/docus/content/4.client-addons/5.cad.md
index 57bc7b2..859e57f 100644
--- a/docus/content/4.client-addons/5.cad.md
+++ b/docus/content/4.client-addons/5.cad.md
@@ -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