diff --git a/arma/client/addons/cad/CfgEventHandlers.hpp b/arma/client/addons/cad/CfgEventHandlers.hpp index 86e43be..289a18f 100644 --- a/arma/client/addons/cad/CfgEventHandlers.hpp +++ b/arma/client/addons/cad/CfgEventHandlers.hpp @@ -1,7 +1,6 @@ class Extended_PreInit_EventHandlers { class ADDON { init = QUOTE(call COMPILE_SCRIPT(XEH_preInit)); - clientInit = QUOTE(call COMPILE_SCRIPT(XEH_preInitClient)); }; }; diff --git a/arma/client/addons/cad/XEH_postInitClient.sqf b/arma/client/addons/cad/XEH_postInitClient.sqf index fdd9bce..dd44a7a 100644 --- a/arma/client/addons/cad/XEH_postInitClient.sqf +++ b/arma/client/addons/cad/XEH_postInitClient.sqf @@ -7,18 +7,20 @@ if (isNil QGVAR(CADUIBridge)) then { call FUNC(initUIBridge); }; call FUNC(openUI); }] call CFUNC(addEventHandler); -[QGVAR(responseTaskCatalog), { - params [["_entries", [], [[]]]]; +[QGVAR(responseHydrateCad), { + params [["_payload", createHashMap, [createHashMap]]]; - if !(isNil QGVAR(CADRepository)) then { - GVAR(CADRepository) call ["setTaskCatalog", [_entries]]; - }; - - GVAR(CADUIBridge) call ["refreshTaskCatalog", []]; + GVAR(CADUIBridge) call ["handleHydrateResponse", [_payload]]; }] call CFUNC(addEventHandler); -[QGVAR(responseTaskAccept), { +[QGVAR(responseCadAssignment), { params [["_result", createHashMap, [createHashMap]]]; - GVAR(CADUIBridge) call ["handleTaskAcceptResponse", [_result]]; + GVAR(CADUIBridge) call ["handleAssignmentResponse", [_result]]; +}] call CFUNC(addEventHandler); + +[QGVAR(responseCadGroupUpdate), { + params [["_result", createHashMap, [createHashMap]]]; + + GVAR(CADUIBridge) call ["handleGroupUpdateResponse", [_result]]; }] call CFUNC(addEventHandler); diff --git a/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf b/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf index 22c93cb..8a10c41 100644 --- a/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf @@ -35,16 +35,46 @@ switch (_event) do { case "cad::ready": { GVAR(CADUIBridge) call ["handleReady", [_control, _data]]; }; - case "cad::tasks::refresh": { - GVAR(CADUIBridge) call ["requestTaskCatalog", []]; + case "cad::refresh": { + GVAR(CADUIBridge) call ["requestHydrate", []]; }; - case "cad::tasks::accept": { + case "cad::tasks::assign": { + private _taskID = ""; + private _groupID = ""; + private _note = ""; + if (_data isEqualType createHashMap) then { + _taskID = _data getOrDefault ["taskID", ""]; + _groupID = _data getOrDefault ["groupID", ""]; + _note = _data getOrDefault ["note", ""]; + }; + + GVAR(CADUIBridge) call ["requestAssignTask", [_taskID, _groupID, _note]]; + }; + case "cad::tasks::acknowledge": { private _taskID = ""; if (_data isEqualType createHashMap) then { _taskID = _data getOrDefault ["taskID", ""]; }; - GVAR(CADUIBridge) call ["requestTaskAccept", [_taskID]]; + GVAR(CADUIBridge) call ["requestAcknowledgeTask", [_taskID]]; + }; + case "cad::tasks::decline": { + private _taskID = ""; + if (_data isEqualType createHashMap) then { + _taskID = _data getOrDefault ["taskID", ""]; + }; + + GVAR(CADUIBridge) call ["requestDeclineTask", [_taskID]]; + }; + case "cad::groups::status": { + private _groupID = ""; + private _status = ""; + if (_data isEqualType createHashMap) then { + _groupID = _data getOrDefault ["groupID", ""]; + _status = _data getOrDefault ["status", ""]; + }; + + GVAR(CADUIBridge) call ["requestGroupStatus", [_groupID, _status]]; }; case "map::zoomIn": { private _mapCtrl = uiNamespace getVariable [QGVAR(MapCtrl), controlNull]; diff --git a/arma/client/addons/cad/functions/fnc_initRepository.sqf b/arma/client/addons/cad/functions/fnc_initRepository.sqf index 6cec168..ba109da 100644 --- a/arma/client/addons/cad/functions/fnc_initRepository.sqf +++ b/arma/client/addons/cad/functions/fnc_initRepository.sqf @@ -25,21 +25,33 @@ GVAR(CADRepository) = createHashMapObject [[ ["#create", compileFinal { _self set ["isLoaded", true]; _self set ["isOpen", false]; - _self set ["taskCatalog", []]; + _self set ["groups", []]; + _self set ["contracts", []]; + _self set ["assignments", []]; + _self set ["activity", []]; + _self set ["session", createHashMap]; }], - ["pushTaskCatalog", compileFinal { + ["pushHydratePayload", compileFinal { params [["_bridge", createHashMap, [createHashMap]]]; if (_bridge isEqualTo createHashMap) exitWith { false }; - _bridge call ["sendEvent", ["cad::tasks::hydrate", createHashMapFromArray [ - ["tasks", +(_self getOrDefault ["taskCatalog", []])] + _bridge call ["sendEvent", ["cad::hydrate", createHashMapFromArray [ + ["groups", +(_self getOrDefault ["groups", []])], + ["contracts", +(_self getOrDefault ["contracts", []])], + ["assignments", +(_self getOrDefault ["assignments", []])], + ["activity", +(_self getOrDefault ["activity", []])], + ["session", +(_self getOrDefault ["session", createHashMap])] ]]] }], - ["setTaskCatalog", compileFinal { - params [["_entries", [], [[]]]]; + ["setHydratePayload", compileFinal { + params [["_payload", createHashMap, [createHashMap]]]; - _self set ["taskCatalog", +_entries]; + _self set ["groups", +(_payload getOrDefault ["groups", []])]; + _self set ["contracts", +(_payload getOrDefault ["contracts", []])]; + _self set ["assignments", +(_payload getOrDefault ["assignments", []])]; + _self set ["activity", +(_payload getOrDefault ["activity", []])]; + _self set ["session", +(_payload getOrDefault ["session", createHashMap])]; true }], ["setOpen", compileFinal { diff --git a/arma/client/addons/cad/functions/fnc_initUIBridge.sqf b/arma/client/addons/cad/functions/fnc_initUIBridge.sqf index 0205f34..ab4cb86 100644 --- a/arma/client/addons/cad/functions/fnc_initUIBridge.sqf +++ b/arma/client/addons/cad/functions/fnc_initUIBridge.sqf @@ -7,7 +7,7 @@ * Public: No * * Description: - * Initializes the CAD UI bridge for sidepanel browser state and task event routing. + * Initializes the CAD UI bridge for sidepanel browser state and CAD event routing. * * Arguments: * None @@ -50,33 +50,73 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [ _screen call ["markReady", [true]]; _self call ["flushPendingEvents", []]; - _self call ["requestTaskCatalog", []]; - _self call ["refreshTaskCatalog", []]; + _self call ["requestHydrate", []]; + _self call ["refreshHydrate", []]; true }], - ["requestTaskCatalog", compileFinal { - [SRPC(task,requestTaskCatalog), [getPlayerUID player]] call CFUNC(serverEvent); + ["requestHydrate", compileFinal { + [SRPC(cad,requestHydrateCad), [getPlayerUID player]] call CFUNC(serverEvent); true }], - ["requestTaskAccept", compileFinal { + ["requestAssignTask", compileFinal { + params [["_taskID", "", [""]], ["_groupID", "", [""]], ["_note", "", [""]]]; + + if (_taskID isEqualTo "" || { _groupID isEqualTo "" }) exitWith { false }; + + [SRPC(cad,requestAssignCadTask), [getPlayerUID player, _taskID, _groupID, _note]] call CFUNC(serverEvent); + true + }], + ["requestAcknowledgeTask", compileFinal { params [["_taskID", "", [""]]]; if (_taskID isEqualTo "") exitWith { false }; - [SRPC(task,requestAcceptTask), [getPlayerUID player, _taskID]] call CFUNC(serverEvent); + [SRPC(cad,requestAcknowledgeCadTask), [getPlayerUID player, _taskID]] call CFUNC(serverEvent); true }], - ["refreshTaskCatalog", compileFinal { - if (isNil QGVAR(CADRepository)) exitWith { false }; - GVAR(CADRepository) call ["pushTaskCatalog", [_self]] + ["requestDeclineTask", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + [SRPC(cad,requestDeclineCadTask), [getPlayerUID player, _taskID]] call CFUNC(serverEvent); + true }], - ["handleTaskAcceptResponse", compileFinal { + ["requestGroupStatus", compileFinal { + params [["_groupID", "", [""]], ["_status", "", [""]]]; + + if (_groupID isEqualTo "" || { _status isEqualTo "" }) exitWith { false }; + + [SRPC(cad,requestUpdateCadGroupStatus), [getPlayerUID player, _groupID, _status]] call CFUNC(serverEvent); + true + }], + ["refreshHydrate", compileFinal { + if (isNil QGVAR(CADRepository)) exitWith { false }; + GVAR(CADRepository) call ["pushHydratePayload", [_self]] + }], + ["handleHydrateResponse", compileFinal { + params [["_payload", createHashMap, [createHashMap]]]; + + if (isNil QGVAR(CADRepository)) exitWith { false }; + + GVAR(CADRepository) call ["setHydratePayload", [_payload]]; + _self call ["refreshHydrate", []] + }], + ["handleAssignmentResponse", compileFinal { params [["_result", createHashMap, [createHashMap]]]; - _self call ["sendEvent", ["cad::tasks::accept::response", createHashMapFromArray [ + _self call ["sendEvent", ["cad::assignment::response", createHashMapFromArray [ ["message", _result getOrDefault ["message", "Task request processed."]], ["success", _result getOrDefault ["success", false]] ]]] + }], + ["handleGroupUpdateResponse", compileFinal { + params [["_result", createHashMap, [createHashMap]]]; + + _self call ["sendEvent", ["cad::group::response", createHashMapFromArray [ + ["message", _result getOrDefault ["message", "Group update processed."]], + ["success", _result getOrDefault ["success", false]] + ]]] }] ]; diff --git a/arma/client/addons/cad/ui/_site/cad-sidepanel.css b/arma/client/addons/cad/ui/_site/cad-sidepanel.css index 15d65fb..112ce7f 100644 --- a/arma/client/addons/cad/ui/_site/cad-sidepanel.css +++ b/arma/client/addons/cad/ui/_site/cad-sidepanel.css @@ -1 +1 @@ -html,body{background:var(--panel);border-left:1px solid var(--stroke);width:100%;height:100%;box-shadow:var(--shadow);-webkit-backdrop-filter:blur(12px);margin:0;padding:0;overflow:hidden}body{opacity:1;visibility:visible}.panel-header{border-bottom:1px solid var(--stroke);background:linear-gradient(#ffffff0d,#0000);justify-content:space-between;align-items:center;padding:14px;display:flex}.panel-header h3{color:var(--accent);text-transform:uppercase;letter-spacing:.8px;font-size:14px;font-weight:650}.panel-content{height:calc(100% - 56px);padding:14px;overflow:auto}.placeholder-message{text-align:center;padding:20px}.placeholder-message p{color:var(--muted);font-size:13px;font-style:italic}.task-toolbar{margin-bottom:10px}.task-toolbar button,.task-accept-btn{color:#f3f6f9;cursor:pointer;background:#1e252be6;border:1px solid #fff3;width:100%;padding:8px 10px}.task-toolbar button:hover,.task-accept-btn:hover{background:#2e3942f2}.task-toolbar button:disabled,.task-accept-btn:disabled{opacity:.55;cursor:default}.task-status-message{color:#cdd6dd;min-height:18px;margin-bottom:10px;font-size:12px}.task-status-message[data-type=success]{color:#79d28a}.task-status-message[data-type=error]{color:#ff8a80}.task-list{flex-direction:column;gap:10px;display:flex}.task-card{background:#0c10149e;border:1px solid #ffffff14;padding:10px}.task-card-header{justify-content:space-between;gap:8px;margin-bottom:8px;display:flex}.task-type{opacity:.7;text-transform:uppercase;font-size:11px}.task-description{margin:0 0 8px;font-size:12px;line-height:1.4}.task-meta{opacity:.8;justify-content:space-between;gap:8px;margin-bottom:8px;font-size:11px;display:flex} \ No newline at end of file +html,body{background:var(--panel);border-left:1px solid var(--stroke);width:100%;height:100%;box-shadow:var(--shadow);-webkit-backdrop-filter:blur(12px);margin:0;padding:0;overflow:hidden}body{opacity:1;visibility:visible}.panel-header{border-bottom:1px solid var(--stroke);background:linear-gradient(#ffffff0d,#0000);justify-content:space-between;align-items:center;padding:14px;display:flex}.panel-header h3{color:var(--accent);text-transform:uppercase;letter-spacing:.8px;font-size:14px;font-weight:650}.panel-content{height:calc(100% - 56px);padding:14px;overflow:auto}.placeholder-message{text-align:center;padding:20px}.placeholder-message p{color:var(--muted);font-size:13px;font-style:italic}.task-toolbar{margin-bottom:10px}.cad-tabs{grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:12px;display:grid}.cad-tab{color:#f3f6f9c7;text-transform:uppercase;letter-spacing:.08em;cursor:pointer;background:#141b21e0;border:1px solid #ffffff24;padding:8px 10px;font-size:11px}.cad-tab:hover{color:#f3f6f9;background:#1f282ff0}.cad-tab.is-active{color:var(--accent);background:#0f283af5;border-color:#5bbbff6b}.cad-tab-panels{min-height:0}.cad-section{display:none}.cad-section.is-active{display:block}.cad-section-header{color:var(--accent);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;font-size:12px;font-weight:700}.task-toolbar button,.task-accept-btn,.task-secondary-btn,.cad-select{color:#f3f6f9;background:#1e252be6;border:1px solid #fff3;width:100%;padding:8px 10px}.task-toolbar button,.task-accept-btn,.task-secondary-btn{cursor:pointer}.task-toolbar button:hover,.task-accept-btn:hover,.task-secondary-btn:hover{background:#2e3942f2}.task-toolbar button:disabled,.task-accept-btn:disabled,.task-secondary-btn:disabled{opacity:.55;cursor:default}.task-status-message{color:#cdd6dd;min-height:18px;margin-bottom:10px;font-size:12px}.task-status-message[data-type=success]{color:#79d28a}.task-status-message[data-type=error]{color:#ff8a80}.task-list{flex-direction:column;gap:10px;display:flex}.task-action-stack,.task-action-row{flex-direction:column;gap:8px;display:flex}.task-action-row{flex-direction:row}.task-card{background:#0c10149e;border:1px solid #ffffff14;padding:10px}.task-card-header{justify-content:space-between;gap:8px;margin-bottom:8px;display:flex}.task-type{opacity:.7;text-transform:uppercase;font-size:11px}.task-description{margin:0 0 8px;font-size:12px;line-height:1.4}.task-meta{opacity:.8;justify-content:space-between;gap:8px;margin-bottom:8px;font-size:11px;display:flex}.task-secondary-btn{background:#3c302deb} \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/cad-sidepanel.js b/arma/client/addons/cad/ui/_site/cad-sidepanel.js index 05e57ab..fa1bdc4 100644 --- a/arma/client/addons/cad/ui/_site/cad-sidepanel.js +++ b/arma/client/addons/cad/ui/_site/cad-sidepanel.js @@ -1 +1 @@ -window.cadTasks={tasks:[],init(){const s=document.getElementById("refreshTasksBtn");s&&s.addEventListener("click",()=>this.refresh()),window.ForgeBridge.on("cad::tasks::hydrate",s=>{this.setTasks(s.tasks||[])}),window.ForgeBridge.on("cad::tasks::accept::response",s=>{this.handleAcceptResponse(!!s.success,s.message||"")}),window.ForgeBridge.ready({loaded:!0})},setTasks(s){this.tasks=Array.isArray(s)?s:[];const t=document.getElementById("taskStatusMessage");!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.render()},setStatus(s,t){const e=document.getElementById("taskStatusMessage");e&&(e.textContent=s||"",e.dataset.type=t||"info")},handleAcceptResponse(s,t){this.setStatus(t||(s?"Task accepted.":"Unable to accept task."),s?"success":"error")},refresh(){this.setStatus("Refreshing tasks...","info"),window.mapUI.sendEvent("cad::tasks::refresh",{})},acceptTask(s){this.setStatus("Submitting acceptance...","info"),window.mapUI.sendEvent("cad::tasks::accept",{taskID:s})},render(){const s=document.getElementById("taskList");s&&(this.tasks.length?s.innerHTML=this.tasks.map(s=>{const t=Array.isArray(s.position)?s.position:[0,0,0],e=!!s.accepted,a=e?`Assigned: ${s.orgID||"Unknown"}`:"Available";return`\n
\n
\n ${s.title||s.taskID}\n ${s.type||"task"}\n
\n

${s.description||""}

\n
\n ${a}\n X: ${Math.round(t[0]||0)} Y: ${Math.round(t[1]||0)}\n
\n \n
\n `}).join(""):s.innerHTML='

No active tasks are available.

')}},window.cadTasks.init(); \ No newline at end of file +window.cadTasks={contracts:[],groups:[],activity:[],session:{},activeTab:"contracts",statuses:["available","en_route","on_task","holding","danger","refit","offline"],init(){const s=document.getElementById("refreshCadBtn");s&&s.addEventListener("click",()=>this.refresh()),document.querySelectorAll(".cad-tab").forEach(s=>{s.addEventListener("click",()=>{this.setActiveTab(s.dataset.tab||"contracts")})}),window.ForgeBridge.on("cad::hydrate",s=>{this.setHydratePayload(s||{})}),window.ForgeBridge.on("cad::assignment::response",s=>{this.handleServerResponse(!!s.success,s.message||"")}),window.ForgeBridge.on("cad::group::response",s=>{this.handleServerResponse(!!s.success,s.message||"")}),window.ForgeBridge.ready({loaded:!0})},setActiveTab(s){this.activeTab=s||"contracts",document.querySelectorAll(".cad-tab").forEach(s=>{s.classList.toggle("is-active",s.dataset.tab===this.activeTab)}),document.querySelectorAll("[data-panel]").forEach(s=>{s.classList.toggle("is-active",s.dataset.panel===this.activeTab)})},setHydratePayload(s){this.contracts=Array.isArray(s.contracts)?s.contracts:[],this.groups=Array.isArray(s.groups)?s.groups:[],this.activity=Array.isArray(s.activity)?s.activity:[],this.session=s.session&&"object"==typeof s.session?s.session:{};const t=document.getElementById("cadStatusMessage");!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.render()},setStatus(s,t){const e=document.getElementById("cadStatusMessage");e&&(e.textContent=s||"",e.dataset.type=t||"")},handleServerResponse(s,t){this.setStatus(t||(s?"CAD update succeeded.":"CAD update failed."),s?"success":"error")},refresh(){this.setStatus("Refreshing board...","info"),window.mapUI.sendEvent("cad::refresh",{})},assignTask(s){const t=document.getElementById(`assign-group-${s}`);t&&t.value?(this.setStatus("Submitting assignment...","info"),window.mapUI.sendEvent("cad::tasks::assign",{taskID:s,groupID:t.value,note:""})):this.setStatus("Select a group before assigning a contract.","error")},acknowledgeTask(s){this.setStatus("Acknowledging contract...","info"),window.mapUI.sendEvent("cad::tasks::acknowledge",{taskID:s})},declineTask(s){this.setStatus("Declining contract...","info"),window.mapUI.sendEvent("cad::tasks::decline",{taskID:s})},updateGroupStatus(s,t){this.setStatus("Updating group status...","info"),window.mapUI.sendEvent("cad::groups::status",{groupID:s,status:t})},getPlayerGroupId(){return this.session.groupId||""},canDispatch(){return!!this.session.isDispatcher},isLeader(){return!!this.session.isLeader},renderContracts(){const s=document.getElementById("taskList");if(!s)return;if(!this.contracts.length)return void(s.innerHTML='

No active contracts are available.

');const t=this.getPlayerGroupId();s.innerHTML=this.contracts.map(s=>{const e=s.taskId||s.taskID||"",a=Array.isArray(s.position)?s.position:[0,0,0],n=s.assignedGroupId||"",i=s.assignmentState||"unassigned",o=this.groups.find(s=>s.groupId===n),r=this.isLeader()&&n===t,c=this.groups.map(s=>``).join("");return`\n
\n
\n ${s.title||e}\n ${s.type||"task"}\n
\n

${s.description||""}

\n
\n ${"unassigned"===i?"Available":`${i}: ${o?o.callsign:n}`}\n X: ${Math.round(a[0]||0)} Y: ${Math.round(a[1]||0)}\n
\n ${this.canDispatch()?`
\n \n \n
`:""}\n ${r&&"assigned"===i?`
\n \n \n
`:""}\n
\n `}).join("")},renderGroups(){const s=document.getElementById("groupList");if(!s)return;if(!this.groups.length)return void(s.innerHTML='

No active groups are available.

');const t=this.getPlayerGroupId();s.innerHTML=this.groups.map(s=>{const e=this.canDispatch()||this.isLeader()&&s.groupId===t,a=this.statuses.map(t=>``).join("");return`\n
\n
\n ${s.callsign||s.groupId}\n ${s.role||"group"}\n
\n
\n Leader: ${s.leaderName||"Unknown"}\n Status: ${s.status||"unknown"}\n
\n
\n Org: ${s.orgId||"default"}\n Task: ${s.currentTaskId||"None"}\n
\n ${e?`
\n \n \n
`:""}\n
\n `}).join("")},renderActivity(){const s=document.getElementById("activityList");s&&(this.activity.length?s.innerHTML=this.activity.slice().reverse().slice(0,8).map(s=>`\n
\n
\n ${s.type||"activity"}\n ${Math.round(s.timestamp||0)}s\n
\n

${s.message||""}

\n
\n `).join(""):s.innerHTML='

No recent activity.

')},render(){this.renderContracts(),this.renderGroups(),this.renderActivity(),this.setActiveTab(this.activeTab)}},window.cadTasks.init(); \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/sidepanel.html b/arma/client/addons/cad/ui/_site/sidepanel.html index 3cd718e..778ef42 100644 --- a/arma/client/addons/cad/ui/_site/sidepanel.html +++ b/arma/client/addons/cad/ui/_site/sidepanel.html @@ -1 +1 @@ -

CAD System

Loading available tasks...

\ No newline at end of file +

CAD System

Contracts

Loading contracts...

Groups

Loading groups...

Activity

No recent activity.

\ No newline at end of file diff --git a/arma/client/addons/cad/ui/src/sidepanel.html b/arma/client/addons/cad/ui/src/sidepanel.html index 8efa438..6457059 100644 --- a/arma/client/addons/cad/ui/src/sidepanel.html +++ b/arma/client/addons/cad/ui/src/sidepanel.html @@ -9,14 +9,67 @@
- +
+
+
+ + +
-
-
-
-

Loading available tasks...

+
+
+
Contracts
+
+
+

Loading contracts...

+
+
+
+
+
Groups
+
+
+

Loading groups...

+
+
+
+
+
Activity
+
+
+

No recent activity.

+
+
diff --git a/arma/client/addons/cad/ui/src/sidepanel.js b/arma/client/addons/cad/ui/src/sidepanel.js index bebcd95..950327d 100644 --- a/arma/client/addons/cad/ui/src/sidepanel.js +++ b/arma/client/addons/cad/ui/src/sidepanel.js @@ -1,94 +1,300 @@ window.cadTasks = { - tasks: [], + contracts: [], + groups: [], + activity: [], + session: {}, + activeTab: "contracts", + statuses: [ + "available", + "en_route", + "on_task", + "holding", + "danger", + "refit", + "offline", + ], init() { - const refreshBtn = document.getElementById("refreshTasksBtn"); + const refreshBtn = document.getElementById("refreshCadBtn"); if (refreshBtn) { refreshBtn.addEventListener("click", () => this.refresh()); } - window.ForgeBridge.on("cad::tasks::hydrate", (payload) => { - this.setTasks(payload.tasks || []); + document.querySelectorAll(".cad-tab").forEach((tab) => { + tab.addEventListener("click", () => { + this.setActiveTab(tab.dataset.tab || "contracts"); + }); }); - window.ForgeBridge.on("cad::tasks::accept::response", (payload) => { - this.handleAcceptResponse(!!payload.success, payload.message || ""); + window.ForgeBridge.on("cad::hydrate", (payload) => { + this.setHydratePayload(payload || {}); + }); + + window.ForgeBridge.on("cad::assignment::response", (payload) => { + this.handleServerResponse(!!payload.success, payload.message || ""); + }); + + window.ForgeBridge.on("cad::group::response", (payload) => { + this.handleServerResponse(!!payload.success, payload.message || ""); }); window.ForgeBridge.ready({ loaded: true }); }, - setTasks(tasks) { - this.tasks = Array.isArray(tasks) ? tasks : []; - const statusEl = document.getElementById("taskStatusMessage"); + setActiveTab(tabName) { + this.activeTab = tabName || "contracts"; + + document.querySelectorAll(".cad-tab").forEach((tab) => { + tab.classList.toggle( + "is-active", + tab.dataset.tab === this.activeTab, + ); + }); + + document.querySelectorAll("[data-panel]").forEach((panel) => { + panel.classList.toggle( + "is-active", + panel.dataset.panel === this.activeTab, + ); + }); + }, + setHydratePayload(payload) { + this.contracts = Array.isArray(payload.contracts) + ? payload.contracts + : []; + this.groups = Array.isArray(payload.groups) ? payload.groups : []; + this.activity = Array.isArray(payload.activity) ? payload.activity : []; + this.session = + payload.session && typeof payload.session === "object" + ? payload.session + : {}; + + const statusEl = document.getElementById("cadStatusMessage"); if ( statusEl && (!statusEl.dataset.type || statusEl.dataset.type === "info") ) { this.setStatus("", ""); } + this.render(); }, setStatus(message, type) { - const statusEl = document.getElementById("taskStatusMessage"); + const statusEl = document.getElementById("cadStatusMessage"); if (!statusEl) { return; } statusEl.textContent = message || ""; - statusEl.dataset.type = type || "info"; + statusEl.dataset.type = type || ""; }, - handleAcceptResponse(success, message) { + handleServerResponse(success, message) { this.setStatus( - message || (success ? "Task accepted." : "Unable to accept task."), + message || + (success ? "CAD update succeeded." : "CAD update failed."), success ? "success" : "error", ); }, refresh() { - this.setStatus("Refreshing tasks...", "info"); - window.mapUI.sendEvent("cad::tasks::refresh", {}); + this.setStatus("Refreshing board...", "info"); + window.mapUI.sendEvent("cad::refresh", {}); }, - acceptTask(taskID) { - this.setStatus("Submitting acceptance...", "info"); - window.mapUI.sendEvent("cad::tasks::accept", { taskID: taskID }); + assignTask(taskID) { + const selector = document.getElementById(`assign-group-${taskID}`); + if (!selector || !selector.value) { + this.setStatus( + "Select a group before assigning a contract.", + "error", + ); + return; + } + + this.setStatus("Submitting assignment...", "info"); + window.mapUI.sendEvent("cad::tasks::assign", { + taskID: taskID, + groupID: selector.value, + note: "", + }); }, - render() { + acknowledgeTask(taskID) { + this.setStatus("Acknowledging contract...", "info"); + window.mapUI.sendEvent("cad::tasks::acknowledge", { taskID: taskID }); + }, + declineTask(taskID) { + this.setStatus("Declining contract...", "info"); + window.mapUI.sendEvent("cad::tasks::decline", { taskID: taskID }); + }, + updateGroupStatus(groupID, status) { + this.setStatus("Updating group status...", "info"); + window.mapUI.sendEvent("cad::groups::status", { + groupID: groupID, + status: status, + }); + }, + getPlayerGroupId() { + return this.session.groupId || ""; + }, + canDispatch() { + return !!this.session.isDispatcher; + }, + isLeader() { + return !!this.session.isLeader; + }, + renderContracts() { const listEl = document.getElementById("taskList"); if (!listEl) { return; } - if (!this.tasks.length) { + if (!this.contracts.length) { listEl.innerHTML = - '

No active tasks are available.

'; + '

No active contracts are available.

'; return; } - listEl.innerHTML = this.tasks + const currentGroupId = this.getPlayerGroupId(); + listEl.innerHTML = this.contracts .map((task) => { + const taskId = task.taskId || task.taskID || ""; const position = Array.isArray(task.position) ? task.position : [0, 0, 0]; - const accepted = !!task.accepted; - const ownerLabel = accepted - ? `Assigned: ${task.orgID || "Unknown"}` - : "Available"; + const assignedGroupId = task.assignedGroupId || ""; + const assignmentState = task.assignmentState || "unassigned"; + const assignedGroup = this.groups.find( + (group) => group.groupId === assignedGroupId, + ); + const isAssignedToLeader = + this.isLeader() && assignedGroupId === currentGroupId; + const groupOptions = this.groups + .map( + (group) => + ``, + ) + .join(""); return ` -
+
- ${task.title || task.taskID} + ${task.title || taskId} ${task.type || "task"}

${task.description || ""}

- ${ownerLabel} + ${assignmentState === "unassigned" ? "Available" : `${assignmentState}: ${assignedGroup ? assignedGroup.callsign : assignedGroupId}`} X: ${Math.round(position[0] || 0)} Y: ${Math.round(position[1] || 0)}
- + ${ + this.canDispatch() + ? `
+ + +
` + : "" + } + ${ + isAssignedToLeader && assignmentState === "assigned" + ? `
+ + +
` + : "" + }
`; }) .join(""); }, + renderGroups() { + const listEl = document.getElementById("groupList"); + if (!listEl) { + return; + } + + if (!this.groups.length) { + listEl.innerHTML = + '

No active groups are available.

'; + return; + } + + const currentGroupId = this.getPlayerGroupId(); + listEl.innerHTML = this.groups + .map((group) => { + const canUpdate = + this.canDispatch() || + (this.isLeader() && group.groupId === currentGroupId); + const statusOptions = this.statuses + .map( + (status) => + ``, + ) + .join(""); + + return ` +
+
+ ${group.callsign || group.groupId} + ${group.role || "group"} +
+
+ Leader: ${group.leaderName || "Unknown"} + Status: ${group.status || "unknown"} +
+
+ Org: ${group.orgId || "default"} + Task: ${group.currentTaskId || "None"} +
+ ${ + canUpdate + ? `
+ + +
` + : "" + } +
+ `; + }) + .join(""); + }, + renderActivity() { + const listEl = document.getElementById("activityList"); + if (!listEl) { + return; + } + + if (!this.activity.length) { + listEl.innerHTML = + '

No recent activity.

'; + return; + } + + listEl.innerHTML = this.activity + .slice() + .reverse() + .slice(0, 8) + .map( + (entry) => ` +
+
+ ${entry.type || "activity"} + ${Math.round(entry.timestamp || 0)}s +
+

${entry.message || ""}

+
+ `, + ) + .join(""); + }, + render() { + this.renderContracts(); + this.renderGroups(); + this.renderActivity(); + this.setActiveTab(this.activeTab); + }, }; window.cadTasks.init(); diff --git a/arma/client/addons/cad/ui/src/styles/sidepanel.css b/arma/client/addons/cad/ui/src/styles/sidepanel.css index 0a74484..5224e84 100644 --- a/arma/client/addons/cad/ui/src/styles/sidepanel.css +++ b/arma/client/addons/cad/ui/src/styles/sidepanel.css @@ -59,23 +59,82 @@ body { margin-bottom: 10px; } +.cad-tabs { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 6px; + margin-bottom: 12px; +} + +.cad-tab { + padding: 8px 10px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(20, 27, 33, 0.88); + color: rgba(243, 246, 249, 0.78); + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 11px; + cursor: pointer; +} + +.cad-tab:hover { + background: rgba(31, 40, 47, 0.94); + color: #f3f6f9; +} + +.cad-tab.is-active { + border-color: rgba(91, 187, 255, 0.42); + background: rgba(15, 40, 58, 0.96); + color: var(--accent); +} + +.cad-tab-panels { + min-height: 0; +} + +.cad-section { + display: none; +} + +.cad-section.is-active { + display: block; +} + +.cad-section-header { + margin-bottom: 8px; + color: var(--accent); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; +} + .task-toolbar button, -.task-accept-btn { +.task-accept-btn, +.task-secondary-btn, +.cad-select { width: 100%; padding: 8px 10px; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(30, 37, 43, 0.9); color: #f3f6f9; +} + +.task-toolbar button, +.task-accept-btn, +.task-secondary-btn { cursor: pointer; } .task-toolbar button:hover, -.task-accept-btn:hover { +.task-accept-btn:hover, +.task-secondary-btn:hover { background: rgba(46, 57, 66, 0.95); } .task-toolbar button:disabled, -.task-accept-btn:disabled { +.task-accept-btn:disabled, +.task-secondary-btn:disabled { opacity: 0.55; cursor: default; } @@ -101,6 +160,17 @@ body { gap: 10px; } +.task-action-stack, +.task-action-row { + display: flex; + flex-direction: column; + gap: 8px; +} + +.task-action-row { + flex-direction: row; +} + .task-card { padding: 10px; border: 1px solid rgba(255, 255, 255, 0.08); @@ -134,3 +204,7 @@ body { font-size: 11px; opacity: 0.8; } + +.task-secondary-btn { + background: rgba(60, 48, 45, 0.92); +} diff --git a/arma/server/addons/cad/$PBOPREFIX$ b/arma/server/addons/cad/$PBOPREFIX$ new file mode 100644 index 0000000..5062385 --- /dev/null +++ b/arma/server/addons/cad/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_server\addons\cad diff --git a/arma/server/addons/cad/CfgEventHandlers.hpp b/arma/server/addons/cad/CfgEventHandlers.hpp new file mode 100644 index 0000000..9b160c1 --- /dev/null +++ b/arma/server/addons/cad/CfgEventHandlers.hpp @@ -0,0 +1,5 @@ +class Extended_PreInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_SCRIPT(XEH_preInit)); + }; +}; diff --git a/arma/server/addons/cad/XEH_PREP.hpp b/arma/server/addons/cad/XEH_PREP.hpp new file mode 100644 index 0000000..552c605 --- /dev/null +++ b/arma/server/addons/cad/XEH_PREP.hpp @@ -0,0 +1 @@ +PREP(initCadStore); diff --git a/arma/server/addons/cad/XEH_preInit.sqf b/arma/server/addons/cad/XEH_preInit.sqf new file mode 100644 index 0000000..832b1fc --- /dev/null +++ b/arma/server/addons/cad/XEH_preInit.sqf @@ -0,0 +1,86 @@ +#include "script_component.hpp" + +PREP_RECOMPILE_START; +#include "XEH_PREP.hpp" +PREP_RECOMPILE_END; + +call FUNC(initCadStore); + +[QGVAR(requestHydrateCad), { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { + ["WARNING", "CAD hydrate request received with empty UID."] call EFUNC(common,log); + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith {}; + + private _payload = GVAR(CadStore) call ["buildHydratePayload", [_uid]]; + [CRPC(cad,responseHydrateCad), [_payload], _player] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); + +[QGVAR(requestAssignCadTask), { + params [ + ["_uid", "", [""]], + ["_taskID", "", [""]], + ["_groupID", "", [""]], + ["_note", "", [""]] + ]; + + if (_uid isEqualTo "" || { _taskID isEqualTo "" } || { _groupID isEqualTo "" }) exitWith { + ["WARNING", "Invalid CAD task assignment payload."] call EFUNC(common,log); + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith {}; + + private _result = GVAR(CadStore) call ["assignTaskToGroup", [_uid, _taskID, _groupID, _note]]; + [CRPC(cad,responseCadAssignment), [_result], _player] call CFUNC(targetEvent); + [CRPC(cad,responseHydrateCad), [GVAR(CadStore) call ["buildHydratePayload", [_uid]]], _player] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); + +[QGVAR(requestAcknowledgeCadTask), { + params [["_uid", "", [""]], ["_taskID", "", [""]]]; + + if (_uid isEqualTo "" || { _taskID isEqualTo "" }) exitWith { + ["WARNING", "Invalid CAD acknowledge payload."] call EFUNC(common,log); + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith {}; + + private _result = GVAR(CadStore) call ["acknowledgeTask", [_uid, _taskID]]; + [CRPC(cad,responseCadAssignment), [_result], _player] call CFUNC(targetEvent); + [CRPC(cad,responseHydrateCad), [GVAR(CadStore) call ["buildHydratePayload", [_uid]]], _player] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); + +[QGVAR(requestDeclineCadTask), { + params [["_uid", "", [""]], ["_taskID", "", [""]]]; + + if (_uid isEqualTo "" || { _taskID isEqualTo "" }) exitWith { + ["WARNING", "Invalid CAD decline payload."] call EFUNC(common,log); + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith {}; + + private _result = GVAR(CadStore) call ["declineTask", [_uid, _taskID]]; + [CRPC(cad,responseCadAssignment), [_result], _player] call CFUNC(targetEvent); + [CRPC(cad,responseHydrateCad), [GVAR(CadStore) call ["buildHydratePayload", [_uid]]], _player] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); + +[QGVAR(requestUpdateCadGroupStatus), { + params [["_uid", "", [""]], ["_groupID", "", [""]], ["_status", "", [""]]]; + + if (_uid isEqualTo "" || { _groupID isEqualTo "" } || { _status isEqualTo "" }) exitWith { + ["WARNING", "Invalid CAD group status payload."] call EFUNC(common,log); + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith {}; + + private _result = GVAR(CadStore) call ["updateGroupStatus", [_uid, _groupID, _status]]; + [CRPC(cad,responseCadGroupUpdate), [_result], _player] call CFUNC(targetEvent); + [CRPC(cad,responseHydrateCad), [GVAR(CadStore) call ["buildHydratePayload", [_uid]]], _player] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); diff --git a/arma/server/addons/cad/config.cpp b/arma/server/addons/cad/config.cpp new file mode 100644 index 0000000..74d149d --- /dev/null +++ b/arma/server/addons/cad/config.cpp @@ -0,0 +1,23 @@ +#include "script_component.hpp" + +class CfgPatches { + class ADDON { + author = AUTHOR; + authors[] = {"IDSolutions"}; + url = ECSTRING(main,url); + name = COMPONENT_NAME; + requiredVersion = REQUIRED_VERSION; + requiredAddons[] = { + "forge_server_main", + "forge_server_common", + "forge_server_actor", + "forge_server_org", + "forge_server_task" + }; + units[] = {}; + weapons[] = {}; + VERSION_CONFIG; + }; +}; + +#include "CfgEventHandlers.hpp" diff --git a/arma/server/addons/cad/functions/fnc_initCadStore.sqf b/arma/server/addons/cad/functions/fnc_initCadStore.sqf new file mode 100644 index 0000000..61bc8f6 --- /dev/null +++ b/arma/server/addons/cad/functions/fnc_initCadStore.sqf @@ -0,0 +1,498 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initCadStore.sqf + * Author: IDSolutions + * Date: 2026-03-29 + * Public: Yes + * + * Description: + * Initializes the CAD store for group tracking, assignment state, + * activity history, and CAD hydrate payloads. + * + * Arguments: + * None + * + * Return Value: + * CAD store object [HASHMAP OBJECT] + * + * Example: + * call forge_server_cad_fnc_initCadStore + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "CadStoreBaseClass"], + ["#create", compileFinal { + _self set ["groupRegistry", createHashMap]; + _self set ["assignmentRegistry", createHashMap]; + _self set ["activityRegistry", []]; + _self set ["validStatuses", [ + "available", + "en_route", + "on_task", + "holding", + "danger", + "refit", + "offline" + ]]; + ["INFO", "CAD Store Initialized!"] call EFUNC(common,log); + }], + ["appendActivity", compileFinal { + params [ + ["_type", "", [""]], + ["_message", "", [""]], + ["_taskID", "", [""]], + ["_groupID", "", [""]], + ["_actorUid", "", [""]] + ]; + + if (_type isEqualTo "" || { _message isEqualTo "" }) exitWith { false }; + + private _activityRegistry = +(_self getOrDefault ["activityRegistry", []]); + _activityRegistry pushBack createHashMapFromArray [ + ["type", _type], + ["message", _message], + ["timestamp", serverTime], + ["taskId", _taskID], + ["groupId", _groupID], + ["actorUid", _actorUid] + ]; + + if ((count _activityRegistry) > 50) then { + _activityRegistry deleteRange [0, (count _activityRegistry) - 50]; + }; + + _self set ["activityRegistry", _activityRegistry]; + true + }], + ["resolveGroupId", compileFinal { + params [["_group", grpNull, [grpNull]]]; + + if (isNull _group) exitWith { "" }; + + private _leader = leader _group; + private _leaderUid = if (isNull _leader) then { "" } else { getPlayerUID _leader }; + if (_leaderUid isNotEqualTo "") exitWith { format ["group:%1", _leaderUid] }; + + private _groupLabel = groupId _group; + if (_groupLabel isNotEqualTo "") exitWith { format ["group:%1", _groupLabel] }; + + str _group + }], + ["canDispatch", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { false }; + + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + if (_actor isEqualTo createHashMap) then { + _actor = EGVAR(actor,ActorStore) call ["init", [_uid]]; + }; + + private _orgID = _actor getOrDefault ["organization", "default"]; + private _org = EGVAR(org,Registry) getOrDefault [_orgID, createHashMap]; + if (_org isEqualTo createHashMap) then { + _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]]; + }; + + if (_org getOrDefault ["owner", ""] isEqualTo _uid) exitWith { true }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith { false }; + + (_orgID isEqualTo "default") && { vehicleVarName _player isEqualTo "ceo" } + }], + ["getCurrentTaskIdForGroup", compileFinal { + params [["_groupID", "", [""]]]; + + if (_groupID isEqualTo "") exitWith { "" }; + + private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; + private _taskID = ""; + { + if ((_y getOrDefault ["groupId", ""]) isNotEqualTo _groupID) then { continue; }; + if !((_y getOrDefault ["state", ""]) in ["assigned", "acknowledged"]) then { continue; }; + if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; }; + + _taskID = _x; + } forEach _assignmentRegistry; + + _taskID + }], + ["syncGroups", compileFinal { + private _previousRegistry = _self getOrDefault ["groupRegistry", createHashMap]; + private _nextRegistry = createHashMap; + + { + if (side _x isNotEqualTo west) then { continue; }; + + private _members = (units _x) select { isPlayer _x }; + if (_members isEqualTo []) then { continue; }; + + private _leader = leader _x; + if (isNull _leader || { !isPlayer _leader }) then { + _leader = _members # 0; + }; + + private _groupID = _self call ["resolveGroupId", [_x]]; + if (_groupID isEqualTo "") then { continue; }; + + private _leaderUid = getPlayerUID _leader; + private _actor = EGVAR(actor,Registry) getOrDefault [_leaderUid, createHashMap]; + if (_actor isEqualTo createHashMap && { _leaderUid isNotEqualTo "" }) then { + _actor = EGVAR(actor,ActorStore) call ["init", [_leaderUid]]; + }; + + private _orgID = _actor getOrDefault ["organization", "default"]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + + private _existingRecord = +(_previousRegistry getOrDefault [_groupID, createHashMap]); + private _memberUids = []; + { + private _memberUid = getPlayerUID _x; + if (_memberUid isNotEqualTo "") then { + _memberUids pushBack _memberUid; + }; + } forEach _members; + + private _record = createHashMapFromArray [ + ["groupId", _groupID], + ["callsign", [groupId _x, _groupID] select ((groupId _x) isEqualTo "")], + ["leaderUid", _leaderUid], + ["leaderName", name _leader], + ["memberUids", _memberUids], + ["orgId", _orgID], + ["role", _existingRecord getOrDefault ["role", "infantry"]], + ["status", _existingRecord getOrDefault ["status", "available"]], + ["position", getPosATL _leader], + ["currentTaskId", _self call ["getCurrentTaskIdForGroup", [_groupID]]], + ["lastUpdate", serverTime] + ]; + + _nextRegistry set [_groupID, _record]; + } forEach allGroups; + + _self set ["groupRegistry", _nextRegistry]; + _nextRegistry + }], + ["getGroupRecord", compileFinal { + params [["_groupID", "", [""]]]; + + if (_groupID isEqualTo "") exitWith { createHashMap }; + + private _groupRegistry = _self call ["syncGroups", []]; + +(_groupRegistry getOrDefault [_groupID, createHashMap]) + }], + ["getPlayerGroupId", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { "" }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith { "" }; + + _self call ["resolveGroupId", [group _player]] + }], + ["isGroupLeader", compileFinal { + params [["_uid", "", [""]], ["_groupID", "", [""]]]; + + if (_uid isEqualTo "" || { _groupID isEqualTo "" }) exitWith { false }; + + private _groupRecord = _self call ["getGroupRecord", [_groupID]]; + (_groupRecord getOrDefault ["leaderUid", ""]) isEqualTo _uid + }], + ["pruneAssignments", compileFinal { + private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; + private _keysToRemove = []; + + { + private _status = EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]; + if !(_status in ["active", ""]) then { + _keysToRemove pushBack _x; + }; + } forEach _assignmentRegistry; + + { + _assignmentRegistry deleteAt _x; + } forEach _keysToRemove; + + _self set ["assignmentRegistry", _assignmentRegistry]; + count _keysToRemove + }], + ["buildContracts", compileFinal { + _self call ["pruneAssignments", []]; + + private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; + private _contracts = []; + + { + private _taskID = _x getOrDefault ["taskID", ""]; + if (_taskID isEqualTo "") then { continue; }; + + private _assignment = _assignmentRegistry getOrDefault [_taskID, createHashMap]; + private _entry = +_x; + _entry set ["taskId", _taskID]; + _entry set ["assignedGroupId", _assignment getOrDefault ["groupId", ""]]; + _entry set ["assignmentState", [_assignment getOrDefault ["state", ""], "unassigned"] select (_assignment isEqualTo createHashMap)]; + _contracts pushBack _entry; + } forEach (EGVAR(task,TaskStore) call ["getActiveTaskCatalog", []]); + + _contracts + }], + ["buildGroups", compileFinal { + private _groupRegistry = _self call ["syncGroups", []]; + private _groups = []; + + { + _groups pushBack +_y; + } forEach _groupRegistry; + + _groups + }], + ["buildHydratePayload", compileFinal { + params [["_uid", "", [""]]]; + + private _activity = +(_self getOrDefault ["activityRegistry", []]); + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + if (_actor isEqualTo createHashMap && { _uid isNotEqualTo "" }) then { + _actor = EGVAR(actor,ActorStore) call ["init", [_uid]]; + }; + + createHashMapFromArray [ + ["groups", _self call ["buildGroups", []]], + ["contracts", _self call ["buildContracts", []]], + ["assignments", values (_self getOrDefault ["assignmentRegistry", createHashMap])], + ["activity", _activity], + ["session", createHashMapFromArray [ + ["uid", _uid], + ["orgId", _actor getOrDefault ["organization", "default"]], + ["isDispatcher", _self call ["canDispatch", [_uid]]], + ["groupId", _self call ["getPlayerGroupId", [_uid]]], + ["isLeader", _self call ["isGroupLeader", [_uid, _self call ["getPlayerGroupId", [_uid]]]]] + ]] + ] + }], + ["notifyPlayer", compileFinal { + params [ + ["_uid", "", [""]], + ["_type", "info", [""]], + ["_title", "CAD", [""]], + ["_message", "", [""]] + ]; + + if (_uid isEqualTo "" || { _message isEqualTo "" }) exitWith { false }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith { false }; + + [CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent); + true + }], + ["assignTaskToGroup", compileFinal { + params [ + ["_requesterUid", "", [""]], + ["_taskID", "", [""]], + ["_groupID", "", [""]], + ["_note", "", [""]] + ]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to assign task."], + ["assignment", createHashMap] + ]; + + if !(_self call ["canDispatch", [_requesterUid]]) exitWith { + _result set ["message", "You are not authorized to assign contracts."]; + _result + }; + + if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_taskID]]) isNotEqualTo "active") exitWith { + _result set ["message", "Task is no longer active."]; + _result + }; + + private _groupRecord = _self call ["getGroupRecord", [_groupID]]; + if (_groupRecord isEqualTo createHashMap) exitWith { + _result set ["message", "Selected group is unavailable."]; + _result + }; + + private _leaderUid = _groupRecord getOrDefault ["leaderUid", ""]; + if (_leaderUid isEqualTo "") exitWith { + _result set ["message", "Selected group has no online leader."]; + _result + }; + + private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer); + private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; + private _assignment = createHashMapFromArray [ + ["taskId", _taskID], + ["groupId", _groupID], + ["assignedByUid", _requesterUid], + ["assignedByName", ["Dispatcher", name _requesterPlayer] select (_requesterPlayer isNotEqualTo objNull)], + ["assignedAt", serverTime], + ["state", "assigned"], + ["note", _note] + ]; + + _assignmentRegistry set [_taskID, _assignment]; + _self set ["assignmentRegistry", _assignmentRegistry]; + + _self call ["appendActivity", [ + "task_assigned", + format ["%1 assigned %2 to %3.", _assignment get "assignedByName", _taskID, _groupRecord getOrDefault ["callsign", _groupID]], + _taskID, + _groupID, + _requesterUid + ]]; + + _self call ["notifyPlayer", [ + _leaderUid, + "info", + "Tasks", + format ["Contract assigned: %1. Open CAD to review and acknowledge.", _taskID] + ]]; + + _result set ["success", true]; + _result set ["message", "Task assigned."]; + _result set ["assignment", _assignment]; + _result + }], + ["acknowledgeTask", compileFinal { + params [["_requesterUid", "", [""]], ["_taskID", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to acknowledge task."], + ["assignment", createHashMap] + ]; + + private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; + private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]); + if (_assignment isEqualTo createHashMap) exitWith { + _result set ["message", "Task is not assigned."]; + _result + }; + + private _groupID = _assignment getOrDefault ["groupId", ""]; + if !(_self call ["isGroupLeader", [_requesterUid, _groupID]]) exitWith { + _result set ["message", "Only the assigned group leader can acknowledge this task."]; + _result + }; + + private _bindResult = EGVAR(task,TaskStore) call ["bindTaskOwnership", [_taskID, _requesterUid]]; + if !(_bindResult getOrDefault ["success", false]) exitWith { + _result set ["message", _bindResult getOrDefault ["message", "Failed to bind task ownership."]]; + _result + }; + + _assignment set ["state", "acknowledged"]; + _assignment set ["acknowledgedAt", serverTime]; + _assignmentRegistry set [_taskID, _assignment]; + _self set ["assignmentRegistry", _assignmentRegistry]; + + _self call ["appendActivity", [ + "task_acknowledged", + format ["%1 acknowledged %2.", _requesterUid, _taskID], + _taskID, + _groupID, + _requesterUid + ]]; + + _result set ["success", true]; + _result set ["message", "Task acknowledged."]; + _result set ["assignment", _assignment]; + _result + }], + ["declineTask", compileFinal { + params [["_requesterUid", "", [""]], ["_taskID", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to decline task."], + ["assignment", createHashMap] + ]; + + private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; + private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]); + if (_assignment isEqualTo createHashMap) exitWith { + _result set ["message", "Task is not assigned."]; + _result + }; + + private _groupID = _assignment getOrDefault ["groupId", ""]; + if !(_self call ["isGroupLeader", [_requesterUid, _groupID]]) exitWith { + _result set ["message", "Only the assigned group leader can decline this task."]; + _result + }; + + _assignment set ["state", "declined"]; + _assignment set ["declinedAt", serverTime]; + _assignmentRegistry set [_taskID, _assignment]; + _self set ["assignmentRegistry", _assignmentRegistry]; + + _self call ["appendActivity", [ + "task_declined", + format ["%1 declined %2.", _requesterUid, _taskID], + _taskID, + _groupID, + _requesterUid + ]]; + + _result set ["success", true]; + _result set ["message", "Task declined."]; + _result set ["assignment", _assignment]; + _result + }], + ["updateGroupStatus", compileFinal { + params [["_requesterUid", "", [""]], ["_groupID", "", [""]], ["_status", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to update group status."], + ["group", createHashMap] + ]; + + private _finalStatus = toLowerANSI _status; + if !(_finalStatus in (_self getOrDefault ["validStatuses", []])) exitWith { + _result set ["message", "Invalid group status."]; + _result + }; + + private _isAuthorized = (_self call ["isGroupLeader", [_requesterUid, _groupID]]) || { _self call ["canDispatch", [_requesterUid]] }; + if !_isAuthorized exitWith { + _result set ["message", "You are not authorized to update that group."]; + _result + }; + + private _groupRegistry = _self call ["syncGroups", []]; + private _groupRecord = +(_groupRegistry getOrDefault [_groupID, createHashMap]); + if (_groupRecord isEqualTo createHashMap) exitWith { + _result set ["message", "Group could not be resolved."]; + _result + }; + + _groupRecord set ["status", _finalStatus]; + _groupRecord set ["lastUpdate", serverTime]; + _groupRegistry set [_groupID, _groupRecord]; + _self set ["groupRegistry", _groupRegistry]; + + _self call ["appendActivity", [ + "group_status", + format ["%1 updated %2 to %3.", _requesterUid, _groupRecord getOrDefault ["callsign", _groupID], _finalStatus], + "", + _groupID, + _requesterUid + ]]; + + _result set ["success", true]; + _result set ["message", "Group status updated."]; + _result set ["group", _groupRecord]; + _result + }] +]; + +GVAR(CadStore) = createHashMapObject [GVAR(CadStoreBaseClass)]; +GVAR(CadStore) diff --git a/arma/server/addons/cad/script_component.hpp b/arma/server/addons/cad/script_component.hpp new file mode 100644 index 0000000..e5f508d --- /dev/null +++ b/arma/server/addons/cad/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT cad +#define COMPONENT_BEAUTIFIED CAD +#include "\forge\forge_server\addons\main\script_mod.hpp" + +// #define DEBUG_MODE_FULL +// #define DISABLE_COMPILE_CACHE +// #define ENABLE_PERFORMANCE_COUNTERS + +#include "\forge\forge_server\addons\main\script_macros.hpp"