Refactor CAD sidepanel to hydrate board data

- Replace task-only refresh flow with hydrate/assignment/group events
- Add contracts, groups, and activity tabs to the client sidepanel
- Introduce server-side CAD store and request handlers
This commit is contained in:
Jacob Schmidt 2026-03-29 22:17:07 -05:00
parent 45a4f7460a
commit 0e9d0d3dc4
18 changed files with 1114 additions and 75 deletions

View File

@ -1,7 +1,6 @@
class Extended_PreInit_EventHandlers { class Extended_PreInit_EventHandlers {
class ADDON { class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit)); init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_preInitClient));
}; };
}; };

View File

@ -7,18 +7,20 @@ if (isNil QGVAR(CADUIBridge)) then { call FUNC(initUIBridge); };
call FUNC(openUI); call FUNC(openUI);
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
[QGVAR(responseTaskCatalog), { [QGVAR(responseHydrateCad), {
params [["_entries", [], [[]]]]; params [["_payload", createHashMap, [createHashMap]]];
if !(isNil QGVAR(CADRepository)) then { GVAR(CADUIBridge) call ["handleHydrateResponse", [_payload]];
GVAR(CADRepository) call ["setTaskCatalog", [_entries]];
};
GVAR(CADUIBridge) call ["refreshTaskCatalog", []];
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
[QGVAR(responseTaskAccept), { [QGVAR(responseCadAssignment), {
params [["_result", createHashMap, [createHashMap]]]; 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); }] call CFUNC(addEventHandler);

View File

@ -35,16 +35,46 @@ switch (_event) do {
case "cad::ready": { case "cad::ready": {
GVAR(CADUIBridge) call ["handleReady", [_control, _data]]; GVAR(CADUIBridge) call ["handleReady", [_control, _data]];
}; };
case "cad::tasks::refresh": { case "cad::refresh": {
GVAR(CADUIBridge) call ["requestTaskCatalog", []]; 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 = ""; private _taskID = "";
if (_data isEqualType createHashMap) then { if (_data isEqualType createHashMap) then {
_taskID = _data getOrDefault ["taskID", ""]; _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": { case "map::zoomIn": {
private _mapCtrl = uiNamespace getVariable [QGVAR(MapCtrl), controlNull]; private _mapCtrl = uiNamespace getVariable [QGVAR(MapCtrl), controlNull];

View File

@ -25,21 +25,33 @@ GVAR(CADRepository) = createHashMapObject [[
["#create", compileFinal { ["#create", compileFinal {
_self set ["isLoaded", true]; _self set ["isLoaded", true];
_self set ["isOpen", false]; _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]]]; params [["_bridge", createHashMap, [createHashMap]]];
if (_bridge isEqualTo createHashMap) exitWith { false }; if (_bridge isEqualTo createHashMap) exitWith { false };
_bridge call ["sendEvent", ["cad::tasks::hydrate", createHashMapFromArray [ _bridge call ["sendEvent", ["cad::hydrate", createHashMapFromArray [
["tasks", +(_self getOrDefault ["taskCatalog", []])] ["groups", +(_self getOrDefault ["groups", []])],
["contracts", +(_self getOrDefault ["contracts", []])],
["assignments", +(_self getOrDefault ["assignments", []])],
["activity", +(_self getOrDefault ["activity", []])],
["session", +(_self getOrDefault ["session", createHashMap])]
]]] ]]]
}], }],
["setTaskCatalog", compileFinal { ["setHydratePayload", compileFinal {
params [["_entries", [], [[]]]]; 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 true
}], }],
["setOpen", compileFinal { ["setOpen", compileFinal {

View File

@ -7,7 +7,7 @@
* Public: No * Public: No
* *
* Description: * 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: * Arguments:
* None * None
@ -50,33 +50,73 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [
_screen call ["markReady", [true]]; _screen call ["markReady", [true]];
_self call ["flushPendingEvents", []]; _self call ["flushPendingEvents", []];
_self call ["requestTaskCatalog", []]; _self call ["requestHydrate", []];
_self call ["refreshTaskCatalog", []]; _self call ["refreshHydrate", []];
true true
}], }],
["requestTaskCatalog", compileFinal { ["requestHydrate", compileFinal {
[SRPC(task,requestTaskCatalog), [getPlayerUID player]] call CFUNC(serverEvent); [SRPC(cad,requestHydrateCad), [getPlayerUID player]] call CFUNC(serverEvent);
true 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", "", [""]]]; params [["_taskID", "", [""]]];
if (_taskID isEqualTo "") exitWith { false }; if (_taskID isEqualTo "") exitWith { false };
[SRPC(task,requestAcceptTask), [getPlayerUID player, _taskID]] call CFUNC(serverEvent); [SRPC(cad,requestAcknowledgeCadTask), [getPlayerUID player, _taskID]] call CFUNC(serverEvent);
true true
}], }],
["refreshTaskCatalog", compileFinal { ["requestDeclineTask", compileFinal {
if (isNil QGVAR(CADRepository)) exitWith { false }; params [["_taskID", "", [""]]];
GVAR(CADRepository) call ["pushTaskCatalog", [_self]]
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]]]; 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."]], ["message", _result getOrDefault ["message", "Task request processed."]],
["success", _result getOrDefault ["success", false]] ["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]]
]]]
}] }]
]; ];

View File

@ -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} 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}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<!doctype html><html><head><meta charset="UTF-8"></head><body><div class="panel-header"><h3>CAD System</h3></div><div class="panel-content"><div class="task-toolbar"><button id="refreshTasksBtn" type="button">Refresh Tasks</button></div><div id="taskStatusMessage" class="task-status-message"></div><div id="taskList" class="task-list"><div class="placeholder-message"><p>Loading available tasks...</p></div></div></div><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const d=document.createElement("style");d.textContent=e,document.head.appendChild(d)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,d)=>e.then(()=>d.endsWith(".css")?this.loadCSS(d):d.endsWith(".js")?this.loadJS(d):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.js"]).catch(e=>console.error("[SIDEPANEL] Load error:",e))</script></body></html> <!doctype html><html><head><meta charset="UTF-8"></head><body><div class="panel-header"><h3>CAD System</h3></div><div class="panel-content"><div class="task-toolbar"><button id="refreshCadBtn" type="button">Refresh Board</button></div><div id="cadStatusMessage" class="task-status-message"></div><div class="cad-tabs" role="tablist" aria-label="CAD Sections"><button id="tabContractsBtn" class="cad-tab is-active" type="button" data-tab="contracts">Contracts</button> <button id="tabGroupsBtn" class="cad-tab" type="button" data-tab="groups">Groups</button> <button id="tabActivityBtn" class="cad-tab" type="button" data-tab="activity">Activity</button></div><div class="cad-tab-panels"><div id="contractsPanel" class="cad-section is-active" data-panel="contracts"><div class="cad-section-header">Contracts</div><div id="taskList" class="task-list"><div class="placeholder-message"><p>Loading contracts...</p></div></div></div><div id="groupsPanel" class="cad-section" data-panel="groups"><div class="cad-section-header">Groups</div><div id="groupList" class="task-list"><div class="placeholder-message"><p>Loading groups...</p></div></div></div><div id="activityPanel" class="cad-section" data-panel="activity"><div class="cad-section-header">Activity</div><div id="activityList" class="task-list"><div class="placeholder-message"><p>No recent activity.</p></div></div></div></div></div><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const d=document.createElement("style");d.textContent=e,document.head.appendChild(d)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,d)=>e.then(()=>d.endsWith(".css")?this.loadCSS(d):d.endsWith(".js")?this.loadJS(d):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.js"]).catch(e=>console.error("[SIDEPANEL] Load error:",e))</script></body></html>

View File

@ -9,14 +9,67 @@
</div> </div>
<div class="panel-content"> <div class="panel-content">
<div class="task-toolbar"> <div class="task-toolbar">
<button id="refreshTasksBtn" type="button"> <button id="refreshCadBtn" type="button">Refresh Board</button>
Refresh Tasks </div>
<div id="cadStatusMessage" class="task-status-message"></div>
<div class="cad-tabs" role="tablist" aria-label="CAD Sections">
<button
id="tabContractsBtn"
class="cad-tab is-active"
type="button"
data-tab="contracts"
>
Contracts
</button>
<button
id="tabGroupsBtn"
class="cad-tab"
type="button"
data-tab="groups"
>
Groups
</button>
<button
id="tabActivityBtn"
class="cad-tab"
type="button"
data-tab="activity"
>
Activity
</button> </button>
</div> </div>
<div id="taskStatusMessage" class="task-status-message"></div> <div class="cad-tab-panels">
<div id="taskList" class="task-list"> <div
<div class="placeholder-message"> id="contractsPanel"
<p>Loading available tasks...</p> class="cad-section is-active"
data-panel="contracts"
>
<div class="cad-section-header">Contracts</div>
<div id="taskList" class="task-list">
<div class="placeholder-message">
<p>Loading contracts...</p>
</div>
</div>
</div>
<div id="groupsPanel" class="cad-section" data-panel="groups">
<div class="cad-section-header">Groups</div>
<div id="groupList" class="task-list">
<div class="placeholder-message">
<p>Loading groups...</p>
</div>
</div>
</div>
<div
id="activityPanel"
class="cad-section"
data-panel="activity"
>
<div class="cad-section-header">Activity</div>
<div id="activityList" class="task-list">
<div class="placeholder-message">
<p>No recent activity.</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,94 +1,300 @@
window.cadTasks = { window.cadTasks = {
tasks: [], contracts: [],
groups: [],
activity: [],
session: {},
activeTab: "contracts",
statuses: [
"available",
"en_route",
"on_task",
"holding",
"danger",
"refit",
"offline",
],
init() { init() {
const refreshBtn = document.getElementById("refreshTasksBtn"); const refreshBtn = document.getElementById("refreshCadBtn");
if (refreshBtn) { if (refreshBtn) {
refreshBtn.addEventListener("click", () => this.refresh()); refreshBtn.addEventListener("click", () => this.refresh());
} }
window.ForgeBridge.on("cad::tasks::hydrate", (payload) => { document.querySelectorAll(".cad-tab").forEach((tab) => {
this.setTasks(payload.tasks || []); tab.addEventListener("click", () => {
this.setActiveTab(tab.dataset.tab || "contracts");
});
}); });
window.ForgeBridge.on("cad::tasks::accept::response", (payload) => { window.ForgeBridge.on("cad::hydrate", (payload) => {
this.handleAcceptResponse(!!payload.success, payload.message || ""); 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 }); window.ForgeBridge.ready({ loaded: true });
}, },
setTasks(tasks) { setActiveTab(tabName) {
this.tasks = Array.isArray(tasks) ? tasks : []; this.activeTab = tabName || "contracts";
const statusEl = document.getElementById("taskStatusMessage");
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 ( if (
statusEl && statusEl &&
(!statusEl.dataset.type || statusEl.dataset.type === "info") (!statusEl.dataset.type || statusEl.dataset.type === "info")
) { ) {
this.setStatus("", ""); this.setStatus("", "");
} }
this.render(); this.render();
}, },
setStatus(message, type) { setStatus(message, type) {
const statusEl = document.getElementById("taskStatusMessage"); const statusEl = document.getElementById("cadStatusMessage");
if (!statusEl) { if (!statusEl) {
return; return;
} }
statusEl.textContent = message || ""; statusEl.textContent = message || "";
statusEl.dataset.type = type || "info"; statusEl.dataset.type = type || "";
}, },
handleAcceptResponse(success, message) { handleServerResponse(success, message) {
this.setStatus( this.setStatus(
message || (success ? "Task accepted." : "Unable to accept task."), message ||
(success ? "CAD update succeeded." : "CAD update failed."),
success ? "success" : "error", success ? "success" : "error",
); );
}, },
refresh() { refresh() {
this.setStatus("Refreshing tasks...", "info"); this.setStatus("Refreshing board...", "info");
window.mapUI.sendEvent("cad::tasks::refresh", {}); window.mapUI.sendEvent("cad::refresh", {});
}, },
acceptTask(taskID) { assignTask(taskID) {
this.setStatus("Submitting acceptance...", "info"); const selector = document.getElementById(`assign-group-${taskID}`);
window.mapUI.sendEvent("cad::tasks::accept", { taskID: 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"); const listEl = document.getElementById("taskList");
if (!listEl) { if (!listEl) {
return; return;
} }
if (!this.tasks.length) { if (!this.contracts.length) {
listEl.innerHTML = listEl.innerHTML =
'<div class="placeholder-message"><p>No active tasks are available.</p></div>'; '<div class="placeholder-message"><p>No active contracts are available.</p></div>';
return; return;
} }
listEl.innerHTML = this.tasks const currentGroupId = this.getPlayerGroupId();
listEl.innerHTML = this.contracts
.map((task) => { .map((task) => {
const taskId = task.taskId || task.taskID || "";
const position = Array.isArray(task.position) const position = Array.isArray(task.position)
? task.position ? task.position
: [0, 0, 0]; : [0, 0, 0];
const accepted = !!task.accepted; const assignedGroupId = task.assignedGroupId || "";
const ownerLabel = accepted const assignmentState = task.assignmentState || "unassigned";
? `Assigned: ${task.orgID || "Unknown"}` const assignedGroup = this.groups.find(
: "Available"; (group) => group.groupId === assignedGroupId,
);
const isAssignedToLeader =
this.isLeader() && assignedGroupId === currentGroupId;
const groupOptions = this.groups
.map(
(group) =>
`<option value="${group.groupId}">${group.callsign || group.groupId}</option>`,
)
.join("");
return ` return `
<div class="task-card" data-task-id="${task.taskID}"> <div class="task-card" data-task-id="${taskId}">
<div class="task-card-header"> <div class="task-card-header">
<strong>${task.title || task.taskID}</strong> <strong>${task.title || taskId}</strong>
<span class="task-type">${task.type || "task"}</span> <span class="task-type">${task.type || "task"}</span>
</div> </div>
<p class="task-description">${task.description || ""}</p> <p class="task-description">${task.description || ""}</p>
<div class="task-meta"> <div class="task-meta">
<span>${ownerLabel}</span> <span>${assignmentState === "unassigned" ? "Available" : `${assignmentState}: ${assignedGroup ? assignedGroup.callsign : assignedGroupId}`}</span>
<span>X: ${Math.round(position[0] || 0)} Y: ${Math.round(position[1] || 0)}</span> <span>X: ${Math.round(position[0] || 0)} Y: ${Math.round(position[1] || 0)}</span>
</div> </div>
<button type="button" class="task-accept-btn" ${accepted ? "disabled" : ""} onclick="window.cadTasks.acceptTask('${task.taskID}')">${accepted ? "Accepted" : "Accept"}</button> ${
this.canDispatch()
? `<div class="task-action-stack">
<select id="assign-group-${taskId}" class="cad-select">
<option value="">Assign to group</option>
${groupOptions}
</select>
<button type="button" class="task-accept-btn" onclick="window.cadTasks.assignTask('${taskId}')">Assign Contract</button>
</div>`
: ""
}
${
isAssignedToLeader && assignmentState === "assigned"
? `<div class="task-action-row">
<button type="button" class="task-accept-btn" onclick="window.cadTasks.acknowledgeTask('${taskId}')">Acknowledge</button>
<button type="button" class="task-secondary-btn" onclick="window.cadTasks.declineTask('${taskId}')">Decline</button>
</div>`
: ""
}
</div> </div>
`; `;
}) })
.join(""); .join("");
}, },
renderGroups() {
const listEl = document.getElementById("groupList");
if (!listEl) {
return;
}
if (!this.groups.length) {
listEl.innerHTML =
'<div class="placeholder-message"><p>No active groups are available.</p></div>';
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) =>
`<option value="${status}" ${status === group.status ? "selected" : ""}>${status.replaceAll("_", " ")}</option>`,
)
.join("");
return `
<div class="task-card" data-group-id="${group.groupId}">
<div class="task-card-header">
<strong>${group.callsign || group.groupId}</strong>
<span class="task-type">${group.role || "group"}</span>
</div>
<div class="task-meta">
<span>Leader: ${group.leaderName || "Unknown"}</span>
<span>Status: ${group.status || "unknown"}</span>
</div>
<div class="task-meta">
<span>Org: ${group.orgId || "default"}</span>
<span>Task: ${group.currentTaskId || "None"}</span>
</div>
${
canUpdate
? `<div class="task-action-stack">
<select id="group-status-${group.groupId}" class="cad-select">
${statusOptions}
</select>
<button type="button" class="task-accept-btn" onclick="window.cadTasks.updateGroupStatus('${group.groupId}', document.getElementById('group-status-${group.groupId}').value)">Update Status</button>
</div>`
: ""
}
</div>
`;
})
.join("");
},
renderActivity() {
const listEl = document.getElementById("activityList");
if (!listEl) {
return;
}
if (!this.activity.length) {
listEl.innerHTML =
'<div class="placeholder-message"><p>No recent activity.</p></div>';
return;
}
listEl.innerHTML = this.activity
.slice()
.reverse()
.slice(0, 8)
.map(
(entry) => `
<div class="task-card">
<div class="task-card-header">
<strong>${entry.type || "activity"}</strong>
<span class="task-type">${Math.round(entry.timestamp || 0)}s</span>
</div>
<p class="task-description">${entry.message || ""}</p>
</div>
`,
)
.join("");
},
render() {
this.renderContracts();
this.renderGroups();
this.renderActivity();
this.setActiveTab(this.activeTab);
},
}; };
window.cadTasks.init(); window.cadTasks.init();

View File

@ -59,23 +59,82 @@ body {
margin-bottom: 10px; 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-toolbar button,
.task-accept-btn { .task-accept-btn,
.task-secondary-btn,
.cad-select {
width: 100%; width: 100%;
padding: 8px 10px; padding: 8px 10px;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(30, 37, 43, 0.9); background: rgba(30, 37, 43, 0.9);
color: #f3f6f9; color: #f3f6f9;
}
.task-toolbar button,
.task-accept-btn,
.task-secondary-btn {
cursor: pointer; cursor: pointer;
} }
.task-toolbar button:hover, .task-toolbar button:hover,
.task-accept-btn:hover { .task-accept-btn:hover,
.task-secondary-btn:hover {
background: rgba(46, 57, 66, 0.95); background: rgba(46, 57, 66, 0.95);
} }
.task-toolbar button:disabled, .task-toolbar button:disabled,
.task-accept-btn:disabled { .task-accept-btn:disabled,
.task-secondary-btn:disabled {
opacity: 0.55; opacity: 0.55;
cursor: default; cursor: default;
} }
@ -101,6 +160,17 @@ body {
gap: 10px; gap: 10px;
} }
.task-action-stack,
.task-action-row {
display: flex;
flex-direction: column;
gap: 8px;
}
.task-action-row {
flex-direction: row;
}
.task-card { .task-card {
padding: 10px; padding: 10px;
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
@ -134,3 +204,7 @@ body {
font-size: 11px; font-size: 11px;
opacity: 0.8; opacity: 0.8;
} }
.task-secondary-btn {
background: rgba(60, 48, 45, 0.92);
}

View File

@ -0,0 +1 @@
forge\forge_server\addons\cad

View File

@ -0,0 +1,5 @@
class Extended_PreInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
};
};

View File

@ -0,0 +1 @@
PREP(initCadStore);

View File

@ -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);

View File

@ -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"

View File

@ -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)

View File

@ -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"