Merge development into master: shared Web UI runtime, bridge-driven UIs, and server-authoritative store flow #1
@ -10,7 +10,7 @@ if (isNil QGVAR(VAClass)) then { call FUNC(initVAClass); };
|
||||
[QGVAR(responseInitLocker), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(LockerClass) call ["sync", [_data, true]];
|
||||
GVAR(LockerClass) call ["sync", [_data]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncLocker), {
|
||||
@ -26,7 +26,7 @@ if (isNil QGVAR(VAClass)) then { call FUNC(initVAClass); };
|
||||
[QGVAR(responseInitVA), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(VAClass) call ["sync", [_data, true]];
|
||||
GVAR(VAClass) call ["sync", [_data]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncVA), {
|
||||
|
||||
@ -45,7 +45,7 @@ GVAR(VABaseClass) = compileFinal createHashMapFromArray [
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["sync", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _vArsenal = _self get "vArsenal";
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
@ -53,7 +53,6 @@ GVAR(VABaseClass) = compileFinal createHashMapFromArray [
|
||||
{
|
||||
_vArsenal set [_x, _y];
|
||||
|
||||
if (_jip) then {
|
||||
switch (_x) do {
|
||||
case "items": { _self call ["applyItems", []]; };
|
||||
case "weapons": { _self call ["applyWeapons", []]; };
|
||||
@ -61,7 +60,6 @@ GVAR(VABaseClass) = compileFinal createHashMapFromArray [
|
||||
case "backpacks": { _self call ["applyBackpacks", []]; };
|
||||
default {};
|
||||
};
|
||||
};
|
||||
} forEach _data;
|
||||
|
||||
_self set ["vArsenal", _vArsenal];
|
||||
|
||||
@ -29,9 +29,6 @@
|
||||
this.setFunds(payload.portalData.funds || 0);
|
||||
this.setMembers([...(payload.portalData.members || [])]);
|
||||
this.setCreditLines([...(payload.portalData.creditLines || [])]);
|
||||
this.setTreasuryNotice({ type: "", text: "" });
|
||||
this.setModal(null);
|
||||
this.setOrgDisbanded(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,26 @@
|
||||
"mask",
|
||||
]);
|
||||
|
||||
function appendChild(el, child) {
|
||||
if (child === null || child === undefined || child === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(child)) {
|
||||
child.forEach((entry) => appendChild(el, entry));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof child === "string" || typeof child === "number") {
|
||||
el.appendChild(document.createTextNode(String(child)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (child instanceof Node) {
|
||||
el.appendChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
function h(tag, props = {}, ...children) {
|
||||
const isSvg = SVG_TAGS.has(tag);
|
||||
const el = isSvg
|
||||
@ -30,44 +50,132 @@
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
if (key.startsWith("on") && typeof value === "function") {
|
||||
el.addEventListener(key.substring(2).toLowerCase(), value);
|
||||
} else if (key === "className") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "className") {
|
||||
if (isSvg) {
|
||||
el.setAttribute("class", value);
|
||||
} else {
|
||||
el.className = value;
|
||||
}
|
||||
} else if (key === "style" && typeof value === "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "style" && typeof value === "object") {
|
||||
Object.assign(el.style, value);
|
||||
} else if (typeof value === "boolean") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "value" && "value" in el) {
|
||||
el.value = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "checked" && "checked" in el) {
|
||||
el.checked = Boolean(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
if (value) {
|
||||
el.setAttribute(key, "");
|
||||
} else {
|
||||
el.removeAttribute(key);
|
||||
}
|
||||
} else if (value === null || value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
el.removeAttribute(key);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
el.setAttribute(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
children.forEach((child) => {
|
||||
if (typeof child === "string" || typeof child === "number") {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else if (child instanceof Node) {
|
||||
el.appendChild(child);
|
||||
} else if (Array.isArray(child)) {
|
||||
child.forEach((c) => el.appendChild(c));
|
||||
}
|
||||
});
|
||||
|
||||
children.forEach((child) => appendChild(el, child));
|
||||
return el;
|
||||
}
|
||||
|
||||
let rootContainer = null;
|
||||
let rootComponent = null;
|
||||
const injectedStyles = new Set();
|
||||
let rerenderQueued = false;
|
||||
|
||||
function captureScrollState(container) {
|
||||
if (!container) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
container.querySelectorAll("[data-preserve-scroll-id]"),
|
||||
).map((node) => ({
|
||||
id: node.getAttribute("data-preserve-scroll-id"),
|
||||
scrollTop: node.scrollTop,
|
||||
scrollLeft: node.scrollLeft,
|
||||
}));
|
||||
}
|
||||
|
||||
function captureViewportScroll(container) {
|
||||
if (!container) {
|
||||
return { scrollTop: 0, scrollLeft: 0 };
|
||||
}
|
||||
|
||||
const viewport = container.querySelector("main");
|
||||
if (!viewport) {
|
||||
return {
|
||||
scrollTop: window.scrollY || window.pageYOffset || 0,
|
||||
scrollLeft: window.scrollX || window.pageXOffset || 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scrollTop: viewport.scrollTop,
|
||||
scrollLeft: viewport.scrollLeft,
|
||||
};
|
||||
}
|
||||
|
||||
function restoreScrollState(container, entries) {
|
||||
if (!container || !Array.isArray(entries) || entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (!entry || !entry.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = container.querySelector(
|
||||
`[data-preserve-scroll-id="${entry.id}"]`,
|
||||
);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.scrollTop = Number(entry.scrollTop || 0);
|
||||
target.scrollLeft = Number(entry.scrollLeft || 0);
|
||||
});
|
||||
}
|
||||
|
||||
function restoreViewportScroll(container, viewportScroll) {
|
||||
if (!container || !viewportScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewport = container.querySelector("main");
|
||||
if (!viewport) {
|
||||
window.scrollTo(
|
||||
Number(viewportScroll.scrollLeft || 0),
|
||||
Number(viewportScroll.scrollTop || 0),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
viewport.scrollTop = Number(viewportScroll.scrollTop || 0);
|
||||
viewport.scrollLeft = Number(viewportScroll.scrollLeft || 0);
|
||||
}
|
||||
|
||||
function render(component, container) {
|
||||
rootContainer = container;
|
||||
@ -75,13 +183,31 @@
|
||||
rerender();
|
||||
}
|
||||
|
||||
function rerender() {
|
||||
function flushRerender() {
|
||||
if (!rootContainer || !rootComponent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportScroll = captureViewportScroll(rootContainer);
|
||||
const scrollState = captureScrollState(rootContainer);
|
||||
rootContainer.innerHTML = "";
|
||||
rootContainer.appendChild(rootComponent());
|
||||
requestAnimationFrame(() => {
|
||||
restoreScrollState(rootContainer, scrollState);
|
||||
restoreViewportScroll(rootContainer, viewportScroll);
|
||||
});
|
||||
}
|
||||
|
||||
function rerender() {
|
||||
if (rerenderQueued) {
|
||||
return;
|
||||
}
|
||||
|
||||
rerenderQueued = true;
|
||||
requestAnimationFrame(() => {
|
||||
rerenderQueued = false;
|
||||
flushRerender();
|
||||
});
|
||||
}
|
||||
|
||||
function ensureScopedStyle(id, cssText) {
|
||||
@ -111,6 +237,7 @@
|
||||
const runtime = {
|
||||
h,
|
||||
render,
|
||||
rerender,
|
||||
createSignal,
|
||||
ensureScopedStyle,
|
||||
};
|
||||
|
||||
@ -16,7 +16,9 @@
|
||||
|
||||
OrgPortal.data.applyLoginPayload(payload);
|
||||
OrgPortal.store.hydrateFromPayload(payload);
|
||||
if (RegistryApp.store.getView() !== "portal") {
|
||||
RegistryApp.store.setView("portal");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
@ -2,3 +2,9 @@
|
||||
|
||||
if (isNil QGVAR(StoreClass)) then { call FUNC(initStoreClass); };
|
||||
if (isNil QGVAR(StoreUIBridge)) then { call FUNC(initStoreUIBridge); };
|
||||
|
||||
[QGVAR(responseCheckout), {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(StoreUIBridge) call ["handleCheckoutResponse", [_payload]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
@ -246,6 +246,28 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
_sortedItems sort true;
|
||||
_sortedItems apply { _x select 1 }
|
||||
}],
|
||||
["isVehicleCategory", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
(toLowerANSI _category) in ["cars", "armor", "helis", "planes", "naval", "other"]
|
||||
}],
|
||||
["buildPayloadCategory", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
switch (toLowerANSI _category) do {
|
||||
case "ammo": { "magazine" };
|
||||
case "primary";
|
||||
case "secondary";
|
||||
case "handgun": { "weapon" };
|
||||
case "cars";
|
||||
case "armor";
|
||||
case "helis";
|
||||
case "planes";
|
||||
case "naval";
|
||||
case "other": { toLowerANSI _category };
|
||||
default { "item" };
|
||||
}
|
||||
}],
|
||||
["buildCategoryItems", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
@ -256,6 +278,14 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
if (_categoryKey in (keys _catalogCache)) exitWith { _catalogCache get _categoryKey };
|
||||
|
||||
private _items = _self call ["scanCategoryItems", [_categoryKey]];
|
||||
private _payloadCategory = _self call ["buildPayloadCategory", [_categoryKey]];
|
||||
private _entryKind = ["item", "vehicle"] select (_self call ["isVehicleCategory", [_categoryKey]]);
|
||||
|
||||
{
|
||||
_x set ["category", _payloadCategory];
|
||||
_x set ["entryKind", _entryKind];
|
||||
} forEach _items;
|
||||
|
||||
_catalogCache set [_categoryKey, _items];
|
||||
_self set ["catalogCache", _catalogCache];
|
||||
|
||||
@ -298,15 +328,40 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
["items", _items]
|
||||
]]];
|
||||
}],
|
||||
["refreshWorkspace", compileFinal {
|
||||
private _payload = GVAR(StoreClass) call ["buildUIPayload", []];
|
||||
_self call ["sendBridgeEvent", ["store::workspace::hydrate", _payload]];
|
||||
}],
|
||||
["handleCheckoutRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _items = _data getOrDefault ["items", []];
|
||||
private _paymentMethod = _data getOrDefault ["paymentMethod", "cash"];
|
||||
private _message = format ["Checkout integration is not wired yet. Received %1 queued line(s) using %2.", count _items, _paymentMethod];
|
||||
private _uid = getPlayerUID player;
|
||||
private _checkoutJson = _data getOrDefault ["checkoutJson", ""];
|
||||
|
||||
diag_log format ["[FORGE:Client:Store] Checkout request received: %1", _data];
|
||||
_self call ["sendBridgeEvent", ["store::checkout::failure", createHashMapFromArray [["message", _message]]]];
|
||||
if (_uid isEqualTo "" || { _checkoutJson isEqualTo "" }) exitWith {
|
||||
_self call ["sendBridgeEvent", ["store::checkout::failure", createHashMapFromArray [
|
||||
["message", "Add at least one supported item before checkout."]
|
||||
]]];
|
||||
};
|
||||
|
||||
diag_log format ["[FORGE:Client:Store] Checkout request forwarded to server: %1", _checkoutJson];
|
||||
[SRPC(store,requestCheckout), [_uid, _checkoutJson]] call CFUNC(serverEvent);
|
||||
}],
|
||||
["handleCheckoutResponse", compileFinal {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
private _success = _payload getOrDefault ["success", false];
|
||||
private _bridgeEvent = ["store::checkout::failure", "store::checkout::success"] select _success;
|
||||
_self call ["sendBridgeEvent", [_bridgeEvent, _payload]];
|
||||
|
||||
if (_success) then {
|
||||
[] spawn {
|
||||
sleep 0.05;
|
||||
if !(isNil QGVAR(StoreUIBridge)) then {
|
||||
GVAR(StoreUIBridge) call ["refreshWorkspace", []];
|
||||
};
|
||||
};
|
||||
};
|
||||
}]
|
||||
];
|
||||
|
||||
|
||||
@ -51,6 +51,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === "store::workspace::hydrate") {
|
||||
StorefrontApp.data.applyHydratePayload(payloadData);
|
||||
store.hydrateWorkspace(payloadData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === "store::checkout::success") {
|
||||
store.setIsCheckingOut(false);
|
||||
store.setCartItems([]);
|
||||
|
||||
@ -19,6 +19,54 @@
|
||||
}, 3200);
|
||||
}
|
||||
|
||||
function normalizeCheckoutItem(item) {
|
||||
return {
|
||||
classname: String(item?.code || "").trim(),
|
||||
category: String(item?.category || "")
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
entryKind: String(item?.entryKind || "item")
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
quantity: Math.max(1, Number(item?.quantity || 1)),
|
||||
};
|
||||
}
|
||||
|
||||
function buildCheckoutPayload(cartItems, paymentMethod, totalPrice) {
|
||||
const payload = {
|
||||
items: [],
|
||||
vehicles: [],
|
||||
totalPrice,
|
||||
paymentMethod,
|
||||
};
|
||||
|
||||
cartItems.forEach((item) => {
|
||||
const normalizedItem = normalizeCheckoutItem(item);
|
||||
|
||||
if (normalizedItem.entryKind === "vehicle") {
|
||||
for (
|
||||
let index = 0;
|
||||
index < normalizedItem.quantity;
|
||||
index += 1
|
||||
) {
|
||||
payload.vehicles.push({
|
||||
classname: normalizedItem.classname,
|
||||
category: normalizedItem.category,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
payload.items.push({
|
||||
classname: normalizedItem.classname,
|
||||
category: normalizedItem.category,
|
||||
quantity: normalizedItem.quantity,
|
||||
});
|
||||
});
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function applySearchQuery(value) {
|
||||
store.setSearchQuery(String(value || "").trim());
|
||||
store.resetCatalogPage();
|
||||
@ -149,6 +197,8 @@
|
||||
code: item.code,
|
||||
name: item.name,
|
||||
price: item.price,
|
||||
category: item.category,
|
||||
entryKind: item.entryKind,
|
||||
quantity: 1,
|
||||
},
|
||||
];
|
||||
@ -159,6 +209,8 @@
|
||||
{},
|
||||
nextItems[existingIndex],
|
||||
{
|
||||
category: item.category,
|
||||
entryKind: item.entryKind,
|
||||
quantity: nextItems[existingIndex].quantity + 1,
|
||||
},
|
||||
);
|
||||
@ -266,14 +318,14 @@
|
||||
|
||||
store.setIsCheckingOut(true);
|
||||
|
||||
const checkoutPayload = buildCheckoutPayload(
|
||||
cartItems,
|
||||
selectedPaymentSource.id,
|
||||
summary.total,
|
||||
);
|
||||
|
||||
const sent = bridge.requestCheckout({
|
||||
actorUid: session.actorUid,
|
||||
actorName: session.actorName,
|
||||
paymentMethod: selectedPaymentSource.id,
|
||||
paymentLabel: selectedPaymentSource.label,
|
||||
items: cartItems,
|
||||
subtotal: summary.subtotal,
|
||||
total: summary.total,
|
||||
checkoutJson: JSON.stringify(checkoutPayload),
|
||||
});
|
||||
|
||||
if (!sent) {
|
||||
|
||||
@ -4,6 +4,32 @@
|
||||
SharedLogic.createStorefrontStore = function createStorefrontStore({
|
||||
createSignal,
|
||||
}) {
|
||||
function normalizeCatalogItem(item) {
|
||||
return {
|
||||
className: String(item?.className || item?.code || ""),
|
||||
code: String(item?.code || item?.className || ""),
|
||||
name: String(item?.name || item?.displayName || ""),
|
||||
description: String(item?.description || ""),
|
||||
price: String(item?.price || ""),
|
||||
image: String(item?.image || ""),
|
||||
type: String(item?.type || ""),
|
||||
category: String(item?.category || ""),
|
||||
entryKind: String(item?.entryKind || "item"),
|
||||
quantity: Math.max(0, Number(item?.quantity || 0)),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCartItem(item) {
|
||||
return {
|
||||
code: String(item?.code || ""),
|
||||
name: String(item?.name || ""),
|
||||
price: String(item?.price || "$0"),
|
||||
category: String(item?.category || ""),
|
||||
entryKind: String(item?.entryKind || "item"),
|
||||
quantity: Math.max(1, Number(item?.quantity || 1)),
|
||||
};
|
||||
}
|
||||
|
||||
class StorefrontStore {
|
||||
constructor() {
|
||||
[this.getView, this.setView] = createSignal("categories");
|
||||
@ -147,18 +173,7 @@
|
||||
|
||||
this.setCatalogItemsByKey((currentItemsByKey) =>
|
||||
Object.assign({}, currentItemsByKey, {
|
||||
[categoryKey]: items.map((item) => ({
|
||||
className: String(
|
||||
item.className || item.code || "",
|
||||
),
|
||||
code: String(item.code || item.className || ""),
|
||||
name: String(item.name || item.displayName || ""),
|
||||
description: String(item.description || ""),
|
||||
price: String(item.price || ""),
|
||||
image: String(item.image || ""),
|
||||
type: String(item.type || ""),
|
||||
quantity: Math.max(0, Number(item.quantity || 0)),
|
||||
})),
|
||||
[categoryKey]: items.map(normalizeCatalogItem),
|
||||
}),
|
||||
);
|
||||
|
||||
@ -239,12 +254,7 @@
|
||||
: [];
|
||||
|
||||
this.setCartItems(
|
||||
cartItems.map((item) => ({
|
||||
code: String(item.code || ""),
|
||||
name: String(item.name || ""),
|
||||
price: String(item.price || "$0"),
|
||||
quantity: Math.max(1, Number(item.quantity || 1)),
|
||||
})),
|
||||
cartItems.map(normalizeCartItem),
|
||||
);
|
||||
this.setCartOpen(false);
|
||||
this.setIsCheckingOut(false);
|
||||
@ -256,6 +266,19 @@
|
||||
payload?.workspace || payload?.storeConfig || {},
|
||||
);
|
||||
}
|
||||
|
||||
hydrateWorkspace(payload) {
|
||||
const cartItems = Array.isArray(payload?.cartItems)
|
||||
? payload.cartItems
|
||||
: [];
|
||||
|
||||
this.setCartItems(cartItems.map(normalizeCartItem));
|
||||
this.setCartOpen(false);
|
||||
this.setIsCheckingOut(false);
|
||||
this.ensureSelectedPaymentSource(
|
||||
payload?.workspace || payload?.storeConfig || {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new StorefrontStore();
|
||||
|
||||
@ -195,6 +195,62 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
|
||||
[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]]];
|
||||
|
||||
|
||||
@ -90,7 +90,7 @@ PREP_RECOMPILE_END;
|
||||
private _finalData = GVAR(VAStore) call ["get", [GVAR(VARegistry), "owned:locker:fetch", _uid, _field]];
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
|
||||
[CRPC(locker,responseSyncVArsenal), [_finalData], _player] call CFUNC(targetEvent);
|
||||
[CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestSetVA), {
|
||||
@ -101,7 +101,7 @@ PREP_RECOMPILE_END;
|
||||
private _hashMap = GVAR(VAStore) call ["set", [GVAR(VARegistry), "owned:locker:update", _uid, _field, _value, _sync]];
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
|
||||
[CRPC(locker,responseSyncVArsenal), [_hashMap], _player] call CFUNC(targetEvent);
|
||||
[CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestMSetVA), {
|
||||
@ -113,7 +113,7 @@ PREP_RECOMPILE_END;
|
||||
private _hashMap = GVAR(VAStore) call ["mset", [GVAR(VARegistry), "owned:locker:update", _uid, _fieldValuePairs, _sync]];
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
|
||||
[CRPC(locker,responseSyncVArsenal), [_hashMap], _player] call CFUNC(targetEvent);
|
||||
[CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestSaveVA), {
|
||||
@ -124,7 +124,7 @@ PREP_RECOMPILE_END;
|
||||
private _finalData = GVAR(VAStore) call ["save", [GVAR(VARegistry), "owned:locker:update", _uid]];
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
|
||||
[CRPC(locker,responseSyncVArsenal), [_finalData], _player] call CFUNC(targetEvent);
|
||||
[CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestRemoveVA), {
|
||||
|
||||
@ -70,6 +70,68 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [
|
||||
[CRPC(locker,responseInitLocker), [_finalLocker], _player] call CFUNC(targetEvent);
|
||||
|
||||
_finalLocker
|
||||
}],
|
||||
["grantItems", compileFinal {
|
||||
params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Locker grant failed."],
|
||||
["patch", createHashMap],
|
||||
["granted", []],
|
||||
["locker", createHashMap]
|
||||
];
|
||||
|
||||
private _locker = +(GVAR(Registry) getOrDefault [_uid, createHashMap]);
|
||||
private _patch = createHashMap;
|
||||
private _granted = [];
|
||||
|
||||
{
|
||||
private _className = _x getOrDefault ["classname", ""];
|
||||
private _category = toLowerANSI (_x getOrDefault ["category", ""]);
|
||||
private _quantity = floor ((_x getOrDefault ["quantity", 0]) max 0);
|
||||
private _lockerCategory = switch (_category) do {
|
||||
case "item": { "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];
|
||||
};
|
||||
|
||||
private _entry = +(_locker getOrDefault [_className, createHashMap]);
|
||||
private _amount = _entry getOrDefault ["amount", 0];
|
||||
private _updatedEntry = createHashMapFromArray [
|
||||
["amount", (_amount + _quantity)],
|
||||
["classname", _className],
|
||||
["category", _lockerCategory]
|
||||
];
|
||||
|
||||
_locker set [_className, _updatedEntry];
|
||||
_patch set [_className, _updatedEntry];
|
||||
_granted pushBack (createHashMapFromArray [
|
||||
["classname", _className],
|
||||
["category", _lockerCategory],
|
||||
["quantity", _quantity]
|
||||
]);
|
||||
} forEach _items;
|
||||
|
||||
if ((count (keys _locker)) > 25) exitWith {
|
||||
_result set ["message", "Locker capacity would exceed 25 unique items. Clear space before checkout."];
|
||||
_result
|
||||
};
|
||||
if (_commit) then { GVAR(Registry) set [_uid, _locker]; };
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", ""];
|
||||
_result set ["patch", _patch];
|
||||
_result set ["granted", _granted];
|
||||
_result set ["locker", _locker];
|
||||
_result
|
||||
}]
|
||||
];
|
||||
|
||||
|
||||
@ -89,6 +89,53 @@ GVAR(VABaseStore) = compileFinal createHashMapFromArray [
|
||||
[CRPC(locker,responseInitVA), [_finalVArsenal], _player] call CFUNC(targetEvent);
|
||||
|
||||
_finalVArsenal
|
||||
}],
|
||||
["unlockItems", compileFinal {
|
||||
params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "VA unlock failed."],
|
||||
["patch", createHashMap],
|
||||
["arsenal", createHashMap]
|
||||
];
|
||||
|
||||
private _defaultArsenal = GVAR(VArsenalModel) call ["defaults", []];
|
||||
private _arsenal = +(GVAR(VARegistry) getOrDefault [_uid, _defaultArsenal]);
|
||||
private _patch = createHashMap;
|
||||
private _categoriesToSync = [];
|
||||
|
||||
{
|
||||
private _item = _x;
|
||||
private _className = _item getOrDefault ["classname", ""];
|
||||
private _category = toLowerANSI (_item getOrDefault ["category", ""]);
|
||||
private _arsenalCategory = switch (_category) do {
|
||||
case "item": { "items" };
|
||||
case "weapon": { "weapons" };
|
||||
case "magazine": { "magazines" };
|
||||
case "backpack": { "backpacks" };
|
||||
default { "items" };
|
||||
};
|
||||
|
||||
private _categoryUnlocks = +(_arsenal getOrDefault [_arsenalCategory, []]);
|
||||
_categoryUnlocks pushBackUnique _className;
|
||||
_arsenal set [_arsenalCategory, _categoryUnlocks];
|
||||
_categoriesToSync pushBackUnique _arsenalCategory;
|
||||
} forEach _items;
|
||||
|
||||
{
|
||||
private _category = _x;
|
||||
private _categoryUnlocks = _arsenal getOrDefault [_category, []];
|
||||
_patch set [_category, _categoryUnlocks];
|
||||
} forEach _categoriesToSync;
|
||||
|
||||
if (_commit) then { GVAR(VARegistry) set [_uid, _arsenal]; };
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", ""];
|
||||
_result set ["patch", _patch];
|
||||
_result set ["arsenal", _arsenal];
|
||||
_result
|
||||
}]
|
||||
];
|
||||
|
||||
|
||||
@ -39,3 +39,6 @@ if (isNil QEGVAR(locker,VAStore)) then { call EFUNC(locker,initVAStore); };
|
||||
|
||||
// Org
|
||||
if (isNil QEGVAR(org,OrgStore)) then { call EFUNC(org,initOrgStore); };
|
||||
|
||||
// Store
|
||||
if (isNil QEGVAR(store,StoreStore)) then { call EFUNC(store,initStoreStore); };
|
||||
|
||||
@ -1 +1,3 @@
|
||||
PREP(initOrgStore);
|
||||
PREP(memberService);
|
||||
PREP(treasuryService);
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* File: fnc_initOrgStore.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-02-13
|
||||
* Last Update: 2026-02-13
|
||||
* Last Update: 2026-03-13
|
||||
* Public: Yes
|
||||
*
|
||||
* Description:
|
||||
@ -21,6 +21,9 @@
|
||||
* call forge_server_org_fnc_initOrgStore
|
||||
*/
|
||||
|
||||
if (isNil QGVAR(OrgMembershipService)) then { call FUNC(memberService); };
|
||||
if (isNil QGVAR(OrgTreasuryService)) then { call FUNC(treasuryService); };
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(OrgModel) = compileFinal createHashMapObject [[
|
||||
["#type", "OrgModel"],
|
||||
@ -43,7 +46,6 @@ GVAR(OrgModel) = compileFinal createHashMapObject [[
|
||||
params [["_org", createHashMap, [createHashMap]]];
|
||||
|
||||
private _defaults = _self call ["defaults", []];
|
||||
|
||||
{
|
||||
if !(_x in _org) then { _org set [_x, _y]; };
|
||||
} forEach _defaults;
|
||||
@ -104,115 +106,31 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
_defaultOrg
|
||||
};
|
||||
|
||||
private _finalOrg = createHashMap;
|
||||
|
||||
private _defaultOrg = createHashMap;
|
||||
if (_result == "true") then {
|
||||
_finalOrg = _self call ["fetch", ["org:get", "default"]];
|
||||
_defaultOrg = _self call ["fetch", ["org:get", "default"]];
|
||||
} else {
|
||||
_finalOrg set ["id", "default"];
|
||||
_finalOrg set ["owner", "server"];
|
||||
_finalOrg set ["name", "Forge Dynamics"];
|
||||
_finalOrg set ["funds", 200000];
|
||||
_finalOrg set ["reputation", 0];
|
||||
_finalOrg set ["credit_lines", createHashMap];
|
||||
_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];
|
||||
|
||||
private _json = _self call ["toJSON", [_finalOrg]];
|
||||
["org:create", ["default", _json]] call EFUNC(extension,extCall);
|
||||
private _defaultJson = _self call ["toJSON", [_defaultOrg]];
|
||||
["org:create", ["default", _defaultJson]] call EFUNC(extension,extCall);
|
||||
};
|
||||
|
||||
GVAR(Registry) set ["default", _finalOrg];
|
||||
GVAR(Registry) set ["default", _defaultOrg];
|
||||
}],
|
||||
["verifyMember", compileFinal {
|
||||
params [["_org", createHashMap, [createHashMap]], ["_orgID", "", [""]], ["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]];
|
||||
|
||||
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { _org };
|
||||
|
||||
private _members = _org getOrDefault ["members", createHashMap];
|
||||
if ((_members getOrDefault [_uid, objNull]) isNotEqualTo objNull) exitWith { _org };
|
||||
|
||||
["org:members:add", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"];
|
||||
if (!_memberSuccess) then {
|
||||
["WARNING", format ["Failed to add %1 to org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log);
|
||||
};
|
||||
|
||||
private _memberName = _actor getOrDefault ["name", ""];
|
||||
if (_memberName isEqualTo "" && { _player isNotEqualTo objNull }) then { _memberName = name _player; };
|
||||
if (_memberName isEqualTo "") then { _memberName = "Unknown"; };
|
||||
|
||||
private _finalMembers = +_members;
|
||||
_finalMembers set [_uid, createHashMapFromArray [["uid", _uid], ["name", _memberName]]];
|
||||
_org set ["members", _finalMembers];
|
||||
|
||||
_org
|
||||
}],
|
||||
["loadById", compileFinal {
|
||||
params [["_orgID", "", [""]]];
|
||||
|
||||
if (_orgID isEqualTo "") exitWith { createHashMap };
|
||||
|
||||
private _cached = GVAR(Registry) getOrDefault [_orgID, createHashMap];
|
||||
if (_cached isNotEqualTo createHashMap) exitWith { _cached };
|
||||
|
||||
["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 _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
|
||||
GVAR(OrgMembershipService) call ["verifyMember", _this]
|
||||
}],
|
||||
["addMember", compileFinal {
|
||||
params [
|
||||
["_orgID", "", [""]],
|
||||
["_uid", "", [""]],
|
||||
["_player", objNull, [objNull]],
|
||||
["_actor", createHashMap, [createHashMap]]
|
||||
];
|
||||
|
||||
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap };
|
||||
|
||||
private _org = _self call ["loadById", [_orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith { _org };
|
||||
|
||||
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
|
||||
GVAR(Registry) set [_orgID, _org, true];
|
||||
_org
|
||||
GVAR(OrgMembershipService) call ["addMember", _this]
|
||||
}],
|
||||
["removeMember", compileFinal {
|
||||
params [
|
||||
["_orgID", "", [""]],
|
||||
["_uid", "", [""]]
|
||||
];
|
||||
|
||||
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap };
|
||||
|
||||
private _org = _self call ["loadById", [_orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith { _org };
|
||||
|
||||
["org:members:remove", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"];
|
||||
if (!_memberSuccess) exitWith {
|
||||
["WARNING", format ["Failed to remove %1 from org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log);
|
||||
createHashMap
|
||||
};
|
||||
|
||||
private _members = +(_org getOrDefault ["members", createHashMap]);
|
||||
_members deleteAt _uid;
|
||||
_org set ["members", _members];
|
||||
GVAR(Registry) set [_orgID, _org, true];
|
||||
|
||||
_org
|
||||
GVAR(OrgMembershipService) call ["removeMember", _this]
|
||||
}],
|
||||
["delete", compileFinal {
|
||||
params [["_orgID", "", [""]]];
|
||||
@ -238,256 +156,55 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
_result
|
||||
}],
|
||||
["restoreDefaultMembership", compileFinal {
|
||||
params [
|
||||
["_uid", "", [""]],
|
||||
["_player", objNull, [objNull]],
|
||||
["_actor", createHashMap, [createHashMap]]
|
||||
];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", ""],
|
||||
["actorPatch", createHashMap]
|
||||
];
|
||||
|
||||
if (_uid isEqualTo "") exitWith {
|
||||
_result set ["message", "A valid player UID is required."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _resolvedPlayer = _player;
|
||||
if (_resolvedPlayer isEqualTo objNull) then {
|
||||
_resolvedPlayer = [_uid] call EFUNC(common,getPlayer);
|
||||
};
|
||||
|
||||
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 _defaultOrg = _self call ["addMember", ["default", _uid, _resolvedPlayer, EGVAR(actor,Registry) getOrDefault [_uid, _resolvedActor]]];
|
||||
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
|
||||
GVAR(OrgMembershipService) call ["restoreDefaultMembership", _this]
|
||||
}],
|
||||
["leave", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", ""],
|
||||
["actorPatch", createHashMap],
|
||||
["notification", []]
|
||||
];
|
||||
|
||||
if (_uid isEqualTo "") exitWith {
|
||||
_result set ["message", "A valid player UID is required."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
|
||||
private _orgID = _actor getOrDefault ["organization", ""];
|
||||
if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith {
|
||||
_result set ["message", "You are already assigned to the default organization."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _org = _self call ["loadById", [_orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Unable to load organization data for leave request."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _ownerUid = _org getOrDefault ["owner", ""];
|
||||
if (_ownerUid isEqualTo _uid) exitWith {
|
||||
_result set ["message", "Organization owners must disband the organization instead of leaving it."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _orgName = _org getOrDefault ["name", "Organization"];
|
||||
private _updatedOrg = _self call ["removeMember", [_orgID, _uid]];
|
||||
if (_updatedOrg isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Failed to remove you from the organization roster."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _defaultResult = _self call ["restoreDefaultMembership", [_uid, _player, _actor]];
|
||||
if !(_defaultResult getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _defaultResult getOrDefault ["message", "Failed to restore default organization membership."]];
|
||||
_result
|
||||
};
|
||||
|
||||
private _message = format ["You left %1 and returned to the default organization.", _orgName];
|
||||
_result set ["success", true];
|
||||
_result set ["message", _message];
|
||||
_result set ["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]];
|
||||
_result set ["notification", ["info", "Organization Left", _message, 6000]];
|
||||
_result
|
||||
GVAR(OrgMembershipService) call ["leave", _this]
|
||||
}],
|
||||
["disband", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", ""],
|
||||
["members", []]
|
||||
];
|
||||
|
||||
if (_uid isEqualTo "") exitWith {
|
||||
_result set ["message", "A valid player UID is required."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
|
||||
private _orgID = _actor getOrDefault ["organization", ""];
|
||||
if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith {
|
||||
_result set ["message", "Only active player organizations can be disbanded."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _org = _self call ["loadById", [_orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Unable to load organization data for disbanding."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _ownerUid = _org getOrDefault ["owner", ""];
|
||||
if (_ownerUid isEqualTo "" || { _ownerUid isNotEqualTo _uid }) exitWith {
|
||||
_result set ["message", "Only the organization owner can disband this organization."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _orgName = _org getOrDefault ["name", "Organization"];
|
||||
private _memberMap = _org getOrDefault ["members", createHashMap];
|
||||
private _memberUids = keys _memberMap;
|
||||
if !(_uid in _memberUids) then {
|
||||
_memberUids pushBack _uid;
|
||||
};
|
||||
|
||||
private _deleteResult = _self call ["delete", [_orgID]];
|
||||
if !(_deleteResult getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _deleteResult getOrDefault ["message", "Failed to disband organization."]];
|
||||
_result
|
||||
};
|
||||
|
||||
private _memberResults = [];
|
||||
{
|
||||
private _memberUid = _x;
|
||||
if (_memberUid isNotEqualTo "") then {
|
||||
private _memberPlayer = [_memberUid] call EFUNC(common,getPlayer);
|
||||
private _defaultResult = _self call ["restoreDefaultMembership", [_memberUid, _memberPlayer, EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap]]];
|
||||
if !(_defaultResult getOrDefault ["success", false]) then {
|
||||
["WARNING", format ["Failed to restore default org for %1 after disbanding %2: %3", _memberUid, _orgID, _defaultResult getOrDefault ["message", "Unknown error."]]] call EFUNC(common,log);
|
||||
};
|
||||
|
||||
private _responseMessage = [
|
||||
format ["%1 has been disbanded.", _orgName],
|
||||
format ["Your organization, %1, has been disbanded.", _orgName]
|
||||
] select (_memberUid isEqualTo _uid);
|
||||
|
||||
private _notificationParams = [
|
||||
["warning", "Organization Disbanded", _responseMessage, 6000],
|
||||
["success", "Organization Disbanded", _responseMessage, 6000]
|
||||
] select (_memberUid isEqualTo _uid);
|
||||
|
||||
_memberResults pushBack (createHashMapFromArray [
|
||||
["uid", _memberUid],
|
||||
["requester", _memberUid isEqualTo _uid],
|
||||
["message", _responseMessage],
|
||||
["notification", _notificationParams],
|
||||
["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]]
|
||||
]);
|
||||
};
|
||||
} forEach _memberUids;
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", format ["%1 has been disbanded.", _orgName]];
|
||||
_result set ["members", _memberResults];
|
||||
_result
|
||||
GVAR(OrgMembershipService) call ["disband", _this]
|
||||
}],
|
||||
["assignCreditLine", compileFinal {
|
||||
params [
|
||||
["_requesterUid", "", [""]],
|
||||
["_memberUid", "", [""]],
|
||||
["_memberName", "", [""]],
|
||||
["_amount", 0, [0]]
|
||||
];
|
||||
GVAR(OrgTreasuryService) call ["assignCreditLine", _this]
|
||||
}],
|
||||
["buildChargeResult", compileFinal {
|
||||
GVAR(OrgTreasuryService) call ["buildChargeResult", _this]
|
||||
}],
|
||||
["chargeCheckout", compileFinal {
|
||||
GVAR(OrgTreasuryService) call ["chargeCheckout", _this]
|
||||
}],
|
||||
["loadById", compileFinal {
|
||||
params [["_orgID", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", ""],
|
||||
["patch", createHashMap],
|
||||
["memberUids", []]
|
||||
];
|
||||
if (_orgID isEqualTo "") exitWith { createHashMap };
|
||||
|
||||
if (
|
||||
_requesterUid isEqualTo ""
|
||||
|| { _memberUid isEqualTo "" }
|
||||
|| { _amount <= 0 }
|
||||
) exitWith {
|
||||
_result set ["message", "A valid requester, member, and credit amount are required."];
|
||||
_result
|
||||
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 _memberRows = _self call ["fetch", ["org:members:get", _orgID]];
|
||||
if !(_memberRows isEqualType []) then {
|
||||
_memberRows = [];
|
||||
};
|
||||
|
||||
private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
|
||||
private _orgID = _requesterActor getOrDefault ["organization", "default"];
|
||||
if (_orgID isEqualTo "") then {
|
||||
_orgID = "default";
|
||||
private _memberMap = createHashMap;
|
||||
{
|
||||
private _memberUid = _x getOrDefault ["uid", ""];
|
||||
if (_memberUid isNotEqualTo "") then {
|
||||
_memberMap set [_memberUid, _x];
|
||||
};
|
||||
} forEach _memberRows;
|
||||
|
||||
private _org = _self call ["loadById", [_orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Unable to load organization data for credit line assignment."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _ownerUid = _org getOrDefault ["owner", ""];
|
||||
private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
|
||||
private _isDefaultOrg = (_orgID isEqualTo "default") || { toLower _ownerUid isEqualTo "server" };
|
||||
private _isDefaultOrgCeo = _isDefaultOrg
|
||||
&& { _requesterPlayer isNotEqualTo objNull }
|
||||
&& { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" };
|
||||
private _canManageTreasury = (_ownerUid isEqualTo _requesterUid) || _isDefaultOrgCeo;
|
||||
|
||||
if !_canManageTreasury exitWith {
|
||||
_result set ["message", "Only the organization leader or CEO can manage treasury actions."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _members = _org getOrDefault ["members", createHashMap];
|
||||
private _memberRecord = _members getOrDefault [_memberUid, createHashMap];
|
||||
if (_memberRecord isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Selected member was not found in the organization roster."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _resolvedMemberName = _memberRecord getOrDefault ["name", _memberName];
|
||||
if (_resolvedMemberName isEqualTo "") then {
|
||||
_resolvedMemberName = _memberName;
|
||||
};
|
||||
|
||||
private _creditLines = +(_org getOrDefault ["credit_lines", createHashMap]);
|
||||
_creditLines set [_memberUid, createHashMapFromArray [
|
||||
["uid", _memberUid],
|
||||
["name", _resolvedMemberName],
|
||||
["amount", _amount]
|
||||
]];
|
||||
|
||||
private _patch = _self call ["set", [GVAR(Registry), "org:update", _orgID, "credit_lines", _creditLines, true]];
|
||||
private _memberUids = keys _members;
|
||||
if !(_requesterUid in _memberUids) then {
|
||||
_memberUids pushBack _requesterUid;
|
||||
};
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", format ["Credit line of $%1 assigned to %2.", [_amount] call BIS_fnc_numberText, _resolvedMemberName]];
|
||||
_result set ["patch", _patch];
|
||||
_result set ["memberUids", _memberUids];
|
||||
_result
|
||||
_org set ["members", _memberMap];
|
||||
GVAR(Registry) set [_orgID, _org, true];
|
||||
_org
|
||||
}],
|
||||
["register", compileFinal {
|
||||
params [["_uid", "", [""]], ["_orgName", "", [""]]];
|
||||
@ -507,7 +224,6 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
|
||||
private _existingOrgID = _actor getOrDefault ["organization", ""];
|
||||
|
||||
if (_existingOrgID isNotEqualTo "" && { toLower _existingOrgID isNotEqualTo "default" }) exitWith {
|
||||
_result set ["message", "Player already belongs to an organization."];
|
||||
_result
|
||||
@ -549,7 +265,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
_result
|
||||
};
|
||||
|
||||
if (_createResult isNotEqualTo "") then { _org = _self call ["toHashMap", [_createResult]]; };
|
||||
if (_createResult isNotEqualTo "") then {
|
||||
_org = _self call ["toHashMap", [_createResult]];
|
||||
};
|
||||
|
||||
_org set ["members", createHashMap];
|
||||
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
|
||||
@ -576,18 +294,20 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
private _actor = EGVAR(actor,Registry) get _uid;
|
||||
private _orgID = _actor get "organization";
|
||||
if (_orgID isEqualTo "") then { _orgID = "default" };
|
||||
|
||||
private _cached = GVAR(Registry) getOrDefault [_orgID, nil];
|
||||
if !(isNil { _cached }) exitWith {
|
||||
private _cachedOwner = _cached getOrDefault ["owner", ""];
|
||||
if (_orgID isEqualTo "default" || { _cachedOwner isEqualTo _uid }) then {
|
||||
_cached = _self call ["verifyMember", [_cached, _orgID, _uid, _player, _actor]];
|
||||
if (_orgID isEqualTo "") then {
|
||||
_orgID = "default";
|
||||
};
|
||||
GVAR(Registry) set [_orgID, _cached, true];
|
||||
[CRPC(org,responseInitOrg), [_cached], _player] call CFUNC(targetEvent);
|
||||
|
||||
_cached
|
||||
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"];
|
||||
@ -595,18 +315,19 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
["ERROR", format ["Failed to check for org %1! Using fallback org.", _orgID]] call EFUNC(common,log);
|
||||
|
||||
private _fallbackOrg = GVAR(Registry) getOrDefault ["default", createHashMap];
|
||||
private _entry = createHashMapFromArray [["orgID", _orgID]];
|
||||
GVAR(IndexRegistry) set [_uid, _entry];
|
||||
GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]];
|
||||
|
||||
if (_orgID isEqualTo "default") then {
|
||||
_fallbackOrg = _self call ["verifyMember", [_fallbackOrg, _orgID, _uid, _player, _actor]];
|
||||
};
|
||||
|
||||
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;
|
||||
_fallbackOrg
|
||||
};
|
||||
|
||||
private _finalOrg = createHashMap;
|
||||
|
||||
if (_result == "true") then {
|
||||
_finalOrg = _self call ["loadById", [_orgID]];
|
||||
["INFO", format ["Found org for %1", _orgID]] call EFUNC(common,log);
|
||||
@ -616,13 +337,13 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
_orgID = "default";
|
||||
};
|
||||
|
||||
private _entry = createHashMapFromArray [["orgID", _orgID]];
|
||||
GVAR(IndexRegistry) set [_uid, _entry];
|
||||
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);
|
||||
|
||||
|
||||
243
arma/server/addons/org/functions/fnc_memberService.sqf
Normal file
243
arma/server/addons/org/functions/fnc_memberService.sqf
Normal file
@ -0,0 +1,243 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(OrgMembershipServiceBase) = compileFinal createHashMapFromArray [
|
||||
["#type", "OrgMembershipService"],
|
||||
["buildMembershipResult", compileFinal {
|
||||
params [["_message", "", [""]]];
|
||||
|
||||
createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", _message],
|
||||
["actorPatch", createHashMap]
|
||||
]
|
||||
}],
|
||||
["verifyMember", compileFinal {
|
||||
params [["_org", createHashMap, [createHashMap]], ["_orgID", "", [""]], ["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]];
|
||||
|
||||
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { _org };
|
||||
|
||||
private _members = _org getOrDefault ["members", createHashMap];
|
||||
if ((_members getOrDefault [_uid, objNull]) isNotEqualTo objNull) exitWith { _org };
|
||||
|
||||
["org:members:add", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"];
|
||||
if (!_memberSuccess) then {
|
||||
["WARNING", format ["Failed to add %1 to org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log);
|
||||
};
|
||||
|
||||
private _memberName = _actor getOrDefault ["name", ""];
|
||||
if (_memberName isEqualTo "" && { _player isNotEqualTo objNull }) then {
|
||||
_memberName = name _player;
|
||||
};
|
||||
if (_memberName isEqualTo "") then {
|
||||
_memberName = "Unknown";
|
||||
};
|
||||
|
||||
private _updatedMembers = +_members;
|
||||
_updatedMembers set [_uid, createHashMapFromArray [["uid", _uid], ["name", _memberName]]];
|
||||
_org set ["members", _updatedMembers];
|
||||
|
||||
_org
|
||||
}],
|
||||
["addMember", compileFinal {
|
||||
params [["_orgID", "", [""]], ["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]];
|
||||
|
||||
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap };
|
||||
|
||||
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith { _org };
|
||||
|
||||
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
|
||||
GVAR(Registry) set [_orgID, _org, true];
|
||||
|
||||
_org
|
||||
}],
|
||||
["removeMember", compileFinal {
|
||||
params [["_orgID", "", [""]], ["_uid", "", [""]]];
|
||||
|
||||
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap };
|
||||
|
||||
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith { _org };
|
||||
|
||||
["org:members:remove", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"];
|
||||
if (!_memberSuccess) exitWith {
|
||||
["WARNING", format ["Failed to remove %1 from org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log);
|
||||
createHashMap
|
||||
};
|
||||
|
||||
private _updatedMembers = +(_org getOrDefault ["members", createHashMap]);
|
||||
_updatedMembers deleteAt _uid;
|
||||
_org set ["members", _updatedMembers];
|
||||
GVAR(Registry) set [_orgID, _org, true];
|
||||
|
||||
_org
|
||||
}],
|
||||
["restoreDefaultMembership", compileFinal {
|
||||
params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]];
|
||||
|
||||
private _result = _self call ["buildMembershipResult", []];
|
||||
if (_uid isEqualTo "") exitWith {
|
||||
_result set ["message", "A valid player UID is required."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _resolvedPlayer = _player;
|
||||
if (_resolvedPlayer isEqualTo objNull) then {
|
||||
_resolvedPlayer = [_uid] call EFUNC(common,getPlayer);
|
||||
};
|
||||
|
||||
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 _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
|
||||
}],
|
||||
["leave", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", ""],
|
||||
["actorPatch", createHashMap],
|
||||
["notification", []]
|
||||
];
|
||||
|
||||
if (_uid isEqualTo "") exitWith {
|
||||
_result set ["message", "A valid player UID is required."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
|
||||
private _orgID = _actor getOrDefault ["organization", ""];
|
||||
if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith {
|
||||
_result set ["message", "You are already assigned to the default organization."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Unable to load organization data for leave request."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _ownerUid = _org getOrDefault ["owner", ""];
|
||||
if (_ownerUid isEqualTo _uid) exitWith {
|
||||
_result set ["message", "Organization owners must disband the organization instead of leaving it."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _orgName = _org getOrDefault ["name", "Organization"];
|
||||
private _updatedOrg = _self call ["removeMember", [_orgID, _uid]];
|
||||
if (_updatedOrg isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Failed to remove you from the organization roster."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _defaultResult = _self call ["restoreDefaultMembership", [_uid, _player, _actor]];
|
||||
if !(_defaultResult getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _defaultResult getOrDefault ["message", "Failed to restore default organization membership."]];
|
||||
_result
|
||||
};
|
||||
|
||||
private _message = format ["You left %1 and returned to the default organization.", _orgName];
|
||||
_result set ["success", true];
|
||||
_result set ["message", _message];
|
||||
_result set ["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]];
|
||||
_result set ["notification", ["info", "Organization Left", _message, 6000]];
|
||||
_result
|
||||
}],
|
||||
["disband", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", ""],
|
||||
["members", []]
|
||||
];
|
||||
|
||||
if (_uid isEqualTo "") exitWith {
|
||||
_result set ["message", "A valid player UID is required."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
|
||||
private _orgID = _actor getOrDefault ["organization", ""];
|
||||
if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith {
|
||||
_result set ["message", "Only active player organizations can be disbanded."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Unable to load organization data for disbanding."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _ownerUid = _org getOrDefault ["owner", ""];
|
||||
if (_ownerUid isEqualTo "" || { _ownerUid isNotEqualTo _uid }) exitWith {
|
||||
_result set ["message", "Only the organization owner can disband this organization."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _orgName = _org getOrDefault ["name", "Organization"];
|
||||
private _memberMap = _org getOrDefault ["members", createHashMap];
|
||||
private _memberUids = keys _memberMap;
|
||||
if !(_uid in _memberUids) then {
|
||||
_memberUids pushBack _uid;
|
||||
};
|
||||
|
||||
private _deleteResult = GVAR(OrgStore) call ["delete", [_orgID]];
|
||||
if !(_deleteResult getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _deleteResult getOrDefault ["message", "Failed to disband organization."]];
|
||||
_result
|
||||
};
|
||||
|
||||
private _memberResults = [];
|
||||
{
|
||||
private _memberUid = _x;
|
||||
if (_memberUid isNotEqualTo "") then {
|
||||
private _memberPlayer = [_memberUid] call EFUNC(common,getPlayer);
|
||||
private _memberActor = EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap];
|
||||
private _defaultResult = _self call ["restoreDefaultMembership", [_memberUid, _memberPlayer, _memberActor]];
|
||||
if !(_defaultResult getOrDefault ["success", false]) then {
|
||||
["WARNING", format ["Failed to restore default org for %1 after disbanding %2: %3", _memberUid, _orgID, _defaultResult getOrDefault ["message", "Unknown error."]]] call EFUNC(common,log);
|
||||
};
|
||||
|
||||
private _responseMessage = [
|
||||
format ["%1 has been disbanded.", _orgName],
|
||||
format ["Your organization, %1, has been disbanded.", _orgName]
|
||||
] select (_memberUid isEqualTo _uid);
|
||||
|
||||
private _notificationParams = [
|
||||
["warning", "Organization Disbanded", _responseMessage, 6000],
|
||||
["success", "Organization Disbanded", _responseMessage, 6000]
|
||||
] select (_memberUid isEqualTo _uid);
|
||||
|
||||
_memberResults pushBack (createHashMapFromArray [
|
||||
["uid", _memberUid],
|
||||
["requester", _memberUid isEqualTo _uid],
|
||||
["message", _responseMessage],
|
||||
["notification", _notificationParams],
|
||||
["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]]
|
||||
]);
|
||||
};
|
||||
} forEach _memberUids;
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", format ["%1 has been disbanded.", _orgName]];
|
||||
_result set ["members", _memberResults];
|
||||
_result
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(OrgMembershipService) = createHashMapObject [GVAR(OrgMembershipServiceBase)];
|
||||
164
arma/server/addons/org/functions/fnc_treasuryService.sqf
Normal file
164
arma/server/addons/org/functions/fnc_treasuryService.sqf
Normal file
@ -0,0 +1,164 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(OrgTreasuryServiceBase) = compileFinal createHashMapFromArray [
|
||||
["#type", "OrgTreasuryService"],
|
||||
["buildChargeResult", compileFinal {
|
||||
params [["_message", "Unable to process organization payment.", [""]]];
|
||||
|
||||
createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", _message],
|
||||
["patch", createHashMap],
|
||||
["memberUids", []]
|
||||
]
|
||||
}],
|
||||
["resolveOrgMemberUids", compileFinal {
|
||||
params [["_org", createHashMap, [createHashMap]], ["_requesterUid", "", [""]]];
|
||||
|
||||
private _memberUids = keys (_org getOrDefault ["members", createHashMap]);
|
||||
if !(_requesterUid in _memberUids) then { _memberUids pushBack _requesterUid; };
|
||||
|
||||
_memberUids
|
||||
}],
|
||||
["canManageTreasury", compileFinal {
|
||||
params [["_orgID", "", [""]], ["_org", createHashMap, [createHashMap]], ["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]]];
|
||||
|
||||
private _ownerUid = _org getOrDefault ["owner", ""];
|
||||
private _isDefaultOrg = (_orgID isEqualTo "default") || { toLowerANSI _ownerUid isEqualTo "server" };
|
||||
private _isDefaultOrgCeo = _isDefaultOrg
|
||||
&& { _requesterPlayer isNotEqualTo objNull }
|
||||
&& { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" };
|
||||
|
||||
(_ownerUid isEqualTo _requesterUid) || _isDefaultOrgCeo
|
||||
}],
|
||||
["assignCreditLine", compileFinal {
|
||||
params [["_requesterUid", "", [""]], ["_memberUid", "", [""]], ["_memberName", "", [""]], ["_amount", 0, [0]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", ""],
|
||||
["patch", createHashMap],
|
||||
["memberUids", []]
|
||||
];
|
||||
|
||||
if (
|
||||
_requesterUid isEqualTo ""
|
||||
|| { _memberUid isEqualTo "" }
|
||||
|| { _amount <= 0 }
|
||||
) exitWith {
|
||||
_result set ["message", "A valid requester, member, and credit amount are required."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
|
||||
private _orgID = _requesterActor getOrDefault ["organization", "default"];
|
||||
if (_orgID isEqualTo "") then { _orgID = "default"; };
|
||||
|
||||
private _org = GVAR(OrgStore) call ["loadById", [_orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Unable to load organization data for credit line assignment."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
|
||||
if !(_self call ["canManageTreasury", [_orgID, _org, _requesterUid, _requesterPlayer]]) exitWith {
|
||||
_result set ["message", "Only the organization leader or CEO can manage treasury actions."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _members = _org getOrDefault ["members", createHashMap];
|
||||
private _memberRecord = _members getOrDefault [_memberUid, createHashMap];
|
||||
if (_memberRecord isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Selected member was not found in the organization roster."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _resolvedMemberName = _memberRecord getOrDefault ["name", _memberName];
|
||||
if (_resolvedMemberName isEqualTo "") then { _resolvedMemberName = _memberName; };
|
||||
|
||||
private _creditLines = +(_org getOrDefault ["credit_lines", createHashMap]);
|
||||
_creditLines set [_memberUid, createHashMapFromArray [
|
||||
["uid", _memberUid],
|
||||
["name", _resolvedMemberName],
|
||||
["amount", _amount]
|
||||
]];
|
||||
|
||||
private _patch = GVAR(OrgStore) call ["set", [GVAR(Registry), "org:update", _orgID, "credit_lines", _creditLines, true]];
|
||||
private _memberUids = _self call ["resolveOrgMemberUids", [_org, _requesterUid]];
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", format ["Credit line of $%1 assigned to %2.", [_amount] call BIS_fnc_numberText, _resolvedMemberName]];
|
||||
_result set ["patch", _patch];
|
||||
_result set ["memberUids", _memberUids];
|
||||
_result
|
||||
}],
|
||||
["chargeCheckout", compileFinal {
|
||||
params [["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]], ["_source", "org_funds", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]];
|
||||
|
||||
private _result = _self call ["buildChargeResult", []];
|
||||
private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
|
||||
private _orgID = _requesterActor getOrDefault ["organization", "default"];
|
||||
if (_orgID isEqualTo "") then { _orgID = "default"; };
|
||||
|
||||
private _org = GVAR(Registry) getOrDefault [_orgID, createHashMap];
|
||||
if (_org isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Organization data is unavailable for checkout."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _memberUids = _self call ["resolveOrgMemberUids", [_org, _requesterUid]];
|
||||
|
||||
switch (toLowerANSI _source) do {
|
||||
case "org_funds": {
|
||||
if !(_self call ["canManageTreasury", [_orgID, _org, _requesterUid, _requesterPlayer]]) exitWith {
|
||||
_result set ["message", "Only the organization leader or CEO can charge org funds."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _funds = _org getOrDefault ["funds", 0];
|
||||
if (_funds < _amount) exitWith {
|
||||
_result set ["message", "Organization funds cannot cover this checkout."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _patch = createHashMapFromArray [["funds", (_funds - _amount)]];
|
||||
if (_commit) then { _patch = GVAR(OrgStore) call ["mset", [GVAR(Registry), "org:update", _orgID, _patch, false]]; };
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", ""];
|
||||
_result set ["patch", _patch];
|
||||
_result set ["memberUids", _memberUids];
|
||||
_result
|
||||
};
|
||||
case "credit_line": {
|
||||
private _creditLines = +(_org getOrDefault ["credit_lines", createHashMap]);
|
||||
private _memberCredit = +(_creditLines getOrDefault [_requesterUid, createHashMap]);
|
||||
private _creditAmount = _memberCredit getOrDefault ["amount", 0];
|
||||
if (_creditAmount < _amount) exitWith {
|
||||
_result set ["message", "Assigned credit line cannot cover this checkout."];
|
||||
_result
|
||||
};
|
||||
|
||||
_memberCredit set ["uid", _requesterUid];
|
||||
_memberCredit set ["amount", (_creditAmount - _amount)];
|
||||
_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]]; };
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", ""];
|
||||
_result set ["patch", _patch];
|
||||
_result set ["memberUids", _memberUids];
|
||||
_result
|
||||
};
|
||||
default {
|
||||
_result set ["message", "Selected organization payment source is unsupported."];
|
||||
_result
|
||||
};
|
||||
};
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(OrgTreasuryService) = createHashMapObject [GVAR(OrgTreasuryServiceBase)];
|
||||
@ -1,2 +1 @@
|
||||
// PREP(initStore);
|
||||
// PREP(initStoreStore);
|
||||
PREP(initStoreStore);
|
||||
|
||||
@ -5,3 +5,17 @@ PREP_RECOMPILE_START;
|
||||
PREP_RECOMPILE_END;
|
||||
|
||||
// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
||||
|
||||
[QGVAR(requestCheckout), {
|
||||
params [["_uid", "", [""]], ["_payloadJson", "", [""]]];
|
||||
|
||||
if (_uid isEqualTo "" || { _payloadJson isEqualTo "" }) exitWith {
|
||||
diag_log "[FORGE:Server:Store] Invalid checkout request payload."
|
||||
};
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
if (_player isEqualTo objNull) exitWith {};
|
||||
|
||||
private _result = GVAR(StoreStore) call ["checkout", [_uid, _player, _payloadJson]];
|
||||
[CRPC(store,responseCheckout), [_result], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
168
arma/server/addons/store/functions/fnc_initStoreStore.sqf
Normal file
168
arma/server/addons/store/functions/fnc_initStoreStore.sqf
Normal file
@ -0,0 +1,168 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initStoreStore.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-12
|
||||
* Last Update: 2026-03-12
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the server-side store checkout flow.
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
|
||||
["#type", "StoreBaseStore"],
|
||||
["#create", compileFinal {
|
||||
["INFO", "Store checkout service initialized!"] call EFUNC(common,log);
|
||||
}],
|
||||
["buildResult", compileFinal {
|
||||
params [["_message", "Checkout failed.", [""]], ["_paymentMethod", "cash", [""]]];
|
||||
|
||||
createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", _message],
|
||||
["paymentMethod", _paymentMethod],
|
||||
["chargedTotal", 0],
|
||||
["lockerGranted", []],
|
||||
["unsupportedLines", []],
|
||||
["bankPatch", createHashMap],
|
||||
["orgPatch", createHashMap],
|
||||
["orgTargetUids", []]
|
||||
]
|
||||
}],
|
||||
["formatCurrency", compileFinal {
|
||||
params [["_amount", 0, [0]]];
|
||||
|
||||
format ["$%1", [_amount max 0] call BIS_fnc_numberText]
|
||||
}],
|
||||
["applyPaymentPatch", compileFinal {
|
||||
params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_paymentMethod", "cash", [""]], ["_total", 0, [0]], ["_commit", false, [false]]];
|
||||
|
||||
private _result = _self call ["buildResult", ["Unable to process payment.", _paymentMethod]];
|
||||
private _payment = switch (toLowerANSI _paymentMethod) do {
|
||||
case "cash";
|
||||
case "bank": {
|
||||
EGVAR(bank,BankStore) call ["chargeCheckout", [_uid, _paymentMethod, _total, _commit]]
|
||||
};
|
||||
case "org_funds";
|
||||
case "credit_line": {
|
||||
EGVAR(org,OrgStore) call ["chargeCheckout", [_uid, _player, _paymentMethod, _total, _commit]]
|
||||
};
|
||||
default {
|
||||
createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Selected payment source is unsupported."],
|
||||
["patch", createHashMap],
|
||||
["memberUids", []]
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
if !(_payment getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _payment getOrDefault ["message", "Unable to process payment."]];
|
||||
_result
|
||||
};
|
||||
|
||||
private _patch = _payment getOrDefault ["patch", createHashMap];
|
||||
if ((_paymentMethod isEqualTo "cash") || { _paymentMethod isEqualTo "bank" }) then {
|
||||
_result set ["bankPatch", _patch];
|
||||
} else {
|
||||
_result set ["orgPatch", _patch];
|
||||
_result set ["orgTargetUids", _payment getOrDefault ["memberUids", []]];
|
||||
};
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", ""];
|
||||
_result
|
||||
}],
|
||||
["checkout", compileFinal {
|
||||
params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_payloadJson", "", [""]]];
|
||||
|
||||
private _result = _self call ["buildResult", ["Checkout failed.", "cash"]];
|
||||
private _payload = fromJSON _payloadJson;
|
||||
private _paymentMethod = toLowerANSI (_payload getOrDefault ["paymentMethod", "cash"]);
|
||||
private _totalPrice = floor ((_payload getOrDefault ["totalPrice", 0]) max 0);
|
||||
private _items = _payload getOrDefault ["items", []];
|
||||
private _vehicles = _payload getOrDefault ["vehicles", []];
|
||||
|
||||
_result set ["paymentMethod", _paymentMethod];
|
||||
_result set ["chargedTotal", _totalPrice];
|
||||
|
||||
if (_items isEqualTo [] && { _vehicles isEqualTo [] }) exitWith {
|
||||
_result set ["message", "Add at least one item before checkout."];
|
||||
_result
|
||||
};
|
||||
|
||||
if (_vehicles isNotEqualTo []) exitWith {
|
||||
_result set ["unsupportedLines", _vehicles apply {
|
||||
createHashMapFromArray [
|
||||
["classname", _x getOrDefault ["classname", ""]],
|
||||
["category", _x getOrDefault ["category", "vehicle"]],
|
||||
["reason", "Vehicles are handled through the virtual garage flow."]
|
||||
]
|
||||
}];
|
||||
_result set ["message", "Vehicle purchases are not wired yet."];
|
||||
_result
|
||||
};
|
||||
|
||||
if (_totalPrice <= 0) exitWith {
|
||||
_result set ["message", "Checkout total must be greater than zero."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _lockerPreview = EGVAR(locker,LockerStore) call ["grantItems", [_uid, _items, false]];
|
||||
if !(_lockerPreview getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _lockerPreview getOrDefault ["message", "Locker grant failed."]];
|
||||
_result
|
||||
};
|
||||
|
||||
private _vaPreview = EGVAR(locker,VAStore) call ["unlockItems", [_uid, _items, false]];
|
||||
if !(_vaPreview getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _vaPreview getOrDefault ["message", "VA unlock failed."]];
|
||||
_result
|
||||
};
|
||||
|
||||
_result set ["lockerGranted", _lockerPreview getOrDefault ["granted", []]];
|
||||
|
||||
private _paymentPreview = _self call ["applyPaymentPatch", [_uid, _player, _paymentMethod, _totalPrice, false]];
|
||||
if !(_paymentPreview getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _paymentPreview getOrDefault ["message", "Payment failed."]];
|
||||
_result
|
||||
};
|
||||
|
||||
private _payment = _self call ["applyPaymentPatch", [_uid, _player, _paymentMethod, _totalPrice, true]];
|
||||
private _lockerResult = EGVAR(locker,LockerStore) call ["grantItems", [_uid, _items, true]];
|
||||
private _vaResult = EGVAR(locker,VAStore) call ["unlockItems", [_uid, _items, true]];
|
||||
private _lockerPatch = _lockerResult getOrDefault ["patch", createHashMap];
|
||||
private _vaPatch = _vaResult getOrDefault ["patch", createHashMap];
|
||||
|
||||
if (keys _lockerPatch isNotEqualTo []) then { [CRPC(locker,responseSyncLocker), [_lockerPatch], _player] call CFUNC(targetEvent); };
|
||||
if (keys _vaPatch isNotEqualTo []) then { [CRPC(locker,responseSyncVA), [_vaPatch], _player] call CFUNC(targetEvent); };
|
||||
|
||||
private _bankPatch = _payment getOrDefault ["bankPatch", createHashMap];
|
||||
if (keys _bankPatch isNotEqualTo []) then { [CRPC(bank,responseSyncBank), [_bankPatch], _player] call CFUNC(targetEvent); };
|
||||
|
||||
private _orgPatch = _payment getOrDefault ["orgPatch", createHashMap];
|
||||
if (keys _orgPatch isNotEqualTo []) then {
|
||||
{
|
||||
private _memberPlayer = [_x] call EFUNC(common,getPlayer);
|
||||
if (_memberPlayer isNotEqualTo objNull) then {
|
||||
[CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent);
|
||||
};
|
||||
} forEach (_payment getOrDefault ["orgTargetUids", []]);
|
||||
};
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", format [
|
||||
"Checkout completed. %1 charged, %2 locker grant(s).",
|
||||
_self call ["formatCurrency", [_totalPrice]],
|
||||
count (_lockerResult getOrDefault ["granted", []])
|
||||
]];
|
||||
_result
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(StoreStore) = createHashMapObject [GVAR(StoreBaseStore)];
|
||||
GVAR(StoreStore)
|
||||
@ -127,6 +127,16 @@ impl<C: RedisClient> OrgRepository for RedisOrgRepository<C> {
|
||||
}
|
||||
|
||||
// Reconstruct Org from JSON object
|
||||
if matches!(
|
||||
json_map.get("credit_lines"),
|
||||
Some(serde_json::Value::Array(lines)) if lines.is_empty()
|
||||
) {
|
||||
json_map.insert(
|
||||
"credit_lines".to_string(),
|
||||
serde_json::Value::Object(serde_json::Map::new()),
|
||||
);
|
||||
}
|
||||
|
||||
let json_obj = serde_json::Value::Object(json_map);
|
||||
match serde_json::from_value::<Org>(json_obj) {
|
||||
Ok(org) => Ok(Some(org)),
|
||||
|
||||
@ -25,6 +25,31 @@ pub struct OrgService<R: OrgRepository> {
|
||||
}
|
||||
|
||||
impl<R: OrgRepository> OrgService<R> {
|
||||
fn normalize_org_value(
|
||||
mut org_value: serde_json::Value,
|
||||
key_override: Option<String>,
|
||||
) -> Result<Org, String> {
|
||||
let org_object = org_value
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| "Org payload must be a JSON object".to_string())?;
|
||||
|
||||
if let Some(key) = key_override {
|
||||
org_object.insert("id".to_string(), serde_json::Value::String(key));
|
||||
}
|
||||
|
||||
if matches!(
|
||||
org_object.get("credit_lines"),
|
||||
Some(serde_json::Value::Array(lines)) if lines.is_empty()
|
||||
) {
|
||||
org_object.insert(
|
||||
"credit_lines".to_string(),
|
||||
serde_json::Value::Object(serde_json::Map::new()),
|
||||
);
|
||||
}
|
||||
|
||||
serde_json::from_value::<Org>(org_value).map_err(|e| format!("Invalid Org JSON: {}", e))
|
||||
}
|
||||
|
||||
/// Creates a new organization service with the provided repository.
|
||||
///
|
||||
/// The repository must be initialized and ready for use.
|
||||
@ -37,12 +62,9 @@ impl<R: OrgRepository> OrgService<R> {
|
||||
/// Handles validation, duplicate checking, and persistence.
|
||||
/// See [crate README](../README.md) for JSON format and business rules.
|
||||
pub fn create_org(&self, key: String, json_data: String) -> Result<Org, String> {
|
||||
// Parse JSON data to Org struct
|
||||
let mut org: Org =
|
||||
let org_value: serde_json::Value =
|
||||
serde_json::from_str(&json_data).map_err(|e| format!("Invalid Org JSON: {}", e))?;
|
||||
|
||||
// Override ID with the provided parameter (ensures consistency)
|
||||
org.id = key;
|
||||
let org = Self::normalize_org_value(org_value, Some(key))?;
|
||||
|
||||
// Validate organization name is not empty
|
||||
if org.name.trim().is_empty() {
|
||||
@ -60,21 +82,10 @@ impl<R: OrgRepository> OrgService<R> {
|
||||
Ok(org)
|
||||
}
|
||||
|
||||
/// Retrieves an organization by its unique identifier with automatic fallback to default.
|
||||
///
|
||||
/// Implements a fallback pattern: if the organization doesn't exist, the
|
||||
/// organization with ID "default" is retrieved from the repository.
|
||||
pub fn get_org(&self, key: String) -> Result<Org, String> {
|
||||
// Attempt to retrieve organization from repository
|
||||
match self.repository.get_by_id(&key)? {
|
||||
// Organization found - return it
|
||||
Some(org) => Ok(org),
|
||||
// Organization not found - retrieve the default organization instead
|
||||
None => self
|
||||
.repository
|
||||
.get_by_id("default")?
|
||||
.ok_or_else(|| "Default organization not found".to_string()),
|
||||
}
|
||||
self.repository
|
||||
.get_by_id(&key)?
|
||||
.ok_or_else(|| format!("Organization with ID '{}' not found", key))
|
||||
}
|
||||
|
||||
/// Updates an existing organization with new data from JSON.
|
||||
@ -89,7 +100,7 @@ impl<R: OrgRepository> OrgService<R> {
|
||||
};
|
||||
|
||||
// Parse and validate JSON update data
|
||||
let update_data: serde_json::Value =
|
||||
let mut update_data: serde_json::Value =
|
||||
serde_json::from_str(&json_update).map_err(|e| format!("Invalid JSON: {}", e))?;
|
||||
|
||||
// Ensure update data is a JSON object
|
||||
@ -97,6 +108,13 @@ impl<R: OrgRepository> OrgService<R> {
|
||||
return Err("Update data must be a JSON object".to_string());
|
||||
}
|
||||
|
||||
if matches!(
|
||||
update_data.get("credit_lines"),
|
||||
Some(serde_json::Value::Array(lines)) if lines.is_empty()
|
||||
) {
|
||||
update_data["credit_lines"] = serde_json::Value::Object(serde_json::Map::new());
|
||||
}
|
||||
|
||||
// Create a temporary copy to safely apply updates with validation
|
||||
let mut updated_org = org.clone();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user