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``}).join("")},buildTaskTypeOptions(e){return this.taskTypes.map(t=>{const s=t.value||"";return``}).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(""):'

No submitted fields.

';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=>``).join(""),document.getElementById("dispatcherModalStatusSelect").innerHTML=this.statuses.map(e=>``).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='

No open contracts.

');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.title||t}\n ${this.formatTypeLabel(e)}\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 \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.title||t}\n ${e.assignmentState||"assigned"}\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='

No assigned contracts.

'},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 ${e.callsign||e.groupId}\n ${e.role||"group"}\n ${t?'Danger':""}\n
\n
\n ${this.buildGroupEditorButton(e.groupId)}\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.title||e.requestId||"Support Request"}\n ${(e.priority||"priority").replaceAll("_"," ")}\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.type||"activity"}\n ${Math.round(e.timestamp||0)}s\n
\n

${e.message||""}

\n
\n `).join(""):'

No recent activity.

';e.innerHTML=`\n
\n
Support Requests
\n ${t}\n
\n
\n
Recent Activity
\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``}).join("")},buildTaskTypeOptions(e){return this.taskTypes.map(t=>{const s=t.value||"";return``}).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||""):''},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(""):'

No submitted fields.

';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=>``).join(""),document.getElementById("dispatcherModalStatusSelect").innerHTML=this.statuses.map(e=>``).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='

No open contracts.

');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.title||t}\n ${this.formatTypeLabel(e)}\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 \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.title||t}\n ${e.assignmentState||"assigned"}\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='

No assigned contracts.

'},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 ${e.callsign||e.groupId}\n ${e.role||"group"}\n ${t?'Danger':""}\n
\n
\n ${this.buildGroupEditorButton(e.groupId)}\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.title||e.requestId||"Support Request"}\n ${(e.priority||"priority").replaceAll("_"," ")}\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.type||"activity"}\n ${Math.round(e.timestamp||0)}s\n
\n

${e.message||""}

\n
\n `).join(""):'

No recent activity.

';e.innerHTML=`\n
\n
Support Requests
\n ${t}\n
\n
\n
Recent Activity
\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 = + ''; + 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. ![Eden transport location one](images/eden/transport_loc_1.jpg) @@ -171,7 +170,7 @@ transport_2 transport_10 ``` -Place optional arrival markers with matching suffixes: +Place arrival markers with matching suffixes: ```text transport_arrival @@ -729,6 +728,21 @@ Validation: ## Mission Manager Blacklist Markers +Generated missions are configured through mission-local `CfgMissions.hpp`. +Place it in the mission folder and include it from `description.ext` so mission +designers own task weights, reward ranges, time limits, HVT/hostage class +pools, defuse device pools, delivery cargo, destroy targets, and the +`locationReuseCooldown`. + +If a mission does not define `CfgMissions`, the task addon falls back to the +framework copy at `arma/server/addons/task/CfgMissions.hpp`. Treat the +framework file as the default schema and mission copies as the tuning layer. + +The generated mission system supports `attack`, `defend`, `defuse`, +`delivery`, `destroy`, `hostage`, `hvtkill`, and `hvtcapture`. The +`forge_task_enableGenerator` CBA setting gates both timer-based generation and +CAD dispatcher-requested generation. + The dynamic mission generator avoids rectangle and ellipse area markers whose marker name or marker text starts with `blklist`. 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. ![Transport destination submenu](images/player/transport_destination_menu.jpg) 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: ![Player transport destination submenu](images/player/transport_destination_menu.jpg) ![Player transport completion notification](images/player/transport_complete.jpg) - -## Mission-Side Code Requirement - -No mission-side transport service, addAction script, or server event bridge is -required. The framework handles menu discovery, destination selection, pricing, -billing, cargo movement, and EventBus notifications. 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