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 =
- '
';
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.description || ""}
- ${ownerLabel}
+ ${assignmentState === "unassigned" ? "Available" : `${assignmentState}: ${assignedGroup ? assignedGroup.callsign : assignedGroupId}`}
X: ${Math.round(position[0] || 0)} Y: ${Math.round(position[1] || 0)}
-
${accepted ? "Accepted" : "Accept"}
+ ${
+ this.canDispatch()
+ ? `
+
+ Assign to group
+ ${groupOptions}
+
+ Assign Contract
+
`
+ : ""
+ }
+ ${
+ isAssignedToLeader && assignmentState === "assigned"
+ ? `
+ Acknowledge
+ Decline
+
`
+ : ""
+ }
`;
})
.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) =>
+ `
${status.replaceAll("_", " ")} `,
+ )
+ .join("");
+
+ return `
+
+
+
+ Leader: ${group.leaderName || "Unknown"}
+ Status: ${group.status || "unknown"}
+
+
+ Org: ${group.orgId || "default"}
+ Task: ${group.currentTaskId || "None"}
+
+ ${
+ canUpdate
+ ? `
+
+ ${statusOptions}
+
+ Update Status
+
`
+ : ""
+ }
+
+ `;
+ })
+ .join("");
+ },
+ renderActivity() {
+ const listEl = document.getElementById("activityList");
+ if (!listEl) {
+ return;
+ }
+
+ if (!this.activity.length) {
+ listEl.innerHTML =
+ '
';
+ return;
+ }
+
+ listEl.innerHTML = this.activity
+ .slice()
+ .reverse()
+ .slice(0, 8)
+ .map(
+ (entry) => `
+
+
+
${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"