Implement org credit line debt and bank repayment flow #2
@ -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);
|
||||
|
||||
|
||||
@ -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
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
@ -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"
|
||||
|
||||
@ -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"
|
||||
>
|
||||
⚙
|
||||
</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();
|
||||
103
arma/client/addons/cad/ui/src/dispatcher/formatters.js
Normal file
103
arma/client/addons/cad/ui/src/dispatcher/formatters.js
Normal 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}.`;
|
||||
},
|
||||
};
|
||||
255
arma/client/addons/cad/ui/src/dispatcher/index.js
Normal file
255
arma/client/addons/cad/ui/src/dispatcher/index.js
Normal 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();
|
||||
268
arma/client/addons/cad/ui/src/dispatcher/modals.js
Normal file
268
arma/client/addons/cad/ui/src/dispatcher/modals.js
Normal 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";
|
||||
},
|
||||
};
|
||||
325
arma/client/addons/cad/ui/src/dispatcher/render.js
Normal file
325
arma/client/addons/cad/ui/src/dispatcher/render.js
Normal 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"
|
||||
>
|
||||
⚙
|
||||
</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();
|
||||
},
|
||||
};
|
||||
@ -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",
|
||||
|
||||
@ -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]]
|
||||
}]
|
||||
];
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
PREP(initBank);
|
||||
PREP(initMessenger);
|
||||
PREP(initModel);
|
||||
PREP(initPayloadBuilder);
|
||||
PREP(initSessionManager);
|
||||
PREP(initStore);
|
||||
PREP(initValidator);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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
|
||||
}]
|
||||
]];
|
||||
|
||||
|
||||
105
arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf
Normal file
105
arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf
Normal 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)
|
||||
@ -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
|
||||
}]
|
||||
|
||||
@ -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)]
|
||||
]
|
||||
]
|
||||
}]
|
||||
];
|
||||
|
||||
|
||||
@ -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)
|
||||
@ -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", ""];
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
PREP(extCall);
|
||||
PREP(setHandler);
|
||||
PREP(transport);
|
||||
|
||||
@ -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)
|
||||
|
||||
115
arma/server/addons/extension/functions/fnc_transport.sqf
Normal file
115
arma/server/addons/extension/functions/fnc_transport.sqf
Normal 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]
|
||||
};
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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]]]
|
||||
}]
|
||||
];
|
||||
|
||||
|
||||
@ -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", ""];
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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", ""];
|
||||
|
||||
@ -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", ""];
|
||||
|
||||
@ -1 +1,2 @@
|
||||
PREP(initStores);
|
||||
PREP(saveHotState);
|
||||
|
||||
@ -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);
|
||||
}];
|
||||
|
||||
@ -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); };
|
||||
|
||||
84
arma/server/addons/main/functions/fnc_saveHotState.sqf
Normal file
84
arma/server/addons/main/functions/fnc_saveHotState.sqf
Normal 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
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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", ""];
|
||||
|
||||
@ -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]];
|
||||
};
|
||||
|
||||
@ -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];
|
||||
};
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
// ============================================================================
|
||||
|
||||
951
arma/server/extension/src/transport.rs
Normal file
951
arma/server/extension/src/transport.rs
Normal 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
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}`.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}`.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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.
|
||||
///
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(¤t_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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user