Handle bank account sync in UI bridge

- Route bank sync payloads through the client bridge
- Refresh account state without rebuilding the full session
- Split CAD dispatcher UI into modular source files
This commit is contained in:
Jacob Schmidt 2026-04-02 09:10:12 -05:00
parent 4ea7cf7d05
commit 53bc8db7d0
77 changed files with 6026 additions and 1888 deletions

View File

@ -12,7 +12,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
GVAR(BankRepository) call ["markLoaded", []];
if !(isNil QGVAR(BankUIBridge)) then {
GVAR(BankUIBridge) call ["refreshSession", []];
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
};
}] call CFUNC(addEventHandler);
@ -21,7 +21,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
GVAR(BankRepository) call ["markLoaded", []];
if !(isNil QGVAR(BankUIBridge)) then {
GVAR(BankUIBridge) call ["refreshSession", []];
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
};
}] call CFUNC(addEventHandler);

View File

@ -70,6 +70,13 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
_self call ["sendEvent", [_event, _data, _self call ["getActiveBrowserControl", []]]]
}],
["handleAccountSyncResponse", compileFinal {
params [["_data", createHashMap, [createHashMap]]];
if !(_self call ["hasOpenScreen", []]) exitWith { false };
_self call ["sendEvent", ["bank::sync", _data, _self call ["getActiveBrowserControl", []]]]
}],
["handleNoticeResponse", compileFinal {
params [["_type", "error", [""]], ["_message", "", [""]]];

File diff suppressed because one or more lines are too long

View File

@ -12,8 +12,13 @@
store.hydrateFromPayload(payloadData);
}
function syncAccount(payloadData) {
BankApp.data.applyAccountPatch(payloadData);
store.syncAccountPatch();
}
bridge.on("bank::hydrate", hydrate);
bridge.on("bank::sync", hydrate);
bridge.on("bank::sync", syncAccount);
bridge.on("bank::notice", (payloadData) => {
store.finishAction();
if (BankApp.actions) {

View File

@ -30,6 +30,13 @@
BankApp.data = {
account: Object.assign({}, defaultAccount),
session: Object.assign({}, defaultSession),
applyAccountPatch(patch) {
const nextAccount = Object.assign({}, this.account, patch || {});
replaceObject(
this.account,
Object.assign({}, defaultAccount, nextAccount),
);
},
applyHydratePayload(payload) {
replaceObject(
this.session,

View File

@ -60,6 +60,11 @@
this.setAtmView("dashboard");
}
syncAccountPatch() {
this.setPendingAction("");
this.setAccountVersion(this.getAccountVersion() + 1);
}
resetAtm() {
this.setEnteredPin("");
this.setCustomAmount("");

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -311,6 +311,13 @@
</div>
</div>
<div class="dispatch-modal-actions">
<button
id="dispatcherRequestConvertBtn"
type="button"
class="dispatch-btn dispatch-btn-secondary"
>
Convert to Order
</button>
<button
id="dispatcherRequestModalDoneBtn"
type="button"

View File

@ -1,822 +0,0 @@
window.cadDispatcher = {
contracts: [],
requests: [],
groups: [],
activity: [],
session: {},
editingGroupId: "",
viewingRequestId: "",
statuses: [
"available",
"en_route",
"on_task",
"holding",
"danger",
"unavailable",
],
roles: ["infantry", "recon", "armor", "air", "logistics", "support"],
init() {
document
.getElementById("dispatcherCreateOrderBtn")
.addEventListener("click", () => {
this.openOrderModal();
});
document
.getElementById("dispatcherGroupModalCloseBtn")
.addEventListener("click", () => {
this.closeGroupModal();
});
document
.getElementById("dispatcherGroupModalSaveBtn")
.addEventListener("click", () => {
this.applyGroupUpdates();
});
document
.querySelector("#dispatcherGroupModal .dispatch-modal-backdrop")
.addEventListener("click", () => {
this.closeGroupModal();
});
document
.getElementById("dispatcherOrderModalCloseBtn")
.addEventListener("click", () => {
this.closeOrderModal();
});
document
.getElementById("dispatcherOrderModalSaveBtn")
.addEventListener("click", () => {
this.createDispatchOrder();
});
document
.querySelector("#dispatcherOrderModal .dispatch-modal-backdrop")
.addEventListener("click", () => {
this.closeOrderModal();
});
document
.getElementById("dispatcherRequestModalCloseBtn")
.addEventListener("click", () => {
this.closeRequestModal();
});
document
.getElementById("dispatcherRequestModalDoneBtn")
.addEventListener("click", () => {
this.closeRequestModal();
});
document
.querySelector("#dispatcherRequestModal .dispatch-modal-backdrop")
.addEventListener("click", () => {
this.closeRequestModal();
});
window.mapUI.sendEvent("cad::dispatcher::ready", {});
},
receiveHydrate(payload) {
this.contracts = Array.isArray(payload.contracts)
? payload.contracts
: [];
this.requests = Array.isArray(payload.requests) ? payload.requests : [];
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("dispatcherStatusMessage");
if (
statusEl &&
(!statusEl.dataset.type || statusEl.dataset.type === "info")
) {
this.setStatus("", "");
}
this.syncOpenModal();
this.syncOrderModal();
this.syncRequestModal();
this.render();
},
setStatus(message, type) {
const statusEl = document.getElementById("dispatcherStatusMessage");
if (!statusEl) {
return;
}
statusEl.textContent = message || "";
statusEl.dataset.type = type || "";
},
getDangerGroups() {
return this.groups.filter((group) => (group.status || "") === "danger");
},
getSupportAlertRequests() {
return this.requests.filter((request) =>
["medevac_9line", "fire_support", "air_support"].includes(
request.type || "",
),
);
},
buildSupportAlertMessage() {
const alertRequests = this.getSupportAlertRequests();
if (!alertRequests.length) {
return "";
}
const labels = alertRequests.map((request) => {
const groupLabel =
request.groupCallsign || request.groupId || "Unknown Group";
const typeLabel = this.getRequestTypeLabel(
request.type || "request",
);
return `${groupLabel} ${typeLabel}`;
});
return `Support request alert: ${labels.join(", ")}`;
},
getSortedGroups() {
return this.groups.slice().sort((left, right) => {
const leftDanger = (left.status || "") === "danger" ? 0 : 1;
const rightDanger = (right.status || "") === "danger" ? 0 : 1;
if (leftDanger !== rightDanger) {
return leftDanger - rightDanger;
}
const leftCallsign = left.callsign || left.groupId || "";
const rightCallsign = right.callsign || right.groupId || "";
return leftCallsign.localeCompare(rightCallsign);
});
},
isDispatchOrder(entry) {
return (
!!entry.isDispatchOrder || (entry.type || "") === "dispatch_order"
);
},
formatTypeLabel(entry) {
const typeLabel = (entry.type || "task").replaceAll("_", " ");
return this.isDispatchOrder(entry) ? "dispatch order" : typeLabel;
},
getRequestTypeLabel(typeID) {
switch (typeID) {
case "medevac_9line":
return "9-Line MEDEVAC";
case "ace_lace":
return "ACE/LACE";
case "fire_support":
return "Fire Support";
case "air_support":
return "Air Support";
case "logreq":
return "LOGREQ";
default:
return (typeID || "request").replaceAll("_", " ");
}
},
buildGroupOptions(selectedGroupID) {
return this.getSortedGroups()
.map((group) => {
const groupID = group.groupId || "";
return `<option value="${groupID}" ${groupID === selectedGroupID ? "selected" : ""}>${group.callsign || groupID}</option>`;
})
.join("");
},
updateDangerAlert() {
const alertEl = document.getElementById("dispatcherDangerAlert");
if (!alertEl) {
return;
}
const dangerGroups = this.getDangerGroups();
if (!dangerGroups.length) {
alertEl.textContent = "";
alertEl.classList.add("is-hidden");
return;
}
const callsigns = dangerGroups.map(
(group) => group.callsign || group.groupId || "Unknown Group",
);
alertEl.textContent = `Danger alert active: ${callsigns.join(", ")}`;
alertEl.classList.remove("is-hidden");
},
updateRequestAlert() {
const alertEl = document.getElementById("dispatcherRequestAlert");
if (!alertEl) {
return;
}
const alertMessage = this.buildSupportAlertMessage();
if (!alertMessage) {
alertEl.textContent = "";
alertEl.classList.add("is-hidden");
return;
}
alertEl.textContent = alertMessage;
alertEl.classList.remove("is-hidden");
},
openOrderModal() {
this.populateOrderModal();
document
.getElementById("dispatcherOrderModal")
.classList.remove("is-hidden");
},
closeOrderModal() {
document.getElementById("dispatcherOrderNoteInput").value = "";
document.getElementById("dispatcherOrderPrioritySelect").value =
"priority";
document
.getElementById("dispatcherOrderModal")
.classList.add("is-hidden");
},
openRequestModal(requestID) {
const request = this.requests.find(
(entry) => entry.requestId === requestID,
);
if (!request) {
return;
}
this.viewingRequestId = requestID;
this.populateRequestModal(request);
document
.getElementById("dispatcherRequestModal")
.classList.remove("is-hidden");
},
closeRequestModal() {
this.viewingRequestId = "";
document
.getElementById("dispatcherRequestModal")
.classList.add("is-hidden");
},
syncRequestModal() {
if (!this.viewingRequestId) {
return;
}
const request = this.requests.find(
(entry) => entry.requestId === this.viewingRequestId,
);
if (!request) {
this.closeRequestModal();
return;
}
this.populateRequestModal(request);
},
formatRequestFieldLabel(fieldID) {
return (fieldID || "field")
.replaceAll("_", " ")
.replace(/\b\w/g, (character) => character.toUpperCase());
},
formatRequestFieldValue(value) {
if (Array.isArray(value)) {
return value.join(", ");
}
if (value && typeof value === "object") {
return JSON.stringify(value);
}
const text = String(value ?? "").trim();
return text || "Not provided";
},
populateRequestModal(request) {
const fields =
request.fields && typeof request.fields === "object"
? Object.entries(request.fields)
: [];
const fieldsHTML = fields.length
? fields
.map(
([fieldID, value]) => `
<div class="dispatch-detail-row">
<span class="dispatch-detail-label">${this.formatRequestFieldLabel(fieldID)}</span>
<span class="dispatch-detail-value">${this.formatRequestFieldValue(value)}</span>
</div>
`,
)
.join("")
: '<div class="placeholder-message"><p>No submitted fields.</p></div>';
document.getElementById("dispatcherRequestTitle").textContent =
request.title || request.requestId || "Support Request";
document.getElementById("dispatcherRequestPriority").textContent = (
request.priority || "priority"
).replaceAll("_", " ");
document.getElementById("dispatcherRequestGroup").textContent =
request.groupCallsign || request.groupId || "Unknown";
document.getElementById("dispatcherRequestType").textContent =
this.getRequestTypeLabel(request.type || "request");
document.getElementById("dispatcherRequestSummary").textContent =
request.summary || "No summary provided.";
document.getElementById("dispatcherRequestFields").innerHTML =
fieldsHTML;
},
populateOrderModal(selectedAssigneeID, selectedTargetID) {
const sortedGroups = this.getSortedGroups();
const assigneeSelect = document.getElementById(
"dispatcherOrderAssigneeSelect",
);
const targetSelect = document.getElementById(
"dispatcherOrderTargetSelect",
);
if (!assigneeSelect || !targetSelect) {
return;
}
const fallbackAssignee =
selectedAssigneeID || sortedGroups[0]?.groupId || "";
const fallbackTarget =
selectedTargetID ||
sortedGroups.find(
(group) => (group.groupId || "") !== fallbackAssignee,
)?.groupId ||
sortedGroups[0]?.groupId ||
"";
assigneeSelect.innerHTML = this.buildGroupOptions(fallbackAssignee);
targetSelect.innerHTML = this.buildGroupOptions(fallbackTarget);
},
syncOrderModal() {
const modalEl = document.getElementById("dispatcherOrderModal");
if (!modalEl || modalEl.classList.contains("is-hidden")) {
return;
}
this.populateOrderModal(
document.getElementById("dispatcherOrderAssigneeSelect")?.value ||
"",
document.getElementById("dispatcherOrderTargetSelect")?.value || "",
);
},
createDispatchOrder() {
const assigneeGroupID = document.getElementById(
"dispatcherOrderAssigneeSelect",
).value;
const targetGroupID = document.getElementById(
"dispatcherOrderTargetSelect",
).value;
const priority = document.getElementById(
"dispatcherOrderPrioritySelect",
).value;
const note = document.getElementById("dispatcherOrderNoteInput").value;
if (!assigneeGroupID || !targetGroupID) {
this.setStatus(
"Select both an assignee and a target group.",
"error",
);
return;
}
if (assigneeGroupID === targetGroupID) {
this.setStatus(
"Assignee and target groups must be different.",
"error",
);
return;
}
this.setStatus("Creating dispatch order...", "info");
window.mapUI.sendEvent("cad::dispatchOrder::create", {
assigneeGroupID: assigneeGroupID,
targetGroupID: targetGroupID,
note: note.trim(),
priority: priority,
});
this.closeOrderModal();
},
assignTask(taskID) {
const selector = document.getElementById(
`dispatcher-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: "",
});
},
openGroupModal(groupID) {
const group = this.groups.find((entry) => entry.groupId === groupID);
if (!group) {
return;
}
this.editingGroupId = groupID;
document.getElementById("dispatcherModalGroupCallsign").textContent =
group.callsign || group.groupId || "Unknown";
document.getElementById("dispatcherModalGroupLeader").textContent =
group.leaderName || "Unknown";
document.getElementById("dispatcherModalGroupTask").textContent =
group.currentTaskId || "None";
document.getElementById("dispatcherModalGroupOrg").textContent =
group.orgId || "default";
document.getElementById("dispatcherModalRoleSelect").innerHTML =
this.roles
.map(
(role) =>
`<option value="${role}" ${role === group.role ? "selected" : ""}>${role.replaceAll("_", " ")}</option>`,
)
.join("");
document.getElementById("dispatcherModalStatusSelect").innerHTML =
this.statuses
.map(
(status) =>
`<option value="${status}" ${status === group.status ? "selected" : ""}>${status.replaceAll("_", " ")}</option>`,
)
.join("");
document
.getElementById("dispatcherGroupModal")
.classList.remove("is-hidden");
},
closeGroupModal() {
this.editingGroupId = "";
document
.getElementById("dispatcherGroupModal")
.classList.add("is-hidden");
},
syncOpenModal() {
if (!this.editingGroupId) {
return;
}
const group = this.groups.find(
(entry) => entry.groupId === this.editingGroupId,
);
if (!group) {
this.closeGroupModal();
return;
}
document.getElementById("dispatcherModalGroupCallsign").textContent =
group.callsign || group.groupId || "Unknown";
document.getElementById("dispatcherModalGroupLeader").textContent =
group.leaderName || "Unknown";
document.getElementById("dispatcherModalGroupTask").textContent =
group.currentTaskId || "None";
document.getElementById("dispatcherModalGroupOrg").textContent =
group.orgId || "default";
},
applyGroupUpdates() {
if (!this.editingGroupId) {
return;
}
const group = this.groups.find(
(entry) => entry.groupId === this.editingGroupId,
);
if (!group) {
this.closeGroupModal();
return;
}
const roleValue = document.getElementById(
"dispatcherModalRoleSelect",
).value;
const statusValue = document.getElementById(
"dispatcherModalStatusSelect",
).value;
const nextRole =
roleValue && roleValue !== (group.role || "") ? roleValue : "";
const nextStatus =
statusValue && statusValue !== (group.status || "")
? statusValue
: "";
const hasChanges = nextRole || nextStatus;
if (!hasChanges) {
this.setStatus("No group changes to save.", "info");
this.closeGroupModal();
return;
}
this.setStatus("Updating group profile...", "info");
window.mapUI.sendEvent("cad::groups::profile", {
groupID: this.editingGroupId,
role: nextRole,
status: nextStatus,
});
this.closeGroupModal();
},
closeDispatchOrder(taskID) {
if (!taskID) {
return;
}
this.setStatus("Closing dispatch order...", "info");
window.mapUI.sendEvent("cad::dispatchOrder::close", {
taskID: taskID,
});
},
buildGroupEditorButton(groupID) {
return `
<button
type="button"
class="dispatch-icon-btn"
onclick="window.cadDispatcher.openGroupModal('${groupID}')"
aria-label="Edit group"
title="Edit group"
>
&#9881;
</button>
`;
},
buildCloseOrderButton(taskID) {
return `
<button
type="button"
class="dispatch-btn dispatch-btn-secondary"
onclick="window.cadDispatcher.closeDispatchOrder('${taskID}')"
>
Close
</button>
`;
},
buildCloseRequestButton(requestID) {
return `
<button
type="button"
class="dispatch-btn dispatch-btn-secondary"
onclick="event.stopPropagation(); window.cadDispatcher.closeSupportRequest('${requestID}')"
>
Close
</button>
`;
},
closeSupportRequest(requestID) {
if (!requestID) {
return;
}
this.setStatus("Closing support request...", "info");
window.mapUI.sendEvent("cad::supportRequest::close", {
requestID: requestID,
});
},
renderMetrics() {
const assignedContracts = this.contracts.filter(
(entry) => (entry.assignmentState || "unassigned") !== "unassigned",
);
const openContracts = this.contracts.filter(
(entry) => (entry.assignmentState || "unassigned") === "unassigned",
);
const openRequests = this.requests.length;
const supportAlertRequests = this.getSupportAlertRequests();
const dangerGroups = this.groups.filter(
(group) => (group.status || "") === "danger",
);
document.getElementById("metricOpenContracts").textContent =
openContracts.length;
document.getElementById("metricAssignedContracts").textContent =
assignedContracts.length;
document.getElementById("metricActiveGroups").textContent =
this.groups.length;
document.getElementById("metricOpenRequests").textContent =
openRequests;
document.getElementById("metricDangerGroups").textContent =
dangerGroups.length;
const dangerMetricCard = document.getElementById(
"metricDangerGroupsCard",
);
if (dangerMetricCard) {
dangerMetricCard.classList.toggle(
"is-danger",
dangerGroups.length > 0,
);
}
const requestMetricCard = document.getElementById(
"metricOpenRequestsCard",
);
if (requestMetricCard) {
requestMetricCard.classList.toggle(
"is-warning",
supportAlertRequests.length > 0,
);
}
},
renderOpenContracts() {
const container = document.getElementById("dispatcherOpenContracts");
const openContracts = this.contracts.filter(
(entry) => (entry.assignmentState || "unassigned") === "unassigned",
);
if (!openContracts.length) {
container.innerHTML =
'<div class="placeholder-message"><p>No open contracts.</p></div>';
return;
}
const groupOptions = this.buildGroupOptions("");
container.innerHTML = openContracts
.map((task) => {
const taskId = task.taskId || task.taskID || "";
const position = Array.isArray(task.position)
? task.position
: [0, 0, 0];
const targetGroup = this.groups.find(
(group) => group.groupId === (task.targetGroupId || ""),
);
return `
<article class="dispatch-card">
<header class="dispatch-card-header">
<strong>${task.title || taskId}</strong>
<span class="dispatch-badge">${this.formatTypeLabel(task)}</span>
</header>
<p class="dispatch-description">${task.description || ""}</p>
<div class="dispatch-meta">
<span>Unassigned</span>
<span>${window.mapUI.formatPosition(position)}</span>
</div>
<div class="dispatch-meta">
<span>Target: ${targetGroup ? targetGroup.callsign : task.targetGroupCallsign || "None"}</span>
<span>Priority: ${(task.priority || "priority").replaceAll("_", " ")}</span>
</div>
<div class="dispatch-actions">
<select id="dispatcher-assign-group-${taskId}" class="dispatch-select">
<option value="">Assign to group</option>
${groupOptions}
</select>
<button type="button" class="dispatch-btn" onclick="window.cadDispatcher.assignTask('${taskId}')">Assign</button>
</div>
</article>
`;
})
.join("");
},
renderAssignedContracts() {
const container = document.getElementById(
"dispatcherAssignedContracts",
);
const assignedContracts = this.contracts.filter(
(entry) => (entry.assignmentState || "unassigned") !== "unassigned",
);
if (!assignedContracts.length) {
container.innerHTML =
'<div class="placeholder-message"><p>No assigned contracts.</p></div>';
return;
}
container.innerHTML = assignedContracts
.map((task) => {
const taskId = task.taskId || task.taskID || "";
const assignedGroup = this.groups.find(
(group) => group.groupId === (task.assignedGroupId || ""),
);
const targetGroup = this.groups.find(
(group) => group.groupId === (task.targetGroupId || ""),
);
const isDispatchOrder = this.isDispatchOrder(task);
return `
<article class="dispatch-card">
<header class="dispatch-card-header">
<strong>${task.title || taskId}</strong>
<span class="dispatch-badge">${task.assignmentState || "assigned"}</span>
</header>
<p class="dispatch-description">${task.description || ""}</p>
<div class="dispatch-meta">
<span>Group: ${assignedGroup ? assignedGroup.callsign : task.assignedGroupId || "Unknown"}</span>
<span>Type: ${this.formatTypeLabel(task)}</span>
</div>
<div class="dispatch-meta">
<span>Target: ${targetGroup ? targetGroup.callsign : task.targetGroupCallsign || "None"}</span>
<span>Priority: ${(task.priority || "priority").replaceAll("_", " ")}</span>
</div>
${isDispatchOrder ? `<div class="dispatch-actions dispatch-actions-split">${this.buildCloseOrderButton(taskId)}</div>` : ""}
</article>
`;
})
.join("");
},
renderGroups() {
const container = document.getElementById("dispatcherGroups");
if (!this.groups.length) {
container.innerHTML =
'<div class="placeholder-message"><p>No active groups available.</p></div>';
return;
}
container.innerHTML = this.getSortedGroups()
.map((group) => {
const isDanger = (group.status || "") === "danger";
return `
<article class="dispatch-card dispatch-card-group ${isDanger ? "is-danger" : ""}">
<header class="dispatch-card-header">
<div class="dispatch-card-header-main">
<strong>${group.callsign || group.groupId}</strong>
<span class="dispatch-badge">${group.role || "group"}</span>
${isDanger ? '<span class="dispatch-alert-badge">Danger</span>' : ""}
</div>
<div class="dispatch-card-header-actions">
${this.buildGroupEditorButton(group.groupId)}
</div>
</header>
<div class="dispatch-meta">
<span>Leader: ${group.leaderName || "Unknown"}</span>
<span>Status: ${group.status || "unknown"}</span>
</div>
<div class="dispatch-meta">
<span>Org: ${group.orgId || "default"}</span>
<span>Task: ${group.currentTaskId || "None"}</span>
</div>
</article>
`;
})
.join("");
},
renderActivity() {
const container = document.getElementById("dispatcherActivity");
const requestsHTML = this.requests.length
? this.requests
.map(
(request) => `
<article class="dispatch-card dispatch-card-interactive ${["medevac_9line", "fire_support", "air_support"].includes(request.type || "") ? "is-warning" : ""}" onclick="window.cadDispatcher.openRequestModal('${request.requestId || ""}')">
<header class="dispatch-card-header">
<strong>${request.title || request.requestId || "Support Request"}</strong>
<span class="dispatch-badge">${(request.priority || "priority").replaceAll("_", " ")}</span>
</header>
<p class="dispatch-description">${request.summary || ""}</p>
<div class="dispatch-meta">
<span>Group: ${request.groupCallsign || request.groupId || "Unknown"}</span>
<span>${this.getRequestTypeLabel(request.type || "request")}</span>
</div>
<div class="dispatch-actions dispatch-actions-split">
${this.buildCloseRequestButton(request.requestId || "")}
</div>
</article>
`,
)
.join("")
: '<div class="placeholder-message"><p>No active support requests.</p></div>';
const activityHTML = this.activity.length
? this.activity
.slice()
.reverse()
.slice(0, 8)
.map(
(entry) => `
<article class="dispatch-card">
<header class="dispatch-card-header">
<strong>${entry.type || "activity"}</strong>
<span class="dispatch-badge">${Math.round(entry.timestamp || 0)}s</span>
</header>
<p class="dispatch-description">${entry.message || ""}</p>
</article>
`,
)
.join("")
: '<div class="placeholder-message"><p>No recent activity.</p></div>';
container.innerHTML = `
<div class="dispatch-inline-section">
<div class="dispatch-inline-header">Support Requests</div>
${requestsHTML}
</div>
<div class="dispatch-inline-section">
<div class="dispatch-inline-header">Recent Activity</div>
${activityHTML}
</div>
`;
},
render() {
this.updateDangerAlert();
this.updateRequestAlert();
this.renderMetrics();
this.renderOpenContracts();
this.renderAssignedContracts();
this.renderGroups();
this.renderActivity();
},
};
window.cadDispatcher.init();

View File

@ -0,0 +1,103 @@
window.cadDispatcherFormatters = {
getDangerGroups() {
return this.groups.filter((group) => (group.status || "") === "danger");
},
getSupportAlertRequests() {
return this.requests.filter((request) =>
["medevac_9line", "fire_support", "air_support"].includes(
request.type || "",
),
);
},
buildSupportAlertMessage() {
const alertRequests = this.getSupportAlertRequests();
if (!alertRequests.length) {
return "";
}
const labels = alertRequests.map((request) => {
const groupLabel =
request.groupCallsign || request.groupId || "Unknown Group";
const typeLabel = this.getRequestTypeLabel(
request.type || "request",
);
return `${groupLabel} ${typeLabel}`;
});
return `Support request alert: ${labels.join(", ")}`;
},
getSortedGroups() {
return this.groups.slice().sort((left, right) => {
const leftDanger = (left.status || "") === "danger" ? 0 : 1;
const rightDanger = (right.status || "") === "danger" ? 0 : 1;
if (leftDanger !== rightDanger) {
return leftDanger - rightDanger;
}
const leftCallsign = left.callsign || left.groupId || "";
const rightCallsign = right.callsign || right.groupId || "";
return leftCallsign.localeCompare(rightCallsign);
});
},
isDispatchOrder(entry) {
return (
!!entry.isDispatchOrder || (entry.type || "") === "dispatch_order"
);
},
formatTypeLabel(entry) {
const typeLabel = (entry.type || "task").replaceAll("_", " ");
return this.isDispatchOrder(entry) ? "dispatch order" : typeLabel;
},
getRequestTypeLabel(typeID) {
switch (typeID) {
case "medevac_9line":
return "9-Line MEDEVAC";
case "ace_lace":
return "ACE/LACE";
case "fire_support":
return "Fire Support";
case "air_support":
return "Air Support";
case "logreq":
return "LOGREQ";
default:
return (typeID || "request").replaceAll("_", " ");
}
},
buildGroupOptions(selectedGroupID) {
return this.getSortedGroups()
.map((group) => {
const groupID = group.groupId || "";
return `<option value="${groupID}" ${groupID === selectedGroupID ? "selected" : ""}>${group.callsign || groupID}</option>`;
})
.join("");
},
formatRequestFieldLabel(fieldID) {
return (fieldID || "field")
.replaceAll("_", " ")
.replace(/\b\w/g, (character) => character.toUpperCase());
},
formatRequestFieldValue(value) {
if (Array.isArray(value)) {
return value.join(", ");
}
if (value && typeof value === "object") {
return JSON.stringify(value);
}
const text = String(value ?? "").trim();
return text || "Not provided";
},
buildRequestOrderNote(request) {
const typeLabel = this.getRequestTypeLabel(request.type || "request");
const groupLabel =
request.groupCallsign || request.groupId || "Unknown Group";
const summary = (request.summary || "").trim();
return summary
? `${typeLabel} requested by ${groupLabel}. ${summary}`
: `${typeLabel} requested by ${groupLabel}.`;
},
};

View File

@ -0,0 +1,255 @@
const dispatcherFormatters = window.cadDispatcherFormatters || {};
const dispatcherModals = window.cadDispatcherModals || {};
const dispatcherRender = window.cadDispatcherRender || {};
window.cadDispatcher = {
contracts: [],
requests: [],
groups: [],
activity: [],
session: {},
editingGroupId: "",
viewingRequestId: "",
convertingRequestId: "",
statuses: [
"available",
"en_route",
"on_task",
"holding",
"danger",
"unavailable",
],
roles: ["infantry", "recon", "armor", "air", "logistics", "support"],
...dispatcherFormatters,
...dispatcherModals,
...dispatcherRender,
init() {
document
.getElementById("dispatcherCreateOrderBtn")
.addEventListener("click", () => {
this.openOrderModal();
});
document
.getElementById("dispatcherGroupModalCloseBtn")
.addEventListener("click", () => {
this.closeGroupModal();
});
document
.getElementById("dispatcherGroupModalSaveBtn")
.addEventListener("click", () => {
this.applyGroupUpdates();
});
document
.querySelector("#dispatcherGroupModal .dispatch-modal-backdrop")
.addEventListener("click", () => {
this.closeGroupModal();
});
document
.getElementById("dispatcherOrderModalCloseBtn")
.addEventListener("click", () => {
this.closeOrderModal();
});
document
.getElementById("dispatcherOrderModalSaveBtn")
.addEventListener("click", () => {
this.createDispatchOrder();
});
document
.querySelector("#dispatcherOrderModal .dispatch-modal-backdrop")
.addEventListener("click", () => {
this.closeOrderModal();
});
document
.getElementById("dispatcherRequestModalCloseBtn")
.addEventListener("click", () => {
this.closeRequestModal();
});
document
.getElementById("dispatcherRequestModalDoneBtn")
.addEventListener("click", () => {
this.closeRequestModal();
});
document
.getElementById("dispatcherRequestConvertBtn")
.addEventListener("click", () => {
this.convertViewedRequestToOrder();
});
document
.querySelector("#dispatcherRequestModal .dispatch-modal-backdrop")
.addEventListener("click", () => {
this.closeRequestModal();
});
window.mapUI.sendEvent("cad::dispatcher::ready", {});
},
receiveHydrate(payload) {
this.contracts = Array.isArray(payload.contracts)
? payload.contracts
: [];
this.requests = Array.isArray(payload.requests) ? payload.requests : [];
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("dispatcherStatusMessage");
if (
statusEl &&
(!statusEl.dataset.type || statusEl.dataset.type === "info")
) {
this.setStatus("", "");
}
this.syncOpenModal();
this.syncOrderModal();
this.syncRequestModal();
this.render();
},
setStatus(message, type) {
const statusEl = document.getElementById("dispatcherStatusMessage");
if (!statusEl) {
return;
}
statusEl.textContent = message || "";
statusEl.dataset.type = type || "";
},
createDispatchOrder() {
const assigneeGroupID = document.getElementById(
"dispatcherOrderAssigneeSelect",
).value;
const targetGroupID = document.getElementById(
"dispatcherOrderTargetSelect",
).value;
const priority = document.getElementById(
"dispatcherOrderPrioritySelect",
).value;
const note = document.getElementById("dispatcherOrderNoteInput").value;
if (!assigneeGroupID || !targetGroupID) {
this.setStatus(
"Select both an assignee and a target group.",
"error",
);
return;
}
if (assigneeGroupID === targetGroupID) {
this.setStatus(
"Assignee and target groups must be different.",
"error",
);
return;
}
this.setStatus(
this.convertingRequestId
? "Creating dispatch order from request..."
: "Creating dispatch order...",
"info",
);
window.mapUI.sendEvent("cad::dispatchOrder::create", {
assigneeGroupID: assigneeGroupID,
targetGroupID: targetGroupID,
note: note.trim(),
priority: priority,
});
this.closeOrderModal();
},
assignTask(taskID) {
const selector = document.getElementById(
`dispatcher-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: "",
});
},
applyGroupUpdates() {
if (!this.editingGroupId) {
return;
}
const group = this.groups.find(
(entry) => entry.groupId === this.editingGroupId,
);
if (!group) {
this.closeGroupModal();
return;
}
const roleValue = document.getElementById(
"dispatcherModalRoleSelect",
).value;
const statusValue = document.getElementById(
"dispatcherModalStatusSelect",
).value;
const nextRole =
roleValue && roleValue !== (group.role || "") ? roleValue : "";
const nextStatus =
statusValue && statusValue !== (group.status || "")
? statusValue
: "";
const hasChanges = nextRole || nextStatus;
if (!hasChanges) {
this.setStatus("No group changes to save.", "info");
this.closeGroupModal();
return;
}
this.setStatus("Updating group profile...", "info");
window.mapUI.sendEvent("cad::groups::profile", {
groupID: this.editingGroupId,
role: nextRole,
status: nextStatus,
});
this.closeGroupModal();
},
closeDispatchOrder(taskID) {
if (!taskID) {
return;
}
this.setStatus("Closing dispatch order...", "info");
window.mapUI.sendEvent("cad::dispatchOrder::close", {
taskID: taskID,
});
},
closeSupportRequest(requestID) {
if (!requestID) {
return;
}
this.setStatus("Closing support request...", "info");
window.mapUI.sendEvent("cad::supportRequest::close", {
requestID: requestID,
});
},
};
window.cadDispatcher.init();

View File

@ -0,0 +1,268 @@
window.cadDispatcherModals = {
openOrderModal() {
this.convertingRequestId = "";
this.populateOrderModal();
document.getElementById("dispatcherOrderModalTitle").textContent =
"Create Support Order";
document
.getElementById("dispatcherOrderModal")
.classList.remove("is-hidden");
},
closeOrderModal() {
this.convertingRequestId = "";
document.getElementById("dispatcherOrderNoteInput").value = "";
document.getElementById("dispatcherOrderPrioritySelect").value =
"priority";
document.getElementById("dispatcherOrderModalTitle").textContent =
"Create Support Order";
document
.getElementById("dispatcherOrderModal")
.classList.add("is-hidden");
},
openRequestModal(requestID) {
const request = this.requests.find(
(entry) => entry.requestId === requestID,
);
if (!request) {
return;
}
this.viewingRequestId = requestID;
this.populateRequestModal(request);
document
.getElementById("dispatcherRequestModal")
.classList.remove("is-hidden");
},
closeRequestModal() {
this.viewingRequestId = "";
document
.getElementById("dispatcherRequestModal")
.classList.add("is-hidden");
},
syncRequestModal() {
if (!this.viewingRequestId) {
return;
}
const request = this.requests.find(
(entry) => entry.requestId === this.viewingRequestId,
);
if (!request) {
this.closeRequestModal();
return;
}
this.populateRequestModal(request);
},
populateRequestModal(request) {
const fields =
request.fields && typeof request.fields === "object"
? Object.entries(request.fields)
: [];
const fieldsHTML = fields.length
? fields
.map(
([fieldID, value]) => `
<div class="dispatch-detail-row">
<span class="dispatch-detail-label">${this.formatRequestFieldLabel(fieldID)}</span>
<span class="dispatch-detail-value">${this.formatRequestFieldValue(value)}</span>
</div>
`,
)
.join("")
: '<div class="placeholder-message"><p>No submitted fields.</p></div>';
document.getElementById("dispatcherRequestTitle").textContent =
request.title || request.requestId || "Support Request";
document.getElementById("dispatcherRequestPriority").textContent = (
request.priority || "priority"
).replaceAll("_", " ");
document.getElementById("dispatcherRequestGroup").textContent =
request.groupCallsign || request.groupId || "Unknown";
document.getElementById("dispatcherRequestType").textContent =
this.getRequestTypeLabel(request.type || "request");
document.getElementById("dispatcherRequestSummary").textContent =
request.summary || "No summary provided.";
document.getElementById("dispatcherRequestFields").innerHTML =
fieldsHTML;
},
convertRequestToOrder(requestID) {
const request = this.requests.find(
(entry) => (entry.requestId || "") === requestID,
);
if (!request) {
this.setStatus("Selected request is no longer available.", "error");
return;
}
const targetGroupID = request.groupId || "";
if (!targetGroupID) {
this.setStatus(
"Selected request has no owning group to target.",
"error",
);
return;
}
const targetGroup = this.groups.find(
(group) => (group.groupId || "") === targetGroupID,
);
if (!targetGroup) {
this.setStatus(
"Selected request group is no longer available.",
"error",
);
return;
}
this.convertingRequestId = requestID;
this.populateOrderModal({
selectedAssigneeID:
this.getSortedGroups().find(
(group) => (group.groupId || "") !== targetGroupID,
)?.groupId || "",
selectedTargetID: targetGroupID,
note: this.buildRequestOrderNote(request),
priority: request.priority || "priority",
});
document.getElementById("dispatcherOrderModalTitle").textContent =
"Create Order From Request";
document
.getElementById("dispatcherOrderModal")
.classList.remove("is-hidden");
this.setStatus("Preparing dispatch order from request...", "info");
},
convertViewedRequestToOrder() {
if (!this.viewingRequestId) {
return;
}
this.closeRequestModal();
this.convertRequestToOrder(this.viewingRequestId);
},
populateOrderModal(options = {}) {
const sortedGroups = this.getSortedGroups();
const assigneeSelect = document.getElementById(
"dispatcherOrderAssigneeSelect",
);
const targetSelect = document.getElementById(
"dispatcherOrderTargetSelect",
);
const noteInput = document.getElementById("dispatcherOrderNoteInput");
const prioritySelect = document.getElementById(
"dispatcherOrderPrioritySelect",
);
if (!assigneeSelect || !targetSelect) {
return;
}
const selectedAssigneeID = options.selectedAssigneeID || "";
const selectedTargetID = options.selectedTargetID || "";
const fallbackAssignee =
selectedAssigneeID ||
sortedGroups.find(
(group) => (group.groupId || "") !== selectedTargetID,
)?.groupId ||
sortedGroups[0]?.groupId ||
"";
const fallbackTarget =
selectedTargetID ||
sortedGroups.find(
(group) => (group.groupId || "") !== fallbackAssignee,
)?.groupId ||
sortedGroups[0]?.groupId ||
"";
assigneeSelect.innerHTML = this.buildGroupOptions(fallbackAssignee);
targetSelect.innerHTML = this.buildGroupOptions(fallbackTarget);
if (noteInput) {
noteInput.value = options.note || "";
}
if (prioritySelect) {
prioritySelect.value = options.priority || "priority";
}
},
syncOrderModal() {
const modalEl = document.getElementById("dispatcherOrderModal");
if (!modalEl || modalEl.classList.contains("is-hidden")) {
return;
}
this.populateOrderModal({
selectedAssigneeID:
document.getElementById("dispatcherOrderAssigneeSelect")
?.value || "",
selectedTargetID:
document.getElementById("dispatcherOrderTargetSelect")?.value ||
"",
note:
document.getElementById("dispatcherOrderNoteInput")?.value ||
"",
priority:
document.getElementById("dispatcherOrderPrioritySelect")
?.value || "priority",
});
},
openGroupModal(groupID) {
const group = this.groups.find((entry) => entry.groupId === groupID);
if (!group) {
return;
}
this.editingGroupId = groupID;
document.getElementById("dispatcherModalGroupCallsign").textContent =
group.callsign || group.groupId || "Unknown";
document.getElementById("dispatcherModalGroupLeader").textContent =
group.leaderName || "Unknown";
document.getElementById("dispatcherModalGroupTask").textContent =
group.currentTaskId || "None";
document.getElementById("dispatcherModalGroupOrg").textContent =
group.orgId || "default";
document.getElementById("dispatcherModalRoleSelect").innerHTML =
this.roles
.map(
(role) =>
`<option value="${role}" ${role === group.role ? "selected" : ""}>${role.replaceAll("_", " ")}</option>`,
)
.join("");
document.getElementById("dispatcherModalStatusSelect").innerHTML =
this.statuses
.map(
(status) =>
`<option value="${status}" ${status === group.status ? "selected" : ""}>${status.replaceAll("_", " ")}</option>`,
)
.join("");
document
.getElementById("dispatcherGroupModal")
.classList.remove("is-hidden");
},
closeGroupModal() {
this.editingGroupId = "";
document
.getElementById("dispatcherGroupModal")
.classList.add("is-hidden");
},
syncOpenModal() {
if (!this.editingGroupId) {
return;
}
const group = this.groups.find(
(entry) => entry.groupId === this.editingGroupId,
);
if (!group) {
this.closeGroupModal();
return;
}
document.getElementById("dispatcherModalGroupCallsign").textContent =
group.callsign || group.groupId || "Unknown";
document.getElementById("dispatcherModalGroupLeader").textContent =
group.leaderName || "Unknown";
document.getElementById("dispatcherModalGroupTask").textContent =
group.currentTaskId || "None";
document.getElementById("dispatcherModalGroupOrg").textContent =
group.orgId || "default";
},
};

View File

@ -0,0 +1,325 @@
window.cadDispatcherRender = {
updateDangerAlert() {
const alertEl = document.getElementById("dispatcherDangerAlert");
if (!alertEl) {
return;
}
const dangerGroups = this.getDangerGroups();
if (!dangerGroups.length) {
alertEl.textContent = "";
alertEl.classList.add("is-hidden");
return;
}
const callsigns = dangerGroups.map(
(group) => group.callsign || group.groupId || "Unknown Group",
);
alertEl.textContent = `Danger alert active: ${callsigns.join(", ")}`;
alertEl.classList.remove("is-hidden");
},
updateRequestAlert() {
const alertEl = document.getElementById("dispatcherRequestAlert");
if (!alertEl) {
return;
}
const alertMessage = this.buildSupportAlertMessage();
if (!alertMessage) {
alertEl.textContent = "";
alertEl.classList.add("is-hidden");
return;
}
alertEl.textContent = alertMessage;
alertEl.classList.remove("is-hidden");
},
buildGroupEditorButton(groupID) {
return `
<button
type="button"
class="dispatch-icon-btn"
onclick="window.cadDispatcher.openGroupModal('${groupID}')"
aria-label="Edit group"
title="Edit group"
>
&#9881;
</button>
`;
},
buildCloseOrderButton(taskID) {
return `
<button
type="button"
class="dispatch-btn dispatch-btn-secondary"
onclick="window.cadDispatcher.closeDispatchOrder('${taskID}')"
>
Close
</button>
`;
},
buildCloseRequestButton(requestID) {
return `
<button
type="button"
class="dispatch-btn dispatch-btn-secondary"
onclick="event.stopPropagation(); window.cadDispatcher.closeSupportRequest('${requestID}')"
>
Close
</button>
`;
},
buildConvertRequestButton(requestID) {
return `
<button
type="button"
class="dispatch-btn"
onclick="event.stopPropagation(); window.cadDispatcher.convertRequestToOrder('${requestID}')"
>
Convert to Order
</button>
`;
},
renderMetrics() {
const assignedContracts = this.contracts.filter(
(entry) => (entry.assignmentState || "unassigned") !== "unassigned",
);
const openContracts = this.contracts.filter(
(entry) => (entry.assignmentState || "unassigned") === "unassigned",
);
const openRequests = this.requests.length;
const supportAlertRequests = this.getSupportAlertRequests();
const dangerGroups = this.groups.filter(
(group) => (group.status || "") === "danger",
);
document.getElementById("metricOpenContracts").textContent =
openContracts.length;
document.getElementById("metricAssignedContracts").textContent =
assignedContracts.length;
document.getElementById("metricActiveGroups").textContent =
this.groups.length;
document.getElementById("metricOpenRequests").textContent =
openRequests;
document.getElementById("metricDangerGroups").textContent =
dangerGroups.length;
const dangerMetricCard = document.getElementById(
"metricDangerGroupsCard",
);
if (dangerMetricCard) {
dangerMetricCard.classList.toggle(
"is-danger",
dangerGroups.length > 0,
);
}
const requestMetricCard = document.getElementById(
"metricOpenRequestsCard",
);
if (requestMetricCard) {
requestMetricCard.classList.toggle(
"is-warning",
supportAlertRequests.length > 0,
);
}
},
renderOpenContracts() {
const container = document.getElementById("dispatcherOpenContracts");
const openContracts = this.contracts.filter(
(entry) => (entry.assignmentState || "unassigned") === "unassigned",
);
if (!openContracts.length) {
container.innerHTML =
'<div class="placeholder-message"><p>No open contracts.</p></div>';
return;
}
const groupOptions = this.buildGroupOptions("");
container.innerHTML = openContracts
.map((task) => {
const taskId = task.taskId || task.taskID || "";
const position = Array.isArray(task.position)
? task.position
: [0, 0, 0];
const targetGroup = this.groups.find(
(group) => group.groupId === (task.targetGroupId || ""),
);
return `
<article class="dispatch-card">
<header class="dispatch-card-header">
<strong>${task.title || taskId}</strong>
<span class="dispatch-badge">${this.formatTypeLabel(task)}</span>
</header>
<p class="dispatch-description">${task.description || ""}</p>
<div class="dispatch-meta">
<span>Unassigned</span>
<span>${window.mapUI.formatPosition(position)}</span>
</div>
<div class="dispatch-meta">
<span>Target: ${targetGroup ? targetGroup.callsign : task.targetGroupCallsign || "None"}</span>
<span>Priority: ${(task.priority || "priority").replaceAll("_", " ")}</span>
</div>
<div class="dispatch-actions">
<select id="dispatcher-assign-group-${taskId}" class="dispatch-select">
<option value="">Assign to group</option>
${groupOptions}
</select>
<button type="button" class="dispatch-btn" onclick="window.cadDispatcher.assignTask('${taskId}')">Assign</button>
</div>
</article>
`;
})
.join("");
},
renderAssignedContracts() {
const container = document.getElementById(
"dispatcherAssignedContracts",
);
const assignedContracts = this.contracts.filter(
(entry) => (entry.assignmentState || "unassigned") !== "unassigned",
);
if (!assignedContracts.length) {
container.innerHTML =
'<div class="placeholder-message"><p>No assigned contracts.</p></div>';
return;
}
container.innerHTML = assignedContracts
.map((task) => {
const taskId = task.taskId || task.taskID || "";
const assignedGroup = this.groups.find(
(group) => group.groupId === (task.assignedGroupId || ""),
);
const targetGroup = this.groups.find(
(group) => group.groupId === (task.targetGroupId || ""),
);
const isDispatchOrder = this.isDispatchOrder(task);
return `
<article class="dispatch-card">
<header class="dispatch-card-header">
<strong>${task.title || taskId}</strong>
<span class="dispatch-badge">${task.assignmentState || "assigned"}</span>
</header>
<p class="dispatch-description">${task.description || ""}</p>
<div class="dispatch-meta">
<span>Group: ${assignedGroup ? assignedGroup.callsign : task.assignedGroupId || "Unknown"}</span>
<span>Type: ${this.formatTypeLabel(task)}</span>
</div>
<div class="dispatch-meta">
<span>Target: ${targetGroup ? targetGroup.callsign : task.targetGroupCallsign || "None"}</span>
<span>Priority: ${(task.priority || "priority").replaceAll("_", " ")}</span>
</div>
${isDispatchOrder ? `<div class="dispatch-actions dispatch-actions-split">${this.buildCloseOrderButton(taskId)}</div>` : ""}
</article>
`;
})
.join("");
},
renderGroups() {
const container = document.getElementById("dispatcherGroups");
if (!this.groups.length) {
container.innerHTML =
'<div class="placeholder-message"><p>No active groups available.</p></div>';
return;
}
container.innerHTML = this.getSortedGroups()
.map((group) => {
const isDanger = (group.status || "") === "danger";
return `
<article class="dispatch-card dispatch-card-group ${isDanger ? "is-danger" : ""}">
<header class="dispatch-card-header">
<div class="dispatch-card-header-main">
<strong>${group.callsign || group.groupId}</strong>
<span class="dispatch-badge">${group.role || "group"}</span>
${isDanger ? '<span class="dispatch-alert-badge">Danger</span>' : ""}
</div>
<div class="dispatch-card-header-actions">
${this.buildGroupEditorButton(group.groupId)}
</div>
</header>
<div class="dispatch-meta">
<span>Leader: ${group.leaderName || "Unknown"}</span>
<span>Status: ${group.status || "unknown"}</span>
</div>
<div class="dispatch-meta">
<span>Org: ${group.orgId || "default"}</span>
<span>Task: ${group.currentTaskId || "None"}</span>
</div>
</article>
`;
})
.join("");
},
renderActivity() {
const container = document.getElementById("dispatcherActivity");
const requestsHTML = this.requests.length
? this.requests
.map(
(request) => `
<article class="dispatch-card dispatch-card-interactive ${["medevac_9line", "fire_support", "air_support"].includes(request.type || "") ? "is-warning" : ""}" onclick="window.cadDispatcher.openRequestModal('${request.requestId || ""}')">
<header class="dispatch-card-header">
<strong>${request.title || request.requestId || "Support Request"}</strong>
<span class="dispatch-badge">${(request.priority || "priority").replaceAll("_", " ")}</span>
</header>
<p class="dispatch-description">${request.summary || ""}</p>
<div class="dispatch-meta">
<span>Group: ${request.groupCallsign || request.groupId || "Unknown"}</span>
<span>${this.getRequestTypeLabel(request.type || "request")}</span>
</div>
<div class="dispatch-actions dispatch-actions-split">
${this.buildConvertRequestButton(request.requestId || "")}
${this.buildCloseRequestButton(request.requestId || "")}
</div>
</article>
`,
)
.join("")
: '<div class="placeholder-message"><p>No active support requests.</p></div>';
const activityHTML = this.activity.length
? this.activity
.slice()
.reverse()
.slice(0, 8)
.map(
(entry) => `
<article class="dispatch-card">
<header class="dispatch-card-header">
<strong>${entry.type || "activity"}</strong>
<span class="dispatch-badge">${Math.round(entry.timestamp || 0)}s</span>
</header>
<p class="dispatch-description">${entry.message || ""}</p>
</article>
`,
)
.join("")
: '<div class="placeholder-message"><p>No recent activity.</p></div>';
container.innerHTML = `
<div class="dispatch-inline-section">
<div class="dispatch-inline-header">Support Requests</div>
${requestsHTML}
</div>
<div class="dispatch-inline-section">
<div class="dispatch-inline-header">Recent Activity</div>
${activityHTML}
</div>
`;
},
render() {
this.updateDangerAlert();
this.updateRequestAlert();
this.renderMetrics();
this.renderOpenContracts();
this.renderAssignedContracts();
this.renderGroups();
this.renderActivity();
},
};

View File

@ -23,7 +23,12 @@ export default {
{
name: "CAD dispatcher app",
output: "cad-dispatcher.js",
sources: ["src/dispatcher.js"],
sources: [
"src/dispatcher/formatters.js",
"src/dispatcher/modals.js",
"src/dispatcher/render.js",
"src/dispatcher/index.js",
],
},
{
name: "CAD bottombar app",

View File

@ -4,12 +4,13 @@
* File: fnc_initActorStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-02-13
* Last Update: 2026-04-01
* Public: Yes
*
* Description:
* Initializes the actor store for managing player actor data.
* Provides methods for creating, fetching, migrating, and validating actor data.
* Actor hot state is owned by the extension; SQF maintains a compatibility
* mirror for engine-adjacent consumers.
*
* Arguments:
* None
@ -111,12 +112,112 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
GVAR(Registry) = createHashMap;
["INFO", "Actor Store Initialized!"] call EFUNC(common,log);
}],
["cacheActor", compileFinal {
params [["_uid", "", [""]], ["_actor", createHashMap, [createHashMap]]];
if (_uid isEqualTo "" || { !(_actor isEqualType createHashMap) }) exitWith { createHashMap };
private _finalActor = GVAR(ActorModel) call ["migrate", [+_actor]];
GVAR(Registry) set [_uid, _finalActor];
_finalActor
}],
["callHotActor", compileFinal {
params [["_function", "", [""]], ["_arguments", [], [[]]]];
if (_function isEqualTo "") exitWith { createHashMap };
[_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith { createHashMap };
if !(_result isEqualType "") exitWith { createHashMap };
if ((_result find "Error:") == 0) exitWith {
["ERROR", format ["Actor extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log);
createHashMap
};
private _data = fromJSON _result;
if !(_data isEqualType createHashMap) exitWith { createHashMap };
_data
}],
["loadHotActor", compileFinal {
params [["_uid", "", [""]], ["_initialize", false, [false]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _command = ["actor:hot:get", "actor:hot:init"] select _initialize;
private _actor = _self call ["callHotActor", [_command, [_uid]]];
if (_actor isEqualTo createHashMap) exitWith { _actor };
_self call ["cacheActor", [_uid, _actor]]
}],
["normalizeGetArgs", compileFinal {
params ["_rawArguments"];
if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith {
[
_rawArguments param [1, "", [""]],
_rawArguments param [2, "", [""]]
]
};
[
_rawArguments param [0, "", [""]],
_rawArguments param [1, "", [""]]
]
}],
["normalizeSetArgs", compileFinal {
params ["_rawArguments"];
if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith {
[
_rawArguments param [2, "", [""]],
_rawArguments param [3, "", [""]],
_rawArguments param [4, nil, [0, "", [], false, createHashMap, objNull, grpNull]],
_rawArguments param [5, false, [false]]
]
};
[
_rawArguments param [0, "", [""]],
_rawArguments param [1, "", [""]],
_rawArguments param [2, nil, [0, "", [], false, createHashMap, objNull, grpNull]],
_rawArguments param [3, false, [false]]
]
}],
["normalizeMSetArgs", compileFinal {
params ["_rawArguments"];
if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith {
[
_rawArguments param [2, "", [""]],
_rawArguments param [3, createHashMap, [createHashMap]],
_rawArguments param [4, false, [false]]
]
};
[
_rawArguments param [0, "", [""]],
_rawArguments param [1, createHashMap, [createHashMap]],
_rawArguments param [2, false, [false]]
]
}],
["normalizeUidArg", compileFinal {
params ["_rawArguments"];
if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith {
_rawArguments param [1, "", [""]]
};
_rawArguments param [0, "", [""]]
}],
["init", compileFinal {
params [["_uid", "", [""]]];
private _player = [_uid] call EFUNC(common,getPlayer);
private _cached = GVAR(Registry) getOrDefault [_uid, nil];
if !(isNil { _cached }) exitWith { [CRPC(actor,responseInitActor), [_cached], _player] call CFUNC(targetEvent); _cached };
if !(isNil { _cached }) exitWith {
[CRPC(actor,responseInitActor), [_cached], _player] call CFUNC(targetEvent);
_cached
};
["actor:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
@ -124,52 +225,132 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
private _fallbackActor = GVAR(ActorModel) call ["fromPlayer", [_player]];
_fallbackActor set ["uid", _uid];
_fallbackActor = GVAR(ActorModel) call ["migrate", [_fallbackActor]];
_fallbackActor = _self call ["cacheActor", [_uid, _fallbackActor]];
GVAR(Registry) set [_uid, _fallbackActor];
[CRPC(actor,responseInitActor), [_fallbackActor], _player] call CFUNC(targetEvent);
_fallbackActor
};
private _finalActor = createHashMap;
if (_result == "true") then {
_finalActor = _self call ["fetch", ["actor:get", _uid]];
_finalActor = _self call ["loadHotActor", [_uid, true]];
["INFO", format ["Found actor for %1", _uid]] call EFUNC(common,log);
} else {
_finalActor = GVAR(ActorModel) call ["fromPlayer", [_player]];
_finalActor set ["uid", _uid];
private _json = _self call ["toJSON", [_finalActor]];
["actor:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["actor:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"];
if (!_createSuccess) exitWith {
["ERROR", format ["Failed to create actor %1! Using fallback actor.", _uid]] call EFUNC(common,log);
_finalActor = GVAR(ActorModel) call ["migrate", [_finalActor]];
GVAR(Registry) set [_uid, _finalActor];
_finalActor = _self call ["cacheActor", [_uid, _finalActor]];
[CRPC(actor,responseInitActor), [_finalActor], _player] call CFUNC(targetEvent);
_finalActor
};
_finalActor = _self call ["loadHotActor", [_uid, true]];
["INFO", format ["Created new actor for %1", _uid]] call EFUNC(common,log);
};
_finalActor = GVAR(ActorModel) call ["migrate", [_finalActor]];
GVAR(Registry) set [_uid, _finalActor];
if (_finalActor isEqualTo createHashMap) then {
_finalActor = GVAR(ActorModel) call ["fromPlayer", [_player]];
_finalActor set ["uid", _uid];
};
_finalActor = _self call ["cacheActor", [_uid, _finalActor]];
[CRPC(actor,responseInitActor), [_finalActor], _player] call CFUNC(targetEvent);
_finalActor
}],
["get", compileFinal {
call (_self get "normalizeGetArgs") params ["_uid", "_field"];
private _actor = _self call ["loadHotActor", [_uid, false]];
if (_actor isEqualTo createHashMap) then {
_actor = _self call ["loadHotActor", [_uid, true]];
};
if (_field isEqualTo "") exitWith { _actor };
_actor getOrDefault [_field, nil]
}],
["override", compileFinal {
params [
["_uid", "", [""]],
["_data", createHashMap, [createHashMap]],
["_save", false, [false]]
];
if (_uid isEqualTo "" || { !(_data isEqualType createHashMap) }) exitWith { createHashMap };
private _actor = _self call ["callHotActor", ["actor:hot:override", [_uid, toJSON _data]]];
if (_save && { _actor isNotEqualTo createHashMap }) then {
private _savedActor = _self call ["callHotActor", ["actor:hot:save", [_uid]]];
if (_savedActor isNotEqualTo createHashMap) then {
_actor = _savedActor;
} else {
_actor = createHashMap;
};
};
if (_actor isEqualTo createHashMap) exitWith { _actor };
_self call ["cacheActor", [_uid, _actor]]
}],
["set", compileFinal {
call (_self get "normalizeSetArgs") params ["_uid", "_field", "_value", "_sync"];
if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
private _actor = _self call ["get", [_uid, ""]];
if !(_actor isEqualType createHashMap) exitWith { createHashMap };
_actor set [_field, _value];
private _updatedActor = _self call ["override", [_uid, _actor, _sync]];
if !(_updatedActor isEqualType createHashMap) exitWith { createHashMap };
if (_updatedActor isEqualTo createHashMap) exitWith { createHashMap };
createHashMapFromArray [[_field, _updatedActor getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
call (_self get "normalizeMSetArgs") params ["_uid", "_fieldValuePairs", "_sync"];
if (_uid isEqualTo "" || { !(_fieldValuePairs isEqualType createHashMap) }) exitWith { createHashMap };
private _actor = _self call ["get", [_uid, ""]];
if !(_actor isEqualType createHashMap) exitWith { createHashMap };
{ _actor set [_x, _y]; } forEach _fieldValuePairs;
private _updatedActor = _self call ["override", [_uid, _actor, _sync]];
if !(_updatedActor isEqualType createHashMap) exitWith { createHashMap };
if (_updatedActor isEqualTo createHashMap) exitWith { createHashMap };
+_fieldValuePairs
}],
["save", compileFinal {
private _uid = call (_self get "normalizeUidArg");
if (_uid isEqualTo "") exitWith { createHashMap };
private _actor = _self call ["callHotActor", ["actor:hot:save", [_uid]]];
if (_actor isEqualTo createHashMap) exitWith { _actor };
_self call ["cacheActor", [_uid, _actor]]
}],
["remove", compileFinal {
private _uid = call (_self get "normalizeUidArg");
if (_uid isEqualTo "") exitWith { false };
GVAR(Registry) deleteAt _uid;
["actor:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
_isSuccess && { _result isEqualTo "OK" }
}],
["snapshot", compileFinal {
params [["_uid", "", [""]]];
private _player = [_uid] call EFUNC(common,getPlayer);
private _existing = GVAR(Registry) getOrDefault [_uid, createHashMap];
private _finalActor = +_existing;
private _finalActor = +(_self call ["get", [_uid, ""]]);
if (_finalActor isEqualTo createHashMap) then {
if (!(_finalActor isEqualType createHashMap) || (_finalActor isEqualTo createHashMap)) then {
_finalActor = GVAR(ActorModel) call ["defaults", []];
_finalActor set ["uid", _uid];
};
@ -187,10 +368,7 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
["WARNING", format ["No player object found for %1 during actor snapshot, using cached values.", _uid]] call EFUNC(common,log);
};
_finalActor = GVAR(ActorModel) call ["migrate", [_finalActor]];
GVAR(Registry) set [_uid, _finalActor];
_finalActor
_self call ["override", [_uid, _finalActor, false]]
}]
];

View File

@ -1,6 +1,6 @@
PREP(initBank);
PREP(initMessenger);
PREP(initModel);
PREP(initPayloadBuilder);
PREP(initSessionManager);
PREP(initStore);
PREP(initValidator);

View File

@ -20,98 +20,47 @@ PREP_RECOMPILE_END;
GVAR(BankStore) call ["hydrateSession", [_uid, _mode, _resetAuthorization]];
}] call CFUNC(addEventHandler);
[QGVAR(requestGetBank), {
params [["_uid", "", [""]], ["_field", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" };
private _finalData = GVAR(BankStore) call ["get", [GVAR(Registry), _uid, _field]];
if (_field isNotEqualTo "") then {
_finalData = createHashMapFromArray [[_field, _finalData]];
};
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalData]];
}] call CFUNC(addEventHandler);
[QGVAR(requestSetBank), {
params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Key!" };
private _hashMap = GVAR(BankStore) call ["set", [GVAR(Registry), "bank:update", _uid, _field, _value, _sync]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _hashMap]];
}] call CFUNC(addEventHandler);
[QGVAR(requestMSetBank), {
params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid field pairs!" };
private _hashMap = GVAR(BankStore) call ["mset", [GVAR(Registry), "bank:update", _uid, _fieldValuePairs, _sync]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _hashMap]];
}] call CFUNC(addEventHandler);
[QGVAR(requestSaveBank), {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" };
private _finalData = GVAR(BankStore) call ["save", [GVAR(Registry), "bank:update", _uid]];
private _finalData = GVAR(BankStore) call ["save", [_uid]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalData]];
}] call CFUNC(addEventHandler);
[QGVAR(requestRemoveBank), {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" };
GVAR(BankStore) call ["remove", [GVAR(Registry), _uid]];
}] call CFUNC(addEventHandler);
[QGVAR(requestDeposit), {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
private _context = GVAR(BankValidator) call ["validateDeposit", [_uid, _amount]];
if (_context isEqualTo false) exitWith {};
GVAR(BankStore) call ["deposit", [_uid, _amount, _context]];
GVAR(BankStore) call ["deposit", [_uid, _amount]];
}] call CFUNC(addEventHandler);
[QGVAR(requestPayment), {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
private _context = GVAR(BankValidator) call ["validatePayment", [_uid, _amount]];
if (_context isEqualTo false) exitWith {};
GVAR(BankStore) call ["payment", [_uid, _amount, _context]];
GVAR(BankStore) call ["payment", [_uid, _amount]];
}] call CFUNC(addEventHandler);
[QGVAR(requestSubmitPin), {
params [["_uid", "", [""]], ["_pin", "", [""]]];
private _context = GVAR(BankValidator) call ["validateSubmitPin", [_uid, _pin]];
if (_context isEqualTo false) exitWith {};
GVAR(BankSessionManager) call ["submitPin", [_uid, _context]];
GVAR(BankSessionManager) call ["submitPin", [_uid, _pin]];
}] call CFUNC(addEventHandler);
[QGVAR(requestTransfer), {
params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]];
private _context = GVAR(BankValidator) call ["validateTransfer", [_uid, _target, _from, _amount]];
if (_context isEqualTo false) exitWith {};
GVAR(BankStore) call ["transfer", [_uid, _target, _amount, _context]];
GVAR(BankStore) call ["transfer", [_uid, _target, _amount, createHashMapFromArray [["sourceField", _from]]]];
}] call CFUNC(addEventHandler);
[QGVAR(requestWithdraw), {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
private _context = GVAR(BankValidator) call ["validateWithdraw", [_uid, _amount]];
if (_context isEqualTo false) exitWith {};
GVAR(BankStore) call ["withdraw", [_uid, _amount, _context]];
GVAR(BankStore) call ["withdraw", [_uid, _amount]];
}] call CFUNC(addEventHandler);
[QGVAR(requestDepositEarnings), {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
private _context = GVAR(BankValidator) call ["validateDepositEarnings", [_uid, _amount]];
if (_context isEqualTo false) exitWith {};
GVAR(BankStore) call ["depositEarnings", [_uid, _amount, _context]];
GVAR(BankStore) call ["depositEarnings", [_uid, _amount]];
}] call CFUNC(addEventHandler);

View File

@ -4,7 +4,7 @@
* File: fnc_initMessenger.sqf
* Author: IDSolutions
* Date: 2026-03-16
* Last Update: 2026-03-16
* Last Update: 2026-04-02
* Public: No
*
* Description:
@ -25,7 +25,7 @@
#pragma hemtt ignore_variables ["_self"]
GVAR(BankMessenger) = createHashMapObject [[
["#type", "BankMessenger"],
["buildClientAccountPatch", compileFinal {
["buildAccountPatch", compileFinal {
params [["_account", createHashMap, [createHashMap]]];
private _patch = createHashMap;
@ -45,10 +45,10 @@ GVAR(BankMessenger) = createHashMapObject [[
private _player = [_uid] call EFUNC(common,getPlayer);
if (isNull _player) exitWith { false };
[_event, [_self call ["buildClientAccountPatch", [_account]]], _player] call CFUNC(targetEvent);
[_event, [_self call ["buildAccountPatch", [_account]]], _player] call CFUNC(targetEvent);
true
}],
["sendClientNotification", compileFinal {
["sendNotification", compileFinal {
params [["_uid", "", [""]], ["_type", "info", [""]], ["_title", "Bank", [""]], ["_message", "", [""]]];
if (_uid isEqualTo "" || { _message isEqualTo "" }) exitWith { false };
@ -59,7 +59,7 @@ GVAR(BankMessenger) = createHashMapObject [[
[CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent);
true
}],
["sendNotice", compileFinal {
["sendAlert", compileFinal {
params [["_uid", "", [""]], ["_type", "error", [""]], ["_message", "", [""]]];
if (_uid isEqualTo "" || { _message isEqualTo "" }) exitWith { false };

View File

@ -10,7 +10,7 @@
* Description:
* Initializes the bank account data model. Provides default account
* schema, player-based account creation, schema migration for
* existing accounts, and field-level validation.
* existing accounts.
*
* Parameter(s):
* None
@ -61,30 +61,6 @@ GVAR(BankModel) = compileFinal createHashMapObject [[
} forEach _defaults;
_account
}],
["validate", compileFinal {
params [["_account", createHashMap, [createHashMap]]];
private _uid = _account getOrDefault ["uid", ""];
private _name = _account getOrDefault ["name", ""];
private _bank = _account getOrDefault ["bank", 0];
private _cash = _account getOrDefault ["cash", 0];
private _earnings = _account getOrDefault ["earnings", 0];
private _pin = _account getOrDefault ["pin", 1234];
[_uid, _name, _bank, _cash, _earnings, _pin] try {
if (_uid isEqualTo "" || !(_uid isEqualType "")) then { throw "Invalid UID!"; };
if (_name isEqualTo "" || !(_name isEqualType "")) then { throw "Invalid Name!"; };
if (_bank < 0 || !(_bank isEqualType 0)) then { throw "Invalid Bank!"; };
if (_cash < 0 || !(_cash isEqualType 0)) then { throw "Invalid Cash!"; };
if (_earnings < 0 || !(_earnings isEqualType 0)) then { throw "Invalid Earnings!"; };
if (_pin < 1000 || _pin > 9999 || !(_pin isEqualType 0)) then { throw "Invalid Pin!"; };
} catch {
["ERROR", format ["Failed to validate account %1!", _exception]] call EFUNC(common,log);
false
};
true
}]
]];

View File

@ -0,0 +1,105 @@
#include "..\script_component.hpp"
/*
* File: fnc_initPayloadBuilder.sqf
* Author: IDSolutions
* Date: 2026-04-02
* Public: No
*
* Description:
* Initializes the bank payload builder for session/view shaping.
* Keeps hydrate/context construction out of BankStore so the store
* can focus on extension-backed account operations.
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(BankPayloadBuilder) = createHashMapObject [[
["#type", "BankPayloadBuilder"],
["buildOperationContext", compileFinal {
params [["_uid", "", [""]], ["_modeOverride", "", [""]]];
private _session = GVAR(BankSessionManager) call ["getSessionState", [_uid]];
private _mode = if (_modeOverride isEqualTo "") then {
_session getOrDefault ["mode", "bank"]
} else {
GVAR(BankSessionManager) call ["resolveMode", [_modeOverride]]
};
createHashMapFromArray [
["mode", _mode],
["atmAuthorized", _session getOrDefault ["atmAuthorized", false]]
]
}],
["buildTransferContext", compileFinal {
params [["_uid", "", [""]], ["_from", "", [""]]];
private _context = _self call ["buildOperationContext", [_uid]];
_context set ["fromField", _from];
_context
}],
["resolveOrgState", compileFinal {
params [["_uid", "", [""]]];
private _defaultState = createHashMapFromArray [["funds", 0], ["name", ""]];
if (_uid isEqualTo "") exitWith { _defaultState };
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _orgID = _actor getOrDefault ["organization", "default"];
if (_orgID isEqualTo "") then { _orgID = "default"; };
private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) then {
_org = EGVAR(org,OrgStore) call ["loadById", ["default"]];
};
if (_org isEqualTo createHashMap) exitWith { _defaultState };
createHashMapFromArray [["funds", _org getOrDefault ["funds", 0]], ["name", _org getOrDefault ["name", ""]]]
}],
["buildTransferTargets", compileFinal {
params [["_sourceUid", "", [""]]];
private _targets = [];
{
if (isNull _x) then { continue; };
private _targetUid = getPlayerUID _x;
private _targetName = name _x;
if (_targetUid isEqualTo "" || { _targetUid isEqualTo _sourceUid } || { _targetName isEqualTo "" }) then { continue; };
_targets pushBack (createHashMapFromArray [["name", _targetName], ["uid", _targetUid]]);
} forEach allPlayers;
private _targetPairs = _targets apply { [toLowerANSI (_x getOrDefault ["name", ""]), _x] };
_targetPairs sort true;
_targetPairs apply { _x param [1, createHashMap] }
}],
["buildHydratePayload", compileFinal {
params [["_uid", "", [""]], ["_mode", "", [""]], ["_resetAuthorization", false, [false]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _account = GVAR(BankStore) call ["get", [_uid, ""]];
if (_account isEqualTo createHashMap) then {
_account = GVAR(BankStore) call ["init", [_uid]];
};
if (_account isEqualTo createHashMap) exitWith { createHashMap };
private _session = GVAR(BankSessionManager) call ["syncSessionMode", [_uid, _mode, _resetAuthorization]];
private _orgState = _self call ["resolveOrgState", [_uid]];
private _player = [_uid] call EFUNC(common,getPlayer);
private _playerName = if (isNull _player) then { _account getOrDefault ["name", "Unknown"] } else { name _player };
createHashMapFromArray [
["session", createHashMapFromArray [
["atmAuthorized", _session getOrDefault ["atmAuthorized", false]],
["mode", _session getOrDefault ["mode", "bank"]],
["orgFunds", _orgState getOrDefault ["funds", 0]],
["orgName", _orgState getOrDefault ["name", ""]],
["playerName", _playerName],
["transferTargets", _self call ["buildTransferTargets", [_uid]]],
["uid", _uid]
]],
["account", GVAR(BankMessenger) call ["buildAccountPatch", [_account]]]
]
}]
]];
GVAR(BankPayloadBuilder)

View File

@ -4,7 +4,7 @@
* File: fnc_initSessionManager.sqf
* Author: IDSolutions
* Date: 2026-03-16
* Last Update: 2026-03-16
* Last Update: 2026-04-02
* Public: No
*
* Description:
@ -82,10 +82,18 @@ GVAR(BankSessionManager) = createHashMapObject [[
]]]
}],
["submitPin", compileFinal {
params [["_uid", "", [""]], ["_context", createHashMap, [createHashMap]]];
params [["_uid", "", [""]], ["_pin", "", [""]]];
if (_uid isEqualTo "") exitWith { false };
_self call ["setSessionState", [_uid, createHashMapFromArray [["atmAuthorized", false], ["mode", "atm"]]]];
if !(GVAR(BankStore) call ["validatePin", [_uid, _pin]]) exitWith {
GVAR(BankStore) call ["hydrateSession", [_uid, "atm", false]];
false
};
_self call ["setSessionState", [_uid, createHashMapFromArray [["atmAuthorized", true], ["mode", "atm"]]]];
GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", "ATM access granted."]];
GVAR(BankMessenger) call ["sendNotification", [_uid, "info", "Bank", "ATM access granted."]];
GVAR(BankStore) call ["hydrateSession", [_uid, "atm", false]];
true
}]

View File

@ -4,22 +4,13 @@
* File: fnc_initStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-03-16
* Last Update: 2026-04-02
* Public: No
*
* Description:
* Initializes the bank store for managing player bank accounts.
* Handles account lifecycle (init/fetch/create/migrate), transaction
* mutations, checkout charges, and session hydration.
*
* Parameter(s):
* None
*
* Returns:
* Bank store object [HASHMAP OBJECT]
*
* Example(s):
* call forge_server_bank_fnc_initStore
* Bank account truth lives in the extension hot cache; SQF handles
* session state, Arma-facing validation, and client messaging.
*/
#pragma hemtt ignore_variables ["_self"]
@ -27,76 +18,133 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
["#base", EGVAR(common,BaseStore)],
["#type", "BankBaseStore"],
["#create", compileFinal {
GVAR(IndexRegistry) = createHashMap;
GVAR(Registry) = createHashMap;
GVAR(SessionRegistry) = createHashMap;
["INFO", "Bank Store Initialized!"] call EFUNC(common,log);
}],
["buildChargeResult", compileFinal {
params [["_message", "Unable to process bank payment.", [""]]];
["normalizeAccount", compileFinal {
params [["_uid", "", [""]], ["_account", createHashMap, [createHashMap]], ["_playerName", "", [""]]];
createHashMapFromArray [
["success", false],
["message", _message],
["patch", createHashMap]
]
if (_uid isEqualTo "" || { !(_account isEqualType createHashMap) }) exitWith { createHashMap };
private _finalAccount = GVAR(BankModel) call ["migrate", [+_account]];
if ((_finalAccount getOrDefault ["uid", ""]) isEqualTo "") then {
_finalAccount set ["uid", _uid];
};
if ((_finalAccount getOrDefault ["name", ""]) isEqualTo "" && { _playerName isNotEqualTo "" }) then {
_finalAccount set ["name", _playerName];
};
_finalAccount
}],
["buildHydratePayload", compileFinal {
params [["_uid", "", [""]], ["_mode", "", [""]], ["_resetAuthorization", false, [false]]];
["callHotBank", compileFinal {
params [["_function", "", [""]], ["_arguments", [], [[]]]];
private _envelope = _self call ["callHotBankEnvelope", [_function, _arguments]];
_envelope getOrDefault ["data", createHashMap]
}],
["callHotBankEnvelope", compileFinal {
params [["_function", "", [""]], ["_arguments", [], [[]]]];
private _envelope = createHashMapFromArray [["data", createHashMap], ["error", ""]];
if (_function isEqualTo "") exitWith { _envelope };
[_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
_envelope set ["error", format ["Bank backend call '%1' failed.", _function]];
_envelope
};
if !(_result isEqualType "") exitWith {
_envelope set ["error", format ["Bank backend call '%1' returned an invalid response.", _function]];
_envelope
};
if ((_result find "Error:") == 0) exitWith {
["ERROR", format ["Bank extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log);
_envelope set ["error", _result select [7]];
_envelope
};
private _data = fromJSON _result;
if !(_data isEqualType createHashMap) exitWith {
_envelope set ["error", format ["Bank backend call '%1' returned unreadable JSON.", _function]];
_envelope
};
_envelope set ["data", _data];
_envelope
}],
["loadHotBank", compileFinal {
params [["_uid", "", [""]], ["_initialize", false, [false]], ["_playerName", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _account = GVAR(Registry) getOrDefault [_uid, createHashMap];
if (_account isEqualTo createHashMap) then { _account = _self call ["init", [_uid]]; };
if (_account isEqualTo createHashMap) exitWith { createHashMap };
private _command = ["bank:hot:get", "bank:hot:init"] select _initialize;
private _account = _self call ["callHotBank", [_command, [_uid]]];
if (_account isEqualTo createHashMap) exitWith { _account };
private _session = GVAR(BankSessionManager) call ["syncSessionMode", [_uid, _mode, _resetAuthorization]];
private _orgState = _self call ["resolveOrgState", [_uid]];
private _player = [_uid] call EFUNC(common,getPlayer);
private _playerName = if (isNull _player) then {
_account getOrDefault ["name", "Unknown"]
} else {
name _player
_self call ["normalizeAccount", [_uid, _account, _playerName]]
}],
["finalizeMutation", compileFinal {
params [
["_uid", "", [""]],
["_result", createHashMap, [createHashMap]],
["_save", false, [false]]
];
if (_uid isEqualTo "" || { _result isEqualTo createHashMap }) exitWith { createHashMap };
private _account = _result getOrDefault ["account", createHashMap];
private _patch = _result getOrDefault ["patch", createHashMap];
if !(_patch isEqualType createHashMap) then {
_patch = createHashMap;
};
createHashMapFromArray [
["session", createHashMapFromArray [
["atmAuthorized", _session getOrDefault ["atmAuthorized", false]],
["mode", _session getOrDefault ["mode", "bank"]],
["orgFunds", _orgState getOrDefault ["funds", 0]],
["orgName", _orgState getOrDefault ["name", ""]],
["playerName", _playerName],
["transferTargets", _self call ["buildTransferTargets", [_uid]]],
["uid", _uid]
]],
["account", GVAR(BankMessenger) call ["buildClientAccountPatch", [_account]]]
]
if (_save && { _account isNotEqualTo createHashMap }) then {
private _savedAccount = _self call ["callHotBank", ["bank:hot:save", [_uid]]];
if (_savedAccount isEqualTo createHashMap) exitWith { createHashMap };
_account = _savedAccount;
};
if (_account isNotEqualTo createHashMap) then {
_self call ["normalizeAccount", [_uid, _account, ""]];
};
_patch
}],
["buildTransferTargets", compileFinal {
params [["_sourceUid", "", [""]]];
["runMutation", compileFinal {
params [
["_uid", "", [""]],
["_command", "", [""]],
["_arguments", [], [[]]],
["_save", false, [false]],
["_notification", "", [""]]
];
private _targets = [];
{
if (isNull _x) then { continue; };
if (_uid isEqualTo "" || { _command isEqualTo "" }) exitWith { false };
private _targetUid = getPlayerUID _x;
private _targetName = name _x;
if (_targetUid isEqualTo "" || { _targetUid isEqualTo _sourceUid } || { _targetName isEqualTo "" }) then { continue; };
private _envelope = _self call ["callHotBankEnvelope", [_command, _arguments]];
private _result = _envelope getOrDefault ["data", createHashMap];
private _finalPatch = _self call ["finalizeMutation", [_uid, _result, _save]];
if (_finalPatch isEqualTo createHashMap) exitWith {
private _message = _envelope getOrDefault ["error", "Bank operation failed."];
if (_message isNotEqualTo "") then {
GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _message]];
};
false
};
_targets pushBack (createHashMapFromArray [
["name", _targetName],
["uid", _targetUid]
]);
} forEach allPlayers;
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalPatch]];
if (_notification isNotEqualTo "") then {
GVAR(BankMessenger) call ["sendNotification", [_uid, "info", "Bank", _notification]];
};
private _targetPairs = _targets apply { [toLowerANSI (_x getOrDefault ["name", ""]), _x] };
_targetPairs sort true;
_targetPairs apply { _x param [1, createHashMap] }
true
}],
["chargeCheckout", compileFinal {
params [["_uid", "", [""]], ["_source", "cash", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]];
private _result = _self call ["buildChargeResult", []];
private _result = createHashMapFromArray [["success", false], ["message", "Unable to process bank payment."], ["patch", createHashMap]];
private _field = switch (toLowerANSI _source) do {
case "cash": { "cash" };
case "bank": { "bank" };
@ -108,7 +156,7 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
_result
};
private _account = GVAR(Registry) getOrDefault [_uid, createHashMap];
private _account = _self call ["get", [_uid, ""]];
if (_account isEqualTo createHashMap) exitWith {
_result set ["message", "Bank account data is unavailable for checkout."];
_result
@ -116,18 +164,14 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _balance = _account getOrDefault [_field, 0];
if (_balance < _amount) exitWith {
private _message = [
"Bank balance cannot cover this checkout.",
"Cash on hand cannot cover this checkout."
] select (_field isEqualTo "cash");
_result set ["message", _message];
_result set ["message", ["Bank balance cannot cover this checkout.", "Cash on hand cannot cover this checkout."] select (_field isEqualTo "cash")];
_result
};
private _patch = createHashMapFromArray [[_field, (_balance - _amount)]];
if (_commit) then {
_patch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]];
private _result = _self call ["callHotBank", ["bank:hot:patch", [_uid, toJSON _patch]]];
_patch = _self call ["finalizeMutation", [_uid, _result, false]];
};
_result set ["success", true];
@ -136,27 +180,23 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
_result
}],
["deposit", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]], ["_context", createHashMap, [createHashMap]]];
params [["_uid", "", [""]], ["_amount", 0, [0]]];
["INFO", format ["Deposit %1, for %2", _amount, _uid]] call EFUNC(common,log);
private _bank = _context getOrDefault ["bank", 0];
private _cash = _context getOrDefault ["cash", 0];
private _patch = createHashMapFromArray [
["bank", (_bank + _amount)],
["cash", (_cash - _amount)]
];
private _finalPatch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalPatch]];
GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Deposited $%1", [_amount] call EFUNC(common,formatNumber)]]];
true
_self call [
"runMutation",
[
_uid,
"bank:hot:deposit",
[_uid, str _amount, toJSON (GVAR(BankPayloadBuilder) call ["buildOperationContext", [_uid]])],
false,
format ["Deposited $%1", [_amount] call EFUNC(common,formatNumber)]
]
]
}],
["hydrateSession", compileFinal {
params [["_uid", "", [""]], ["_mode", "", [""]], ["_resetAuthorization", false, [false]]];
private _payload = _self call ["buildHydratePayload", [_uid, _mode, _resetAuthorization]];
private _payload = GVAR(BankPayloadBuilder) call ["buildHydratePayload", [_uid, _mode, _resetAuthorization]];
if (_payload isEqualTo createHashMap) exitWith { false };
private _player = [_uid] call EFUNC(common,getPlayer);
@ -172,12 +212,6 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _player = [_uid] call EFUNC(common,getPlayer);
private _playerName = if (isNull _player) then { "Unknown" } else { name _player };
private _cached = GVAR(Registry) getOrDefault [_uid, createHashMap];
if (_cached isNotEqualTo createHashMap) exitWith {
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _cached, CRPC(bank,responseInitBank)]];
_cached
};
["bank:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to check if bank account %1 exists! Using fallback account.", _uid]] call EFUNC(common,log);
@ -188,17 +222,14 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
_fallbackAccount set ["name", _playerName];
};
private _regEntry = createHashMapFromArray [["uid", _uid], ["name", _playerName]];
GVAR(IndexRegistry) set [_uid, _regEntry];
GVAR(Registry) set [_uid, _fallbackAccount];
_fallbackAccount = _self call ["normalizeAccount", [_uid, _fallbackAccount, _playerName]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _fallbackAccount, CRPC(bank,responseInitBank)]];
_fallbackAccount
};
private _finalAccount = createHashMap;
if (_result isEqualTo "true") then {
_finalAccount = _self call ["fetch", ["bank:get", _uid]];
_finalAccount = _self call ["loadHotBank", [_uid, true, _playerName]];
["INFO", format ["Found bank account for %1", _uid]] call EFUNC(common,log);
} else {
_finalAccount = GVAR(BankModel) call ["fromPlayer", [_player]];
@ -212,137 +243,180 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
if (!_createSuccess) exitWith {
["ERROR", format ["Failed to create bank account %1! Using fallback account.", _uid]] call EFUNC(common,log);
private _regEntry = createHashMapFromArray [["uid", _uid], ["name", _playerName]];
GVAR(IndexRegistry) set [_uid, _regEntry];
GVAR(Registry) set [_uid, _finalAccount];
_finalAccount = _self call ["normalizeAccount", [_uid, _finalAccount, _playerName]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalAccount, CRPC(bank,responseInitBank)]];
_finalAccount
};
_finalAccount = _self call ["loadHotBank", [_uid, true, _playerName]];
["INFO", format ["Created new bank account for %1", _uid]] call EFUNC(common,log);
};
_finalAccount = GVAR(BankModel) call ["migrate", [_finalAccount]];
if ((_finalAccount getOrDefault ["uid", ""]) isEqualTo "") then {
if (_finalAccount isEqualTo createHashMap) then {
_finalAccount = GVAR(BankModel) call ["fromPlayer", [_player]];
_finalAccount set ["uid", _uid];
};
if ((_finalAccount getOrDefault ["name", ""]) isEqualTo "") then {
_finalAccount set ["name", _playerName];
if ((_finalAccount getOrDefault ["name", ""]) isEqualTo "") then {
_finalAccount set ["name", _playerName];
};
};
GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["uid", _uid], ["name", _playerName]]];
GVAR(Registry) set [_uid, _finalAccount];
_finalAccount = _self call ["normalizeAccount", [_uid, _finalAccount, _playerName]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalAccount, CRPC(bank,responseInitBank)]];
_finalAccount
}],
["payment", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]], ["_context", createHashMap, [createHashMap]]];
["get", compileFinal {
params [["_uid", "", [""]], ["_field", "", [""]]];
["INFO", format ["Payment %1, for %2", _amount, _uid]] call EFUNC(common,log);
private _bank = _context getOrDefault ["bank", 0];
private _patch = createHashMapFromArray [["bank", (_bank + _amount)]];
private _finalPatch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalPatch]];
GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Paid $%1", [_amount] call EFUNC(common,formatNumber)]]];
true
}],
["resolveOrgState", compileFinal {
params [["_uid", "", [""]]];
private _defaultState = createHashMapFromArray [
["funds", 0],
["name", ""]
];
if (_uid isEqualTo "") exitWith { _defaultState };
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _orgID = _actor getOrDefault ["organization", "default"];
if (_orgID isEqualTo "") then { _orgID = "default"; };
private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) then {
_org = EGVAR(org,OrgStore) call ["loadById", ["default"]];
private _account = _self call ["loadHotBank", [_uid, false, ""]];
if (_account isEqualTo createHashMap) then {
_account = _self call ["loadHotBank", [_uid, true, ""]];
};
if (_org isEqualTo createHashMap) exitWith { _defaultState };
createHashMapFromArray [
["funds", _org getOrDefault ["funds", 0]],
["name", _org getOrDefault ["name", ""]]
if (_field isEqualTo "") exitWith { _account };
_account getOrDefault [_field, nil]
}],
["set", compileFinal {
params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
_self call ["mset", [_uid, createHashMapFromArray [[_field, _value]], _sync]]
}],
["mset", compileFinal {
params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "" || { !(_fieldValuePairs isEqualType createHashMap) }) exitWith { createHashMap };
private _result = _self call ["callHotBank", ["bank:hot:patch", [_uid, toJSON _fieldValuePairs]]];
_self call ["finalizeMutation", [_uid, _result, _sync]]
}],
["save", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _account = _self call ["callHotBank", ["bank:hot:save", [_uid]]];
if (_account isEqualTo createHashMap) exitWith { _account };
_self call ["normalizeAccount", [_uid, _account, ""]]
}],
["payment", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
_self call [
"runMutation",
[
_uid,
"bank:hot:payment",
[_uid, str _amount],
false,
format ["Paid $%1", [_amount] call EFUNC(common,formatNumber)]
]
]
}],
["transfer", compileFinal {
params [["_uid", "", [""]], ["_target", "", [""]], ["_amount", 0, [0]], ["_context", createHashMap, [createHashMap]]];
private _account = _context getOrDefault ["account", createHashMap];
private _targetAccount = _context getOrDefault ["targetAccount", createHashMap];
private _sourceField = _context getOrDefault ["sourceField", "bank"];
private _selected = _context getOrDefault ["sourceBalance", 0];
private _targetBank = _context getOrDefault ["targetBank", 0];
private _transferContext = GVAR(BankPayloadBuilder) call ["buildTransferContext", [_uid, _context getOrDefault ["sourceField", "bank"]]];
private _envelope = _self call [
"callHotBankEnvelope",
[
"bank:hot:transfer",
[_uid, _target, str _amount, toJSON _transferContext]
]
];
private _result = _envelope getOrDefault ["data", createHashMap];
if (_result isEqualTo createHashMap) exitWith { false };
private _sourcePatch = createHashMapFromArray [[_sourceField, (_selected - _amount)]];
private _targetPatch = createHashMapFromArray [["bank", (_targetBank + _amount)]];
private _finalSourcePatch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _sourcePatch, false]];
private _finalTargetPatch = _self call ["mset", [GVAR(Registry), "bank:update", _target, _targetPatch, false]];
private _sourceAccount = _result getOrDefault ["sourceAccount", createHashMap];
private _targetAccount = _result getOrDefault ["targetAccount", createHashMap];
private _finalSourcePatch = _result getOrDefault ["sourcePatch", createHashMap];
private _finalTargetPatch = _result getOrDefault ["targetPatch", createHashMap];
if (
_finalSourcePatch isEqualTo createHashMap
|| { _finalTargetPatch isEqualTo createHashMap }
) exitWith {
private _message = _envelope getOrDefault ["error", "Bank transfer failed."];
if (_message isNotEqualTo "") then {
GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _message]];
};
false
};
if (_sourceAccount isEqualType createHashMap && { _sourceAccount isNotEqualTo createHashMap }) then {
_self call ["normalizeAccount", [_uid, _sourceAccount, ""]];
};
if (_targetAccount isEqualType createHashMap && { _targetAccount isNotEqualTo createHashMap }) then {
_self call ["normalizeAccount", [_target, _targetAccount, ""]];
};
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalSourcePatch]];
GVAR(BankMessenger) call ["sendAccountSync", [_target, _finalTargetPatch]];
private _contextTargetAccount = _context getOrDefault ["targetAccount", createHashMap];
private _contextAccount = _context getOrDefault ["account", createHashMap];
private _targetPlayer = [_target] call EFUNC(common,getPlayer);
private _targetName = if (isNull _targetPlayer) then {
_targetAccount getOrDefault ["name", "Recipient"]
} else {
name _targetPlayer
};
private _targetName = if (isNull _targetPlayer) then { _contextTargetAccount getOrDefault ["name", "Recipient"] } else { name _targetPlayer };
private _player = [_uid] call EFUNC(common,getPlayer);
private _playerName = if (isNull _player) then {
_account getOrDefault ["name", "Unknown"]
} else {
name _player
private _playerName = if (isNull _player) then { _contextAccount getOrDefault ["name", "Unknown"] } else { name _player };
GVAR(BankMessenger) call ["sendNotification", [_uid, "info", "Bank", format ["Transferred $%1 to %2", [_amount] call EFUNC(common,formatNumber), _targetName]]];
GVAR(BankMessenger) call ["sendNotification", [_target, "info", "Bank", format ["Received $%1 from %2", [_amount] call EFUNC(common,formatNumber), _playerName]]];
true
}],
["validatePin", compileFinal {
params [["_uid", "", [""]], ["_pin", "", [""]]];
if (_uid isEqualTo "") exitWith { false };
private _enteredPin = _pin;
if !(_enteredPin isEqualType "") then {
_enteredPin = str _enteredPin;
};
GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Transferred $%1 to %2", [_amount] call EFUNC(common,formatNumber), _targetName]]];
GVAR(BankMessenger) call ["sendClientNotification", [_target, "info", "Bank", format ["Received $%1 from %2", [_amount] call EFUNC(common,formatNumber), _playerName]]];
true
private _envelope = _self call [
"callHotBankEnvelope",
[
"bank:hot:validate_pin",
[_uid, _enteredPin, toJSON (GVAR(BankPayloadBuilder) call ["buildOperationContext", [_uid, "atm"]])]
]
];
private _message = _envelope getOrDefault ["error", ""];
if (_message isNotEqualTo "") then {
GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _message]];
false
} else {
true
}
}],
["withdraw", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]], ["_context", createHashMap, [createHashMap]]];
params [["_uid", "", [""]], ["_amount", 0, [0]]];
["INFO", format ["Withdraw %1, for %2", _amount, _uid]] call EFUNC(common,log);
private _bank = _context getOrDefault ["bank", 0];
private _cash = _context getOrDefault ["cash", 0];
private _patch = createHashMapFromArray [
["bank", (_bank - _amount)],
["cash", (_cash + _amount)]
];
private _finalPatch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalPatch]];
GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Withdrew $%1", [_amount] call EFUNC(common,formatNumber)]]];
true
_self call [
"runMutation",
[
_uid,
"bank:hot:withdraw",
[_uid, str _amount, toJSON (GVAR(BankPayloadBuilder) call ["buildOperationContext", [_uid]])],
false,
format ["Withdrew $%1", [_amount] call EFUNC(common,formatNumber)]
]
]
}],
["depositEarnings", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]], ["_context", createHashMap, [createHashMap]]];
params [["_uid", "", [""]], ["_amount", 0, [0]]];
["INFO", format ["Deposit Earnings %1, for %2", _amount, _uid]] call EFUNC(common,log);
private _bank = _context getOrDefault ["bank", 0];
private _earnings = _context getOrDefault ["earnings", 0];
private _patch = createHashMapFromArray [
["bank", (_bank + _amount)],
["earnings", (_earnings - _amount)]
];
private _finalPatch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalPatch]];
GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Deposited $%1 from earnings", [_amount] call EFUNC(common,formatNumber)]]];
true
_self call [
"runMutation",
[
_uid,
"bank:hot:deposit_earnings",
[_uid, str _amount, toJSON (GVAR(BankPayloadBuilder) call ["buildOperationContext", [_uid]])],
false,
format ["Deposited $%1 from earnings", [_amount] call EFUNC(common,formatNumber)]
]
]
}]
];

View File

@ -1,259 +0,0 @@
#include "..\script_component.hpp"
/*
* File: fnc_validator.sqf
* Author: IDSolutions
* Date: 2026-03-16
* Last Update: 2026-03-16
* Public: No
*
* Description:
* Initializes the bank validator for pre-checking action payloads
* before they reach the bank store. Each method uses try/catch to
* validate inputs and state, sending a notice to the player on
* failure and returning false. On success returns a context hashmap
* containing resolved data (account, balances, etc.) for the store.
*
* Parameter(s):
* None
*
* Returns:
* Validator object [HASHMAP OBJECT]
*
* Example(s):
* call forge_server_bank_fnc_validator
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(BankValidator) = createHashMapObject [[
["#type", "BankValidator"],
["resolveAccount", compileFinal {
params [["_uid", "", [""]]];
private _account = GVAR(Registry) getOrDefault [_uid, createHashMap];
if (_account isEqualTo createHashMap) then {
throw "Bank account data is unavailable.";
};
_account
}],
["validateDeposit", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
private _context = createHashMap;
[_uid, _amount] try {
if (_uid isEqualTo "") then { throw "Empty/Invalid UID!" };
if (_amount <= 0) then { throw "Enter a valid deposit amount." };
private _session = GVAR(BankSessionManager) call ["getSessionState", [_uid]];
if ((_session getOrDefault ["mode", "bank"]) isEqualTo "atm") then {
if !(_session getOrDefault ["atmAuthorized", false]) then {
throw "ATM authorization is required before deposit.";
};
};
private _account = _self call ["resolveAccount", [_uid]];
private _bank = _account getOrDefault ["bank", 0];
private _cash = _account getOrDefault ["cash", 0];
if (_cash < _amount) then { throw "Cash on hand cannot cover that deposit." };
_context set ["account", _account];
_context set ["bank", _bank];
_context set ["cash", _cash];
} catch {
["ERROR", format ["Deposit validation failed: %1", _exception]] call EFUNC(common,log);
GVAR(BankMessenger) call ["sendNotice", [_uid, "error", _exception]];
};
if (_context isEqualTo createHashMap) exitWith { false };
_context
}],
["validateWithdraw", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
private _context = createHashMap;
[_uid, _amount] try {
if (_uid isEqualTo "") then { throw "Empty/Invalid UID!" };
if (_amount <= 0) then { throw "Enter a valid withdrawal amount." };
private _session = GVAR(BankSessionManager) call ["getSessionState", [_uid]];
if ((_session getOrDefault ["mode", "bank"]) isEqualTo "atm") then {
if !(_session getOrDefault ["atmAuthorized", false]) then {
throw "ATM authorization is required before withdrawal.";
};
};
private _account = _self call ["resolveAccount", [_uid]];
private _bank = _account getOrDefault ["bank", 0];
private _cash = _account getOrDefault ["cash", 0];
if (_bank < _amount) then { throw "Bank balance cannot cover that withdrawal." };
_context set ["account", _account];
_context set ["bank", _bank];
_context set ["cash", _cash];
} catch {
["ERROR", format ["Withdraw validation failed: %1", _exception]] call EFUNC(common,log);
GVAR(BankMessenger) call ["sendNotice", [_uid, "error", _exception]];
};
if (_context isEqualTo createHashMap) exitWith { false };
_context
}],
["validateTransfer", compileFinal {
params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]];
private _context = createHashMap;
[_uid, _target, _from, _amount] try {
if (_uid isEqualTo "") then { throw "Empty/Invalid UID!" };
if (_uid isEqualTo _target) then { throw "You cannot transfer funds to yourself." };
if (_amount <= 0) then { throw "Enter a valid transfer amount." };
private _session = GVAR(BankSessionManager) call ["getSessionState", [_uid]];
if ((_session getOrDefault ["mode", "bank"]) isNotEqualTo "bank") then {
throw "Transfers are only available from the full bank interface.";
};
private _account = _self call ["resolveAccount", [_uid]];
private _targetAccount = GVAR(Registry) getOrDefault [_target, createHashMap];
if (_targetAccount isEqualTo createHashMap) then {
_targetAccount = GVAR(BankStore) call ["init", [_target]];
};
if (_targetAccount isEqualTo createHashMap) then {
throw "Selected transfer recipient is unavailable.";
};
private _sourceField = ["bank", "cash"] select (toLowerANSI _from isEqualTo "cash");
private _selected = _account getOrDefault [_sourceField, 0];
if (_selected < _amount) then {
private _message = [
"Bank balance cannot cover that transfer.",
"Cash on hand cannot cover that transfer."
] select (_sourceField isEqualTo "cash");
throw _message;
};
_context set ["account", _account];
_context set ["targetAccount", _targetAccount];
_context set ["sourceField", _sourceField];
_context set ["sourceBalance", _selected];
_context set ["targetBank", _targetAccount getOrDefault ["bank", 0]];
} catch {
["ERROR", format ["Transfer validation failed: %1", _exception]] call EFUNC(common,log);
GVAR(BankMessenger) call ["sendNotice", [_uid, "error", _exception]];
};
if (_context isEqualTo createHashMap) exitWith { false };
_context
}],
["validateDepositEarnings", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
private _context = createHashMap;
[_uid, _amount] try {
if (_uid isEqualTo "") then { throw "Empty/Invalid UID!" };
private _session = GVAR(BankSessionManager) call ["getSessionState", [_uid]];
if ((_session getOrDefault ["mode", "bank"]) isNotEqualTo "bank") then {
throw "Earnings deposits are only available from the full bank interface.";
};
if (_amount <= 0) then { throw "No earnings are available to deposit." };
private _account = _self call ["resolveAccount", [_uid]];
private _bank = _account getOrDefault ["bank", 0];
private _earnings = _account getOrDefault ["earnings", 0];
if (_earnings < _amount) then { throw "Pending earnings cannot cover that deposit request." };
_context set ["account", _account];
_context set ["bank", _bank];
_context set ["earnings", _earnings];
} catch {
["ERROR", format ["DepositEarnings validation failed: %1", _exception]] call EFUNC(common,log);
GVAR(BankMessenger) call ["sendNotice", [_uid, "error", _exception]];
};
if (_context isEqualTo createHashMap) exitWith { false };
_context
}],
["validatePayment", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
private _context = createHashMap;
[_uid, _amount] try {
if (_uid isEqualTo "") then { throw "Empty/Invalid UID!" };
if (_amount <= 0) then { throw "Enter a valid payment amount." };
private _account = _self call ["resolveAccount", [_uid]];
private _bank = _account getOrDefault ["bank", 0];
_context set ["account", _account];
_context set ["bank", _bank];
} catch {
["ERROR", format ["Payment validation failed: %1", _exception]] call EFUNC(common,log);
GVAR(BankMessenger) call ["sendNotice", [_uid, "error", _exception]];
};
if (_context isEqualTo createHashMap) exitWith { false };
_context
}],
["validateSubmitPin", compileFinal {
params [["_uid", "", [""]], ["_pin", "", [""]]];
private _context = createHashMap;
[_uid, _pin] try {
if (_uid isEqualTo "") then { throw "Empty/Invalid UID!" };
private _session = GVAR(BankSessionManager) call ["getSessionState", [_uid]];
if ((_session getOrDefault ["mode", "bank"]) isNotEqualTo "atm") then {
_session = GVAR(BankSessionManager) call ["setSessionState", [_uid, createHashMapFromArray [
["atmAuthorized", false],
["mode", "atm"]
]]];
};
private _account = GVAR(Registry) getOrDefault [_uid, createHashMap];
if (_account isEqualTo createHashMap) then {
_account = GVAR(BankStore) call ["init", [_uid]];
};
if (_account isEqualTo createHashMap) then {
throw "Bank account data is unavailable.";
};
private _enteredPin = _pin;
if !(_enteredPin isEqualType "") then {
_enteredPin = str _enteredPin;
};
if ((count _enteredPin) isNotEqualTo 4) then {
throw "Enter your four-digit access PIN.";
};
private _accountPin = str (_account getOrDefault ["pin", 1234]);
if (_enteredPin isNotEqualTo _accountPin) then {
GVAR(BankSessionManager) call ["setSessionState", [_uid, createHashMapFromArray [["atmAuthorized", false]]]];
throw "Incorrect PIN.";
};
_context set ["account", _account];
_context set ["session", _session];
} catch {
["ERROR", format ["SubmitPin validation failed: %1", _exception]] call EFUNC(common,log);
GVAR(BankMessenger) call ["sendNotice", [_uid, "error", _exception]];
GVAR(BankStore) call ["hydrateSession", [_uid, "atm", false]];
};
if (_context isEqualTo createHashMap) exitWith { false };
_context
}]
]];
GVAR(BankValidator)

View File

@ -31,7 +31,7 @@ GVAR(PermissionServiceBaseClass) = compileFinal createHashMapFromArray [
if (_actor isEqualTo createHashMap) exitWith { false };
private _orgID = _actor getOrDefault ["organization", "default"];
private _org = EGVAR(org,Registry) getOrDefault [_orgID, createHashMap];
private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith { false };
private _owner = _org getOrDefault ["owner", ""];

View File

@ -69,9 +69,12 @@ GVAR(MEconomyStore) = createHashMapObject [[
if (isNull _unit) exitWith { ["WARNING", format ["Invalid unit provided: %1", (name _unit)], nil, nil] call EFUNC(common,log); };
private _uid = getPlayerUID _unit;
private _account = EGVAR(bank,Registry) get _uid;
private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]];
if (_account isEqualTo createHashMap) then {
_account = EGVAR(bank,BankStore) call ["init", [_uid]];
};
if (isNil "_account") exitWith { ["ERROR", format ["No account found for %1. UID: %2", (name _unit), _uid], nil, nil] call EFUNC(common,log); };
if (_account isEqualTo createHashMap) exitWith { ["ERROR", format ["No account found for %1. UID: %2", (name _unit), _uid], nil, nil] call EFUNC(common,log); };
private _bank = _account get "bank";
private _cash = _account get "cash";

View File

@ -1,2 +1,3 @@
PREP(extCall);
PREP(setHandler);
PREP(transport);

View File

@ -4,7 +4,7 @@
* File: fnc_extCall.sqf
* Author: IDSolutions
* Date: 2026-01-03
* Last Update: 2026-01-03
* Last Update: 2026-04-01
* Public: No
*
* Description:
@ -27,14 +27,91 @@ params [["_function", "", [""]], ["_arguments", [], [[]]]];
["INFO", format ["Calling function: %1", _function], nil, nil] call EFUNC(common,log);
private _functionLower = toLower _function;
private _chunkPrefix = "FORGE_TRANSPORT_CHUNK:";
private _chunkPrefixLength = count toArray _chunkPrefix;
private _unsupportedRoutePrefix = "Error: Unsupported transport route";
private _requestChunkSize = 12000;
private _transportResponseFunctions = [
"actor:get",
"actor:create",
"actor:update",
"actor:hot:init",
"actor:hot:get",
"actor:hot:save",
"bank:get",
"bank:create",
"bank:update",
"bank:hot:init",
"bank:hot:get",
"bank:hot:save",
"cad:view:hydrate",
"cad:groups:build",
"cad:assignments:list",
"cad:orders:list",
"cad:requests:list",
"cad:activity:recent",
"org:members:get",
"org:assets:get",
"org:fleet:get"
];
private _requiresRedis = !(_functionLower in ["status", "version"])
&& (_functionLower find "icom:" == 0)
&& (_functionLower find "terrain:" == 0);
if (_requiresRedis) then {
("forge_server" callExtension ["status", []]) params ["_redisStatus", "_statusExtCode", "_statusArmaCode"];
private _callExtensionCommand = {
params [["_command", "", [""]], ["_commandArguments", [], [[]]]];
("forge_server" callExtension [_command, _commandArguments]) params [
"_response",
"_responseExtCode",
"_responseArmaCode"
];
private _responseSuccess = true;
if (_responseArmaCode != 0 && _responseArmaCode != 301) then {
_responseSuccess = false;
private _armaCodeMessage = createHashMapFromArray [
[101, "SYNTAX_ERROR_WRONG_PARAMS_SIZE"],
[102, "SYNTAX_ERROR_WRONG_PARAMS_TYPE"],
[201, "PARAMS_ERROR_TOO_MANY_ARGS"],
[400, "EXTENSION_LOAD_FAILED"],
[403, "EXTENSION_BLOCKED_BY_BATTLEYE"],
[404, "EXTENSION_NOT_FOUND"]
] getOrDefault [_responseArmaCode, format ["UNKNOWN_%1", _responseArmaCode]];
["WARNING", format ["Arma error: %1", _armaCodeMessage], nil, nil] call EFUNC(common,log);
};
if (_responseExtCode != 0) then {
_responseSuccess = false;
if (_responseExtCode == -1) exitWith {
["WARNING", "Extension not available", nil, nil] call EFUNC(common,log);
[_response, false]
};
if (_responseExtCode == 9) exitWith {
["WARNING", format ["Extension error: %1", _response], nil, nil] call EFUNC(common,log);
[_response, false]
};
["WARNING", format ["Extension error: %1", _responseExtCode], nil, nil] call EFUNC(common,log);
};
[_response, _responseSuccess]
};
private _checkRedisAvailability = {
("forge_server" callExtension ["status", []]) params [
"_redisStatus",
"_statusExtCode",
"_statusArmaCode"
];
private _statusSuccess = (_statusExtCode == 0) && (_statusArmaCode == 0 || _statusArmaCode == 301);
if (!_statusSuccess) exitWith {
["WARNING", "Unable to determine Redis status before extension call", nil, nil] call EFUNC(common,log);
["Error: Redis status check failed", false]
@ -44,32 +121,81 @@ if (_requiresRedis) then {
["WARNING", format ["Blocked extension call '%1' because Redis status is '%2'", _function, _redisStatus], nil, nil] call EFUNC(common,log);
[format ["Error: Redis is %1", _redisStatus], false]
};
["", true]
};
("forge_server" callExtension [_function, _arguments]) params ["_result", "_extCode", "_armaCode"];
private _buildTransportArgumentsJson = {
params [["_rawArguments", [], [[]]]];
private _success = true;
private _stringArguments = _rawArguments apply {
if (_x isEqualType "") exitWith { _x };
if (_x isEqualType true) exitWith { ["false", "true"] select _x };
str _x
};
if (_armaCode != 0 && _armaCode != 301) then {
_success = false;
private _armaCodeMessage = createHashMapFromArray [
[101, "SYNTAX_ERROR_WRONG_PARAMS_SIZE"],
[102, "SYNTAX_ERROR_WRONG_PARAMS_TYPE"],
[201, "PARAMS_ERROR_TOO_MANY_ARGS"],
// [301, "EXECUTION_WARNING_TAKES_TOO_LONG"],
[400, "EXTENSION_LOAD_FAILED"],
[403, "EXTENSION_BLOCKED_BY_BATTLEYE"],
[404, "EXTENSION_NOT_FOUND"]
] getOrDefault [_armaCode, format ["UNKNOWN_%1", _armaCode]];
["WARNING", format ["Arma error: %1", _armaCodeMessage], nil, nil] call EFUNC(common,log);
if !(_stringArguments isEqualType []) then {
_stringArguments = [_stringArguments];
};
private _encodedArguments = [];
{
_encodedArguments pushBack (toJSON _x);
} forEach _stringArguments;
format ["[%1]", _encodedArguments joinString ","]
};
if (_extCode != 0) then {
_success = false;
if (_extCode == -1) exitWith { ["WARNING", "Extension not available", nil, nil] call EFUNC(common,log); };
if (_extCode == 9) exitWith { ["WARNING", format ["Extension error: %1", _result], nil, nil] call EFUNC(common,log); };
if (_requiresRedis) exitWith {
[_function, _arguments] call _checkRedisAvailability params ["_redisResult", "_redisSuccess"];
if (!_redisSuccess) exitWith { [_redisResult, false] };
["WARNING", format ["Extension error: %1", _extCode], nil, nil] call EFUNC(common,log);
if (_functionLower in ["status", "version"]) exitWith {
[_function, _arguments] call _callExtensionCommand
};
[_function, _arguments] call _callExtensionCommand
};
[_result, _success]
if (_functionLower in ["status", "version"]) exitWith {
[_function, _arguments] call _callExtensionCommand
};
private _argumentsJson = [_arguments] call _buildTransportArgumentsJson;
private _usesTransportResponse = _functionLower in _transportResponseFunctions;
private _usesChunkedRequest = (count toArray _argumentsJson) > _requestChunkSize;
if !(_usesTransportResponse || { _usesChunkedRequest }) exitWith {
[_function, _arguments] call _callExtensionCommand
};
private _transportCommand = "transport:invoke";
private _transportArguments = [_function, _argumentsJson];
if (_usesChunkedRequest) then {
["stage", _function, _argumentsJson, _requestChunkSize, _callExtensionCommand] call FUNC(transport) params [
"_stagedTransportCommand",
"_stagedTransportArguments",
"_stageSuccess"
];
if (!_stageSuccess) exitWith {
["Error: Failed to stage chunked extension request", false]
};
_transportCommand = _stagedTransportCommand;
_transportArguments = _stagedTransportArguments;
};
[_transportCommand, _transportArguments] call _callExtensionCommand params ["_result", "_success"];
if (
_success
&& { _result isEqualType "" }
&& { (_result find _unsupportedRoutePrefix) == 0 }
&& { !_usesChunkedRequest }
) exitWith {
[_function, _arguments] call _callExtensionCommand
};
["assemble", _result, _success, _chunkPrefix, _chunkPrefixLength, _callExtensionCommand] call FUNC(transport)

View File

@ -0,0 +1,115 @@
#include "..\script_component.hpp"
/*
* File: fnc_transport.sqf
* Author: IDSolutions
* Date: 2026-04-01
* Public: No
*
* Description:
* Shared transport helper for staging oversized requests and assembling
* chunked responses.
*
* Parameter(s):
* 0: Mode <STRING>
* "stage": 1=function, 2=argumentsJson, 3=chunkSize, 4=invoker
* "assemble": 1=response, 2=success, 3=chunkPrefix, 4=chunkPrefixLength, 5=invoker
*
* Returns:
* Depends on mode.
*/
params [["_mode", "", [""]]];
switch (_mode) do {
case "stage": {
_this params [
"_mode",
["_transportFunction", "", [""]],
["_argumentsJson", "", [""]],
["_requestChunkSize", 12000, [0]],
["_callExtensionCommand", {}, [{}]]
];
private _transferID = format [
"req_%1_%2",
floor (diag_tickTime * 1000),
floor (random 1000000000)
];
for "_offset" from 0 to ((count toArray _argumentsJson) - 1) step _requestChunkSize do {
private _chunk = _argumentsJson select [_offset, _requestChunkSize];
["transport:request:append", [_transferID, _chunk]] call _callExtensionCommand params [
"_appendResult",
"_appendSuccess"
];
if (!_appendSuccess || { !(_appendResult isEqualType "") } || { (_appendResult find "Error:") == 0 }) exitWith {
_transferID = "";
};
};
if (_transferID isEqualTo "") exitWith {
["", [], false]
};
[
"transport:invoke_stored",
[_transportFunction, _transferID],
true
]
};
case "assemble": {
_this params [
"_mode",
["_response", "", [""]],
["_responseSuccess", false, [true]],
["_chunkPrefix", "", [""]],
["_chunkPrefixLength", 0, [0]],
["_callExtensionCommand", {}, [{}]]
];
if !(_responseSuccess && { _response isEqualType "" } && { (_response find _chunkPrefix) == 0 }) exitWith {
[_response, _responseSuccess]
};
private _chunkEnvelope = fromJSON (_response select [_chunkPrefixLength]);
if !(_chunkEnvelope isEqualType createHashMap) exitWith {
["Error: Invalid extension chunk envelope", false]
};
private _transferID = _chunkEnvelope getOrDefault ["transferId", ""];
private _chunkCount = _chunkEnvelope getOrDefault ["chunkCount", 0];
if (_transferID isEqualTo "" || { !(_chunkCount isEqualType 0) } || { _chunkCount < 1 }) exitWith {
["Error: Invalid extension chunk metadata", false]
};
private _assembledResponse = "";
private _chunkReadSuccess = true;
for "_index" from 0 to (_chunkCount - 1) do {
["transport:response:get", [_transferID, str _index]] call _callExtensionCommand params [
"_chunkResult",
"_chunkSuccess"
];
if (!_chunkSuccess || { !(_chunkResult isEqualType "") } || { (_chunkResult find "Error:") == 0 }) exitWith {
_chunkReadSuccess = false;
_assembledResponse = "Error: Failed to retrieve chunked extension response";
};
_assembledResponse = _assembledResponse + _chunkResult;
};
["transport:response:clear", [_transferID]] call _callExtensionCommand;
[_assembledResponse, _chunkReadSuccess]
};
default {
["Error: Unsupported extension transport mode", false]
};
};

View File

@ -18,7 +18,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" };
private _finalData = GVAR(GarageStore) call ["get", [GVAR(Registry), "garage:get", _uid, _field]];
private _finalData = GVAR(GarageStore) call ["get", [_uid, _field]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncGarage), [_finalData], _player] call CFUNC(targetEvent);
@ -29,7 +29,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "" || _key isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID or Key!" };
private _hashMap = GVAR(GarageStore) call ["set", [GVAR(Registry), "garage:update", _uid, _key, _value, _sync]];
private _hashMap = GVAR(GarageStore) call ["set", [_uid, _key, _value, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncGarage), [_hashMap], _player] call CFUNC(targetEvent);
@ -41,7 +41,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid field pairs!" };
private _hashMap = GVAR(GarageStore) call ["mset", [GVAR(Registry), "garage:update", _uid, _fieldValuePairs, _sync]];
private _hashMap = GVAR(GarageStore) call ["mset", [_uid, _fieldValuePairs, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncGarage), [_hashMap], _player] call CFUNC(targetEvent);
@ -52,7 +52,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" };
private _finalData = GVAR(GarageStore) call ["save", [GVAR(Registry), "garage:update", _uid]];
private _finalData = GVAR(GarageStore) call ["save", [_uid]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncGarage), [_finalData], _player] call CFUNC(targetEvent);
@ -62,7 +62,7 @@ PREP_RECOMPILE_END;
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" };
GVAR(GarageStore) call ["remove", [GVAR(Registry), _uid]];
GVAR(GarageStore) call ["remove", [_uid]];
}] call CFUNC(addEventHandler);
[QGVAR(requestStoreVehicle), {
@ -90,18 +90,15 @@ PREP_RECOMPILE_END;
["hit_points", fromJSON _hitPointsJson]
]);
["garage:add", [_uid, _payloadJson]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
private _garage = GVAR(GarageStore) call ["storeVehicle", [_uid, _payloadJson]];
if (_garage isEqualTo createHashMap) exitWith {
[CRPC(garage,responseGarageAction), [createHashMapFromArray [
["action", "store"],
["success", false],
["message", format ["Failed to store vehicle: %1", _result]]
["message", "Failed to store vehicle."]
]], _player] call CFUNC(targetEvent);
};
private _garage = fromJSON _result;
GVAR(Registry) set [_uid, _garage];
[CRPC(garage,responseSyncGarage), [_garage], _player] call CFUNC(targetEvent);
[CRPC(garage,responseGarageAction), [createHashMapFromArray [
["action", "store"],
@ -123,18 +120,15 @@ PREP_RECOMPILE_END;
};
private _payloadJson = toJSON (createHashMapFromArray [["plate", _plate]]);
["garage:remove", [_uid, _payloadJson]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
private _garage = GVAR(GarageStore) call ["retrieveVehicle", [_uid, _payloadJson]];
if (_garage isEqualTo createHashMap) exitWith {
[CRPC(garage,responseGarageAction), [createHashMapFromArray [
["action", "retrieve"],
["success", false],
["message", format ["Failed to retrieve vehicle: %1", _result]]
["message", "Failed to retrieve vehicle."]
]], _player] call CFUNC(targetEvent);
};
private _garage = fromJSON _result;
GVAR(Registry) set [_uid, _garage];
[CRPC(garage,responseSyncGarage), [_garage], _player] call CFUNC(targetEvent);
[CRPC(garage,responseGarageAction), [createHashMapFromArray [
["action", "retrieve"],
@ -155,7 +149,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" };
private _finalData = GVAR(VGarageStore) call ["get", [GVAR(VGRegistry), "owned:garage:fetch", _uid, _field]];
private _finalData = GVAR(VGarageStore) call ["get", [_uid, _field]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncVG), [_finalData], _player] call CFUNC(targetEvent);
@ -166,7 +160,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "" || _key isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID or Key!" };
private _hashMap = GVAR(VGarageStore) call ["set", [GVAR(VGRegistry), "", _uid, _key, _value, _sync]];
private _hashMap = GVAR(VGarageStore) call ["set", [_uid, _key, _value, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncVG), [_hashMap], _player] call CFUNC(targetEvent);
@ -178,7 +172,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid field pairs!" };
private _hashMap = GVAR(VGarageStore) call ["mset", [GVAR(VGRegistry), "", _uid, _fieldValuePairs, _sync]];
private _hashMap = GVAR(VGarageStore) call ["mset", [_uid, _fieldValuePairs, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncVG), [_hashMap], _player] call CFUNC(targetEvent);
@ -189,7 +183,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" };
private _finalData = GVAR(VGarageStore) call ["save", [GVAR(VGRegistry), "", _uid]];
private _finalData = GVAR(VGarageStore) call ["save", [_uid]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(garage,responseSyncVG), [_finalData], _player] call CFUNC(targetEvent);
@ -199,5 +193,5 @@ PREP_RECOMPILE_END;
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" };
GVAR(VGarageStore) call ["remove", [GVAR(VGRegistry), _uid]];
GVAR(VGarageStore) call ["remove", [_uid]];
}] call CFUNC(addEventHandler);

View File

@ -4,12 +4,12 @@
* File: fnc_initGarageStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-02-13
* Last Update: 2026-04-01
* Public: No
*
* Description:
* Initializes the Garage store for managing player vehicles.
* Provides methods for syncing, saving, and applying vehicles to the player's garage.
* Garage hot state is owned by the extension; SQF acts as a thin bridge.
*
* Arguments:
* None
@ -26,50 +26,151 @@ GVAR(GarageBaseStore) = compileFinal createHashMapFromArray [
["#base", EGVAR(common,BaseStore)],
["#type", "GarageBaseStore"],
["#create", compileFinal {
GVAR(Registry) = createHashMap;
["INFO", "Garage Store Initialized!"] call EFUNC(common,log);
}],
["callHotGarage", compileFinal {
params [["_function", "", [""]], ["_arguments", [], [[]]]];
if (_function isEqualTo "") exitWith { createHashMap };
[_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith { createHashMap };
if !(_result isEqualType "") exitWith { createHashMap };
if ((_result find "Error:") == 0) exitWith {
["ERROR", format ["Garage extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log);
createHashMap
};
private _data = fromJSON _result;
if !(_data isEqualType createHashMap) exitWith { createHashMap };
_data
}],
["loadHotGarage", compileFinal {
params [["_uid", "", [""]], ["_initialize", false, [false]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _command = ["garage:hot:get", "garage:hot:init"] select _initialize;
_self call ["callHotGarage", [_command, [_uid]]]
}],
["init", compileFinal {
params [["_uid", "", [""]]];
private _player = [_uid] call EFUNC(common,getPlayer);
private _cached = GVAR(Registry) getOrDefault [_uid, nil];
if !(isNil { _cached }) exitWith { [CRPC(garage,responseInitGarage), [_cached], _player] call CFUNC(targetEvent); _cached };
if (isNull _player) exitWith { createHashMap };
["garage:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to check if garage %1 exists! Using fallback garage.", _uid]] call EFUNC(common,log);
private _fallbackGarage = createHashMap;
GVAR(Registry) set [_uid, _fallbackGarage];
[CRPC(garage,responseInitGarage), [_fallbackGarage], _player] call CFUNC(targetEvent);
_fallbackGarage
private _garage = _self call ["loadHotGarage", [_uid, true]];
if (_garage isEqualTo createHashMap) then {
["ERROR", format ["Failed to initialize garage for %1! Using fallback garage.", _uid]] call EFUNC(common,log);
};
private _finalGarage = createHashMap;
[CRPC(garage,responseInitGarage), [_garage], _player] call CFUNC(targetEvent);
_garage
}],
["get", compileFinal {
params [["_uid", "", [""]], ["_field", "", [""]]];
if (_result == "true") then {
_finalGarage = _self call ["fetch", ["garage:get", _uid]];
["INFO", format ["Found garage for %1", _uid]] call EFUNC(common,log);
} else {
["garage:create", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to create garage for %1! Using fallback garage.", _uid]] call EFUNC(common,log);
private _garage = _self call ["loadHotGarage", [_uid, false]];
if (_garage isEqualTo createHashMap) then {
_garage = _self call ["loadHotGarage", [_uid, true]];
};
GVAR(Registry) set [_uid, _finalGarage];
[CRPC(garage,responseInitGarage), [_finalGarage], _player] call CFUNC(targetEvent);
if (_field isEqualTo "") exitWith { _garage };
_garage getOrDefault [_field, createHashMap]
}],
["override", compileFinal {
params [
["_uid", "", [""]],
["_data", createHashMap, [createHashMap]],
["_save", false, [false]]
];
_finalGarage
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_data isEqualType createHashMap) exitWith { createHashMap };
private _garage = _self call ["callHotGarage", ["garage:hot:override", [_uid, toJSON _data]]];
if (_save && { _garage isNotEqualTo createHashMap }) then {
private _savedGarage = _self call ["callHotGarage", ["garage:hot:save", [_uid]]];
if (_savedGarage isNotEqualTo createHashMap) then {
_garage = _savedGarage;
} else {
_garage = createHashMap;
};
["INFO", format ["Created new garage for %1", _uid]] call EFUNC(common,log);
};
GVAR(Registry) set [_uid, _finalGarage];
[CRPC(garage,responseInitGarage), [_finalGarage], _player] call CFUNC(targetEvent);
_garage
}],
["set", compileFinal {
params [
["_uid", "", [""]],
["_field", "", [""]],
["_value", nil, [0, "", [], false, createHashMap, objNull, grpNull]],
["_sync", false, [false]]
];
_finalGarage
if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
private _garage = _self call ["get", [_uid, ""]];
if !(_garage isEqualType createHashMap) exitWith { createHashMap };
_garage set [_field, _value];
private _updatedGarage = _self call ["override", [_uid, _garage, _sync]];
if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap };
if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap };
createHashMapFromArray [[_field, _updatedGarage getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
params [
["_uid", "", [""]],
["_fieldValuePairs", createHashMap, [createHashMap]],
["_sync", false, [false]]
];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap };
private _garage = _self call ["get", [_uid, ""]];
if !(_garage isEqualType createHashMap) exitWith { createHashMap };
{ _garage set [_x, _y]; } forEach _fieldValuePairs;
private _updatedGarage = _self call ["override", [_uid, _garage, _sync]];
if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap };
if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap };
+_fieldValuePairs
}],
["save", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
_self call ["callHotGarage", ["garage:hot:save", [_uid]]]
}],
["remove", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { false };
["garage:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
_isSuccess && { _result isEqualTo "OK" }
}],
["storeVehicle", compileFinal {
params [
["_uid", "", [""]],
["_payloadJson", "", [""]]
];
if (_uid isEqualTo "" || { _payloadJson isEqualTo "" }) exitWith { createHashMap };
_self call ["callHotGarage", ["garage:hot:add", [_uid, _payloadJson]]]
}],
["retrieveVehicle", compileFinal {
params [
["_uid", "", [""]],
["_payloadJson", "", [""]]
];
if (_uid isEqualTo "" || { _payloadJson isEqualTo "" }) exitWith { createHashMap };
_self call ["callHotGarage", ["garage:hot:remove_vehicle", [_uid, _payloadJson]]]
}]
];

View File

@ -4,12 +4,12 @@
* File: fnc_initVGStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-02-13
* Last Update: 2026-04-01
* Public: No
*
* Description:
* Initializes the Virtual Garage store for managing player vehicle unlocks.
* Provides methods for syncing, saving, and applying virtual vehicles to BIS Garage.
* Virtual garage hot state is owned by the extension; SQF acts as a thin bridge.
*
* Arguments:
* None
@ -42,55 +42,134 @@ GVAR(VGBaseStore) = compileFinal createHashMapFromArray [
["#base", EGVAR(common,BaseStore)],
["#type", "VGBaseStore"],
["#create", compileFinal {
GVAR(VGRegistry) = createHashMap;
["INFO", "VGarage Store Initialized!"] call EFUNC(common,log);
}],
["callHotVGarage", compileFinal {
params [["_function", "", [""]], ["_arguments", [], [[]]]];
if (_function isEqualTo "") exitWith { createHashMap };
[_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith { createHashMap };
if !(_result isEqualType "") exitWith { createHashMap };
if ((_result find "Error:") == 0) exitWith {
["ERROR", format ["VGarage extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log);
createHashMap
};
private _data = fromJSON _result;
if !(_data isEqualType createHashMap) exitWith { createHashMap };
_data
}],
["loadHotVGarage", compileFinal {
params [["_uid", "", [""]], ["_initialize", false, [false]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _command = ["owned:garage:hot:fetch", "owned:garage:hot:init"] select _initialize;
_self call ["callHotVGarage", [_command, [_uid]]]
}],
["init", compileFinal {
params [["_uid", "", [""]]];
private _player = [_uid] call EFUNC(common,getPlayer);
private _cached = GVAR(VGRegistry) getOrDefault [_uid, nil];
if !(isNil { _cached }) exitWith {
[CRPC(garage,responseInitVG), [_cached], _player] call CFUNC(targetEvent);
_cached
if (isNull _player) exitWith { createHashMap };
private _garage = _self call ["loadHotVGarage", [_uid, true]];
if (_garage isEqualTo createHashMap) then {
_garage = GVAR(VGarageModel) call ["defaults", []];
["ERROR", format ["Failed to initialize virtual garage for %1! Using fallback virtual garage.", _uid]] call EFUNC(common,log);
};
["owned:garage:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to check if virtual garage %1 exists! Using fallback virtual garage.", _uid]] call EFUNC(common,log);
[CRPC(garage,responseInitVG), [_garage], _player] call CFUNC(targetEvent);
_garage
}],
["get", compileFinal {
params [["_uid", "", [""]], ["_field", "", [""]]];
private _fallbackVGarage = GVAR(VGarageModel) call ["defaults", []];
GVAR(VGRegistry) set [_uid, _fallbackVGarage];
[CRPC(garage,responseInitVG), [_fallbackVGarage], _player] call CFUNC(targetEvent);
_fallbackVGarage
private _garage = _self call ["loadHotVGarage", [_uid, false]];
if (_garage isEqualTo createHashMap) then {
_garage = _self call ["loadHotVGarage", [_uid, true]];
};
private _finalVGarage = createHashMap;
if (_field isEqualTo "") exitWith { _garage };
_garage getOrDefault [_field, []]
}],
["override", compileFinal {
params [
["_uid", "", [""]],
["_data", createHashMap, [createHashMap]],
["_save", false, [false]]
];
if (_result == "true") then {
_finalVGarage = _self call ["fetch", ["owned:garage:fetch", _uid]];
["INFO", format ["Found virtual garage for %1", _uid]] call EFUNC(common,log);
} else {
_finalVGarage = GVAR(VGarageModel) call ["defaults", []];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_data isEqualType createHashMap) exitWith { createHashMap };
["owned:garage:create", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to create virtual garage for %1! Using fallback virtual garage.", _uid]] call EFUNC(common,log);
GVAR(VGRegistry) set [_uid, _finalVGarage];
[CRPC(garage,responseInitVG), [_finalVGarage], _player] call CFUNC(targetEvent);
_finalVGarage
private _garage = _self call ["callHotVGarage", ["owned:garage:hot:override", [_uid, toJSON _data]]];
if (_save && { _garage isNotEqualTo createHashMap }) then {
private _savedGarage = _self call ["callHotVGarage", ["owned:garage:hot:save", [_uid]]];
if (_savedGarage isNotEqualTo createHashMap) then {
_garage = _savedGarage;
} else {
_garage = createHashMap;
};
["INFO", format ["Created new virtual garage for %1", _uid]] call EFUNC(common,log);
};
GVAR(VGRegistry) set [_uid, _finalVGarage];
[CRPC(garage,responseInitVG), [_finalVGarage], _player] call CFUNC(targetEvent);
_garage
}],
["set", compileFinal {
params [
["_uid", "", [""]],
["_field", "", [""]],
["_value", nil, [[], "", 0, false, createHashMap]],
["_sync", false, [false]]
];
_finalVGarage
if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
private _garage = _self call ["loadHotVGarage", [_uid, false]];
if !(_garage isEqualType createHashMap) exitWith { createHashMap };
_garage set [_field, _value];
private _updatedGarage = _self call ["override", [_uid, _garage, _sync]];
if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap };
if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap };
createHashMapFromArray [[_field, _updatedGarage getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
params [
["_uid", "", [""]],
["_fieldValuePairs", createHashMap, [createHashMap]],
["_sync", false, [false]]
];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap };
private _garage = _self call ["loadHotVGarage", [_uid, false]];
if !(_garage isEqualType createHashMap) exitWith { createHashMap };
{ _garage set [_x, _y]; } forEach _fieldValuePairs;
private _updatedGarage = _self call ["override", [_uid, _garage, _sync]];
if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap };
if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap };
+_fieldValuePairs
}],
["save", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
_self call ["callHotVGarage", ["owned:garage:hot:save", [_uid]]]
}],
["remove", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { false };
["owned:garage:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
_isSuccess && { _result isEqualTo "OK" }
}],
["grantVehicles", compileFinal {
params [["_uid", "", [""]], ["_vehicles", [], [[]]], ["_commit", false, [false]]];
@ -103,8 +182,11 @@ GVAR(VGBaseStore) = compileFinal createHashMapFromArray [
["garage", createHashMap]
];
private _defaultGarage = GVAR(VGarageModel) call ["defaults", []];
private _garage = +(GVAR(VGRegistry) getOrDefault [_uid, _defaultGarage]);
private _garage = +(_self call ["loadHotVGarage", [_uid, false]]);
if (_garage isEqualTo createHashMap) then {
_garage = GVAR(VGarageModel) call ["defaults", []];
};
private _patch = createHashMap;
private _granted = [];
private _categoriesToSync = [];
@ -136,7 +218,18 @@ GVAR(VGBaseStore) = compileFinal createHashMapFromArray [
_patch set [_category, _garage getOrDefault [_category, []]];
} forEach _categoriesToSync;
if (_commit) then { GVAR(VGRegistry) set [_uid, _garage]; };
if (_commit) then {
private _savedGarage = _self call ["override", [_uid, _garage, false]];
if !(_savedGarage isEqualType createHashMap) exitWith {
_result set ["message", "Virtual garage cache update returned invalid data."];
_result
};
if (_savedGarage isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to update virtual garage cache."];
_result
};
_garage = _savedGarage;
};
_result set ["success", true];
_result set ["message", ""];

View File

@ -18,7 +18,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" };
private _finalData = GVAR(LockerStore) call ["get", [GVAR(Registry), "locker:get", _uid, _field]];
private _finalData = GVAR(LockerStore) call ["get", [_uid, _field]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncLocker), [_finalData], _player] call CFUNC(targetEvent);
@ -29,7 +29,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID or Field!" };
private _hashMap = GVAR(LockerStore) call ["set", [GVAR(Registry), "locker:update", _uid, _field, _value, _sync]];
private _hashMap = GVAR(LockerStore) call ["set", [_uid, _field, _value, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncLocker), [_hashMap], _player] call CFUNC(targetEvent);
@ -41,7 +41,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid field pairs!" };
private _hashMap = GVAR(LockerStore) call ["mset", [GVAR(Registry), "locker:update", _uid, _fieldValuePairs, _sync]];
private _hashMap = GVAR(LockerStore) call ["mset", [_uid, _fieldValuePairs, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncLocker), [_hashMap], _player] call CFUNC(targetEvent);
@ -52,7 +52,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" };
private _finalData = GVAR(LockerStore) call ["save", [GVAR(Registry), "locker:update", _uid]];
private _finalData = GVAR(LockerStore) call ["save", [_uid]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncLocker), [_finalData], _player] call CFUNC(targetEvent);
@ -62,10 +62,10 @@ PREP_RECOMPILE_END;
params [["_uid", "", [""]], ["_data", createHashMap, [createHashMap]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" };
GVAR(Registry) set [_uid, _data];
private _finalData = GVAR(LockerStore) call ["override", [_uid, _data, false]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncLocker), [_data], _player] call CFUNC(targetEvent);
[CRPC(locker,responseSyncLocker), [_finalData], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestRemoveLocker), {
@ -87,7 +87,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" };
private _finalData = GVAR(VAStore) call ["get", [GVAR(VARegistry), "owned:locker:fetch", _uid, _field]];
private _finalData = GVAR(VAStore) call ["get", [_uid, _field]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent);
@ -98,7 +98,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID or Field!" };
private _hashMap = GVAR(VAStore) call ["set", [GVAR(VARegistry), "owned:locker:update", _uid, _field, _value, _sync]];
private _hashMap = GVAR(VAStore) call ["set", [_uid, _field, _value, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent);
@ -110,7 +110,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid field pairs!" };
private _hashMap = GVAR(VAStore) call ["mset", [GVAR(VARegistry), "owned:locker:update", _uid, _fieldValuePairs, _sync]];
private _hashMap = GVAR(VAStore) call ["mset", [_uid, _fieldValuePairs, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent);
@ -121,7 +121,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" };
private _finalData = GVAR(VAStore) call ["save", [GVAR(VARegistry), "owned:locker:update", _uid]];
private _finalData = GVAR(VAStore) call ["save", [_uid]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent);
@ -131,5 +131,5 @@ PREP_RECOMPILE_END;
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" };
GVAR(VAStore) call ["remove", [GVAR(VARegistry), _uid]];
GVAR(VAStore) call ["remove", [_uid]];
}] call CFUNC(addEventHandler);

View File

@ -4,12 +4,12 @@
* File: fnc_initLockerStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-02-13
* Last Update: 2026-04-01
* Public: No
*
* Description:
* Initializes the Locker store for managing player locker items.
* Provides methods for syncing, saving, and applying locker items to the player's locker.
* Locker hot state is owned by the extension; SQF acts as a thin bridge.
*
* Arguments:
* None
@ -26,50 +26,133 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [
["#base", EGVAR(common,BaseStore)],
["#type", "LockerBaseStore"],
["#create", compileFinal {
GVAR(Registry) = createHashMap;
["INFO", "Locker Store Initialized!"] call EFUNC(common,log);
}],
["callHotLocker", compileFinal {
params [["_function", "", [""]], ["_arguments", [], [[]]]];
if (_function isEqualTo "") exitWith { createHashMap };
[_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith { createHashMap };
if !(_result isEqualType "") exitWith { createHashMap };
if ((_result find "Error:") == 0) exitWith {
["ERROR", format ["Locker extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log);
createHashMap
};
private _data = fromJSON _result;
if !(_data isEqualType createHashMap) exitWith { createHashMap };
_data
}],
["loadHotLocker", compileFinal {
params [["_uid", "", [""]], ["_initialize", false, [false]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _command = ["locker:hot:get", "locker:hot:init"] select _initialize;
_self call ["callHotLocker", [_command, [_uid]]]
}],
["init", compileFinal {
params [["_uid", "", [""]]];
private _player = [_uid] call EFUNC(common,getPlayer);
private _cached = GVAR(Registry) getOrDefault [_uid, nil];
if !(isNil { _cached }) exitWith { [CRPC(locker,responseInitLocker), [_cached], _player] call CFUNC(targetEvent); _cached };
if (isNull _player) exitWith { createHashMap };
["locker:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to check if locker %1 exists! Using fallback locker.", _uid]] call EFUNC(common,log);
private _fallbackLocker = createHashMap;
GVAR(Registry) set [_uid, _fallbackLocker];
[CRPC(locker,responseInitLocker), [_fallbackLocker], _player] call CFUNC(targetEvent);
_fallbackLocker
private _locker = _self call ["loadHotLocker", [_uid, true]];
if (_locker isEqualTo createHashMap) then {
["ERROR", format ["Failed to initialize locker for %1! Using fallback locker.", _uid]] call EFUNC(common,log);
};
private _finalLocker = createHashMap;
[CRPC(locker,responseInitLocker), [_locker], _player] call CFUNC(targetEvent);
_locker
}],
["get", compileFinal {
params [["_uid", "", [""]], ["_field", "", [""]]];
if (_result == "true") then {
_finalLocker = _self call ["fetch", ["locker:get", _uid]];
["INFO", format ["Found locker for %1", _uid]] call EFUNC(common,log);
} else {
["locker:create", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to create locker for %1! Using fallback locker.", _uid]] call EFUNC(common,log);
private _locker = _self call ["loadHotLocker", [_uid, false]];
if (_locker isEqualTo createHashMap) then {
_locker = _self call ["loadHotLocker", [_uid, true]];
};
GVAR(Registry) set [_uid, _finalLocker];
[CRPC(locker,responseInitLocker), [_finalLocker], _player] call CFUNC(targetEvent);
if (_field isEqualTo "") exitWith { _locker };
_locker getOrDefault [_field, createHashMap]
}],
["override", compileFinal {
params [
["_uid", "", [""]],
["_data", createHashMap, [createHashMap]],
["_save", false, [false]]
];
_finalLocker
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_data isEqualType createHashMap) exitWith { createHashMap };
private _locker = _self call ["callHotLocker", ["locker:hot:override", [_uid, toJSON _data]]];
if (_save && { _locker isNotEqualTo createHashMap }) then {
private _savedLocker = _self call ["callHotLocker", ["locker:hot:save", [_uid]]];
if (_savedLocker isNotEqualTo createHashMap) then {
_locker = _savedLocker;
} else {
_locker = createHashMap;
};
["INFO", format ["Created new locker for %1", _uid]] call EFUNC(common,log);
};
GVAR(Registry) set [_uid, _finalLocker];
[CRPC(locker,responseInitLocker), [_finalLocker], _player] call CFUNC(targetEvent);
_locker
}],
["set", compileFinal {
params [
["_uid", "", [""]],
["_field", "", [""]],
["_value", nil, [0, "", [], false, createHashMap, objNull, grpNull]],
["_sync", false, [false]]
];
_finalLocker
if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
private _locker = _self call ["get", [_uid, ""]];
if !(_locker isEqualType createHashMap) exitWith { createHashMap };
_locker set [_field, _value];
private _updatedLocker = _self call ["override", [_uid, _locker, _sync]];
if !(_updatedLocker isEqualType createHashMap) exitWith { createHashMap };
if (_updatedLocker isEqualTo createHashMap) exitWith { createHashMap };
createHashMapFromArray [[_field, _updatedLocker getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
params [
["_uid", "", [""]],
["_fieldValuePairs", createHashMap, [createHashMap]],
["_sync", false, [false]]
];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap };
private _locker = _self call ["get", [_uid, ""]];
if !(_locker isEqualType createHashMap) exitWith { createHashMap };
{ _locker set [_x, _y]; } forEach _fieldValuePairs;
private _updatedLocker = _self call ["override", [_uid, _locker, _sync]];
if !(_updatedLocker isEqualType createHashMap) exitWith { createHashMap };
if (_updatedLocker isEqualTo createHashMap) exitWith { createHashMap };
+_fieldValuePairs
}],
["save", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
_self call ["callHotLocker", ["locker:hot:save", [_uid]]]
}],
["remove", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { false };
["locker:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
_isSuccess && { _result isEqualTo "OK" }
}],
["grantItems", compileFinal {
params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]];
@ -82,7 +165,7 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [
["locker", createHashMap]
];
private _locker = +(GVAR(Registry) getOrDefault [_uid, createHashMap]);
private _locker = +(_self call ["get", [_uid, ""]]);
private _patch = createHashMap;
private _granted = [];
@ -124,7 +207,19 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [
_result set ["message", "Locker capacity would exceed 25 unique items. Clear space before checkout."];
_result
};
if (_commit) then { GVAR(Registry) set [_uid, _locker]; };
if (_commit) then {
private _savedLocker = _self call ["override", [_uid, _locker, false]];
if !(_savedLocker isEqualType createHashMap) exitWith {
_result set ["message", "Locker cache update returned invalid data."];
_result
};
if (_savedLocker isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to update locker cache."];
_result
};
_locker = _savedLocker;
};
_result set ["success", true];
_result set ["message", ""];

View File

@ -4,12 +4,12 @@
* File: fnc_initVAStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-03-27
* Last Update: 2026-04-01
* Public: No
*
* Description:
* Initializes the Virtual Arsenal store for managing player arsenal unlocks.
* Provides methods for syncing, saving, and applying virtual items to BIS Arsenal.
* Virtual arsenal hot state is owned by the extension; SQF acts as a thin bridge.
*
* Arguments:
* None
@ -40,55 +40,134 @@ GVAR(VABaseStore) = compileFinal createHashMapFromArray [
["#base", EGVAR(common,BaseStore)],
["#type", "VABaseStore"],
["#create", compileFinal {
GVAR(VARegistry) = createHashMap;
["INFO", "VArsenal Store Initialized!"] call EFUNC(common,log);
}],
["callHotVArsenal", compileFinal {
params [["_function", "", [""]], ["_arguments", [], [[]]]];
if (_function isEqualTo "") exitWith { createHashMap };
[_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith { createHashMap };
if !(_result isEqualType "") exitWith { createHashMap };
if ((_result find "Error:") == 0) exitWith {
["ERROR", format ["VArsenal extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log);
createHashMap
};
private _data = fromJSON _result;
if !(_data isEqualType createHashMap) exitWith { createHashMap };
_data
}],
["loadHotVArsenal", compileFinal {
params [["_uid", "", [""]], ["_initialize", false, [false]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _command = ["owned:locker:hot:fetch", "owned:locker:hot:init"] select _initialize;
_self call ["callHotVArsenal", [_command, [_uid]]]
}],
["init", compileFinal {
params [["_uid", "", [""]]];
private _player = [_uid] call EFUNC(common,getPlayer);
private _cached = GVAR(VARegistry) getOrDefault [_uid, nil];
if !(isNil { _cached }) exitWith {
[CRPC(locker,responseInitVA), [_cached], _player] call CFUNC(targetEvent);
_cached
if (isNull _player) exitWith { createHashMap };
private _arsenal = _self call ["loadHotVArsenal", [_uid, true]];
if (_arsenal isEqualTo createHashMap) then {
_arsenal = GVAR(VArsenalModel) call ["defaults", []];
["ERROR", format ["Failed to initialize virtual arsenal for %1! Using fallback virtual arsenal.", _uid]] call EFUNC(common,log);
};
["owned:locker:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to check if virtual arsenal %1 exists! Using fallback virtual arsenal.", _uid]] call EFUNC(common,log);
[CRPC(locker,responseInitVA), [_arsenal], _player] call CFUNC(targetEvent);
_arsenal
}],
["get", compileFinal {
params [["_uid", "", [""]], ["_field", "", [""]]];
private _fallbackVArsenal = GVAR(VArsenalModel) call ["defaults", []];
GVAR(VARegistry) set [_uid, _fallbackVArsenal];
[CRPC(locker,responseInitVA), [_fallbackVArsenal], _player] call CFUNC(targetEvent);
_fallbackVArsenal
private _arsenal = _self call ["loadHotVArsenal", [_uid, false]];
if (_arsenal isEqualTo createHashMap) then {
_arsenal = _self call ["loadHotVArsenal", [_uid, true]];
};
private _finalVArsenal = createHashMap;
if (_field isEqualTo "") exitWith { _arsenal };
_arsenal getOrDefault [_field, []]
}],
["override", compileFinal {
params [
["_uid", "", [""]],
["_data", createHashMap, [createHashMap]],
["_save", false, [false]]
];
if (_result == "true") then {
_finalVArsenal = _self call ["fetch", ["owned:locker:fetch", _uid]];
["INFO", format ["Found virtual arsenal for %1", _uid]] call EFUNC(common,log);
} else {
_finalVArsenal = GVAR(VArsenalModel) call ["defaults", []];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_data isEqualType createHashMap) exitWith { createHashMap };
["owned:locker:create", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to create virtual arsenal for %1! Using fallback virtual arsenal.", _uid]] call EFUNC(common,log);
GVAR(VARegistry) set [_uid, _finalVArsenal];
[CRPC(locker,responseInitVA), [_finalVArsenal], _player] call CFUNC(targetEvent);
_finalVArsenal
private _arsenal = _self call ["callHotVArsenal", ["owned:locker:hot:override", [_uid, toJSON _data]]];
if (_save && { _arsenal isNotEqualTo createHashMap }) then {
private _savedArsenal = _self call ["callHotVArsenal", ["owned:locker:hot:save", [_uid]]];
if (_savedArsenal isNotEqualTo createHashMap) then {
_arsenal = _savedArsenal;
} else {
_arsenal = createHashMap;
};
["INFO", format ["Created new virtual arsenal for %1", _uid]] call EFUNC(common,log);
};
GVAR(VARegistry) set [_uid, _finalVArsenal];
[CRPC(locker,responseInitVA), [_finalVArsenal], _player] call CFUNC(targetEvent);
_arsenal
}],
["set", compileFinal {
params [
["_uid", "", [""]],
["_field", "", [""]],
["_value", nil, [[], "", 0, false, createHashMap]],
["_sync", false, [false]]
];
_finalVArsenal
if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
private _arsenal = _self call ["get", [_uid, ""]];
if !(_arsenal isEqualType createHashMap) exitWith { createHashMap };
_arsenal set [_field, _value];
private _updatedArsenal = _self call ["override", [_uid, _arsenal, _sync]];
if !(_updatedArsenal isEqualType createHashMap) exitWith { createHashMap };
if (_updatedArsenal isEqualTo createHashMap) exitWith { createHashMap };
createHashMapFromArray [[_field, _updatedArsenal getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
params [
["_uid", "", [""]],
["_fieldValuePairs", createHashMap, [createHashMap]],
["_sync", false, [false]]
];
if (_uid isEqualTo "") exitWith { createHashMap };
if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap };
private _arsenal = _self call ["get", [_uid, ""]];
if !(_arsenal isEqualType createHashMap) exitWith { createHashMap };
{ _arsenal set [_x, _y]; } forEach _fieldValuePairs;
private _updatedArsenal = _self call ["override", [_uid, _arsenal, _sync]];
if !(_updatedArsenal isEqualType createHashMap) exitWith { createHashMap };
if (_updatedArsenal isEqualTo createHashMap) exitWith { createHashMap };
+_fieldValuePairs
}],
["save", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
_self call ["callHotVArsenal", ["owned:locker:hot:save", [_uid]]]
}],
["remove", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { false };
["owned:locker:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
_isSuccess && { _result isEqualTo "OK" }
}],
["unlockItems", compileFinal {
params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]];
@ -100,8 +179,10 @@ GVAR(VABaseStore) = compileFinal createHashMapFromArray [
["arsenal", createHashMap]
];
private _defaultArsenal = GVAR(VArsenalModel) call ["defaults", []];
private _arsenal = +(GVAR(VARegistry) getOrDefault [_uid, _defaultArsenal]);
private _arsenal = +(_self call ["get", [_uid, ""]]);
if (_arsenal isEqualTo createHashMap) then {
_arsenal = GVAR(VArsenalModel) call ["defaults", []];
};
private _patch = createHashMap;
private _categoriesToSync = [];
@ -129,7 +210,18 @@ GVAR(VABaseStore) = compileFinal createHashMapFromArray [
_patch set [_category, _categoryUnlocks];
} forEach _categoriesToSync;
if (_commit) then { GVAR(VARegistry) set [_uid, _arsenal]; };
if (_commit) then {
private _savedArsenal = _self call ["override", [_uid, _arsenal, false]];
if !(_savedArsenal isEqualType createHashMap) exitWith {
_result set ["message", "Virtual arsenal cache update returned invalid data."];
_result
};
if (_savedArsenal isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to update virtual arsenal cache."];
_result
};
_arsenal = _savedArsenal;
};
_result set ["success", true];
_result set ["message", ""];

View File

@ -1 +1,2 @@
PREP(initStores);
PREP(saveHotState);

View File

@ -63,4 +63,16 @@ addMissionEventHandler ["PlayerConnected", {
addMissionEventHandler ["PlayerDisconnected", {
params ["_id", "_uid", "_name", "_jip", "_owner", "_idStr"];
if (_uid isEqualTo "") exitWith {};
[_uid] call FUNC(saveHotState);
}];
addMissionEventHandler ["Ended", {
[""] call FUNC(saveHotState);
}];
addMissionEventHandler ["MPEnded", {
[""] call FUNC(saveHotState);
}];

View File

@ -26,8 +26,8 @@ if (isNil QEGVAR(actor,ActorStore)) then { call EFUNC(actor,initActorStore); };
if (isNil QEGVAR(bank,BankSessionManager)) then { call EFUNC(bank,initSessionManager); };
if (isNil QEGVAR(bank,BankMessenger)) then { call EFUNC(bank,initMessenger); };
if (isNil QEGVAR(bank,BankModel)) then { call EFUNC(bank,initModel); };
if (isNil QEGVAR(bank,BankPayloadBuilder)) then { call EFUNC(bank,initPayloadBuilder); };
if (isNil QEGVAR(bank,BankStore)) then { call EFUNC(bank,initStore); };
if (isNil QEGVAR(bank,BankValidator)) then { call EFUNC(bank,initValidator); };
// Garage
if (isNil QEGVAR(garage,GarageStore)) then { call EFUNC(garage,initGarageStore); };

View File

@ -0,0 +1,84 @@
#include "..\script_component.hpp"
/*
* File: fnc_saveHotState.sqf
* Author: IDSolutions
* Date: 2026-04-01
* Public: No
*
* Description:
* Flushes extension-backed hot state for a single UID or every known UID.
*
* Arguments:
* 0: UID to flush. Empty string flushes all known players. <STRING>
*
* Return Value:
* True if the flush routine completed. <BOOL>
*/
params [["_uid", "", [""]]];
private _uids = [];
if (_uid isEqualTo "") then {
{
if (isNull _x) then { continue; };
private _playerUid = getPlayerUID _x;
if (_playerUid isNotEqualTo "") then {
_uids pushBackUnique _playerUid;
};
} forEach allPlayers;
if !(isNil QEGVAR(actor,Registry)) then {
{
if (_x isNotEqualTo "") then {
_uids pushBackUnique _x;
};
} forEach keys EGVAR(actor,Registry);
};
} else {
_uids pushBack _uid;
};
{
private _flushUid = _x;
if (_flushUid isEqualTo "") then { continue; };
private _orgID = "default";
if !(isNil QEGVAR(org,OrgStore)) then {
_orgID = EGVAR(org,OrgStore) call ["resolveOrgIdForUid", [_flushUid]];
if (_orgID isEqualTo "") then {
_orgID = "default";
};
};
if !(isNil QEGVAR(actor,ActorStore)) then {
EGVAR(actor,ActorStore) call ["snapshot", [_flushUid]];
EGVAR(actor,ActorStore) call ["save", [_flushUid]];
};
if !(isNil QEGVAR(bank,BankStore)) then {
EGVAR(bank,BankStore) call ["save", [_flushUid]];
};
if !(isNil QEGVAR(locker,LockerStore)) then {
EGVAR(locker,LockerStore) call ["save", [_flushUid]];
};
if !(isNil QEGVAR(locker,VAStore)) then {
EGVAR(locker,VAStore) call ["save", [_flushUid]];
};
if !(isNil QEGVAR(garage,GarageStore)) then {
EGVAR(garage,GarageStore) call ["save", [_flushUid]];
};
if !(isNil QEGVAR(garage,VGarageStore)) then {
EGVAR(garage,VGarageStore) call ["save", [_flushUid]];
};
if !(isNil QEGVAR(org,OrgStore)) then {
EGVAR(org,OrgStore) call ["saveById", [_orgID]];
};
} forEach _uids;
true

View File

@ -57,9 +57,8 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" };
private _index = GVAR(IndexRegistry) get _uid;
private _key = _index get "orgID";
private _finalData = GVAR(OrgStore) call ["get", [GVAR(Registry), _key, _field]];
private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]];
private _finalData = GVAR(OrgStore) call ["get", [_key, _field]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(org,responseSyncOrg), [_finalData], _player] call CFUNC(targetEvent);
@ -70,9 +69,8 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID or Field!" };
private _index = GVAR(IndexRegistry) get _uid;
private _key = _index get "orgID";
GVAR(OrgStore) call ["set", [GVAR(Registry), "org:update", _key, _field, _value, _sync]];
private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]];
GVAR(OrgStore) call ["set", [_key, _field, _value, _sync]];
}] call CFUNC(addEventHandler);
[QGVAR(requestMSetOrg), {
@ -81,10 +79,9 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid field pairs!" };
private _index = GVAR(IndexRegistry) get _uid;
private _key = _index get "orgID";
private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]];
GVAR(OrgStore) call ["mset", [GVAR(Registry), "org:update", _key, _fieldValuePairs, _sync]];
GVAR(OrgStore) call ["mset", [_key, _fieldValuePairs, _sync]];
}] call CFUNC(addEventHandler);
[QGVAR(requestAssignCreditLine), {
@ -125,8 +122,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" };
private _index = GVAR(IndexRegistry) get _uid;
private _key = _index get "orgID";
private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]];
GVAR(OrgStore) call ["saveById", [_key]];
}] call CFUNC(addEventHandler);
@ -135,8 +131,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" };
private _index = GVAR(IndexRegistry) get _uid;
private _key = _index get "orgID";
private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]];
GVAR(OrgStore) call ["delete", [_key]];
}] call CFUNC(addEventHandler);

View File

@ -4,12 +4,12 @@
* File: fnc_initOrgStore.sqf
* Author: IDSolutions
* Date: 2026-02-13
* Last Update: 2026-03-13
* Last Update: 2026-04-01
* Public: Yes
*
* Description:
* Initializes the org store for managing player organizations.
* Provides methods for creating, fetching, and updating organizations.
* Org hot state is owned by the extension; SQF acts as the bridge.
*
* Arguments:
* None
@ -118,8 +118,6 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["#base", EGVAR(common,BaseStore)],
["#type", "OrgBaseStore"],
["#create", compileFinal {
GVAR(IndexRegistry) = createHashMap;
GVAR(Registry) = createHashMap;
["INFO", "Org Store Initialized!"] call EFUNC(common,log);
["org:exists", ["default"]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
@ -137,34 +135,170 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["fleet", createHashMap],
["members", createHashMap]
];
GVAR(Registry) set ["default", _defaultOrg];
_defaultOrg
};
private _defaultOrg = createHashMap;
if (_result == "true") then {
_defaultOrg = _self call ["fetch", ["org:get", "default"]];
} else {
_defaultOrg set ["id", "default"];
_defaultOrg set ["owner", "server"];
_defaultOrg set ["name", "Forge Dynamics"];
_defaultOrg set ["funds", 200000];
_defaultOrg set ["reputation", 0];
_defaultOrg set ["credit_lines", createHashMap];
if (_result != "true") then {
private _defaultOrg = createHashMapFromArray [
["id", "default"],
["owner", "server"],
["name", "Forge Dynamics"],
["funds", 200000],
["reputation", 0],
["credit_lines", createHashMap],
["assets", createHashMap],
["fleet", createHashMap],
["members", createHashMap]
];
private _defaultJson = _self call ["toJSON", [_defaultOrg]];
["org:create", ["default", _defaultJson]] call EFUNC(extension,extCall);
};
_defaultOrg = GVAR(OrgModel) call ["migrate", [_defaultOrg]];
private _defaultAssets = _self call ["fetch", ["org:assets:get", "default"]];
if !(_defaultAssets isEqualType createHashMap) then { _defaultAssets = createHashMap; };
_defaultOrg set ["assets", _defaultAssets];
private _defaultFleet = _self call ["fetch", ["org:fleet:get", "default"]];
if !(_defaultFleet isEqualType createHashMap) then { _defaultFleet = createHashMap; };
_defaultOrg set ["fleet", _defaultFleet];
GVAR(Registry) set ["default", _defaultOrg];
private _loadedDefaultOrg = _self call ["loadHotOrg", ["default", true]];
if (_loadedDefaultOrg isEqualTo createHashMap) then {
_loadedDefaultOrg = createHashMapFromArray [
["id", "default"],
["owner", "server"],
["name", "Forge Dynamics"],
["funds", 200000],
["reputation", 0],
["credit_lines", createHashMap],
["assets", createHashMap],
["fleet", createHashMap],
["members", createHashMap]
];
};
_loadedDefaultOrg
}],
["callHotOrg", compileFinal {
params [["_function", "", [""]], ["_arguments", [], [[]]]];
if (_function isEqualTo "") exitWith { createHashMap };
[_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith { createHashMap };
if !(_result isEqualType "") exitWith { createHashMap };
if ((_result find "Error:") == 0) exitWith {
["ERROR", format ["Org extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log);
createHashMap
};
private _data = fromJSON _result;
if !(_data isEqualType createHashMap) exitWith { createHashMap };
_self call ["syncHotOrg", [_data]]
}],
["syncHotOrg", compileFinal {
params [["_org", createHashMap, [createHashMap]]];
if !(_org isEqualType createHashMap) exitWith { createHashMap };
private _migratedOrg = GVAR(OrgModel) call ["migrate", [+_org]];
private _orgID = _migratedOrg getOrDefault ["id", ""];
if (_orgID isEqualTo "") exitWith { createHashMap };
_migratedOrg
}],
["resolveOrgIdForUid", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { "default" };
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _orgID = _actor getOrDefault ["organization", "default"];
if (_orgID isEqualTo "") then { _orgID = "default"; };
_orgID
}],
["loadForUid", compileFinal {
params [["_uid", "", [""]]];
private _orgID = _self call ["resolveOrgIdForUid", [_uid]];
_self call ["loadById", [_orgID]]
}],
["loadHotOrg", compileFinal {
params [["_orgID", "", [""]], ["_initialize", false, [false]]];
if (_orgID isEqualTo "") exitWith { createHashMap };
private _command = ["org:hot:get", "org:hot:init"] select _initialize;
_self call ["callHotOrg", [_command, [_orgID]]]
}],
["get", compileFinal {
params [["_orgID", "", [""]], ["_field", "", [""]]];
private _org = _self call ["loadHotOrg", [_orgID, false]];
if (_org isEqualTo createHashMap) then {
_org = _self call ["loadHotOrg", [_orgID, true]];
};
if (_field isEqualTo "") exitWith { _org };
_org getOrDefault [_field, createHashMap]
}],
["override", compileFinal {
params [
["_orgID", "", [""]],
["_org", createHashMap, [createHashMap]],
["_save", false, [false]]
];
if (_orgID isEqualTo "") exitWith { createHashMap };
if !(_org isEqualType createHashMap) exitWith { createHashMap };
private _normalizedOrg = +_org;
_normalizedOrg set ["id", _normalizedOrg getOrDefault ["id", _orgID]];
private _result = _self call ["callHotOrg", ["org:hot:override", [_orgID, toJSON _normalizedOrg]]];
if (_save && { _result isNotEqualTo createHashMap }) then {
private _savedOrg = _self call ["callHotOrg", ["org:hot:save", [_orgID]]];
if (_savedOrg isNotEqualTo createHashMap) then {
_result = _savedOrg;
} else {
_result = createHashMap;
};
};
_result
}],
["set", compileFinal {
params [
["_orgID", "", [""]],
["_field", "", [""]],
["_value", nil, [[], "", 0, false, createHashMap]],
["_sync", false, [false]]
];
if (_orgID isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
private _org = _self call ["get", [_orgID, ""]];
if !(_org isEqualType createHashMap) exitWith { createHashMap };
_org set [_field, _value];
private _updatedOrg = _self call ["override", [_orgID, _org, _sync]];
if !(_updatedOrg isEqualType createHashMap) exitWith { createHashMap };
if (_updatedOrg isEqualTo createHashMap) exitWith { createHashMap };
createHashMapFromArray [[_field, _updatedOrg getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
params [
["_orgID", "", [""]],
["_fieldValuePairs", createHashMap, [createHashMap]],
["_sync", false, [false]]
];
if (_orgID isEqualTo "") exitWith { createHashMap };
if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap };
private _org = _self call ["get", [_orgID, ""]];
if !(_org isEqualType createHashMap) exitWith { createHashMap };
{ _org set [_x, _y]; } forEach _fieldValuePairs;
private _updatedOrg = _self call ["override", [_orgID, _org, _sync]];
if !(_updatedOrg isEqualType createHashMap) exitWith { createHashMap };
if (_updatedOrg isEqualTo createHashMap) exitWith { createHashMap };
+_fieldValuePairs
}],
["verifyMember", compileFinal {
GVAR(OrgMembershipService) call ["verifyMember", _this]
@ -194,7 +328,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result
};
GVAR(Registry) deleteAt _orgID;
["org:hot:remove", [_orgID]] call EFUNC(extension,extCall);
_result set ["success", true];
_result
}],
@ -232,7 +366,6 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
// before shaping the portal payload. This prevents stale org caches from
// omitting the current member while still resolving owner metadata.
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
GVAR(Registry) set [_orgID, _org, true];
private _name = _org getOrDefault ["name", ""];
private _id = _org getOrDefault ["id", _orgID];
@ -357,33 +490,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
if (_orgID isEqualTo "") exitWith { createHashMap };
private _org = GVAR(Registry) getOrDefault [_orgID, createHashMap];
if (_org isEqualTo createHashMap) then {
_org = _self call ["loadById", [_orgID]];
};
if (_org isEqualTo createHashMap) exitWith { createHashMap };
private _coreOrg = createHashMapFromArray [
["id", _org getOrDefault ["id", _orgID]],
["owner", _org getOrDefault ["owner", ""]],
["name", _org getOrDefault ["name", ""]],
["funds", _org getOrDefault ["funds", 0]],
["reputation", _org getOrDefault ["reputation", 0]],
["credit_lines", _org getOrDefault ["credit_lines", createHashMap]]
];
private _coreJson = _self call ["toJSON", [_coreOrg]];
["org:update", [_orgID, _coreJson]] call EFUNC(extension,extCall);
private _assets = _org getOrDefault ["assets", createHashMap];
private _assetsJson = _self call ["toJSON", [_assets]];
["org:assets:update", [_orgID, _assetsJson]] call EFUNC(extension,extCall);
private _fleet = _org getOrDefault ["fleet", createHashMap];
private _fleetJson = _self call ["toJSON", [_fleet]];
["org:fleet:update", [_orgID, _fleetJson]] call EFUNC(extension,extCall);
_org
_self call ["callHotOrg", ["org:hot:save", [_orgID]]]
}],
["addAssets", compileFinal {
params [["_requesterUid", "", [""]], ["_assets", [], [[]]], ["_commit", false, [false]], ["_orgID", "", [""]]];
@ -408,10 +515,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
};
if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; };
private _org = GVAR(Registry) getOrDefault [_resolvedOrgID, createHashMap];
if (_org isEqualTo createHashMap) then {
_org = _self call ["loadById", [_resolvedOrgID]];
};
private _org = _self call ["loadById", [_resolvedOrgID]];
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Organization data is unavailable for asset updates."];
_result
@ -437,17 +541,10 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_assetMap set [_category, _categoryMap];
} forEach _assets;
private _patch = _self call ["mset", [
GVAR(Registry),
"org:update",
_resolvedOrgID,
createHashMapFromArray [["assets", _assetMap]],
false
]];
if (_commit) then {
private _assetJson = _self call ["toJSON", [_assetMap]];
["org:assets:update", [_resolvedOrgID, _assetJson]] call EFUNC(extension,extCall);
private _patch = _self call ["mset", [_resolvedOrgID, createHashMapFromArray [["assets", _assetMap]], false]];
if (_patch isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to update organization asset cache."];
_result
};
_result set ["success", true];
@ -479,10 +576,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
};
if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; };
private _org = GVAR(Registry) getOrDefault [_resolvedOrgID, createHashMap];
if (_org isEqualTo createHashMap) then {
_org = _self call ["loadById", [_resolvedOrgID]];
};
private _org = _self call ["loadById", [_resolvedOrgID]];
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Organization data is unavailable for fleet updates."];
_result
@ -518,17 +612,10 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_fleetIndex = _fleetIndex + 1;
} forEach _vehicles;
private _patch = _self call ["mset", [
GVAR(Registry),
"org:update",
_resolvedOrgID,
createHashMapFromArray [["fleet", _fleet]],
false
]];
if (_commit) then {
private _fleetJson = _self call ["toJSON", [_fleet]];
["org:fleet:update", [_resolvedOrgID, _fleetJson]] call EFUNC(extension,extCall);
private _patch = _self call ["mset", [_resolvedOrgID, createHashMapFromArray [["fleet", _fleet]], false]];
if (_patch isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to update organization fleet cache."];
_result
};
_result set ["success", true];
@ -542,43 +629,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
if (_orgID isEqualTo "") exitWith { createHashMap };
private _cachedOrg = GVAR(Registry) getOrDefault [_orgID, createHashMap];
if (_cachedOrg isNotEqualTo createHashMap) exitWith { _cachedOrg };
["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"];
if (!_existsSuccess || { _existsResult isNotEqualTo "true" }) exitWith { createHashMap };
private _org = _self call ["fetch", ["org:get", _orgID]];
if (_org isEqualTo createHashMap) exitWith { _org };
_org = GVAR(OrgModel) call ["migrate", [_org]];
private _assets = _self call ["fetch", ["org:assets:get", _orgID]];
if !(_assets isEqualType createHashMap) then {
_assets = createHashMap;
};
_org set ["assets", _assets];
private _fleet = _self call ["fetch", ["org:fleet:get", _orgID]];
if !(_fleet isEqualType createHashMap) then {
_fleet = createHashMap;
};
_org set ["fleet", _fleet];
private _memberRows = _self call ["fetch", ["org:members:get", _orgID]];
if !(_memberRows isEqualType []) then {
_memberRows = [];
};
private _memberMap = createHashMap;
{
private _memberUid = _x getOrDefault ["uid", ""];
if (_memberUid isNotEqualTo "") then {
_memberMap set [_memberUid, _x];
};
} forEach _memberRows;
_org set ["members", _memberMap];
GVAR(Registry) set [_orgID, _org, true];
_org
_self call ["loadHotOrg", [_orgID, true]]
}],
["register", compileFinal {
params [["_uid", "", [""]], ["_orgName", "", [""]]];
@ -651,10 +702,36 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
};
};
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, true]];
GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]];
GVAR(Registry) set [_orgID, _org, true];
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, false]];
private _updatedActor = EGVAR(actor,ActorStore) call ["get", [_uid, ""]];
if (
!(_updatedActor isEqualType createHashMap)
|| { _updatedActor isEqualTo createHashMap }
|| { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID }
) then {
private _forcedActor = +_actor;
if !(_forcedActor isEqualType createHashMap) then {
_forcedActor = EGVAR(actor,ActorModel) call ["defaults", []];
_forcedActor set ["uid", _uid];
};
_forcedActor set ["organization", _orgID];
_updatedActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, false]];
if (_updatedActor isEqualType createHashMap && { _updatedActor isNotEqualTo createHashMap }) then {
_actorPatch = createHashMapFromArray [["organization", _orgID]];
};
};
if (
!(_updatedActor isEqualType createHashMap)
|| { _updatedActor isEqualTo createHashMap }
|| { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID }
) exitWith {
_result set ["message", "Failed to assign the player to the new organization."];
_result
};
_org = _self call ["override", [_orgID, _org, false]];
_result set ["success", true];
_result set ["org", _org];
_result set ["actorPatch", _actorPatch];
@ -670,53 +747,18 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_orgID = "default";
};
private _cachedOrg = GVAR(Registry) getOrDefault [_orgID, nil];
if !(isNil { _cachedOrg }) exitWith {
private _cachedOwner = _cachedOrg getOrDefault ["owner", ""];
if (_orgID isEqualTo "default" || { _cachedOwner isEqualTo _uid }) then {
_cachedOrg = _self call ["verifyMember", [_cachedOrg, _orgID, _uid, _player, _actor]];
};
GVAR(Registry) set [_orgID, _cachedOrg, true];
[CRPC(org,responseInitOrg), [_cachedOrg], _player] call CFUNC(targetEvent);
_cachedOrg
};
["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to check for org %1! Using fallback org.", _orgID]] call EFUNC(common,log);
private _fallbackOrg = GVAR(Registry) getOrDefault ["default", createHashMap];
GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]];
if (_orgID isEqualTo "default") then {
_fallbackOrg = _self call ["verifyMember", [_fallbackOrg, _orgID, _uid, _player, _actor]];
};
GVAR(Registry) set [_orgID, _fallbackOrg, true];
[CRPC(org,responseInitOrg), [_fallbackOrg], _player] call CFUNC(targetEvent);
_fallbackOrg
};
private _finalOrg = createHashMap;
if (_result == "true") then {
_finalOrg = _self call ["loadById", [_orgID]];
["INFO", format ["Found org for %1", _orgID]] call EFUNC(common,log);
} else {
private _finalOrg = _self call ["loadById", [_orgID]];
if (_finalOrg isEqualTo createHashMap) then {
["WARNING", format ["No existing org found for %1, using default org.", _uid]] call EFUNC(common,log);
_finalOrg = GVAR(Registry) getOrDefault ["default", createHashMap];
_finalOrg = _self call ["loadById", ["default"]];
_orgID = "default";
};
GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]];
private _finalOwner = _finalOrg getOrDefault ["owner", ""];
if (_orgID isEqualTo "default" || { _finalOwner isEqualTo _uid }) then {
_finalOrg = _self call ["verifyMember", [_finalOrg, _orgID, _uid, _player, _actor]];
};
GVAR(Registry) set [_orgID, _finalOrg, true];
[CRPC(org,responseInitOrg), [_finalOrg], _player] call CFUNC(targetEvent);
_finalOrg

View File

@ -36,6 +36,7 @@ GVAR(OrgMembershipServiceBase) = compileFinal createHashMapFromArray [
private _updatedMembers = +_members;
_updatedMembers set [_uid, createHashMapFromArray [["uid", _uid], ["name", _memberName]]];
_org set ["members", _updatedMembers];
_org = GVAR(OrgStore) call ["override", [_orgID, _org, false]];
_org
}],
@ -48,7 +49,6 @@ GVAR(OrgMembershipServiceBase) = compileFinal createHashMapFromArray [
if (_org isEqualTo createHashMap) exitWith { _org };
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
GVAR(Registry) set [_orgID, _org, true];
_org
}],
@ -69,7 +69,7 @@ GVAR(OrgMembershipServiceBase) = compileFinal createHashMapFromArray [
private _updatedMembers = +(_org getOrDefault ["members", createHashMap]);
_updatedMembers deleteAt _uid;
_org set ["members", _updatedMembers];
GVAR(Registry) set [_orgID, _org, true];
_org = GVAR(OrgStore) call ["override", [_orgID, _org, false]];
_org
}],
@ -88,15 +88,45 @@ GVAR(OrgMembershipServiceBase) = compileFinal createHashMapFromArray [
};
private _resolvedActor = EGVAR(actor,Registry) getOrDefault [_uid, _actor];
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", "default", true]];
private _defaultActor = EGVAR(actor,Registry) getOrDefault [_uid, _resolvedActor];
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", "default", false]];
private _defaultActor = EGVAR(actor,ActorStore) call ["get", [_uid, ""]];
if !(_defaultActor isEqualType createHashMap) then {
_defaultActor = +_resolvedActor;
};
if (
(_defaultActor isEqualTo createHashMap)
|| { toLowerANSI (_defaultActor getOrDefault ["organization", ""]) isNotEqualTo "default" }
) then {
private _forcedActor = +_resolvedActor;
if (_forcedActor isEqualTo createHashMap) then {
_forcedActor = EGVAR(actor,ActorModel) call ["defaults", []];
_forcedActor set ["uid", _uid];
};
_forcedActor set ["organization", "default"];
_defaultActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, false]];
if (_defaultActor isEqualType createHashMap && { _defaultActor isNotEqualTo createHashMap }) then {
_actorPatch = createHashMapFromArray [["organization", "default"]];
};
};
if (
!(_defaultActor isEqualType createHashMap)
|| { _defaultActor isEqualTo createHashMap }
|| { toLowerANSI (_defaultActor getOrDefault ["organization", ""]) isNotEqualTo "default" }
) exitWith {
_result set ["message", "Failed to restore default organization membership."];
_result
};
private _defaultOrg = _self call ["addMember", ["default", _uid, _resolvedPlayer, _defaultActor]];
if (_defaultOrg isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to restore default organization membership."];
_result
};
GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", "default"]]];
_result set ["success", true];
_result set ["actorPatch", _actorPatch];
_result

View File

@ -86,7 +86,7 @@ GVAR(OrgTreasuryServiceBase) = compileFinal createHashMapFromArray [
["amount", _amount]
]];
private _patch = GVAR(OrgStore) call ["set", [GVAR(Registry), "org:update", _orgID, "credit_lines", _creditLines, true]];
private _patch = GVAR(OrgStore) call ["set", [_orgID, "credit_lines", _creditLines, false]];
private _memberUids = _self call ["resolveOrgMemberUids", [_org, _requesterUid]];
_result set ["success", true];
@ -103,7 +103,7 @@ GVAR(OrgTreasuryServiceBase) = compileFinal createHashMapFromArray [
private _orgID = _requesterActor getOrDefault ["organization", "default"];
if (_orgID isEqualTo "") then { _orgID = "default"; };
private _org = GVAR(Registry) getOrDefault [_orgID, createHashMap];
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Organization data is unavailable for checkout."];
_result
@ -125,7 +125,7 @@ GVAR(OrgTreasuryServiceBase) = compileFinal createHashMapFromArray [
};
private _patch = createHashMapFromArray [["funds", (_funds - _amount)]];
if (_commit) then { _patch = GVAR(OrgStore) call ["mset", [GVAR(Registry), "org:update", _orgID, _patch, false]]; };
if (_commit) then { _patch = GVAR(OrgStore) call ["mset", [_orgID, _patch, false]]; };
_result set ["success", true];
_result set ["message", ""];
@ -147,7 +147,7 @@ GVAR(OrgTreasuryServiceBase) = compileFinal createHashMapFromArray [
_creditLines set [_requesterUid, _memberCredit];
private _patch = createHashMapFromArray [["credit_lines", _creditLines]];
if (_commit) then { _patch = GVAR(OrgStore) call ["mset", [GVAR(Registry), "org:update", _orgID, _patch, false]]; };
if (_commit) then { _patch = GVAR(OrgStore) call ["mset", [_orgID, _patch, false]]; };
_result set ["success", true];
_result set ["message", ""];

View File

@ -40,7 +40,7 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
private _isDefaultOrg = false;
private _isDefaultOrgCeo = false;
private _bankAccount = EGVAR(bank,Registry) getOrDefault [_uid, createHashMap];
private _bankAccount = EGVAR(bank,BankStore) call ["get", [_uid, ""]];
if (_bankAccount isEqualTo createHashMap) then {
_bankAccount = EGVAR(bank,BankStore) call ["init", [_uid]];
};

View File

@ -96,10 +96,7 @@ private _syncOrgPatch = {
};
if (_funds > 0) then {
private _org = EGVAR(org,Registry) getOrDefault [_orgID, createHashMap];
if (_org isEqualTo createHashMap) then {
_org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
};
private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) then {
["ERROR", format ["Failed to load organization %1 for task %2 funds reward.", _orgID, _taskID]] call EFUNC(common,log);
@ -108,8 +105,6 @@ if (_funds > 0) then {
private _patch = EGVAR(org,OrgStore) call [
"set",
[
EGVAR(org,Registry),
"org:update",
_orgID,
"funds",
((_org getOrDefault ["funds", 0]) + _funds),
@ -203,7 +198,7 @@ if (count _vehicles > 0) then {
if (_success) then {
private _orgName = "";
private _org = EGVAR(org,Registry) getOrDefault [_orgID, createHashMap];
private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
if (_org isNotEqualTo createHashMap) then {
_orgName = _org getOrDefault ["name", _orgID];
};

View File

@ -35,11 +35,7 @@ if (_minRating > 0) then {
private _orgID = _requesterActor getOrDefault ["organization", "default"];
if (_orgID isEqualTo "") then { _orgID = "default"; };
private _org = EGVAR(org,Registry) getOrDefault [_orgID, createHashMap];
if (_org isEqualTo createHashMap) then {
_org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
};
private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
private _orgReputation = _org getOrDefault ["reputation", 0];
if (_orgReputation < _minRating) exitWith {
private _message = format ["Organization reputation of %1 does not meet the minimum required reputation of %2.", _orgReputation, _minRating];

View File

@ -351,11 +351,7 @@ GVAR(TaskStore) = createHashMapObject [[
private _resolvedOrgID = _ownership getOrDefault ["orgID", ""];
if (_resolvedOrgID isEqualTo "") exitWith { _result };
private _org = EGVAR(org,Registry) getOrDefault [_resolvedOrgID, createHashMap];
if (_org isEqualTo createHashMap) then {
_org = EGVAR(org,OrgStore) call ["loadById", [_resolvedOrgID]];
};
private _org = EGVAR(org,OrgStore) call ["loadById", [_resolvedOrgID]];
private _memberUids = [];
if (_org isNotEqualTo createHashMap) then {
_memberUids = EGVAR(org,OrgTreasuryService) call ["resolveOrgMemberUids", [_org, _requesterUid]];
@ -448,32 +444,39 @@ GVAR(TaskStore) = createHashMapObject [[
private _participantSnapshots = +(_participantRegistry getOrDefault [_taskID, createHashMap]);
if (_participantSnapshots isEqualTo createHashMap) exitWith { _result };
private _rewardContext = _self call ["resolveRewardContext", [_taskID]];
private _participantUids = keys _participantSnapshots;
if (_participantUids isEqualTo [] && { _delta > 0 }) then {
private _requesterUid = _rewardContext getOrDefault ["requesterUid", ""];
if (_requesterUid isNotEqualTo "") then {
private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
if (!isNull _requesterPlayer) then {
_participantUids pushBack _requesterUid;
_participantSnapshots set [_requesterUid, createHashMapFromArray [
["startRating", rating _requesterPlayer]
]];
_participantRegistry set [_taskID, _participantSnapshots];
_self set ["participantRegistry", _participantRegistry];
["WARNING", format ["Task %1 had no tracked participants at payout time; falling back to requester %2 for personal earnings.", _taskID, _requesterUid]] call EFUNC(common,log);
};
};
};
if (_participantUids isEqualTo []) exitWith { _result };
private _orgIds = [];
private _contributions = createHashMap;
private _totalContribution = 0;
{
private _uid = _x;
private _player = [_uid] call EFUNC(common,getPlayer);
if (isNull _player) then { continue; };
if (_delta > 0) then {
{
private _uid = _x;
private _player = [_uid] call EFUNC(common,getPlayer);
if (isNull _player) then { continue; };
private _snapshot = _participantSnapshots getOrDefault [_uid, createHashMap];
private _startRating = _snapshot getOrDefault ["startRating", rating _player];
private _ratingDelta = (rating _player) - _startRating;
private _contribution = _ratingDelta max 0;
if (_delta < 0) then {
_contribution = (0 - _ratingDelta) max 0;
};
if (_contribution <= 0) then { continue; };
_contributions set [_uid, _contribution];
_totalContribution = _totalContribution + _contribution;
} forEach _participantUids;
_contributions set [_uid, 1];
_totalContribution = _totalContribution + 1;
} forEach _participantUids;
};
if (_totalContribution <= 0) exitWith {
_self call ["clearTask", [_taskID]];
@ -496,7 +499,7 @@ GVAR(TaskStore) = createHashMapObject [[
private _contribution = _contributions getOrDefault [_uid, 0];
if (_contribution <= 0) then { continue; };
private _account = EGVAR(bank,Registry) getOrDefault [_uid, createHashMap];
private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]];
if (_account isEqualTo createHashMap) then {
_account = EGVAR(bank,BankStore) call ["init", [_uid]];
};
@ -509,26 +512,22 @@ GVAR(TaskStore) = createHashMapObject [[
private _patch = EGVAR(bank,BankStore) call [
"mset",
[
EGVAR(bank,Registry),
"bank:update",
_uid,
createHashMapFromArray [["earnings", (_earnings + _earningsDelta)]],
false
]
];
if !(_patch isEqualType createHashMap) then { continue; };
if (_patch isEqualTo createHashMap) then { continue; };
EGVAR(bank,BankMessenger) call ["sendAccountSync", [_uid, _patch]];
};
};
} forEach _participantUids;
private _rewardContext = _self call ["resolveRewardContext", [_taskID]];
private _ownerOrgID = _rewardContext getOrDefault ["orgID", ""];
if (_ownerOrgID isNotEqualTo "") then {
private _org = EGVAR(org,Registry) getOrDefault [_ownerOrgID, createHashMap];
if (_org isEqualTo createHashMap) then {
_org = EGVAR(org,OrgStore) call ["loadById", [_ownerOrgID]];
};
private _org = EGVAR(org,OrgStore) call ["loadById", [_ownerOrgID]];
if (_org isNotEqualTo createHashMap) then {
private _reputation = _org getOrDefault ["reputation", 0];
@ -536,8 +535,6 @@ GVAR(TaskStore) = createHashMapObject [[
private _patch = EGVAR(org,OrgStore) call [
"set",
[
EGVAR(org,Registry),
"org:update",
_ownerOrgID,
"reputation",
_nextReputation,

View File

@ -4,8 +4,8 @@
//! Handles SQF command mapping and parameter validation.
use arma_rs::{CallContext, Group};
use forge_repositories::RedisActorRepository;
use forge_services::ActorService;
use forge_repositories::{InMemoryActorHotRepository, RedisActorRepository};
use forge_services::{ActorHotStateService, ActorService};
use std::sync::LazyLock;
use crate::adapters::ExtensionRedisClient;
@ -21,6 +21,14 @@ static ACTOR_SERVICE: LazyLock<ActorService<RedisActorRepository<ExtensionRedisC
let repository = RedisActorRepository::new(redis_client);
ActorService::new(repository)
});
static HOT_ACTOR_SERVICE: LazyLock<
ActorHotStateService<RedisActorRepository<ExtensionRedisClient>, InMemoryActorHotRepository>,
> = LazyLock::new(|| {
let redis_client = ExtensionRedisClient::new();
let repository = RedisActorRepository::new(redis_client);
let hot_repository = InMemoryActorHotRepository::new();
ActorHotStateService::new(repository, hot_repository)
});
/// Creates the Arma 3 command group for actor operations.
///
@ -32,6 +40,86 @@ pub fn group() -> Group {
.command("update", update_actor)
.command("exists", actor_exists)
.command("delete", delete_actor)
.group(
"hot",
Group::new()
.command("init", init_hot_actor)
.command("get", get_hot_actor)
.command("override", override_hot_actor)
.command("save", save_hot_actor)
.command("remove", remove_hot_actor),
)
}
fn serialize_hot_actor(actor: forge_models::Actor) -> String {
match serde_json::to_string(&actor) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize hot actor: {}", error),
}
}
pub(crate) fn init_hot_actor(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_ACTOR_SERVICE.init_actor(resolved_uid) {
Ok(actor) => serialize_hot_actor(actor),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn get_hot_actor(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_ACTOR_SERVICE.get_actor(resolved_uid) {
Ok(actor) => serialize_hot_actor(actor),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn override_hot_actor(
call_context: CallContext,
key: String,
json_data: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_ACTOR_SERVICE.override_actor(resolved_uid, json_data) {
Ok(actor) => serialize_hot_actor(actor),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn save_hot_actor(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_ACTOR_SERVICE.save_actor(resolved_uid) {
Ok(saved_actor) => serialize_hot_actor(saved_actor),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn remove_hot_actor(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_ACTOR_SERVICE.remove_actor(resolved_uid) {
Ok(_) => "OK".to_string(),
Err(error) => format!("Error: {}", error),
}
}
/// Retrieves an actor by key/UID.

View File

@ -4,8 +4,12 @@
//! Handles SQF command mapping and parameter validation.
use arma_rs::{CallContext, Group};
use forge_repositories::RedisBankRepository;
use forge_services::BankService;
use forge_models::{
BankMutationResult, BankOperationContext, BankPinContext, BankTransferContext,
BankTransferResult,
};
use forge_repositories::{InMemoryBankHotRepository, RedisBankRepository};
use forge_services::{BankHotStateService, BankService};
use std::sync::LazyLock;
use crate::adapters::ExtensionRedisClient;
@ -21,6 +25,14 @@ static BANK_SERVICE: LazyLock<BankService<RedisBankRepository<ExtensionRedisClie
let repository = RedisBankRepository::new(redis_client);
BankService::new(repository)
});
static HOT_BANK_SERVICE: LazyLock<
BankHotStateService<RedisBankRepository<ExtensionRedisClient>, InMemoryBankHotRepository>,
> = LazyLock::new(|| {
let redis_client = ExtensionRedisClient::new();
let repository = RedisBankRepository::new(redis_client);
let hot_repository = InMemoryBankHotRepository::new();
BankHotStateService::new(repository, hot_repository)
});
/// Creates the Arma 3 command group for bank operations.
///
@ -32,6 +44,286 @@ pub fn group() -> Group {
.command("update", update_bank)
.command("exists", bank_exists)
.command("delete", delete_bank)
.group(
"hot",
Group::new()
.command("init", init_hot_bank)
.command("get", get_hot_bank)
.command("override", override_hot_bank)
.command("patch", patch_hot_bank)
.command("deposit", deposit_hot_bank)
.command("withdraw", withdraw_hot_bank)
.command("payment", payment_hot_bank)
.command("deposit_earnings", deposit_earnings_hot_bank)
.command("transfer", transfer_hot_bank)
.command("validate_pin", validate_pin_hot_bank)
.command("save", save_hot_bank)
.command("remove", remove_hot_bank),
)
}
fn serialize_hot_bank(bank: forge_models::Bank) -> String {
match serde_json::to_string(&bank) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize hot bank: {}", error),
}
}
fn serialize_hot_bank_mutation(result: BankMutationResult) -> String {
match serde_json::to_string(&result) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize hot bank mutation: {}", error),
}
}
fn serialize_hot_bank_transfer(result: BankTransferResult) -> String {
match serde_json::to_string(&result) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize hot bank transfer: {}", error),
}
}
fn parse_amount(amount: String, label: &str) -> Result<f64, String> {
amount
.parse::<f64>()
.map_err(|error| format!("Invalid {} amount '{}': {}", label, amount, error))
}
fn parse_operation_context(json_context: String) -> Result<BankOperationContext, String> {
serde_json::from_str(&json_context)
.map_err(|error| format!("Invalid bank operation context: {}", error))
}
fn parse_transfer_context(json_context: String) -> Result<BankTransferContext, String> {
serde_json::from_str(&json_context)
.map_err(|error| format!("Invalid bank transfer context: {}", error))
}
fn parse_pin_context(json_context: String) -> Result<BankPinContext, String> {
serde_json::from_str(&json_context)
.map_err(|error| format!("Invalid bank PIN context: {}", error))
}
pub(crate) fn init_hot_bank(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_BANK_SERVICE.init_bank(resolved_uid) {
Ok(bank) => serialize_hot_bank(bank),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn get_hot_bank(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_BANK_SERVICE.get_bank(resolved_uid) {
Ok(bank) => serialize_hot_bank(bank),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn override_hot_bank(
call_context: CallContext,
key: String,
json_data: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_BANK_SERVICE.override_bank(resolved_uid.clone(), json_data) {
Ok(bank) => serialize_hot_bank(bank),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn patch_hot_bank(call_context: CallContext, key: String, json_patch: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_BANK_SERVICE.patch_bank(resolved_uid, json_patch) {
Ok(result) => serialize_hot_bank_mutation(result),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn deposit_hot_bank(
call_context: CallContext,
key: String,
amount: String,
json_context: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let amount = match parse_amount(amount, "deposit") {
Ok(value) => value,
Err(error) => return format!("Error: {}", error),
};
let context = match parse_operation_context(json_context) {
Ok(value) => value,
Err(error) => return format!("Error: {}", error),
};
match HOT_BANK_SERVICE.deposit(resolved_uid, amount, context) {
Ok(result) => serialize_hot_bank_mutation(result),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn withdraw_hot_bank(
call_context: CallContext,
key: String,
amount: String,
json_context: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let amount = match parse_amount(amount, "withdraw") {
Ok(value) => value,
Err(error) => return format!("Error: {}", error),
};
let context = match parse_operation_context(json_context) {
Ok(value) => value,
Err(error) => return format!("Error: {}", error),
};
match HOT_BANK_SERVICE.withdraw(resolved_uid, amount, context) {
Ok(result) => serialize_hot_bank_mutation(result),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn payment_hot_bank(call_context: CallContext, key: String, amount: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let amount = match parse_amount(amount, "payment") {
Ok(value) => value,
Err(error) => return format!("Error: {}", error),
};
match HOT_BANK_SERVICE.payment(resolved_uid, amount) {
Ok(result) => serialize_hot_bank_mutation(result),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn deposit_earnings_hot_bank(
call_context: CallContext,
key: String,
amount: String,
json_context: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let amount = match parse_amount(amount, "deposit earnings") {
Ok(value) => value,
Err(error) => return format!("Error: {}", error),
};
let context = match parse_operation_context(json_context) {
Ok(value) => value,
Err(error) => return format!("Error: {}", error),
};
match HOT_BANK_SERVICE.deposit_earnings(resolved_uid, amount, context) {
Ok(result) => serialize_hot_bank_mutation(result),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn transfer_hot_bank(
call_context: CallContext,
source_key: String,
target_key: String,
amount: String,
json_context: String,
) -> String {
let resolved_source_uid = match resolve_uid(&source_key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", source_key),
};
let resolved_target_uid = match resolve_uid(&target_key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", target_key),
};
let amount = match parse_amount(amount, "transfer") {
Ok(value) => value,
Err(error) => return format!("Error: {}", error),
};
let context = match parse_transfer_context(json_context) {
Ok(value) => value,
Err(error) => return format!("Error: {}", error),
};
match HOT_BANK_SERVICE.transfer(resolved_source_uid, resolved_target_uid, context, amount) {
Ok(result) => serialize_hot_bank_transfer(result),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn validate_pin_hot_bank(
call_context: CallContext,
key: String,
pin: String,
json_context: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let context = match parse_pin_context(json_context) {
Ok(value) => value,
Err(error) => return format!("Error: {}", error),
};
match HOT_BANK_SERVICE.validate_pin(resolved_uid, pin, context) {
Ok(_) => "{}".to_string(),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn save_hot_bank(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_BANK_SERVICE.save_bank(resolved_uid) {
Ok(saved_bank) => serialize_hot_bank(saved_bank),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn remove_hot_bank(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_BANK_SERVICE.remove_bank(resolved_uid) {
Ok(_) => "OK".to_string(),
Err(error) => format!("Error: {}", error),
}
}
/// Retrieves an bank by key/UID.

View File

@ -63,107 +63,107 @@ pub fn group() -> Group {
.group("view", Group::new().command("hydrate", hydrate_view))
}
fn append_activity(json_data: String) -> String {
pub(crate) fn append_activity(json_data: String) -> String {
serialize_ok(CAD_SERVICE.append_activity(json_data))
}
fn recent_activity(limit: String) -> String {
pub(crate) fn recent_activity(limit: String) -> String {
serialize_json(CAD_SERVICE.recent_activity(limit))
}
fn list_assignments() -> String {
pub(crate) fn list_assignments() -> String {
serialize_json(CAD_SERVICE.list_assignments())
}
fn assign_assignment(entry_id: String, json_data: String) -> String {
pub(crate) fn assign_assignment(entry_id: String, json_data: String) -> String {
serialize_json(CAD_SERVICE.assign_assignment(entry_id, json_data))
}
fn acknowledge_assignment(entry_id: String, json_data: String) -> String {
pub(crate) fn acknowledge_assignment(entry_id: String, json_data: String) -> String {
serialize_json(CAD_SERVICE.acknowledge_assignment(entry_id, json_data))
}
fn decline_assignment(entry_id: String, json_data: String) -> String {
pub(crate) fn decline_assignment(entry_id: String, json_data: String) -> String {
serialize_json(CAD_SERVICE.decline_assignment(entry_id, json_data))
}
fn upsert_assignment(entry_id: String, json_data: String) -> String {
pub(crate) fn upsert_assignment(entry_id: String, json_data: String) -> String {
serialize_ok(CAD_SERVICE.upsert_assignment(entry_id, json_data))
}
fn delete_assignment(entry_id: String) -> String {
pub(crate) fn delete_assignment(entry_id: String) -> String {
serialize_ok(CAD_SERVICE.delete_assignment(entry_id))
}
fn list_orders() -> String {
pub(crate) fn list_orders() -> String {
serialize_json(CAD_SERVICE.list_orders())
}
fn create_order(json_data: String) -> String {
pub(crate) fn create_order(json_data: String) -> String {
serialize_json(CAD_SERVICE.create_order(json_data))
}
fn create_order_from_context(json_data: String) -> String {
pub(crate) fn create_order_from_context(json_data: String) -> String {
serialize_json(CAD_SERVICE.create_order_from_context(json_data))
}
fn close_order(entry_id: String) -> String {
pub(crate) fn close_order(entry_id: String) -> String {
serialize_json(CAD_SERVICE.close_order(entry_id))
}
fn upsert_order(entry_id: String, json_data: String) -> String {
pub(crate) fn upsert_order(entry_id: String, json_data: String) -> String {
serialize_ok(CAD_SERVICE.upsert_order(entry_id, json_data))
}
fn delete_order(entry_id: String) -> String {
pub(crate) fn delete_order(entry_id: String) -> String {
serialize_ok(CAD_SERVICE.delete_order(entry_id))
}
fn list_requests() -> String {
pub(crate) fn list_requests() -> String {
serialize_json(CAD_SERVICE.list_requests())
}
fn submit_request(json_data: String) -> String {
pub(crate) fn submit_request(json_data: String) -> String {
serialize_json(CAD_SERVICE.submit_request(json_data))
}
fn submit_request_from_context(json_data: String) -> String {
pub(crate) fn submit_request_from_context(json_data: String) -> String {
serialize_json(CAD_SERVICE.submit_request_from_context(json_data))
}
fn close_request(entry_id: String) -> String {
pub(crate) fn close_request(entry_id: String) -> String {
serialize_json(CAD_SERVICE.close_request(entry_id))
}
fn upsert_request(entry_id: String, json_data: String) -> String {
pub(crate) fn upsert_request(entry_id: String, json_data: String) -> String {
serialize_ok(CAD_SERVICE.upsert_request(entry_id, json_data))
}
fn delete_request(entry_id: String) -> String {
pub(crate) fn delete_request(entry_id: String) -> String {
serialize_ok(CAD_SERVICE.delete_request(entry_id))
}
fn list_profiles() -> String {
pub(crate) fn list_profiles() -> String {
serialize_json(CAD_SERVICE.list_profiles())
}
fn update_profile_from_context(json_data: String) -> String {
pub(crate) fn update_profile_from_context(json_data: String) -> String {
serialize_json(CAD_SERVICE.update_profile_from_context(json_data))
}
fn upsert_profile(entry_id: String, json_data: String) -> String {
pub(crate) fn upsert_profile(entry_id: String, json_data: String) -> String {
serialize_ok(CAD_SERVICE.upsert_profile(entry_id, json_data))
}
fn delete_profile(entry_id: String) -> String {
pub(crate) fn delete_profile(entry_id: String) -> String {
serialize_ok(CAD_SERVICE.delete_profile(entry_id))
}
fn build_groups(json_data: String) -> String {
pub(crate) fn build_groups(json_data: String) -> String {
serialize_json(CAD_SERVICE.build_groups(json_data))
}
fn hydrate_view(json_data: String) -> String {
pub(crate) fn hydrate_view(json_data: String) -> String {
serialize_json(CAD_SERVICE.build_hydrate_payload(json_data))
}

View File

@ -4,8 +4,8 @@
use arma_rs::{CallContext, Group};
use forge_models::Vehicle;
use forge_repositories::RedisGarageRepository;
use forge_services::GarageService;
use forge_repositories::{InMemoryGarageHotRepository, RedisGarageRepository};
use forge_services::{GarageHotStateService, GarageService};
use std::collections::HashMap;
use std::sync::LazyLock;
@ -20,6 +20,14 @@ static GARAGE_SERVICE: LazyLock<GarageService<RedisGarageRepository<ExtensionRed
let repository = RedisGarageRepository::new(redis_client);
GarageService::new(repository)
});
static HOT_GARAGE_SERVICE: LazyLock<
GarageHotStateService<RedisGarageRepository<ExtensionRedisClient>, InMemoryGarageHotRepository>,
> = LazyLock::new(|| {
let redis_client = ExtensionRedisClient::new();
let repository = RedisGarageRepository::new(redis_client);
let hot_repository = InMemoryGarageHotRepository::new();
GarageHotStateService::new(repository, hot_repository)
});
/// Creates the Arma 3 command group for garage operations.
///
@ -34,6 +42,148 @@ pub fn group() -> Group {
.command("remove", remove_vehicle)
.command("delete", delete_garage)
.command("exists", garage_exists)
.group(
"hot",
Group::new()
.command("init", init_hot_garage)
.command("get", get_hot_garage)
.command("override", override_hot_garage)
.command("save", save_hot_garage)
.command("remove", remove_hot_garage)
.command("add", add_hot_vehicle)
.command("remove_vehicle", remove_hot_vehicle),
)
}
fn serialize_hot_vehicles(garage: forge_models::garage::Garage) -> String {
match serde_json::to_string(&garage.vehicles) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize hot garage: {}", error),
}
}
pub(crate) fn init_hot_garage(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_GARAGE_SERVICE.init_garage(resolved_uid) {
Ok(garage) => serialize_hot_vehicles(garage),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn get_hot_garage(call_context: CallContext, key: String) -> String {
init_hot_garage(call_context, key)
}
pub(crate) fn override_hot_garage(
call_context: CallContext,
key: String,
json_data: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let vehicles: HashMap<String, Vehicle> = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid JSON data: {}", error),
};
match HOT_GARAGE_SERVICE.override_garage(resolved_uid, vehicles) {
Ok(garage) => serialize_hot_vehicles(garage),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn save_hot_garage(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_GARAGE_SERVICE.save_garage(resolved_uid) {
Ok(saved_garage) => serialize_hot_vehicles(saved_garage),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn remove_hot_garage(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_GARAGE_SERVICE.remove_garage(resolved_uid) {
Ok(_) => "OK".to_string(),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn add_hot_vehicle(call_context: CallContext, key: String, json_data: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let data: serde_json::Value = match serde_json::from_str(&json_data) {
Ok(d) => d,
Err(error) => return format!("Error: Invalid JSON data: {}", error),
};
let classname = match data.get("classname").and_then(|v| v.as_str()) {
Some(c) => c.to_string(),
None => return "Error: Missing or invalid classname".to_string(),
};
let fuel = match data.get("fuel").and_then(|v| v.as_f64()) {
Some(f) => f,
None => return "Error: Missing or invalid fuel".to_string(),
};
let damage = match data.get("damage").and_then(|v| v.as_f64()) {
Some(d) => d,
None => return "Error: Missing or invalid damage".to_string(),
};
let hit_points_json = match data.get("hit_points") {
Some(hp) => match serde_json::to_string(hp) {
Ok(s) => s,
Err(error) => return format!("Error: Failed to serialize hit_points: {}", error),
},
None => return "Error: Missing hit_points".to_string(),
};
match HOT_GARAGE_SERVICE.add_vehicle(resolved_uid, classname, fuel, damage, hit_points_json) {
Ok(garage) => serialize_hot_vehicles(garage),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn remove_hot_vehicle(
call_context: CallContext,
key: String,
json_data: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let data: serde_json::Value = match serde_json::from_str(&json_data) {
Ok(d) => d,
Err(error) => return format!("Error: Invalid JSON data: {}", error),
};
let plate = match data.get("plate").and_then(|v| v.as_str()) {
Some(p) => p.to_string(),
None => return "Error: Missing or invalid plate".to_string(),
};
match HOT_GARAGE_SERVICE.remove_vehicle(resolved_uid, plate) {
Ok(garage) => serialize_hot_vehicles(garage),
Err(error) => format!("Error: {}", error),
}
}
/// Creates a new empty garage for a player.

View File

@ -23,6 +23,7 @@ mod log;
pub mod org;
pub mod redis;
pub mod terrain;
pub mod transport;
pub mod v_garage;
pub mod v_locker;
@ -70,6 +71,7 @@ fn init() -> Extension {
.group("locker", locker::group())
.group("org", org::group())
.group("terrain", terrain::group())
.group("transport", transport::group())
.group(
"owned",
Group::new()

View File

@ -1,7 +1,7 @@
use arma_rs::{CallContext, Group};
use forge_models::locker::Item;
use forge_repositories::RedisLockerRepository;
use forge_services::LockerService;
use forge_repositories::{InMemoryLockerHotRepository, RedisLockerRepository};
use forge_services::{LockerHotStateService, LockerService};
use std::collections::HashMap;
use std::sync::LazyLock;
@ -15,6 +15,14 @@ static LOCKER_SERVICE: LazyLock<LockerService<RedisLockerRepository<ExtensionRed
let repository = RedisLockerRepository::new(redis_client);
LockerService::new(repository)
});
static HOT_LOCKER_SERVICE: LazyLock<
LockerHotStateService<RedisLockerRepository<ExtensionRedisClient>, InMemoryLockerHotRepository>,
> = LazyLock::new(|| {
let redis_client = ExtensionRedisClient::new();
let repository = RedisLockerRepository::new(redis_client);
let hot_repository = InMemoryLockerHotRepository::new();
LockerHotStateService::new(repository, hot_repository)
});
/// Creates the Arma 3 command group for locker operations.
///
@ -29,6 +37,83 @@ pub fn group() -> Group {
.command("remove", remove_item)
.command("delete", delete_locker)
.command("exists", locker_exists)
.group(
"hot",
Group::new()
.command("init", init_hot_locker)
.command("get", get_hot_locker)
.command("override", override_hot_locker)
.command("save", save_hot_locker)
.command("remove", remove_hot_locker),
)
}
fn serialize_hot_items(locker: forge_models::locker::Locker) -> String {
match serde_json::to_string(&locker.items) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize hot locker: {}", error),
}
}
pub(crate) fn init_hot_locker(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_LOCKER_SERVICE.init_locker(resolved_uid) {
Ok(locker) => serialize_hot_items(locker),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn get_hot_locker(call_context: CallContext, key: String) -> String {
init_hot_locker(call_context, key)
}
pub(crate) fn override_hot_locker(
call_context: CallContext,
key: String,
json_data: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let items: std::collections::HashMap<String, Item> = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid JSON data: {}", error),
};
match HOT_LOCKER_SERVICE.override_locker(resolved_uid, items) {
Ok(locker) => serialize_hot_items(locker),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn save_hot_locker(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_LOCKER_SERVICE.save_locker(resolved_uid) {
Ok(saved_locker) => serialize_hot_items(saved_locker),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn remove_hot_locker(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_LOCKER_SERVICE.remove_locker(resolved_uid) {
Ok(_) => "OK".to_string(),
Err(error) => format!("Error: {}", error),
}
}
/// Creates a new empty locker for a player.

View File

@ -4,8 +4,9 @@
//! Handles SQF command mapping and parameter validation.
use arma_rs::Group;
use forge_repositories::RedisOrgRepository;
use forge_services::OrgService;
use forge_models::HotOrgRecord;
use forge_repositories::{InMemoryOrgHotRepository, RedisOrgRepository};
use forge_services::{OrgHotStateService, OrgService};
use std::sync::LazyLock;
use crate::adapters::ExtensionRedisClient;
@ -20,6 +21,14 @@ static ORG_SERVICE: LazyLock<OrgService<RedisOrgRepository<ExtensionRedisClient>
let repository = RedisOrgRepository::new(redis_client);
OrgService::new(repository)
});
static HOT_ORG_SERVICE: LazyLock<
OrgHotStateService<RedisOrgRepository<ExtensionRedisClient>, InMemoryOrgHotRepository>,
> = LazyLock::new(|| {
let redis_client = ExtensionRedisClient::new();
let repository = RedisOrgRepository::new(redis_client);
let hot_repository = InMemoryOrgHotRepository::new();
OrgHotStateService::new(repository, hot_repository)
});
/// Creates the Arma 3 command group for organization operations.
///
@ -31,6 +40,15 @@ pub fn group() -> Group {
.command("update", update_org)
.command("exists", org_exists)
.command("delete", delete_org)
.group(
"hot",
Group::new()
.command("init", init_hot_org)
.command("get", get_hot_org)
.command("override", override_hot_org)
.command("save", save_hot_org)
.command("remove", remove_hot_org),
)
.group(
"assets",
Group::new()
@ -52,6 +70,53 @@ pub fn group() -> Group {
)
}
fn serialize_hot_org(org: HotOrgRecord) -> String {
match serde_json::to_string(&org) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize hot org: {}", error),
}
}
pub(crate) fn init_hot_org(org_id: String) -> String {
match HOT_ORG_SERVICE.init_org(org_id) {
Ok(org) => serialize_hot_org(org),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn get_hot_org(org_id: String) -> String {
match HOT_ORG_SERVICE.get_org(org_id) {
Ok(org) => serialize_hot_org(org),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn override_hot_org(org_id: String, json_data: String) -> String {
let hot_org: HotOrgRecord = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid org JSON: {}", error),
};
match HOT_ORG_SERVICE.override_org(org_id, hot_org) {
Ok(org) => serialize_hot_org(org),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn save_hot_org(org_id: String) -> String {
match HOT_ORG_SERVICE.save_org(org_id) {
Ok(org) => serialize_hot_org(org),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn remove_hot_org(org_id: String) -> String {
match HOT_ORG_SERVICE.remove_org(org_id) {
Ok(_) => "OK".to_string(),
Err(error) => format!("Error: {}", error),
}
}
// ============================================================================
// Organization Asset Operations
// ============================================================================

View File

@ -0,0 +1,951 @@
//! Shared transport helpers for oversized extension requests and responses.
//!
//! This module provides a routed invoke path that accepts JSON-encoded string
//! arguments, supports request staging for large payloads, and stores oversized
//! responses in memory for chunked retrieval by SQF.
use arma_rs::{CallContext, Group};
use serde::Serialize;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{LazyLock, Mutex as StdMutex};
use crate::{actor, bank, cad, garage, locker, org, v_garage, v_locker};
const CHUNK_PREFIX: &str = "FORGE_TRANSPORT_CHUNK:";
const RESPONSE_CHUNK_SIZE: usize = 12_000;
const UNSUPPORTED_ROUTE_PREFIX: &str = "Unsupported transport route";
static REQUEST_STORE: LazyLock<StdMutex<HashMap<String, String>>> =
LazyLock::new(|| StdMutex::new(HashMap::new()));
static RESPONSE_STORE: LazyLock<StdMutex<HashMap<String, Vec<String>>>> =
LazyLock::new(|| StdMutex::new(HashMap::new()));
static TRANSFER_SEQUENCE: AtomicU64 = AtomicU64::new(1);
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ChunkEnvelope {
transfer_id: String,
chunk_count: usize,
total_size: usize,
}
pub fn group() -> Group {
Group::new()
.command("invoke", invoke)
.command("invoke_stored", invoke_stored)
.group(
"request",
Group::new()
.command("append", append_request_chunk)
.command("clear", clear_request_chunks),
)
.group(
"response",
Group::new()
.command("get", get_response_chunk)
.command("clear", clear_response_chunks),
)
}
fn append_request_chunk(transfer_id: String, chunk: String) -> String {
let mut store = REQUEST_STORE.lock().unwrap();
store.entry(transfer_id).or_default().push_str(&chunk);
"OK".to_string()
}
fn clear_request_chunks(transfer_id: String) -> String {
REQUEST_STORE.lock().unwrap().remove(&transfer_id);
"OK".to_string()
}
fn get_response_chunk(transfer_id: String, index: String) -> String {
let chunk_index = match index.parse::<usize>() {
Ok(value) => value,
Err(error) => return format!("Error: Invalid response chunk index: {error}"),
};
let store = RESPONSE_STORE.lock().unwrap();
let Some(chunks) = store.get(&transfer_id) else {
return format!("Error: Response transfer '{transfer_id}' was not found");
};
chunks.get(chunk_index).cloned().unwrap_or_else(|| {
format!(
"Error: Response chunk {} was not found for '{}'",
chunk_index, transfer_id
)
})
}
fn clear_response_chunks(transfer_id: String) -> String {
RESPONSE_STORE.lock().unwrap().remove(&transfer_id);
"OK".to_string()
}
fn invoke(call_context: CallContext, function_name: String, arguments_json: String) -> String {
invoke_internal(call_context, function_name, arguments_json)
}
fn invoke_stored(call_context: CallContext, function_name: String, transfer_id: String) -> String {
let Some(arguments_json) = REQUEST_STORE.lock().unwrap().remove(&transfer_id) else {
return format!("Error: Request transfer '{transfer_id}' was not found");
};
invoke_internal(call_context, function_name, arguments_json)
}
fn invoke_internal(
call_context: CallContext,
function_name: String,
arguments_json: String,
) -> String {
let arguments: Vec<String> = match parse_transport_arguments(&arguments_json) {
Ok(value) => value,
Err(error) => return format!("Error: Invalid transport arguments JSON: {error}"),
};
let result = match route_command(call_context, &function_name, arguments) {
Ok(value) => value,
Err(error) => format!("Error: {error}"),
};
chunk_response_if_needed(result)
}
fn parse_transport_arguments(arguments_json: &str) -> Result<Vec<String>, String> {
let value: serde_json::Value =
serde_json::from_str(arguments_json).map_err(|error| error.to_string())?;
parse_transport_argument_value(value)
}
fn parse_transport_argument_value(value: serde_json::Value) -> Result<Vec<String>, String> {
match value {
serde_json::Value::Array(values) => Ok(values
.into_iter()
.map(|entry| match entry {
serde_json::Value::String(string_value) => string_value,
other => other.to_string(),
})
.collect()),
serde_json::Value::String(value) => {
let trimmed = value.trim();
if trimmed.starts_with('[') || trimmed.starts_with('{') || trimmed.eq("null") {
if let Ok(nested_value) = serde_json::from_str::<serde_json::Value>(trimmed) {
return parse_transport_argument_value(nested_value);
}
}
Ok(vec![value])
}
serde_json::Value::Null => Ok(Vec::new()),
other => Err(format!("expected string or array but received {}", other)),
}
}
fn route_command(
call_context: CallContext,
function_name: &str,
arguments: Vec<String>,
) -> Result<String, String> {
match function_name {
"actor:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::get_actor(call_context, arguments[0].clone()))
}
"actor:create" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(actor::create_actor(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"actor:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(actor::update_actor(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"actor:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::actor_exists(call_context, arguments[0].clone()))
}
"actor:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::delete_actor(call_context, arguments[0].clone()))
}
"actor:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::init_hot_actor(call_context, arguments[0].clone()))
}
"actor:hot:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::get_hot_actor(call_context, arguments[0].clone()))
}
"actor:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(actor::override_hot_actor(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"actor:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::save_hot_actor(call_context, arguments[0].clone()))
}
"actor:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::remove_hot_actor(call_context, arguments[0].clone()))
}
"bank:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::get_bank(call_context, arguments[0].clone()))
}
"bank:create" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(bank::create_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"bank:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(bank::update_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"bank:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::bank_exists(call_context, arguments[0].clone()))
}
"bank:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::delete_bank(call_context, arguments[0].clone()))
}
"bank:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::init_hot_bank(call_context, arguments[0].clone()))
}
"bank:hot:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::get_hot_bank(call_context, arguments[0].clone()))
}
"bank:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(bank::override_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"bank:hot:patch" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(bank::patch_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"bank:hot:deposit" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(bank::deposit_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"bank:hot:withdraw" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(bank::withdraw_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"bank:hot:payment" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(bank::payment_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"bank:hot:deposit_earnings" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(bank::deposit_earnings_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"bank:hot:transfer" => {
expect_arg_count(function_name, &arguments, 4)?;
Ok(bank::transfer_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
arguments[3].clone(),
))
}
"bank:hot:validate_pin" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(bank::validate_pin_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"bank:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::save_hot_bank(call_context, arguments[0].clone()))
}
"bank:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::remove_hot_bank(call_context, arguments[0].clone()))
}
"org:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::get_org(arguments[0].clone()))
}
"org:create" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::create_org(arguments[0].clone(), arguments[1].clone()))
}
"org:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::update_org(arguments[0].clone(), arguments[1].clone()))
}
"org:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::org_exists(arguments[0].clone()))
}
"org:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::delete_org(arguments[0].clone()))
}
"org:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::init_hot_org(arguments[0].clone()))
}
"org:hot:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::get_hot_org(arguments[0].clone()))
}
"org:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::override_hot_org(
arguments[0].clone(),
arguments[1].clone(),
))
}
"org:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::save_hot_org(arguments[0].clone()))
}
"org:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::remove_hot_org(arguments[0].clone()))
}
"org:assets:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::get_assets(arguments[0].clone()))
}
"org:assets:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::update_assets(
arguments[0].clone(),
arguments[1].clone(),
))
}
"org:fleet:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::get_fleet(arguments[0].clone()))
}
"org:fleet:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::update_fleet(
arguments[0].clone(),
arguments[1].clone(),
))
}
"org:members:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::get_members(arguments[0].clone()))
}
"org:members:add" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::add_member(arguments[0].clone(), arguments[1].clone()))
}
"org:members:remove" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::remove_member(
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:create" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::create_garage(call_context, arguments[0].clone()))
}
"garage:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::get_garage(call_context, arguments[0].clone()))
}
"garage:add" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::add_vehicle(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::update_garage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:patch" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::patch_vehicle(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:remove" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::remove_vehicle(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::delete_garage(call_context, arguments[0].clone()))
}
"garage:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::garage_exists(call_context, arguments[0].clone()))
}
"garage:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::init_hot_garage(call_context, arguments[0].clone()))
}
"garage:hot:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::get_hot_garage(call_context, arguments[0].clone()))
}
"garage:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::override_hot_garage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::save_hot_garage(call_context, arguments[0].clone()))
}
"garage:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::remove_hot_garage(
call_context,
arguments[0].clone(),
))
}
"garage:hot:add" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::add_hot_vehicle(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:hot:remove_vehicle" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::remove_hot_vehicle(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:create" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::create_locker(call_context, arguments[0].clone()))
}
"locker:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::get_locker(call_context, arguments[0].clone()))
}
"locker:add" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(locker::add_item(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(locker::update_locker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:patch" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(locker::patch_item(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:remove" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(locker::remove_item(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::delete_locker(call_context, arguments[0].clone()))
}
"locker:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::locker_exists(call_context, arguments[0].clone()))
}
"locker:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::init_hot_locker(call_context, arguments[0].clone()))
}
"locker:hot:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::get_hot_locker(call_context, arguments[0].clone()))
}
"locker:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(locker::override_hot_locker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::save_hot_locker(call_context, arguments[0].clone()))
}
"locker:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::remove_hot_locker(
call_context,
arguments[0].clone(),
))
}
"owned:garage:create" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::create_vgarage(call_context, arguments[0].clone()))
}
"owned:garage:fetch" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::fetch_vgarage(call_context, arguments[0].clone()))
}
"owned:garage:get" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_garage::get_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:garage:add" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_garage::add_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:garage:remove" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_garage::remove_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:garage:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::delete_vgarage(call_context, arguments[0].clone()))
}
"owned:garage:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::vgarage_exists(call_context, arguments[0].clone()))
}
"owned:garage:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::init_hot_vgarage(
call_context,
arguments[0].clone(),
))
}
"owned:garage:hot:fetch" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::fetch_hot_vgarage(
call_context,
arguments[0].clone(),
))
}
"owned:garage:hot:get" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_garage::get_hot_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:garage:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_garage::override_hot_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:garage:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::save_hot_vgarage(
call_context,
arguments[0].clone(),
))
}
"owned:garage:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::remove_hot_vgarage(
call_context,
arguments[0].clone(),
))
}
"owned:garage:hot:add" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_garage::add_hot_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:garage:hot:remove_item" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_garage::remove_hot_vgarage_item(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:locker:create" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::create_vlocker(call_context, arguments[0].clone()))
}
"owned:locker:fetch" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::fetch_vlocker(call_context, arguments[0].clone()))
}
"owned:locker:get" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_locker::get_vlocker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:locker:add" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_locker::add_vlocker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:locker:remove" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_locker::remove_vlocker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:locker:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::delete_vlocker(call_context, arguments[0].clone()))
}
"owned:locker:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::vlocker_exists(call_context, arguments[0].clone()))
}
"owned:locker:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::init_hot_vlocker(
call_context,
arguments[0].clone(),
))
}
"owned:locker:hot:fetch" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::fetch_hot_vlocker(
call_context,
arguments[0].clone(),
))
}
"owned:locker:hot:get" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_locker::get_hot_vlocker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:locker:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_locker::override_hot_vlocker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:locker:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::save_hot_vlocker(
call_context,
arguments[0].clone(),
))
}
"owned:locker:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::remove_hot_vlocker(
call_context,
arguments[0].clone(),
))
}
"cad:activity:append" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::append_activity(arguments[0].clone()))
}
"cad:activity:recent" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::recent_activity(arguments[0].clone()))
}
"cad:assignments:list" => {
expect_arg_count(function_name, &arguments, 0)?;
Ok(cad::list_assignments())
}
"cad:assignments:assign" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::assign_assignment(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:assignments:acknowledge" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::acknowledge_assignment(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:assignments:decline" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::decline_assignment(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:assignments:upsert" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::upsert_assignment(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:assignments:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::delete_assignment(arguments[0].clone()))
}
"cad:orders:list" => {
expect_arg_count(function_name, &arguments, 0)?;
Ok(cad::list_orders())
}
"cad:orders:create" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::create_order(arguments[0].clone()))
}
"cad:orders:create_from_context" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::create_order_from_context(arguments[0].clone()))
}
"cad:orders:close" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::close_order(arguments[0].clone()))
}
"cad:orders:upsert" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::upsert_order(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:orders:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::delete_order(arguments[0].clone()))
}
"cad:requests:list" => {
expect_arg_count(function_name, &arguments, 0)?;
Ok(cad::list_requests())
}
"cad:requests:submit" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::submit_request(arguments[0].clone()))
}
"cad:requests:submit_from_context" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::submit_request_from_context(arguments[0].clone()))
}
"cad:requests:close" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::close_request(arguments[0].clone()))
}
"cad:requests:upsert" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::upsert_request(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:requests:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::delete_request(arguments[0].clone()))
}
"cad:profiles:list" => {
expect_arg_count(function_name, &arguments, 0)?;
Ok(cad::list_profiles())
}
"cad:profiles:update_from_context" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::update_profile_from_context(arguments[0].clone()))
}
"cad:profiles:upsert" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::upsert_profile(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:profiles:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::delete_profile(arguments[0].clone()))
}
"cad:groups:build" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::build_groups(arguments[0].clone()))
}
"cad:view:hydrate" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::hydrate_view(arguments[0].clone()))
}
_ => Err(format!(
"{UNSUPPORTED_ROUTE_PREFIX} for function '{function_name}'"
)),
}
}
fn expect_arg_count(
function_name: &str,
arguments: &[String],
expected_count: usize,
) -> Result<(), String> {
if arguments.len() == expected_count {
return Ok(());
}
Err(format!(
"Transport route '{}' expected {} arguments but received {}",
function_name,
expected_count,
arguments.len()
))
}
fn chunk_response_if_needed(result: String) -> String {
if result.len() <= RESPONSE_CHUNK_SIZE {
return result;
}
let transfer_id = next_transfer_id("rsp");
let chunks = split_string_chunks(&result, RESPONSE_CHUNK_SIZE);
let envelope = ChunkEnvelope {
transfer_id: transfer_id.clone(),
chunk_count: chunks.len(),
total_size: result.len(),
};
RESPONSE_STORE.lock().unwrap().insert(transfer_id, chunks);
format!(
"{CHUNK_PREFIX}{}",
serde_json::to_string(&envelope)
.unwrap_or_else(|error| format!("{{\"error\":\"{error}\"}}"))
)
}
fn next_transfer_id(prefix: &str) -> String {
let sequence = TRANSFER_SEQUENCE.fetch_add(1, Ordering::Relaxed);
format!("{prefix}_{sequence}")
}
fn split_string_chunks(input: &str, max_bytes: usize) -> Vec<String> {
if input.is_empty() {
return vec![String::new()];
}
let mut chunks = Vec::new();
let mut chunk_start = 0usize;
let mut chunk_len = 0usize;
for (index, character) in input.char_indices() {
let char_len = character.len_utf8();
if chunk_len > 0 && chunk_len + char_len > max_bytes {
chunks.push(input[chunk_start..index].to_string());
chunk_start = index;
chunk_len = 0;
}
chunk_len += char_len;
}
chunks.push(input[chunk_start..].to_string());
chunks
}

View File

@ -1,7 +1,7 @@
use arma_rs::{CallContext, Group};
use forge_models::VehicleCategory;
use forge_repositories::RedisVGarageRepository;
use forge_services::VGarageService;
use forge_models::{VGarage, VehicleCategory};
use forge_repositories::{InMemoryVGarageHotRepository, RedisVGarageRepository};
use forge_services::{VGarageHotStateService, VGarageService};
use std::sync::LazyLock;
use crate::adapters::ExtensionRedisClient;
@ -14,6 +14,17 @@ static VGARAGE_SERVICE: LazyLock<VGarageService<RedisVGarageRepository<Extension
let repository = RedisVGarageRepository::new(redis_client);
VGarageService::new(repository)
});
static HOT_VGARAGE_SERVICE: LazyLock<
VGarageHotStateService<
RedisVGarageRepository<ExtensionRedisClient>,
InMemoryVGarageHotRepository,
>,
> = LazyLock::new(|| {
let redis_client = ExtensionRedisClient::new();
let repository = RedisVGarageRepository::new(redis_client);
let hot_repository = InMemoryVGarageHotRepository::new();
VGarageHotStateService::new(repository, hot_repository)
});
/// Creates the Arma 3 command group for virtual garage operations.
///
@ -27,6 +38,180 @@ pub fn group() -> Group {
.command("remove", remove_vgarage)
.command("delete", delete_vgarage)
.command("exists", vgarage_exists)
.group(
"hot",
Group::new()
.command("init", init_hot_vgarage)
.command("fetch", fetch_hot_vgarage)
.command("get", get_hot_vgarage)
.command("override", override_hot_vgarage)
.command("save", save_hot_vgarage)
.command("remove", remove_hot_vgarage)
.command("add", add_hot_vgarage)
.command("remove_item", remove_hot_vgarage_item),
)
}
fn serialize_hot_vgarage(garage: VGarage) -> String {
match serde_json::to_string(&garage) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize hot virtual garage: {}", error),
}
}
pub(crate) fn init_hot_vgarage(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_VGARAGE_SERVICE.init_garage(&resolved_uid) {
Ok(garage) => serialize_hot_vgarage(garage),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn fetch_hot_vgarage(call_context: CallContext, key: String) -> String {
init_hot_vgarage(call_context, key)
}
pub(crate) fn get_hot_vgarage(call_context: CallContext, key: String, field: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let items = match HOT_VGARAGE_SERVICE.get_garage(&resolved_uid, &field) {
Ok(items) => items,
Err(error) => return format!("Error: {}", error),
};
match serde_json::to_string(&items) {
Ok(json) => json,
Err(error) => format!(
"Error: Failed to serialize hot virtual garage field: {}",
error
),
}
}
pub(crate) fn override_hot_vgarage(
call_context: CallContext,
key: String,
json_data: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let garage: VGarage = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid virtual garage JSON: {}", error),
};
match HOT_VGARAGE_SERVICE.override_garage(&resolved_uid, garage) {
Ok(garage) => serialize_hot_vgarage(garage),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn save_hot_vgarage(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_VGARAGE_SERVICE.save_garage(&resolved_uid) {
Ok(garage) => serialize_hot_vgarage(garage),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn remove_hot_vgarage(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_VGARAGE_SERVICE.remove_hot_garage(&resolved_uid) {
Ok(_) => "OK".to_string(),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn add_hot_vgarage(
call_context: CallContext,
key: String,
category: String,
classnames_json: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let category_enum = match category.to_lowercase().as_str() {
"cars" => VehicleCategory::Cars,
"armor" => VehicleCategory::Armor,
"helis" => VehicleCategory::Helis,
"planes" => VehicleCategory::Planes,
"naval" => VehicleCategory::Naval,
"other" => VehicleCategory::Other,
_ => {
return format!(
"Error: Invalid category '{}'. Valid options: cars, armor, helis, planes, naval, other",
category
);
}
};
let classnames: Vec<String> = match serde_json::from_str(&classnames_json) {
Ok(names) => names,
Err(error) => return format!("Error: Invalid JSON array: {}", error),
};
match HOT_VGARAGE_SERVICE.add_garage(&resolved_uid, category_enum, classnames) {
Ok(garage) => match serde_json::to_string(&garage.get(category_enum)) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize category: {}", error),
},
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn remove_hot_vgarage_item(
call_context: CallContext,
key: String,
category: String,
classname: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let category_enum = match category.to_lowercase().as_str() {
"cars" => VehicleCategory::Cars,
"armor" => VehicleCategory::Armor,
"heli" | "helis" => VehicleCategory::Helis,
"planes" => VehicleCategory::Planes,
"naval" => VehicleCategory::Naval,
"other" => VehicleCategory::Other,
_ => {
return format!(
"Error: Invalid category '{}'. Valid options: cars, armor, helis, planes, naval, other",
category
);
}
};
match HOT_VGARAGE_SERVICE.remove_garage(&resolved_uid, category_enum, &classname) {
Ok(garage) => match serde_json::to_string(&garage.get(category_enum)) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize category: {}", error),
},
Err(error) => format!("Error: {}", error),
}
}
/// Creates a new empty virtual garage for a player.

View File

@ -1,7 +1,7 @@
use arma_rs::{CallContext, Group};
use forge_models::EquipmentCategory;
use forge_repositories::RedisVLockerRepository;
use forge_services::VLockerService;
use forge_models::{EquipmentCategory, VLocker};
use forge_repositories::{InMemoryVLockerHotRepository, RedisVLockerRepository};
use forge_services::{VLockerHotStateService, VLockerService};
use std::sync::LazyLock;
use crate::adapters::ExtensionRedisClient;
@ -14,6 +14,17 @@ static VLOCKER_SERVICE: LazyLock<VLockerService<RedisVLockerRepository<Extension
let repository = RedisVLockerRepository::new(redis_client);
VLockerService::new(repository)
});
static HOT_VLOCKER_SERVICE: LazyLock<
VLockerHotStateService<
RedisVLockerRepository<ExtensionRedisClient>,
InMemoryVLockerHotRepository,
>,
> = LazyLock::new(|| {
let redis_client = ExtensionRedisClient::new();
let repository = RedisVLockerRepository::new(redis_client);
let hot_repository = InMemoryVLockerHotRepository::new();
VLockerHotStateService::new(repository, hot_repository)
});
/// Creates the Arma 3 command group for virtual locker operations.
///
@ -27,6 +38,104 @@ pub fn group() -> Group {
.command("remove", remove_vlocker)
.command("delete", delete_vlocker)
.command("exists", vlocker_exists)
.group(
"hot",
Group::new()
.command("init", init_hot_vlocker)
.command("fetch", fetch_hot_vlocker)
.command("get", get_hot_vlocker)
.command("override", override_hot_vlocker)
.command("save", save_hot_vlocker)
.command("remove", remove_hot_vlocker),
)
}
fn serialize_hot_vlocker(locker: VLocker) -> String {
match serde_json::to_string(&locker) {
Ok(json) => json,
Err(error) => format!("Error: Failed to serialize hot virtual locker: {}", error),
}
}
pub(crate) fn init_hot_vlocker(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_VLOCKER_SERVICE.init_locker(&resolved_uid) {
Ok(locker) => serialize_hot_vlocker(locker),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn fetch_hot_vlocker(call_context: CallContext, key: String) -> String {
init_hot_vlocker(call_context, key)
}
pub(crate) fn get_hot_vlocker(call_context: CallContext, key: String, field: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let items = match HOT_VLOCKER_SERVICE.get_locker(&resolved_uid, &field) {
Ok(items) => items,
Err(error) => return format!("Error: {}", error),
};
match serde_json::to_string(&items) {
Ok(json) => json,
Err(error) => format!(
"Error: Failed to serialize hot virtual locker field: {}",
error
),
}
}
pub(crate) fn override_hot_vlocker(
call_context: CallContext,
key: String,
json_data: String,
) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
let locker: VLocker = match serde_json::from_str(&json_data) {
Ok(data) => data,
Err(error) => return format!("Error: Invalid virtual locker JSON: {}", error),
};
match HOT_VLOCKER_SERVICE.override_locker(&resolved_uid, locker) {
Ok(locker) => serialize_hot_vlocker(locker),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn save_hot_vlocker(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_VLOCKER_SERVICE.save_locker(&resolved_uid) {
Ok(locker) => serialize_hot_vlocker(locker),
Err(error) => format!("Error: {}", error),
}
}
pub(crate) fn remove_hot_vlocker(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid,
None => return format!("Error: Failed to resolve UID for key: {}", key),
};
match HOT_VLOCKER_SERVICE.remove_locker(&resolved_uid) {
Ok(_) => "OK".to_string(),
Err(error) => format!("Error: {}", error),
}
}
/// Creates a new empty virtual locker for a player.

View File

@ -1,6 +1,7 @@
use arma_rs::{FromArma, IntoArma};
use forge_shared::BankValidationError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bank {
@ -13,6 +14,43 @@ pub struct Bank {
pub transactions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BankMutationResult {
pub account: Bank,
pub patch: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BankTransferResult {
pub source_account: Bank,
pub source_patch: HashMap<String, serde_json::Value>,
pub target_account: Bank,
pub target_patch: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BankOperationContext {
pub mode: String,
pub atm_authorized: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BankTransferContext {
pub mode: String,
pub atm_authorized: bool,
pub from_field: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BankPinContext {
pub mode: String,
}
impl Bank {
pub fn new<S: Into<String>>(uid: S, name: S, pin: u64) -> Result<Self, BankValidationError> {
let bank = Self {

View File

@ -8,7 +8,10 @@ pub mod v_garage;
pub mod v_locker;
pub use actor::Actor;
pub use bank::Bank;
pub use bank::{
Bank, BankMutationResult, BankOperationContext, BankPinContext, BankTransferContext,
BankTransferResult,
};
pub use cad::{
CadActivityEntry, CadAssignmentMutationResult, CadDispatchOrderContextSeed,
CadDispatchOrderCreateSeed, CadDispatchOrderMutationResult, CadGroupBuildSeed,
@ -17,6 +20,6 @@ pub use cad::{
};
pub use garage::{Garage, HitPoints, Vehicle};
pub use locker::{Item, Locker};
pub use org::{CreditLineSummary, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry};
pub use org::{CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry};
pub use v_garage::{VGarage, VehicleCategory};
pub use v_locker::{EquipmentCategory, VLocker};

View File

@ -48,6 +48,23 @@ pub struct MemberSummary {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HotOrgRecord {
pub id: String,
pub owner: String,
pub name: String,
pub funds: f64,
pub reputation: i64,
#[serde(default)]
pub credit_lines: HashMap<String, CreditLineSummary>,
#[serde(default)]
pub assets: HashMap<String, HashMap<String, OrgAssetEntry>>,
#[serde(default)]
pub fleet: HashMap<String, OrgFleetEntry>,
#[serde(default)]
pub members: HashMap<String, MemberSummary>,
}
impl Org {
pub fn new<S: Into<String>>(id: S, owner: S, name: S) -> Result<Self, OrgValidationError> {
let org = Self {
@ -128,6 +145,41 @@ impl Org {
}
}
impl HotOrgRecord {
pub fn from_parts(
org: Org,
assets: HashMap<String, HashMap<String, OrgAssetEntry>>,
fleet: HashMap<String, OrgFleetEntry>,
members: Vec<MemberSummary>,
) -> Self {
Self {
id: org.id,
owner: org.owner,
name: org.name,
funds: org.funds,
reputation: org.reputation,
credit_lines: org.credit_lines,
assets,
fleet,
members: members
.into_iter()
.map(|member| (member.uid.clone(), member))
.collect(),
}
}
pub fn into_org(self) -> Org {
Org {
id: self.id,
owner: self.owner,
name: self.name,
funds: self.funds,
reputation: self.reputation,
credit_lines: self.credit_lines,
}
}
}
impl FromArma for Org {
fn from_arma(s: String) -> Result<Self, arma_rs::FromArmaError> {
serde_json::from_str(&s)

View File

@ -29,6 +29,7 @@ impl VLocker {
"G_Combat".to_string(),
"H_Cap_blk_ION".to_string(),
"H_HelmetB".to_string(),
"ACE_EarPlugs".to_string(),
"ItemCompass".to_string(),
"ItemGPS".to_string(),
"ItemMap".to_string(),

View File

@ -7,6 +7,8 @@
use forge_models::Actor;
use forge_shared::{RedisClient, parse_json_value, parse_redis_value};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
/// Repository trait defining the contract for actor data operations.
///
@ -30,6 +32,48 @@ pub trait ActorRepository: Send + Sync {
fn exists(&self, id: &str) -> Result<bool, String>;
}
pub trait ActorHotRepository: Send + Sync {
fn get(&self, id: &str) -> Result<Option<Actor>, String>;
fn save(&self, actor: &Actor) -> Result<(), String>;
fn delete(&self, id: &str) -> Result<(), String>;
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryActorHotRepository {
state: Arc<RwLock<HashMap<String, Actor>>>,
}
impl InMemoryActorHotRepository {
pub fn new() -> Self {
Self::default()
}
}
impl ActorHotRepository for InMemoryActorHotRepository {
fn get(&self, id: &str) -> Result<Option<Actor>, String> {
self.state
.read()
.map(|state| state.get(id).cloned())
.map_err(|_| "Actor hot state lock poisoned.".to_string())
}
fn save(&self, actor: &Actor) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Actor hot state lock poisoned.".to_string())?
.insert(actor.uid.clone(), actor.clone());
Ok(())
}
fn delete(&self, id: &str) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Actor hot state lock poisoned.".to_string())?
.remove(id);
Ok(())
}
}
/// Redis-based implementation of the ActorRepository trait.
///
/// This implementation uses Redis hash maps to store actor data, providing

View File

@ -7,6 +7,8 @@
use forge_models::Bank;
use forge_shared::{RedisClient, parse_json_value, parse_redis_value};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
/// Repository trait defining the contract for bank data operations.
///
@ -30,6 +32,48 @@ pub trait BankRepository: Send + Sync {
fn exists(&self, id: &str) -> Result<bool, String>;
}
pub trait BankHotRepository: Send + Sync {
fn get(&self, id: &str) -> Result<Option<Bank>, String>;
fn save(&self, bank: &Bank) -> Result<(), String>;
fn delete(&self, id: &str) -> Result<(), String>;
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryBankHotRepository {
state: Arc<RwLock<HashMap<String, Bank>>>,
}
impl InMemoryBankHotRepository {
pub fn new() -> Self {
Self::default()
}
}
impl BankHotRepository for InMemoryBankHotRepository {
fn get(&self, id: &str) -> Result<Option<Bank>, String> {
self.state
.read()
.map(|state| state.get(id).cloned())
.map_err(|_| "Bank hot state lock poisoned.".to_string())
}
fn save(&self, bank: &Bank) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Bank hot state lock poisoned.".to_string())?
.insert(bank.uid.clone(), bank.clone());
Ok(())
}
fn delete(&self, id: &str) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Bank hot state lock poisoned.".to_string())?
.remove(id);
Ok(())
}
}
/// Redis-based implementation of the BankRepository trait.
///
/// This implementation uses Redis hash maps to store bank data, providing

View File

@ -6,6 +6,7 @@
use forge_models::{Garage, Vehicle};
use forge_shared::RedisClient;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
/// Repository trait defining the contract for garage data operations.
pub trait GarageRepository: Send + Sync {
@ -25,6 +26,48 @@ pub trait GarageRepository: Send + Sync {
fn exists(&self, uid: &str) -> Result<bool, String>;
}
pub trait GarageHotRepository: Send + Sync {
fn get(&self, uid: &str) -> Result<Option<Garage>, String>;
fn save(&self, garage: &Garage, uid: &str) -> Result<(), String>;
fn delete(&self, uid: &str) -> Result<(), String>;
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryGarageHotRepository {
state: Arc<RwLock<HashMap<String, Garage>>>,
}
impl InMemoryGarageHotRepository {
pub fn new() -> Self {
Self::default()
}
}
impl GarageHotRepository for InMemoryGarageHotRepository {
fn get(&self, uid: &str) -> Result<Option<Garage>, String> {
self.state
.read()
.map(|state| state.get(uid).cloned())
.map_err(|_| "Garage hot state lock poisoned.".to_string())
}
fn save(&self, garage: &Garage, uid: &str) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Garage hot state lock poisoned.".to_string())?
.insert(uid.to_string(), garage.clone());
Ok(())
}
fn delete(&self, uid: &str) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Garage hot state lock poisoned.".to_string())?
.remove(uid);
Ok(())
}
}
/// Redis-based implementation of the GarageRepository trait.
///
/// Stores each player's garage as a single JSON string array with the key format `garage:{uid}`.

View File

@ -7,14 +7,24 @@ pub mod org;
pub mod v_garage;
pub mod v_locker;
pub use actor::{ActorRepository, RedisActorRepository};
pub use bank::{BankRepository, RedisBankRepository};
pub use actor::{
ActorHotRepository, ActorRepository, InMemoryActorHotRepository, RedisActorRepository,
};
pub use bank::{BankHotRepository, BankRepository, InMemoryBankHotRepository, RedisBankRepository};
pub use cad::{CadRepository, InMemoryCadRepository};
pub use garage::{GarageRepository, RedisGarageRepository};
pub use locker::{LockerRepository, RedisLockerRepository};
pub use org::{OrgRepository, RedisOrgRepository};
pub use v_garage::{RedisVGarageRepository, VGarageRepository};
pub use v_locker::{RedisVLockerRepository, VLockerRepository};
pub use garage::{
GarageHotRepository, GarageRepository, InMemoryGarageHotRepository, RedisGarageRepository,
};
pub use locker::{
InMemoryLockerHotRepository, LockerHotRepository, LockerRepository, RedisLockerRepository,
};
pub use org::{InMemoryOrgHotRepository, OrgHotRepository, OrgRepository, RedisOrgRepository};
pub use v_garage::{
InMemoryVGarageHotRepository, RedisVGarageRepository, VGarageHotRepository, VGarageRepository,
};
pub use v_locker::{
InMemoryVLockerHotRepository, RedisVLockerRepository, VLockerHotRepository, VLockerRepository,
};
// Re-export RedisClient from shared library for convenience
pub use forge_shared::RedisClient;

View File

@ -6,6 +6,7 @@
use forge_models::{Item, Locker};
use forge_shared::RedisClient;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
/// Repository trait defining the contract for locker data operations.
pub trait LockerRepository: Send + Sync {
@ -25,6 +26,48 @@ pub trait LockerRepository: Send + Sync {
fn exists(&self, uid: &str) -> Result<bool, String>;
}
pub trait LockerHotRepository: Send + Sync {
fn get(&self, uid: &str) -> Result<Option<Locker>, String>;
fn save(&self, locker: &Locker, uid: &str) -> Result<(), String>;
fn delete(&self, uid: &str) -> Result<(), String>;
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryLockerHotRepository {
state: Arc<RwLock<HashMap<String, Locker>>>,
}
impl InMemoryLockerHotRepository {
pub fn new() -> Self {
Self::default()
}
}
impl LockerHotRepository for InMemoryLockerHotRepository {
fn get(&self, uid: &str) -> Result<Option<Locker>, String> {
self.state
.read()
.map(|state| state.get(uid).cloned())
.map_err(|_| "Locker hot state lock poisoned.".to_string())
}
fn save(&self, locker: &Locker, uid: &str) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Locker hot state lock poisoned.".to_string())?
.insert(uid.to_string(), locker.clone());
Ok(())
}
fn delete(&self, uid: &str) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Locker hot state lock poisoned.".to_string())?
.remove(uid);
Ok(())
}
}
/// Redis-based implementation of the LockerRepository trait.
///
/// Stores each player's locker as a single JSON string array with the key format `locker:{uid}`.

View File

@ -5,9 +5,10 @@
//!
//! For full documentation and examples, see the [crate README](../README.md).
use forge_models::{MemberSummary, Org, OrgAssetEntry, OrgFleetEntry};
use forge_models::{HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry};
use forge_shared::{RedisClient, parse_json_value, parse_redis_value};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
/// Repository trait defining the contract for organization data operations.
///
@ -63,6 +64,48 @@ pub trait OrgRepository: Send + Sync {
) -> Result<(), String>;
}
pub trait OrgHotRepository: Send + Sync {
fn get(&self, id: &str) -> Result<Option<HotOrgRecord>, String>;
fn save(&self, org: &HotOrgRecord) -> Result<(), String>;
fn delete(&self, id: &str) -> Result<(), String>;
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryOrgHotRepository {
state: Arc<RwLock<HashMap<String, HotOrgRecord>>>,
}
impl InMemoryOrgHotRepository {
pub fn new() -> Self {
Self::default()
}
}
impl OrgHotRepository for InMemoryOrgHotRepository {
fn get(&self, id: &str) -> Result<Option<HotOrgRecord>, String> {
self.state
.read()
.map(|state| state.get(id).cloned())
.map_err(|_| "Org hot state lock poisoned.".to_string())
}
fn save(&self, org: &HotOrgRecord) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Org hot state lock poisoned.".to_string())?
.insert(org.id.clone(), org.clone());
Ok(())
}
fn delete(&self, id: &str) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Org hot state lock poisoned.".to_string())?
.remove(id);
Ok(())
}
}
/// Redis-based implementation of the OrgRepository trait.
///
/// Uses Redis hash maps for organization data providing

View File

@ -11,6 +11,8 @@
use forge_models::VGarage;
use forge_shared::{RedisClient, parse_json_value, parse_redis_value};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
/// Repository trait defining the contract for virtual garage data operations.
pub trait VGarageRepository: Send + Sync {
@ -34,6 +36,48 @@ pub trait VGarageRepository: Send + Sync {
fn exists(&self, uid: &str) -> Result<bool, String>;
}
pub trait VGarageHotRepository: Send + Sync {
fn get(&self, uid: &str) -> Result<Option<VGarage>, String>;
fn save(&self, garage: &VGarage, uid: &str) -> Result<(), String>;
fn delete(&self, uid: &str) -> Result<(), String>;
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryVGarageHotRepository {
state: Arc<RwLock<HashMap<String, VGarage>>>,
}
impl InMemoryVGarageHotRepository {
pub fn new() -> Self {
Self::default()
}
}
impl VGarageHotRepository for InMemoryVGarageHotRepository {
fn get(&self, uid: &str) -> Result<Option<VGarage>, String> {
self.state
.read()
.map(|state| state.get(uid).cloned())
.map_err(|_| "Virtual garage hot state lock poisoned.".to_string())
}
fn save(&self, garage: &VGarage, uid: &str) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Virtual garage hot state lock poisoned.".to_string())?
.insert(uid.to_string(), garage.clone());
Ok(())
}
fn delete(&self, uid: &str) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Virtual garage hot state lock poisoned.".to_string())?
.remove(uid);
Ok(())
}
}
/// Redis-based implementation of the VGarageRepository trait.
///
/// Stores each player's virtual garage as a Redis hash with six fields:

View File

@ -9,6 +9,8 @@
use forge_models::VLocker;
use forge_shared::{RedisClient, parse_json_value, parse_redis_value};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
/// Repository trait defining the contract for virtual locker data operations.
pub trait VLockerRepository: Send + Sync {
@ -32,6 +34,48 @@ pub trait VLockerRepository: Send + Sync {
fn exists(&self, uid: &str) -> Result<bool, String>;
}
pub trait VLockerHotRepository: Send + Sync {
fn get(&self, uid: &str) -> Result<Option<VLocker>, String>;
fn save(&self, locker: &VLocker, uid: &str) -> Result<(), String>;
fn delete(&self, uid: &str) -> Result<(), String>;
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryVLockerHotRepository {
state: Arc<RwLock<HashMap<String, VLocker>>>,
}
impl InMemoryVLockerHotRepository {
pub fn new() -> Self {
Self::default()
}
}
impl VLockerHotRepository for InMemoryVLockerHotRepository {
fn get(&self, uid: &str) -> Result<Option<VLocker>, String> {
self.state
.read()
.map(|state| state.get(uid).cloned())
.map_err(|_| "Virtual locker hot state lock poisoned.".to_string())
}
fn save(&self, locker: &VLocker, uid: &str) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Virtual locker hot state lock poisoned.".to_string())?
.insert(uid.to_string(), locker.clone());
Ok(())
}
fn delete(&self, uid: &str) -> Result<(), String> {
self.state
.write()
.map_err(|_| "Virtual locker hot state lock poisoned.".to_string())?
.remove(uid);
Ok(())
}
}
/// Redis-based implementation of the VLockerRepository trait.
///
/// Stores each player's virtual locker as a Redis hash with four fields:

View File

@ -6,7 +6,7 @@
//! For full documentation, architecture, and examples, see the [crate README](../README.md).
use forge_models::Actor;
use forge_repositories::ActorRepository;
use forge_repositories::{ActorHotRepository, ActorRepository};
use forge_shared::{generate_email, generate_phone_number};
/// Service layer implementation for actor business logic and operations.
@ -24,6 +24,64 @@ pub struct ActorService<R: ActorRepository> {
repository: R,
}
pub struct ActorHotStateService<R: ActorRepository, H: ActorHotRepository> {
service: ActorService<R>,
repository: H,
}
impl<R: ActorRepository, H: ActorHotRepository> ActorHotStateService<R, H> {
pub fn new(repository: R, hot_repository: H) -> Self {
Self {
service: ActorService::new(repository),
repository: hot_repository,
}
}
pub fn init_actor(&self, key: String) -> Result<Actor, String> {
if let Some(actor) = self.repository.get(&key)? {
return Ok(actor);
}
let actor = self.service.get_actor(key)?;
self.repository.save(&actor)?;
Ok(actor)
}
pub fn get_actor(&self, key: String) -> Result<Actor, String> {
self.init_actor(key)
}
pub fn override_actor(&self, key: String, json_data: String) -> Result<Actor, String> {
let mut actor: Actor =
serde_json::from_str(&json_data).map_err(|e| format!("Invalid Actor JSON: {}", e))?;
actor.uid = key;
actor
.validate()
.map_err(|e| format!("Validation failed: {}", e))?;
self.repository.save(&actor)?;
Ok(actor)
}
pub fn save_actor(&self, key: String) -> Result<Actor, String> {
let actor = self
.repository
.get(&key)?
.ok_or_else(|| format!("Actor with UID '{}' not found in hot state", key))?;
let actor_json = serde_json::to_string(&actor)
.map_err(|e| format!("Failed to serialize actor: {}", e))?;
let saved_actor = self.service.update_actor(key, actor_json)?;
self.repository.save(&saved_actor)?;
Ok(saved_actor)
}
pub fn remove_actor(&self, key: String) -> Result<(), String> {
self.repository.delete(&key)
}
}
impl<R: ActorRepository> ActorService<R> {
/// Creates a new actor service with the provided repository.
///

View File

@ -5,8 +5,13 @@
//!
//! For full documentation, architecture, and examples, see the [crate README](../README.md).
use forge_models::Bank;
use forge_repositories::BankRepository;
use forge_models::{
Bank, BankMutationResult, BankOperationContext, BankPinContext, BankTransferContext,
BankTransferResult,
};
use forge_repositories::{BankHotRepository, BankRepository};
use serde_json::{Value, json};
use std::collections::HashMap;
/// Service layer implementation for bank business logic and operations.
///
@ -23,6 +28,371 @@ pub struct BankService<R: BankRepository> {
repository: R,
}
pub struct BankHotStateService<R: BankRepository, H: BankHotRepository> {
service: BankService<R>,
repository: H,
}
impl<R: BankRepository, H: BankHotRepository> BankHotStateService<R, H> {
pub fn new(repository: R, hot_repository: H) -> Self {
Self {
service: BankService::new(repository),
repository: hot_repository,
}
}
pub fn init_bank(&self, key: String) -> Result<Bank, String> {
if let Some(bank) = self.repository.get(&key)? {
return Ok(bank);
}
let bank = self.service.get_bank(key)?;
self.repository.save(&bank)?;
Ok(bank)
}
pub fn get_bank(&self, key: String) -> Result<Bank, String> {
self.init_bank(key)
}
pub fn override_bank(&self, key: String, json_data: String) -> Result<Bank, String> {
let mut bank: Bank =
serde_json::from_str(&json_data).map_err(|e| format!("Invalid Bank JSON: {}", e))?;
bank.uid = key;
bank.validate()
.map_err(|e| format!("Validation failed: {}", e))?;
self.repository.save(&bank)?;
Ok(bank)
}
pub fn patch_bank(
&self,
key: String,
json_patch: String,
) -> Result<BankMutationResult, String> {
let patch_value: Value =
serde_json::from_str(&json_patch).map_err(|e| format!("Invalid patch JSON: {}", e))?;
let patch_object = patch_value
.as_object()
.ok_or_else(|| "Patch data must be a JSON object".to_string())?;
let mut bank = self.get_bank(key.clone())?;
let mut patch = HashMap::new();
for (field, value) in patch_object {
apply_bank_field(&mut bank, field, value)?;
patch.insert(field.clone(), current_bank_field_value(&bank, field)?);
}
bank.validate()
.map_err(|e| format!("Validation failed: {}", e))?;
self.repository.save(&bank)?;
Ok(BankMutationResult {
account: bank,
patch,
})
}
pub fn deposit(
&self,
key: String,
amount: f64,
context: BankOperationContext,
) -> Result<BankMutationResult, String> {
if amount <= 0.0 {
return Err("Deposit amount must be greater than zero".to_string());
}
validate_atm_access(&context, "deposit")?;
let mut bank = self.get_bank(key)?;
if bank.cash < amount {
return Err("Cash on hand cannot cover that deposit.".to_string());
}
bank.cash -= amount;
bank.bank += amount;
bank.validate()
.map_err(|e| format!("Validation failed: {}", e))?;
self.repository.save(&bank)?;
Ok(BankMutationResult {
account: bank.clone(),
patch: build_patch(&bank, &["bank", "cash"])?,
})
}
pub fn withdraw(
&self,
key: String,
amount: f64,
context: BankOperationContext,
) -> Result<BankMutationResult, String> {
if amount <= 0.0 {
return Err("Withdrawal amount must be greater than zero".to_string());
}
validate_atm_access(&context, "withdrawal")?;
let mut bank = self.get_bank(key)?;
if bank.bank < amount {
return Err("Bank balance cannot cover that withdrawal.".to_string());
}
bank.bank -= amount;
bank.cash += amount;
bank.validate()
.map_err(|e| format!("Validation failed: {}", e))?;
self.repository.save(&bank)?;
Ok(BankMutationResult {
account: bank.clone(),
patch: build_patch(&bank, &["bank", "cash"])?,
})
}
pub fn payment(&self, key: String, amount: f64) -> Result<BankMutationResult, String> {
if amount <= 0.0 {
return Err("Payment amount must be greater than zero".to_string());
}
let mut bank = self.get_bank(key)?;
bank.bank += amount;
bank.validate()
.map_err(|e| format!("Validation failed: {}", e))?;
self.repository.save(&bank)?;
Ok(BankMutationResult {
account: bank.clone(),
patch: build_patch(&bank, &["bank"])?,
})
}
pub fn deposit_earnings(
&self,
key: String,
amount: f64,
context: BankOperationContext,
) -> Result<BankMutationResult, String> {
if amount <= 0.0 {
return Err("Deposit earnings amount must be greater than zero".to_string());
}
validate_bank_mode(&context, "Earnings deposits")?;
let mut bank = self.get_bank(key)?;
if bank.earnings < amount {
return Err("Pending earnings cannot cover that deposit request.".to_string());
}
bank.bank += amount;
bank.earnings -= amount;
bank.validate()
.map_err(|e| format!("Validation failed: {}", e))?;
self.repository.save(&bank)?;
Ok(BankMutationResult {
account: bank.clone(),
patch: build_patch(&bank, &["bank", "earnings"])?,
})
}
pub fn transfer(
&self,
source_key: String,
target_key: String,
context: BankTransferContext,
amount: f64,
) -> Result<BankTransferResult, String> {
if amount <= 0.0 {
return Err("Transfer amount must be greater than zero".to_string());
}
validate_bank_mode(
&BankOperationContext {
mode: context.mode.clone(),
atm_authorized: context.atm_authorized,
},
"Transfers",
)?;
if source_key == target_key {
return Err("You cannot transfer funds to yourself.".to_string());
}
let mut source_account = self.get_bank(source_key)?;
let mut target_account = self.get_bank(target_key)?;
let source_field = match context.from_field.trim().to_ascii_lowercase().as_str() {
"cash" => "cash",
_ => "bank",
};
let source_balance = match source_field {
"cash" => source_account.cash,
_ => source_account.bank,
};
if source_balance < amount {
return Err(match source_field {
"cash" => "Cash on hand cannot cover that transfer.".to_string(),
_ => "Bank balance cannot cover that transfer.".to_string(),
});
}
match source_field {
"cash" => source_account.cash -= amount,
_ => source_account.bank -= amount,
}
target_account.bank += amount;
source_account
.validate()
.map_err(|e| format!("Validation failed: {}", e))?;
target_account
.validate()
.map_err(|e| format!("Validation failed: {}", e))?;
self.repository.save(&source_account)?;
self.repository.save(&target_account)?;
Ok(BankTransferResult {
source_patch: build_patch(&source_account, &[source_field])?,
source_account,
target_patch: build_patch(&target_account, &["bank"])?,
target_account,
})
}
pub fn validate_pin(
&self,
key: String,
pin: String,
context: BankPinContext,
) -> Result<(), String> {
if !context.mode.eq_ignore_ascii_case("atm") {
return Err("PIN entry is only available from an ATM session.".to_string());
}
if pin.len() != 4 || !pin.chars().all(|character| character.is_ascii_digit()) {
return Err("Enter your four-digit access PIN.".to_string());
}
let bank = self.get_bank(key)?;
if pin != bank.pin.to_string() {
return Err("Incorrect PIN.".to_string());
}
Ok(())
}
pub fn save_bank(&self, key: String) -> Result<Bank, String> {
let bank = self
.repository
.get(&key)?
.ok_or_else(|| format!("Bank with UID '{}' not found in hot state", key))?;
let bank_json =
serde_json::to_string(&bank).map_err(|e| format!("Failed to serialize bank: {}", e))?;
let saved_bank = self.service.update_bank(key, bank_json)?;
self.repository.save(&saved_bank)?;
Ok(saved_bank)
}
pub fn remove_bank(&self, key: String) -> Result<(), String> {
self.repository.delete(&key)
}
}
fn apply_bank_field(bank: &mut Bank, field: &str, value: &Value) -> Result<(), String> {
match field {
"uid" => Ok(()),
"name" => {
bank.name = value
.as_str()
.ok_or_else(|| "Name must be a string".to_string())?
.to_string();
Ok(())
}
"bank" => {
bank.bank = value
.as_f64()
.ok_or_else(|| "Bank balance must be a number".to_string())?;
Ok(())
}
"cash" => {
bank.cash = value
.as_f64()
.ok_or_else(|| "Cash must be a number".to_string())?;
Ok(())
}
"earnings" => {
bank.earnings = value
.as_f64()
.ok_or_else(|| "Earnings must be a number".to_string())?;
Ok(())
}
"pin" => {
bank.pin = value
.as_u64()
.ok_or_else(|| "PIN must be a number".to_string())?;
Ok(())
}
"transactions" => {
let values = value
.as_array()
.ok_or_else(|| "Transactions must be an array".to_string())?;
bank.transactions = values
.iter()
.map(|entry| {
entry
.as_str()
.map(|item| item.to_string())
.ok_or_else(|| "Transactions must contain strings".to_string())
})
.collect::<Result<Vec<_>, _>>()?;
Ok(())
}
_ => Err(format!("Unknown field: {}", field)),
}
}
fn current_bank_field_value(bank: &Bank, field: &str) -> Result<Value, String> {
match field {
"uid" => Ok(json!(bank.uid)),
"name" => Ok(json!(bank.name)),
"bank" => Ok(json!(bank.bank)),
"cash" => Ok(json!(bank.cash)),
"earnings" => Ok(json!(bank.earnings)),
"pin" => Ok(json!(bank.pin)),
"transactions" => Ok(json!(bank.transactions)),
_ => Err(format!("Unknown field: {}", field)),
}
}
fn build_patch(bank: &Bank, fields: &[&str]) -> Result<HashMap<String, Value>, String> {
let mut patch = HashMap::new();
for field in fields {
patch.insert((*field).to_string(), current_bank_field_value(bank, field)?);
}
Ok(patch)
}
fn validate_atm_access(context: &BankOperationContext, action: &str) -> Result<(), String> {
if context.mode.eq_ignore_ascii_case("atm") && !context.atm_authorized {
return Err(format!("ATM authorization is required before {}.", action));
}
Ok(())
}
fn validate_bank_mode(context: &BankOperationContext, action: &str) -> Result<(), String> {
if !context.mode.eq_ignore_ascii_case("bank") {
return Err(format!(
"{} are only available from the full bank interface.",
action
));
}
Ok(())
}
impl<R: BankRepository> BankService<R> {
/// Creates a new bank service with the provided repository.
///

View File

@ -3,7 +3,7 @@
//! Handles validation, storage, and retrieval of player vehicle garages.
use forge_models::garage::{Garage, HitPoints, Vehicle};
use forge_repositories::GarageRepository;
use forge_repositories::{GarageHotRepository, GarageRepository};
use std::collections::HashMap;
use uuid::Uuid;
@ -12,6 +12,11 @@ pub struct GarageService<R: GarageRepository> {
repository: R,
}
pub struct GarageHotStateService<R: GarageRepository, H: GarageHotRepository> {
service: GarageService<R>,
repository: H,
}
impl<R: GarageRepository> GarageService<R> {
/// Creates a new garage service with the provided repository.
pub fn new(repository: R) -> Self {
@ -170,3 +175,86 @@ impl<R: GarageRepository> GarageService<R> {
self.repository.exists(&key)
}
}
impl<R: GarageRepository, H: GarageHotRepository> GarageHotStateService<R, H> {
pub fn new(repository: R, hot_repository: H) -> Self {
Self {
service: GarageService::new(repository),
repository: hot_repository,
}
}
pub fn init_garage(&self, uid: String) -> Result<Garage, String> {
if let Some(garage) = self.repository.get(&uid)? {
return Ok(garage);
}
let garage = match self.service.get_garage(uid.clone()) {
Ok(garage) => garage,
Err(_) => self.service.create_garage(uid.clone())?,
};
self.repository.save(&garage, &uid)?;
Ok(garage)
}
pub fn get_garage(&self, uid: String) -> Result<Garage, String> {
self.init_garage(uid)
}
pub fn override_garage(
&self,
uid: String,
vehicles: HashMap<String, Vehicle>,
) -> Result<Garage, String> {
for vehicle in vehicles.values() {
vehicle
.validate()
.map_err(|e| format!("Validation failed for vehicle {}: {}", vehicle.plate, e))?;
}
let garage = Garage { vehicles };
if garage.vehicles.len() > 5 {
return Err("Garage exceeds maximum capacity of 5 vehicles.".to_string());
}
self.repository.save(&garage, &uid)?;
Ok(garage)
}
pub fn save_garage(&self, uid: String) -> Result<Garage, String> {
let garage = self
.repository
.get(&uid)?
.ok_or_else(|| format!("No garage found for player '{}'", uid))?;
let saved = self
.service
.update_garage(uid.clone(), garage.vehicles.clone())?;
self.repository.save(&saved, &uid)?;
Ok(saved)
}
pub fn add_vehicle(
&self,
uid: String,
classname: String,
fuel: f64,
damage: f64,
hit_points_json: String,
) -> Result<Garage, String> {
let garage =
self.service
.add_vehicle(uid.clone(), classname, fuel, damage, hit_points_json)?;
self.repository.save(&garage, &uid)?;
Ok(garage)
}
pub fn remove_vehicle(&self, uid: String, plate: String) -> Result<Garage, String> {
let garage = self.service.remove_vehicle(uid.clone(), plate)?;
self.repository.save(&garage, &uid)?;
Ok(garage)
}
pub fn remove_garage(&self, uid: String) -> Result<(), String> {
self.repository.delete(&uid)
}
}

View File

@ -7,11 +7,11 @@ pub mod org;
pub mod v_garage;
pub mod v_locker;
pub use actor::ActorService;
pub use bank::BankService;
pub use actor::{ActorHotStateService, ActorService};
pub use bank::{BankHotStateService, BankService};
pub use cad::{CadStateService, CadViewService};
pub use garage::GarageService;
pub use locker::LockerService;
pub use org::OrgService;
pub use v_garage::VGarageService;
pub use v_locker::VLockerService;
pub use garage::{GarageHotStateService, GarageService};
pub use locker::{LockerHotStateService, LockerService};
pub use org::{OrgHotStateService, OrgService};
pub use v_garage::{VGarageHotStateService, VGarageService};
pub use v_locker::{VLockerHotStateService, VLockerService};

View File

@ -3,7 +3,7 @@
//! Handles validation, storage, and retrieval of player item lockers.
use forge_models::locker::{Item, Locker};
use forge_repositories::LockerRepository;
use forge_repositories::{LockerHotRepository, LockerRepository};
use std::collections::HashMap;
/// Service layer implementation for locker business logic and operations.
@ -11,6 +11,11 @@ pub struct LockerService<R: LockerRepository> {
repository: R,
}
pub struct LockerHotStateService<R: LockerRepository, H: LockerHotRepository> {
service: LockerService<R>,
repository: H,
}
impl<R: LockerRepository> LockerService<R> {
/// Creates a new locker service with the provided repository.
pub fn new(repository: R) -> Self {
@ -141,3 +146,59 @@ impl<R: LockerRepository> LockerService<R> {
self.repository.exists(&uid)
}
}
impl<R: LockerRepository, H: LockerHotRepository> LockerHotStateService<R, H> {
pub fn new(repository: R, hot_repository: H) -> Self {
Self {
service: LockerService::new(repository),
repository: hot_repository,
}
}
pub fn init_locker(&self, uid: String) -> Result<Locker, String> {
if let Some(locker) = self.repository.get(&uid)? {
return Ok(locker);
}
let locker = match self.service.get_locker(uid.clone()) {
Ok(locker) => locker,
Err(_) => self.service.create_locker(uid.clone())?,
};
self.repository.save(&locker, &uid)?;
Ok(locker)
}
pub fn get_locker(&self, uid: String) -> Result<Locker, String> {
self.init_locker(uid)
}
pub fn override_locker(
&self,
uid: String,
items: HashMap<String, Item>,
) -> Result<Locker, String> {
let locker = Locker { items };
if locker.items.len() > 25 {
return Err("Locker exceeds maximum capacity of 25 items.".to_string());
}
self.repository.save(&locker, &uid)?;
Ok(locker)
}
pub fn save_locker(&self, uid: String) -> Result<Locker, String> {
let locker = self
.repository
.get(&uid)?
.ok_or_else(|| format!("No locker found for player '{}'", uid))?;
let saved = self
.service
.update_locker(uid.clone(), locker.items.clone())?;
self.repository.save(&saved, &uid)?;
Ok(saved)
}
pub fn remove_locker(&self, uid: String) -> Result<(), String> {
self.repository.delete(&uid)
}
}

View File

@ -5,9 +5,11 @@
//!
//! For full documentation, architecture, and examples, see the [crate README](../README.md).
use forge_models::{CreditLineSummary, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry};
use forge_repositories::OrgRepository;
use std::collections::HashMap;
use forge_models::{
CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry,
};
use forge_repositories::{OrgHotRepository, OrgRepository};
use std::collections::{HashMap, HashSet};
/// Service layer implementation for organization business logic and operations.
///
@ -24,6 +26,11 @@ pub struct OrgService<R: OrgRepository> {
repository: R,
}
pub struct OrgHotStateService<R: OrgRepository, H: OrgHotRepository> {
service: OrgService<R>,
repository: H,
}
impl<R: OrgRepository> OrgService<R> {
fn normalize_org_value(
mut org_value: serde_json::Value,
@ -310,3 +317,89 @@ impl<R: OrgRepository> OrgService<R> {
Ok(fleet)
}
}
impl<R: OrgRepository, H: OrgHotRepository> OrgHotStateService<R, H> {
pub fn new(repository: R, hot_repository: H) -> Self {
Self {
service: OrgService::new(repository),
repository: hot_repository,
}
}
pub fn init_org(&self, id: String) -> Result<HotOrgRecord, String> {
if let Some(org) = self.repository.get(&id)? {
return Ok(org);
}
let hot_org = self.hydrate_org(&id)?;
self.repository.save(&hot_org)?;
Ok(hot_org)
}
pub fn get_org(&self, id: String) -> Result<HotOrgRecord, String> {
self.init_org(id)
}
pub fn override_org(
&self,
id: String,
mut hot_org: HotOrgRecord,
) -> Result<HotOrgRecord, String> {
hot_org.id = id;
self.repository.save(&hot_org)?;
Ok(hot_org)
}
pub fn save_org(&self, id: String) -> Result<HotOrgRecord, String> {
let hot_org = self
.repository
.get(&id)?
.ok_or_else(|| format!("Organization with ID '{}' not found", id))?;
let core_org = hot_org.clone().into_org();
let current_members = self
.service
.get_members(id.clone())?
.into_iter()
.map(|member| member.uid)
.collect::<HashSet<_>>();
let target_members = hot_org.members.keys().cloned().collect::<HashSet<_>>();
if self.service.org_exists(id.clone())? {
self.service.repository.update(&core_org)?;
} else {
self.service.repository.create(&core_org)?;
}
self.service
.repository
.update_assets(&id, &hot_org.assets)?;
self.service.repository.update_fleet(&id, &hot_org.fleet)?;
for member_uid in target_members.difference(&current_members) {
self.service.repository.add_member(&id, member_uid)?;
}
for member_uid in current_members.difference(&target_members) {
self.service.repository.remove_member(&id, member_uid)?;
}
self.repository.save(&hot_org)?;
Ok(hot_org)
}
pub fn remove_org(&self, id: String) -> Result<(), String> {
self.repository.delete(&id)
}
fn hydrate_org(&self, id: &str) -> Result<HotOrgRecord, String> {
let org = self
.service
.get_org(id.to_string())
.map_err(|error| format!("Organization with ID '{}' not found: {}", id, error))?;
let assets = self.service.get_assets(id.to_string())?;
let fleet = self.service.get_fleet(id.to_string())?;
let members = self.service.get_members(id.to_string())?;
Ok(HotOrgRecord::from_parts(org, assets, fleet, members))
}
}

View File

@ -4,7 +4,7 @@
//! validation, and orchestration.
use forge_models::{VGarage, VehicleCategory};
use forge_repositories::VGarageRepository;
use forge_repositories::{VGarageHotRepository, VGarageRepository};
/// Service layer implementation for virtual garage business logic and operations.
///
@ -22,6 +22,11 @@ pub struct VGarageService<R: VGarageRepository> {
repository: R,
}
pub struct VGarageHotStateService<R: VGarageRepository, H: VGarageHotRepository> {
service: VGarageService<R>,
repository: H,
}
impl<R: VGarageRepository> VGarageService<R> {
/// Creates a new garage service with the provided repository.
///
@ -54,6 +59,11 @@ impl<R: VGarageRepository> VGarageService<R> {
}
}
pub fn update_garage(&self, uid: &str, garage: &VGarage) -> Result<VGarage, String> {
self.repository.update(uid, garage)?;
Ok(garage.clone())
}
/// Retrieves a specific field from a player's virtual garage.
///
/// Fields: "cars", "armor", "heli", "planes", "naval", "other"
@ -122,3 +132,87 @@ impl<R: VGarageRepository> VGarageService<R> {
self.repository.exists(uid)
}
}
impl<R: VGarageRepository, H: VGarageHotRepository> VGarageHotStateService<R, H> {
pub fn new(repository: R, hot_repository: H) -> Self {
Self {
service: VGarageService::new(repository),
repository: hot_repository,
}
}
pub fn init_garage(&self, uid: &str) -> Result<VGarage, String> {
if let Some(garage) = self.repository.get(uid)? {
return Ok(garage);
}
let garage = match self.service.fetch_garage(uid) {
Ok(garage) => garage,
Err(_) => self.service.create_garage(uid)?,
};
self.repository.save(&garage, uid)?;
Ok(garage)
}
pub fn fetch_garage(&self, uid: &str) -> Result<VGarage, String> {
self.init_garage(uid)
}
pub fn get_garage(&self, uid: &str, field: &str) -> Result<Vec<String>, String> {
let garage = self.init_garage(uid)?;
Ok(match field.to_lowercase().as_str() {
"cars" => garage.cars,
"armor" => garage.armor,
"helis" | "heli" => garage.helis,
"planes" => garage.planes,
"naval" => garage.naval,
"other" => garage.other,
_ => Vec::new(),
})
}
pub fn override_garage(&self, uid: &str, garage: VGarage) -> Result<VGarage, String> {
self.repository.save(&garage, uid)?;
Ok(garage)
}
pub fn save_garage(&self, uid: &str) -> Result<VGarage, String> {
let garage = self
.repository
.get(uid)?
.ok_or_else(|| format!("No garage found for player '{}'", uid))?;
let saved = if self.service.garage_exists(uid)? {
self.service.update_garage(uid, &garage)?
} else {
self.service.create_garage(uid)?
};
self.repository.save(&saved, uid)?;
Ok(saved)
}
pub fn add_garage(
&self,
uid: &str,
category: VehicleCategory,
classnames: Vec<String>,
) -> Result<VGarage, String> {
let garage = self.service.add_garage(uid, category, classnames)?;
self.repository.save(&garage, uid)?;
Ok(garage)
}
pub fn remove_garage(
&self,
uid: &str,
category: VehicleCategory,
classname: &str,
) -> Result<VGarage, String> {
let garage = self.service.remove_garage(uid, category, classname)?;
self.repository.save(&garage, uid)?;
Ok(garage)
}
pub fn remove_hot_garage(&self, uid: &str) -> Result<(), String> {
self.repository.delete(uid)
}
}

View File

@ -4,7 +4,7 @@
//! validation, and orchestration.
use forge_models::{EquipmentCategory, VLocker};
use forge_repositories::VLockerRepository;
use forge_repositories::{VLockerHotRepository, VLockerRepository};
/// Service layer implementation for virtual locker business logic and operations.
///
@ -22,6 +22,11 @@ pub struct VLockerService<R: VLockerRepository> {
repository: R,
}
pub struct VLockerHotStateService<R: VLockerRepository, H: VLockerHotRepository> {
service: VLockerService<R>,
repository: H,
}
impl<R: VLockerRepository> VLockerService<R> {
/// Creates a new locker service with the provided repository.
///
@ -54,6 +59,11 @@ impl<R: VLockerRepository> VLockerService<R> {
}
}
pub fn update_locker(&self, uid: &str, locker: &VLocker) -> Result<VLocker, String> {
self.repository.update(uid, locker)?;
Ok(locker.clone())
}
/// Retrieves a specific field from a player's virtual locker.
///
/// Fields: "items", "weapons", "magazines", "backpacks"
@ -122,3 +132,63 @@ impl<R: VLockerRepository> VLockerService<R> {
self.repository.exists(uid)
}
}
impl<R: VLockerRepository, H: VLockerHotRepository> VLockerHotStateService<R, H> {
pub fn new(repository: R, hot_repository: H) -> Self {
Self {
service: VLockerService::new(repository),
repository: hot_repository,
}
}
pub fn init_locker(&self, uid: &str) -> Result<VLocker, String> {
if let Some(locker) = self.repository.get(uid)? {
return Ok(locker);
}
let locker = match self.service.fetch_locker(uid) {
Ok(locker) => locker,
Err(_) => self.service.create_locker(uid)?,
};
self.repository.save(&locker, uid)?;
Ok(locker)
}
pub fn fetch_locker(&self, uid: &str) -> Result<VLocker, String> {
self.init_locker(uid)
}
pub fn get_locker(&self, uid: &str, field: &str) -> Result<Vec<String>, String> {
let locker = self.init_locker(uid)?;
Ok(match field.to_lowercase().as_str() {
"items" => locker.items,
"weapons" => locker.weapons,
"magazines" => locker.magazines,
"backpacks" => locker.backpacks,
_ => Vec::new(),
})
}
pub fn override_locker(&self, uid: &str, locker: VLocker) -> Result<VLocker, String> {
self.repository.save(&locker, uid)?;
Ok(locker)
}
pub fn save_locker(&self, uid: &str) -> Result<VLocker, String> {
let locker = self
.repository
.get(uid)?
.ok_or_else(|| format!("No locker found for player '{}'", uid))?;
let saved = if self.service.locker_exists(uid)? {
self.service.update_locker(uid, &locker)?
} else {
self.service.create_locker(uid)?
};
self.repository.save(&saved, uid)?;
Ok(saved)
}
pub fn remove_locker(&self, uid: &str) -> Result<(), String> {
self.repository.delete(uid)
}
}