refactor(bank): split monolithic store into focused modules; fix locker grant for attachments

- Split BankBaseStore into SessionManager, Messenger, Model, Store, Validator
- Extract validation logic into BankValidator with try/catch and per-action methods
- Remove duplicate notifications from transaction actions
- Update event handlers to call validator first, forward context to store/session
- Fix locker grantItems: add missing 'attachment' category mapping to 'item'
- Fix locker grantItems: replace exitWith with if/else to prevent skipping remaining items

Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
Jacob Schmidt 2026-03-16 10:54:25 -05:00
parent 68218304ab
commit 6c0ce9e867
21 changed files with 1032 additions and 582 deletions

View File

@ -1,5 +1,4 @@
PREP(handleUIEvents);
PREP(initClass);
PREP(initSessionService);
PREP(initUIBridge);
PREP(openUI);

View File

@ -1,7 +1,6 @@
#include "script_component.hpp"
if (isNil QGVAR(BankClass)) then { call FUNC(initClass); };
if (isNil QGVAR(BankSessionService)) then { call FUNC(initSessionService); };
if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
[QGVAR(initBank), {
@ -26,8 +25,27 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
};
}] call CFUNC(addEventHandler);
[QGVAR(responseHydrateBank), {
params [["_data", createHashMap, [createHashMap]]];
if !(isNil QGVAR(BankUIBridge)) then {
GVAR(BankUIBridge) call ["handleHydrateResponse", [_data, "bank::hydrate"]];
};
}] call CFUNC(addEventHandler);
[QGVAR(responseBankNotice), {
params [
["_type", "error", [""]],
["_message", "", [""]]
];
if !(isNil QGVAR(BankUIBridge)) then {
GVAR(BankUIBridge) call ["handleNoticeResponse", [_type, _message]];
};
}] call CFUNC(addEventHandler);
[{
EGVAR(org,OrgClass) get "isLoaded";
getPlayerUID player isNotEqualTo "";
}, {
[QGVAR(initBank), []] call CFUNC(localEvent);
}] call CFUNC(waitUntilAndExecute);

View File

@ -68,6 +68,11 @@ switch (_event) do {
GVAR(BankUIBridge) call ["handleDepositEarningsRequest", [_data]];
};
};
case "bank::pin::request": {
if !(isNil QGVAR(BankUIBridge)) then {
GVAR(BankUIBridge) call ["handleSubmitPinRequest", [_data]];
};
};
default {
hint format ["Unhandled bank UI event: %1", _event];
};

View File

@ -18,7 +18,6 @@ GVAR(BankBaseClass) = compileFinal createHashMapFromArray [
["bank", 0],
["cash", 0],
["earnings", 0],
["pin", 1234],
["transactions", []]
]];
_self set ["isLoaded", false];

View File

@ -1,80 +0,0 @@
#include "..\script_component.hpp"
/*
* File: fnc_initSessionService.sqf
* Author: IDSolutions
* Public: No
*
* Description:
* Initializes the bank session service that shapes the browser payload.
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(BankSessionServiceBaseClass) = compileFinal createHashMapFromArray [
["#type", "BankSessionServiceBaseClass"],
["buildTransferTargets", compileFinal {
private _targets = [];
{
if (isNull _x || { _x isEqualTo player }) then {
continue;
};
private _uid = getPlayerUID _x;
private _name = name _x;
if (_uid isEqualTo "" || { _name isEqualTo "" }) then {
continue;
};
_targets pushBack (createHashMapFromArray [
["name", _name],
["uid", _uid]
]);
} forEach allPlayers;
private _targetPairs = _targets apply {
[toLowerANSI (_x getOrDefault ["name", ""]), _x]
};
_targetPairs sort true;
_targetPairs apply {
_x param [1, createHashMap]
}
}],
["buildPayload", compileFinal {
params [["_mode", "bank", [""]]];
private _account = if (isNil QGVAR(BankClass)) then {
createHashMap
} else {
GVAR(BankClass) call ["getAccountState", []]
};
private _orgFunds = 0;
private _orgName = "";
if !(isNil QEGVAR(org,OrgClass)) then {
_orgFunds = EGVAR(org,OrgClass) call ["get", ["funds", 0]];
_orgName = EGVAR(org,OrgClass) call ["get", ["name", ""]];
};
createHashMapFromArray [
["session", createHashMapFromArray [
["mode", ["bank", "atm"] select (toLowerANSI _mode isEqualTo "atm")],
["orgFunds", _orgFunds],
["orgName", _orgName],
["playerName", name player],
["transferTargets", _self call ["buildTransferTargets", []]],
["uid", getPlayerUID player]
]],
["account", createHashMapFromArray [
["bank", _account getOrDefault ["bank", 0]],
["cash", _account getOrDefault ["cash", 0]],
["earnings", _account getOrDefault ["earnings", 0]],
["pin", str (_account getOrDefault ["pin", 1234])],
["transactions", _account getOrDefault ["transactions", []]]
]]
]
}]
];
GVAR(BankSessionService) = createHashMapObject [GVAR(BankSessionServiceBaseClass)];
GVAR(BankSessionService)

View File

@ -19,9 +19,6 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
["#create", compileFinal {
_self set ["mode", "bank"];
}],
["buildPayload", compileFinal {
GVAR(BankSessionService) call ["buildPayload", [_self call ["getMode", []]]]
}],
["getActiveBrowserControl", compileFinal {
private _display = uiNamespace getVariable ["RscBank", displayNull];
if (isNull _display) exitWith {
@ -36,14 +33,16 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
["getMode", compileFinal {
_self getOrDefault ["mode", "bank"]
}],
["hasOpenScreen", compileFinal {
private _screen = _self call ["getScreen", []];
private _control = _self call ["getActiveBrowserControl", []];
!(isNull _control) && { _screen call ["isReady", []] }
}],
["handleDepositEarningsRequest", compileFinal {
params [["_data", createHashMap, [createHashMap]]];
private _amount = floor (_data getOrDefault ["amount", 0]);
if (_amount <= 0) exitWith {
_self call ["sendNotice", ["error", "No earnings are available to deposit."]];
};
[SRPC(bank,requestDepositEarnings), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
true
}],
@ -51,22 +50,41 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
params [["_data", createHashMap, [createHashMap]]];
private _amount = floor (_data getOrDefault ["amount", 0]);
if (_amount <= 0) exitWith {
_self call ["sendNotice", ["error", "Enter a valid deposit amount."]];
};
[SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
true
}],
["handleHydrateResponse", compileFinal {
params [["_data", createHashMap, [createHashMap]], ["_event", "bank::hydrate", [""]]];
if !(_self call ["hasOpenScreen", []]) exitWith { false };
_self call ["sendEvent", [_event, _data, _self call ["getActiveBrowserControl", []]]]
}],
["handleNoticeResponse", compileFinal {
params [["_type", "error", [""]], ["_message", "", [""]]];
_self call ["sendNotice", [_type, _message]]
}],
["handleReady", compileFinal {
params [["_control", controlNull, [controlNull]], ["_data", createHashMap, [createHashMap]]];
private _screen = _self call ["getScreen", []];
_screen call ["setControl", [_control]];
_screen call ["markReady", [true]];
_self call ["flushPendingEvents", []];
_self call ["sendEvent", ["bank::hydrate", _self call ["buildPayload", []], _control]];
_self call ["requestHydrate", [true]]
}],
["handleSubmitPinRequest", compileFinal {
params [["_data", createHashMap, [createHashMap]]];
private _pin = _data getOrDefault ["pin", ""];
if !(_pin isEqualType "") then {
_pin = str _pin;
};
[SRPC(bank,requestSubmitPin), [getPlayerUID player, _pin]] call CFUNC(serverEvent);
true
}],
["handleTransferRequest", compileFinal {
params [["_data", createHashMap, [createHashMap]]];
@ -75,18 +93,6 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
private _target = _data getOrDefault ["target", ""];
private _from = toLowerANSI (_data getOrDefault ["from", "bank"]);
if (_target isEqualTo "") exitWith {
_self call ["sendNotice", ["error", "Select a transfer recipient."]];
};
if (_target isEqualTo getPlayerUID player) exitWith {
_self call ["sendNotice", ["error", "You cannot transfer funds to yourself."]];
};
if (_amount <= 0) exitWith {
_self call ["sendNotice", ["error", "Enter a valid transfer amount."]];
};
[SRPC(bank,requestTransfer), [getPlayerUID player, _target, _from, _amount]] call CFUNC(serverEvent);
true
}],
@ -94,23 +100,24 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
params [["_data", createHashMap, [createHashMap]]];
private _amount = floor (_data getOrDefault ["amount", 0]);
if (_amount <= 0) exitWith {
_self call ["sendNotice", ["error", "Enter a valid withdrawal amount."]];
};
[SRPC(bank,requestWithdraw), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
true
}],
["refreshSession", compileFinal {
private _control = _self call ["getActiveBrowserControl", []];
if (isNull _control) exitWith { false };
_self call ["requestHydrate", [false]]
}],
["requestHydrate", compileFinal {
params [["_resetAuthorization", false, [false]]];
_self call ["sendEvent", ["bank::sync", _self call ["buildPayload", []], _control]]
if !(_self call ["hasOpenScreen", []]) exitWith { false };
[SRPC(bank,requestHydrateBank), [getPlayerUID player, _self call ["getMode", []], _resetAuthorization]] call CFUNC(serverEvent);
true
}],
["sendNotice", compileFinal {
params [["_type", "error", [""]], ["_message", "", [""]], ["_control", controlNull, [controlNull]]];
if (_message isEqualTo "") exitWith { false };
if (_message isEqualTo "" || { !(_self call ["hasOpenScreen", []]) }) exitWith { false };
_self call ["sendEvent", ["bank::notice", createHashMapFromArray [
["message", _message],

File diff suppressed because one or more lines are too long

View File

@ -40,6 +40,9 @@
requestRefresh() {
return bridge.send("bank::refresh", {});
},
requestSubmitPin(payload) {
return bridge.send("bank::pin::request", payload);
},
requestTransfer(payload) {
return bridge.send("bank::transfer::request", payload);
},

View File

@ -2,6 +2,7 @@
const BankApp = (window.BankApp = window.BankApp || {});
const defaultSession = {
atmAuthorized: false,
mode: "bank",
orgFunds: 0,
orgName: "",
@ -14,7 +15,6 @@
bank: 0,
cash: 0,
earnings: 0,
pin: "1234",
transactions: [],
};

View File

@ -4,14 +4,6 @@
let noticeTimer = null;
function getAccount() {
return BankApp.data?.account || {};
}
function getSession() {
return BankApp.data?.session || {};
}
function normalizeAmount(value) {
const amount = Math.floor(Number(value || 0));
return Number.isFinite(amount) ? amount : 0;
@ -58,18 +50,6 @@
function requestDeposit(amountValue) {
const amount = normalizeAmount(amountValue);
const account = getAccount();
if (amount <= 0) {
showNotice("error", "Enter a valid deposit amount.");
return false;
}
if (amount > Number(account.cash || 0)) {
showNotice("error", "Cash on hand cannot cover that deposit.");
return false;
}
const bridge = BankApp.bridge;
if (!bridge || typeof bridge.requestDeposit !== "function") {
showNotice("error", "Deposit bridge is unavailable.");
@ -89,18 +69,6 @@
function requestWithdraw(amountValue) {
const amount = normalizeAmount(amountValue);
const account = getAccount();
if (amount <= 0) {
showNotice("error", "Enter a valid withdrawal amount.");
return false;
}
if (amount > Number(account.bank || 0)) {
showNotice("error", "Bank balance cannot cover that withdrawal.");
return false;
}
const bridge = BankApp.bridge;
if (!bridge || typeof bridge.requestWithdraw !== "function") {
showNotice("error", "Withdraw bridge is unavailable.");
@ -120,30 +88,8 @@
function requestTransfer(targetUid, amountValue) {
const amount = normalizeAmount(amountValue);
const session = getSession();
const account = getAccount();
const targetId = String(targetUid || "").trim();
if (!targetId) {
showNotice("error", "Select a transfer recipient.");
return false;
}
if (targetId === String(session.uid || "")) {
showNotice("error", "You cannot transfer funds to yourself.");
return false;
}
if (amount <= 0) {
showNotice("error", "Enter a valid transfer amount.");
return false;
}
if (amount > Number(account.bank || 0)) {
showNotice("error", "Bank balance cannot cover that transfer.");
return false;
}
const bridge = BankApp.bridge;
if (!bridge || typeof bridge.requestTransfer !== "function") {
showNotice("error", "Transfer bridge is unavailable.");
@ -167,21 +113,6 @@
function requestDepositEarnings(amountValue) {
const amount = normalizeAmount(amountValue);
const account = getAccount();
if (amount <= 0) {
showNotice("error", "No earnings are available to deposit.");
return false;
}
if (amount > Number(account.earnings || 0)) {
showNotice(
"error",
"Pending earnings cannot cover that deposit request.",
);
return false;
}
const bridge = BankApp.bridge;
if (!bridge || typeof bridge.requestDepositEarnings !== "function") {
showNotice("error", "Earnings bridge is unavailable.");
@ -224,21 +155,21 @@
function submitPin() {
const enteredPin = String(store.getEnteredPin() || "");
const actualPin = String(getAccount().pin || "1234");
if (enteredPin.length !== 4) {
showNotice("error", "Enter your four-digit access PIN.");
const bridge = BankApp.bridge;
if (!bridge || typeof bridge.requestSubmitPin !== "function") {
showNotice("error", "PIN bridge is unavailable.");
return false;
}
if (enteredPin !== actualPin) {
clearPin();
showNotice("error", "Incorrect PIN.");
store.startAction("pin");
const sent = bridge.requestSubmitPin({ pin: enteredPin });
if (!sent) {
store.finishAction();
showNotice("error", "PIN bridge is unavailable.");
return false;
}
clearPin();
store.setAtmView("menu");
return true;
}
@ -299,7 +230,6 @@
if (success) {
store.setCustomAmount("");
store.setAtmView("menu");
}
return success;
@ -314,10 +244,6 @@
? requestDeposit(amount)
: requestWithdraw(amount);
if (success) {
store.setAtmView("menu");
}
return success;
}

View File

@ -25,19 +25,35 @@
const mode = String(payload?.session?.mode || "bank")
.trim()
.toLowerCase();
const atmAuthorized = Boolean(payload?.session?.atmAuthorized);
const currentMode = this.getMode();
const currentAtmView = this.getAtmView();
const currentPendingAction = this.getPendingAction();
this.setMode(mode === "atm" ? "atm" : "bank");
this.setPendingAction("");
this.setNotice({ text: "", type: "" });
this.setEnteredPin("");
this.setCustomAmount("");
this.setAccountVersion(this.getAccountVersion() + 1);
this.setSessionVersion(this.getSessionVersion() + 1);
if (mode === "atm") {
this.setAtmView(currentMode === "atm" ? currentAtmView : "pin");
if (!atmAuthorized) {
this.setAtmView("pin");
return;
}
if (
currentPendingAction === "deposit" ||
currentPendingAction === "withdraw" ||
currentAtmView === "pin" ||
currentMode !== "atm"
) {
this.setAtmView("menu");
return;
}
this.setAtmView(currentAtmView);
return;
}

View File

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

View File

@ -13,15 +13,24 @@ PREP_RECOMPILE_END;
GVAR(BankStore) call ["init", [_uid]];
}] call CFUNC(addEventHandler);
[QGVAR(requestHydrateBank), {
params [["_uid", "", [""]], ["_mode", "bank", [""]], ["_resetAuthorization", false, [false]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" };
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]];
private _player = [_uid] call EFUNC(common,getPlayer);
if (_field isNotEqualTo "") then {
_finalData = createHashMapFromArray [[_field, _finalData]];
};
[CRPC(bank,responseSyncBank), [_finalData], _player] call CFUNC(targetEvent);
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalData]];
}] call CFUNC(addEventHandler);
[QGVAR(requestSetBank), {
@ -30,9 +39,7 @@ PREP_RECOMPILE_END;
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]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(bank,responseSyncBank), [_hashMap], _player] call CFUNC(targetEvent);
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _hashMap]];
}] call CFUNC(addEventHandler);
[QGVAR(requestMSetBank), {
@ -42,9 +49,7 @@ PREP_RECOMPILE_END;
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]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(bank,responseSyncBank), [_hashMap], _player] call CFUNC(targetEvent);
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _hashMap]];
}] call CFUNC(addEventHandler);
[QGVAR(requestSaveBank), {
@ -53,9 +58,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" };
private _finalData = GVAR(BankStore) call ["save", [GVAR(Registry), "bank:update", _uid]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(bank,responseSyncBank), [_finalData], _player] call CFUNC(targetEvent);
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalData]];
}] call CFUNC(addEventHandler);
[QGVAR(requestRemoveBank), {
@ -68,44 +71,47 @@ PREP_RECOMPILE_END;
[QGVAR(requestDeposit), {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" };
GVAR(BankStore) call ["deposit", [_uid, _amount]];
private _context = GVAR(BankValidator) call ["validateDeposit", [_uid, _amount]];
if (_context isEqualTo false) exitWith {};
GVAR(BankStore) call ["deposit", [_uid, _amount, _context]];
}] call CFUNC(addEventHandler);
[QGVAR(requestPayment), {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" };
GVAR(BankStore) call ["payment", [_uid, _amount]];
private _context = GVAR(BankValidator) call ["validatePayment", [_uid, _amount]];
if (_context isEqualTo false) exitWith {};
GVAR(BankStore) call ["payment", [_uid, _amount, _context]];
}] 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]];
}] call CFUNC(addEventHandler);
[QGVAR(requestTransfer), {
params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]];
if (_uid isEqualTo "" || _target isEqualTo "" || _from isEqualTo "" || _amount isEqualTo 0) exitWith {
diag_log "[FORGE:Server:Bank] Empty/Invalid UID, Target, From Account, or Amount!"
};
if (_uid isEqualTo _target) exitWith {
diag_log format ["[FORGE:Server:Bank] SECURITY: Player %1 attempted self-transfer!", _uid];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(notifications,recieveNotification), ["error", "Bank", "Cannot transfer to yourself!"], _player] call CFUNC(targetEvent);
};
GVAR(BankStore) call ["transfer", [_uid, _target, _from, _amount]];
private _context = GVAR(BankValidator) call ["validateTransfer", [_uid, _target, _from, _amount]];
if (_context isEqualTo false) exitWith {};
GVAR(BankStore) call ["transfer", [_uid, _target, _amount, _context]];
}] call CFUNC(addEventHandler);
[QGVAR(requestWithdraw), {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" };
GVAR(BankStore) call ["withdraw", [_uid, _amount]];
private _context = GVAR(BankValidator) call ["validateWithdraw", [_uid, _amount]];
if (_context isEqualTo false) exitWith {};
GVAR(BankStore) call ["withdraw", [_uid, _amount, _context]];
}] call CFUNC(addEventHandler);
[QGVAR(requestDepositEarnings), {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" };
GVAR(BankStore) call ["depositEarnings", [_uid, _amount]];
private _context = GVAR(BankValidator) call ["validateDepositEarnings", [_uid, _amount]];
if (_context isEqualTo false) exitWith {};
GVAR(BankStore) call ["depositEarnings", [_uid, _amount, _context]];
}] call CFUNC(addEventHandler);

View File

@ -1,326 +0,0 @@
#include "..\script_component.hpp"
/*
* File: fnc_initBankStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-02-17
* Public: Yes
*
* Description:
* Initializes the bank store for managing player bank accounts.
* Provides methods for syncing, saving, and applying bank accounts to the player.
*
* Arguments:
* None
*
* Return Value:
* Bank store object [HASHMAP OBJECT]
*
* Example:
* call forge_server_bank_fnc_initBankStore
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(BankModel) = compileFinal createHashMapObject [[
["#type", "BankModel"],
["defaults", compileFinal {
private _account = createHashMap;
_account set ["uid", ""];
_account set ["name", ""];
_account set ["bank", 0];
_account set ["cash", 0];
_account set ["earnings", 0];
_account set ["pin", 1234];
_account set ["transactions", []];
_account
}],
["fromPlayer", compileFinal {
params [["_player", objNull, [objNull]]];
if (_player isEqualTo objNull) exitWith { _self call ["defaults", []] };
private _account = _self call ["defaults", []];
_account set ["uid", getPlayerUID _player];
_account set ["name", name _player];
_account set ["bank", 0];
_account set ["cash", 0];
_account set ["earnings", 0];
_account set ["pin", 1234];
_account set ["transactions", []];
_account
}],
["migrate", compileFinal {
params [["_account", createHashMap, [createHashMap]]];
private _defaults = _self call ["defaults", []];
{
if !(_x in _account) then { _account set [_x, _y]; };
} 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
}]
]];
GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
["#base", EGVAR(common,BaseStore)],
["#type", "BankBaseStore"],
["#create", compileFinal {
GVAR(IndexRegistry) = createHashMap;
GVAR(Registry) = createHashMap;
["INFO", "Bank Store Initialized!"] call EFUNC(common,log);
}],
["init", compileFinal {
params [["_uid", "", [""]]];
private _player = [_uid] call EFUNC(common,getPlayer);
private _cached = GVAR(Registry) getOrDefault [_uid, nil];
if !(isNil { _cached }) exitWith { [CRPC(bank,responseInitBank), [_cached], _player] call CFUNC(targetEvent); _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);
private _fallbackAccount = GVAR(BankModel) call ["fromPlayer", [_player]];
_fallbackAccount set ["uid", _uid];
private _regEntry = createHashMapFromArray [["uid", _uid], ["name", (name _player)]];
GVAR(IndexRegistry) set [_uid, _regEntry];
GVAR(Registry) set [_uid, _fallbackAccount];
[CRPC(bank,responseInitBank), [_fallbackAccount], _player] call CFUNC(targetEvent);
_fallbackAccount
};
private _finalAccount = createHashMap;
if (_result == "true") then {
_finalAccount = _self call ["fetch", ["bank:get", _uid]];
["INFO", format ["Found bank account for %1", _uid]] call EFUNC(common,log);
} else {
_finalAccount = GVAR(BankModel) call ["fromPlayer", [_player]];
_finalAccount set ["uid", _uid];
private _json = _self call ["toJSON", [_finalAccount]];
["bank:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to create bank account %1! Using fallback account.", _uid]] call EFUNC(common,log);
private _regEntry = createHashMapFromArray [["uid", _uid], ["name", (name _player)]];
GVAR(IndexRegistry) set [_uid, _regEntry];
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseInitBank), [_finalAccount], _player] call CFUNC(targetEvent);
_finalAccount
};
["INFO", format ["Created new bank account for %1", _uid]] call EFUNC(common,log);
};
private _regEntry = createHashMapFromArray [["uid", _uid], ["name", (name _player)]];
GVAR(IndexRegistry) set [_uid, _regEntry];
// _finalAccount = GVAR(BankModel) call ["migrate", [_finalAccount]];
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseInitBank), [_finalAccount], _player] call CFUNC(targetEvent);
_finalAccount
}],
["deposit", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
["INFO", format ["Deposit %1, for %2", _amount, _uid]] call EFUNC(common,log);
private _account = GVAR(Registry) getOrDefault [_uid, nil];
if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); };
private _bank = _account getOrDefault ["bank", 0];
private _cash = _account getOrDefault ["cash", 0];
if (_cash < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); };
private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)], ["cash", (_cash - _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Deposited $%1", _amount]], _player] call CFUNC(targetEvent);
}],
["payment", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
["INFO", format ["Payment %1, for %2", _amount, _uid]] call EFUNC(common,log);
private _account = GVAR(Registry) getOrDefault [_uid, nil];
if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); };
private _bank = _account getOrDefault ["bank", 0];
private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Paid $%1", _amount]], _player] call CFUNC(targetEvent);
}],
["buildChargeResult", compileFinal {
params [["_message", "Unable to process bank payment.", [""]]];
createHashMapFromArray [
["success", false],
["message", _message],
["patch", createHashMap]
]
}],
["chargeCheckout", compileFinal {
params [
["_uid", "", [""]],
["_source", "cash", [""]],
["_amount", 0, [0]],
["_commit", false, [false]]
];
private _result = _self call ["buildChargeResult", []];
private _field = switch (toLowerANSI _source) do {
case "cash": { "cash" };
case "bank": { "bank" };
default { "" };
};
if (_field isEqualTo "") exitWith {
_result set ["message", "Selected bank payment source is unsupported."];
_result
};
private _account = GVAR(Registry) getOrDefault [_uid, createHashMap];
if (_account isEqualTo createHashMap) exitWith {
_result set ["message", "Bank account data is unavailable for checkout."];
_result
};
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
};
private _patch = createHashMapFromArray [[_field, (_balance - _amount)]];
if (_commit) then {
_patch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]];
};
_result set ["success", true];
_result set ["message", ""];
_result set ["patch", _patch];
_result
}],
["transfer", compileFinal {
params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]];
if (_uid isEqualTo _target) exitWith { ["WARNING", format ["Self-transfer attempt blocked for %1", _uid]] call EFUNC(common,log); };
private _account = GVAR(Registry) getOrDefault [_uid, nil];
if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); };
private _targetAccount = GVAR(Registry) getOrDefault [_target, nil];
if (isNil "_targetAccount") exitWith { ["ERROR", "Empty/Invalid Target Account!"] call EFUNC(common,log); };
private _selected = _account getOrDefault [_from, 0];
if (_selected < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); };
private _targetBank = _targetAccount getOrDefault ["bank", 0];
private _finalAccount = createHashMapFromArray [[_from, (_selected - _amount)]];
private _finalTargetBank = createHashMapFromArray [["bank", (_targetBank + _amount)]];
GVAR(Registry) set [_uid, _finalAccount];
GVAR(Registry) set [_target, _finalTargetBank];
private _player = [_uid] call EFUNC(common,getPlayer);
private _targetPlayer = [_target] call EFUNC(common,getPlayer);
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(bank,responseSyncBank), [_finalTargetBank], _targetPlayer] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Transferred $%1 to %2", _amount, (name _targetPlayer)]], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Received $%1 from %2", _amount, (name _player)]], _targetPlayer] call CFUNC(targetEvent);
}],
["withdraw", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
["INFO", format ["Withdraw %1, for %2", _amount, _uid]] call EFUNC(common,log);
private _account = GVAR(Registry) getOrDefault [_uid, nil];
if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); };
private _bank = _account getOrDefault ["bank", 0];
private _cash = _account getOrDefault ["cash", 0];
if (_bank < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); };
private _finalAccount = createHashMapFromArray [["bank", (_bank - _amount)], ["cash", (_cash + _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Withdrew $%1", _amount]], _player] call CFUNC(targetEvent);
}],
["depositEarnings", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
["INFO", format ["Deposit Earnings %1, for %2", _amount, _uid]] call EFUNC(common,log);
private _account = GVAR(Registry) getOrDefault [_uid, nil];
if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); };
private _bank = _account getOrDefault ["bank", 0];
private _earnings = _account getOrDefault ["earnings", 0];
if (_earnings < _amount) exitWith { ["WARNING", "Insufficient Earnings!"] call EFUNC(common,log); };
private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)], ["earnings", (_earnings - _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Deposited $%1 from earnings", _amount]], _player] call CFUNC(targetEvent);
}]
];
GVAR(BankStore) = createHashMapObject [GVAR(BankBaseStore)];
GVAR(BankStore)

View File

@ -0,0 +1,75 @@
#include "..\script_component.hpp"
/*
* File: fnc_initMessenger.sqf
* Author: IDSolutions
* Date: 2026-03-16
* Last Update: 2026-03-16
* Public: No
*
* Description:
* Initializes the bank messenger for all server-to-client
* communication including account syncs, toast notifications,
* and inline bank UI notices.
*
* Parameter(s):
* None
*
* Returns:
* Messenger object [HASHMAP OBJECT]
*
* Example(s):
* call forge_server_bank_fnc_initMessenger
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(BankMessenger) = createHashMapObject [[
["#type", "BankMessenger"],
["buildClientAccountPatch", compileFinal {
params [["_account", createHashMap, [createHashMap]]];
private _patch = createHashMap;
{
if (_x in _account) then {
_patch set [_x, _account get _x];
};
} forEach ["uid", "name", "bank", "cash", "earnings", "transactions"];
_patch
}],
["sendAccountSync", compileFinal {
params [["_uid", "", [""]], ["_account", createHashMap, [createHashMap]], ["_event", CRPC(bank,responseSyncBank), [""]]];
if (_uid isEqualTo "" || { _account isEqualTo createHashMap }) exitWith { false };
private _player = [_uid] call EFUNC(common,getPlayer);
if (isNull _player) exitWith { false };
[_event, [_self call ["buildClientAccountPatch", [_account]]], _player] call CFUNC(targetEvent);
true
}],
["sendClientNotification", compileFinal {
params [["_uid", "", [""]], ["_type", "info", [""]], ["_title", "Bank", [""]], ["_message", "", [""]]];
if (_uid isEqualTo "" || { _message isEqualTo "" }) exitWith { false };
private _player = [_uid] call EFUNC(common,getPlayer);
if (isNull _player) exitWith { false };
[CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent);
true
}],
["sendNotice", compileFinal {
params [["_uid", "", [""]], ["_type", "error", [""]], ["_message", "", [""]]];
if (_uid isEqualTo "" || { _message isEqualTo "" }) exitWith { false };
private _player = [_uid] call EFUNC(common,getPlayer);
if (isNull _player) exitWith { false };
[CRPC(bank,responseBankNotice), [_type, _message], _player] call CFUNC(targetEvent);
true
}]
]];
GVAR(BankMessenger)

View File

@ -0,0 +1,91 @@
#include "..\script_component.hpp"
/*
* File: fnc_initModel.sqf
* Author: IDSolutions
* Date: 2026-03-16
* Last Update: 2026-03-16
* Public: No
*
* Description:
* Initializes the bank account data model. Provides default account
* schema, player-based account creation, schema migration for
* existing accounts, and field-level validation.
*
* Parameter(s):
* None
*
* Returns:
* Bank model object [HASHMAP OBJECT]
*
* Example(s):
* call forge_server_bank_fnc_initModel
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(BankModel) = compileFinal createHashMapObject [[
["#type", "BankModel"],
["defaults", compileFinal {
private _account = createHashMap;
_account set ["uid", ""];
_account set ["name", ""];
_account set ["bank", 0];
_account set ["cash", 0];
_account set ["earnings", 0];
_account set ["pin", 1234];
_account set ["transactions", []];
_account
}],
["fromPlayer", compileFinal {
params [["_player", objNull, [objNull]]];
if (_player isEqualTo objNull) exitWith { _self call ["defaults", []] };
private _account = _self call ["defaults", []];
_account set ["uid", getPlayerUID _player];
_account set ["name", name _player];
_account
}],
["migrate", compileFinal {
params [["_account", createHashMap, [createHashMap]]];
private _defaults = _self call ["defaults", []];
{
if !(_x in _account) then {
_account set [_x, _y];
};
} 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
}]
]];
GVAR(BankModel)

View File

@ -0,0 +1,94 @@
#include "..\script_component.hpp"
/*
* File: fnc_initSessionManager.sqf
* Author: IDSolutions
* Date: 2026-03-16
* Last Update: 2026-03-16
* Public: No
*
* Description:
* Initializes the bank session manager for managing ATM/bank
* session state, mode resolution, and PIN authorization.
*
* Parameter(s):
* None
*
* Returns:
* Session manager object [HASHMAP OBJECT]
*
* Example(s):
* call forge_server_bank_fnc_initSessionManager
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(BankSessionManager) = createHashMapObject [[
["#type", "BankSessionManager"],
["getSessionState", compileFinal {
params [["_uid", "", [""]]];
private _session = GVAR(SessionRegistry) getOrDefault [_uid, createHashMap];
if (_session isEqualTo createHashMap) then {
_session = createHashMapFromArray [
["atmAuthorized", false],
["mode", "bank"]
];
GVAR(SessionRegistry) set [_uid, _session];
};
_session
}],
["setSessionState", compileFinal {
params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _session = +(_self call ["getSessionState", [_uid]]);
{ _session set [_x, _y]; } forEach _fieldValuePairs;
GVAR(SessionRegistry) set [_uid, _session];
_session
}],
["resolveMode", compileFinal {
params [["_mode", "bank", [""]]];
private _finalMode = toLowerANSI _mode;
if !(_finalMode in ["atm", "bank"]) then { _finalMode = "bank"; };
_finalMode
}],
["syncSessionMode", compileFinal {
params [["_uid", "", [""]], ["_mode", "", [""]], ["_resetAuthorization", false, [false]]];
private _current = _self call ["getSessionState", [_uid]];
private _finalMode = if (_mode isEqualTo "") then {
_current getOrDefault ["mode", "bank"]
} else {
_self call ["resolveMode", [_mode]]
};
private _atmAuthorized = _current getOrDefault ["atmAuthorized", false];
if (_finalMode isEqualTo "atm") then {
if (_resetAuthorization || { (_current getOrDefault ["mode", "bank"]) isNotEqualTo "atm" }) then {
_atmAuthorized = false;
};
} else {
_atmAuthorized = false;
};
_self call ["setSessionState", [_uid, createHashMapFromArray [
["atmAuthorized", _atmAuthorized],
["mode", _finalMode]
]]]
}],
["submitPin", compileFinal {
params [["_uid", "", [""]], ["_context", createHashMap, [createHashMap]]];
_self call ["setSessionState", [_uid, createHashMapFromArray [["atmAuthorized", true], ["mode", "atm"]]]];
GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", "ATM access granted."]];
GVAR(BankStore) call ["hydrateSession", [_uid, "atm", false]];
true
}]
]];
GVAR(BankSessionManager)

View File

@ -0,0 +1,350 @@
#include "..\script_component.hpp"
/*
* File: fnc_initStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-03-16
* 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
*/
#pragma hemtt ignore_variables ["_self"]
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.", [""]]];
createHashMapFromArray [
["success", false],
["message", _message],
["patch", createHashMap]
]
}],
["buildHydratePayload", compileFinal {
params [["_uid", "", [""]], ["_mode", "", [""]], ["_resetAuthorization", false, [false]]];
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 _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 ["buildClientAccountPatch", [_account]]]
]
}],
["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] }
}],
["chargeCheckout", compileFinal {
params [["_uid", "", [""]], ["_source", "cash", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]];
private _result = _self call ["buildChargeResult", []];
private _field = switch (toLowerANSI _source) do {
case "cash": { "cash" };
case "bank": { "bank" };
default { "" };
};
if (_field isEqualTo "") exitWith {
_result set ["message", "Selected bank payment source is unsupported."];
_result
};
private _account = GVAR(Registry) getOrDefault [_uid, createHashMap];
if (_account isEqualTo createHashMap) exitWith {
_result set ["message", "Bank account data is unavailable for checkout."];
_result
};
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
};
private _patch = createHashMapFromArray [[_field, (_balance - _amount)]];
if (_commit) then {
_patch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]];
};
_result set ["success", true];
_result set ["message", ""];
_result set ["patch", _patch];
_result
}],
["deposit", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]], ["_context", createHashMap, [createHashMap]]];
["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]]];
true
}],
["hydrateSession", compileFinal {
params [["_uid", "", [""]], ["_mode", "", [""]], ["_resetAuthorization", false, [false]]];
private _payload = _self call ["buildHydratePayload", [_uid, _mode, _resetAuthorization]];
if (_payload isEqualTo createHashMap) exitWith { false };
private _player = [_uid] call EFUNC(common,getPlayer);
if (isNull _player) exitWith { false };
[CRPC(bank,responseHydrateBank), [_payload], _player] call CFUNC(targetEvent);
true
}],
["init", compileFinal {
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
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);
private _fallbackAccount = GVAR(BankModel) call ["fromPlayer", [_player]];
_fallbackAccount set ["uid", _uid];
if ((_fallbackAccount getOrDefault ["name", ""]) isEqualTo "") then {
_fallbackAccount set ["name", _playerName];
};
private _regEntry = createHashMapFromArray [["uid", _uid], ["name", _playerName]];
GVAR(IndexRegistry) set [_uid, _regEntry];
GVAR(Registry) set [_uid, _fallbackAccount];
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]];
["INFO", format ["Found bank account for %1", _uid]] call EFUNC(common,log);
} else {
_finalAccount = GVAR(BankModel) call ["fromPlayer", [_player]];
_finalAccount set ["uid", _uid];
if ((_finalAccount getOrDefault ["name", ""]) isEqualTo "") then {
_finalAccount set ["name", _playerName];
};
private _json = _self call ["toJSON", [_finalAccount]];
["bank:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"];
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];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalAccount, CRPC(bank,responseInitBank)]];
_finalAccount
};
["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 {
_finalAccount set ["uid", _uid];
};
if ((_finalAccount getOrDefault ["name", ""]) isEqualTo "") then {
_finalAccount set ["name", _playerName];
};
GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["uid", _uid], ["name", _playerName]]];
GVAR(Registry) set [_uid, _finalAccount];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalAccount, CRPC(bank,responseInitBank)]];
_finalAccount
}],
["payment", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]], ["_context", createHashMap, [createHashMap]]];
["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]]];
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"]];
};
if (_org isEqualTo createHashMap) exitWith { _defaultState };
createHashMapFromArray [
["funds", _org getOrDefault ["funds", 0]],
["name", _org getOrDefault ["name", ""]]
]
}],
["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 _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]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalSourcePatch]];
GVAR(BankMessenger) call ["sendAccountSync", [_target, _finalTargetPatch]];
private _targetPlayer = [_target] call EFUNC(common,getPlayer);
private _targetName = if (isNull _targetPlayer) then {
_targetAccount 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
};
GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Transferred $%1 to %2", _amount, _targetName]]];
GVAR(BankMessenger) call ["sendClientNotification", [_target, "info", "Bank", format ["Received $%1 from %2", _amount, _playerName]]];
true
}],
["withdraw", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]], ["_context", createHashMap, [createHashMap]]];
["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]]];
true
}],
["depositEarnings", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]], ["_context", createHashMap, [createHashMap]]];
["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]]];
true
}]
];
GVAR(BankStore) = createHashMapObject [GVAR(BankBaseStore)];
GVAR(BankStore)

View File

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

View File

@ -91,18 +91,17 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [
private _category = toLowerANSI (_x getOrDefault ["category", ""]);
private _quantity = floor ((_x getOrDefault ["quantity", 0]) max 0);
private _lockerCategory = switch (_category) do {
case "item": { "item" };
case "item";
case "attachment": { "item" };
case "weapon": { "weapon" };
case "magazine": { "magazine" };
case "backpack": { "backpack" };
default { "" };
};
if (_className isEqualTo "" || { _lockerCategory isEqualTo "" } || { _quantity <= 0 }) exitWith {
_result set ["message", "Checkout item was missing a valid classname, category, or quantity."];
_result set ["success", false];
};
if (_className isEqualTo "" || { _lockerCategory isEqualTo "" } || { _quantity <= 0 }) then {
["WARN", format ["Skipping invalid locker grant entry: %1 (category: %2)", _className, _category]] call EFUNC(common,log);
} else {
private _entry = +(_locker getOrDefault [_className, createHashMap]);
private _amount = _entry getOrDefault ["amount", 0];
private _updatedEntry = createHashMapFromArray [
@ -118,6 +117,7 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [
["category", _lockerCategory],
["quantity", _quantity]
]);
};
} forEach _items;
if ((count (keys _locker)) > 25) exitWith {

View File

@ -23,7 +23,11 @@ if (isNil QEGVAR(common,BaseStore)) then { call EFUNC(common,baseStore); };
if (isNil QEGVAR(actor,ActorStore)) then { call EFUNC(actor,initActorStore); };
// Bank
if (isNil QEGVAR(bank,BankStore)) then { call EFUNC(bank,initBankStore); };
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,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); };