diff --git a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf index 384bd67..4c16639 100644 --- a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf @@ -37,6 +37,10 @@ switch (_event) do { case "actor::open::bank": { [] spawn EFUNC(bank,openUI); }; case "actor::open::cad": { [] spawn EFUNC(cad,openUI); }; case "actor::open::device": { hint "Device interaction is not yet implemented."; }; + case "actor::open::missionSetup": { + diag_log "[FORGE:Client:Actor] Requesting framework mission setup UI."; + [SRPC(task,requestOpenMissionSetup), [player]] call CFUNC(serverEvent); + }; case "actor::open::garage": { private _garageObject = objNull; if (_data isEqualType createHashMap) then { diff --git a/arma/client/addons/actor/functions/fnc_initRepository.sqf b/arma/client/addons/actor/functions/fnc_initRepository.sqf index edb9284..7df756f 100644 --- a/arma/client/addons/actor/functions/fnc_initRepository.sqf +++ b/arma/client/addons/actor/functions/fnc_initRepository.sqf @@ -107,6 +107,10 @@ GVAR(ActorRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["getNearbyActions", compileFinal { params [["_control", controlNull, [controlNull]]]; private _nearbyActions = []; + if !(GETMVAR(forge_server_task_missionSetup_settingsApplied,false)) then { + _nearbyActions pushBack ["missionSetup", true]; + }; + { private _isAtm = _x getVariable ["isAtm", false]; private _isBank = _x getVariable ["isBank", false]; diff --git a/arma/client/addons/actor/ui/_site/script.js b/arma/client/addons/actor/ui/_site/script.js index 3696a2a..30f5e15 100644 --- a/arma/client/addons/actor/ui/_site/script.js +++ b/arma/client/addons/actor/ui/_site/script.js @@ -151,6 +151,12 @@ const actionDefinitions = { description: "View and manage your organization data", action: "actor::open::org", }, + missionSetup: { + id: "missionSetup", + title: "Mission Setup", + description: "Open framework mission setup", + action: "actor::open::missionSetup", + }, store: { id: "store", title: "Store", diff --git a/arma/client/addons/main/script_macros.hpp b/arma/client/addons/main/script_macros.hpp index 6727a66..da5c45a 100644 --- a/arma/client/addons/main/script_macros.hpp +++ b/arma/client/addons/main/script_macros.hpp @@ -67,6 +67,7 @@ #define SETVAR(var1,var2,var3) var1 SETVAR_SYS(var2,var3) #define SETPVAR(var1,var2,var3) var1 SETPVAR_SYS(var2,var3) #define SETMVAR(var1,var2) missionNamespace SETVAR_SYS(var1,var2) +#define SETMPVAR(var1,var2) missionNamespace SETPVAR_SYS(var1,var2) #define SETUVAR(var1,var2) uiNamespace SETVAR_SYS(var1,var2) #define SETPRVAR(var1,var2) profileNamespace SETVAR_SYS(var1,var2) #define SETPAVAR(var1,var2) parsingNamespace SETVAR_SYS(var1,var2) diff --git a/arma/client/addons/mission_setup/$PBOPREFIX$ b/arma/client/addons/mission_setup/$PBOPREFIX$ new file mode 100644 index 0000000..0bed254 --- /dev/null +++ b/arma/client/addons/mission_setup/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_client\addons\mission_setup diff --git a/arma/client/addons/mission_setup/CfgEventHandlers.hpp b/arma/client/addons/mission_setup/CfgEventHandlers.hpp new file mode 100644 index 0000000..289a18f --- /dev/null +++ b/arma/client/addons/mission_setup/CfgEventHandlers.hpp @@ -0,0 +1,11 @@ +class Extended_PreInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_SCRIPT(XEH_preInit)); + }; +}; + +class Extended_PostInit_EventHandlers { + class ADDON { + clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient)); + }; +}; diff --git a/arma/client/addons/mission_setup/XEH_PREP.hpp b/arma/client/addons/mission_setup/XEH_PREP.hpp new file mode 100644 index 0000000..97e1950 --- /dev/null +++ b/arma/client/addons/mission_setup/XEH_PREP.hpp @@ -0,0 +1,3 @@ +PREP(handleUIEvents); +PREP(initRepository); +PREP(openUI); diff --git a/arma/client/addons/mission_setup/XEH_postInitClient.sqf b/arma/client/addons/mission_setup/XEH_postInitClient.sqf new file mode 100644 index 0000000..ab52a06 --- /dev/null +++ b/arma/client/addons/mission_setup/XEH_postInitClient.sqf @@ -0,0 +1,19 @@ +#include "script_component.hpp" + +if (isNil QGVAR(MissionSetupRepository)) then { call FUNC(initRepository); }; + +[CRPC(mission_setup,openMissionSetup), { + diag_log "[FORGE:Client:MissionSetup] Received server open request."; + [] call FUNC(openUI); +}] call CFUNC(addEventHandler); + +[{ + GETVAR(player,FORGE_isLoaded,false) +}, { + diag_log format [ + "[FORGE:Client:MissionSetup] Requesting mission setup open. Player=%1 VarName=%2", + player, + vehicleVarName player + ]; + [SRPC(task,requestOpenMissionSetup), [player]] call CFUNC(serverEvent); +}] call CFUNC(waitUntilAndExecute); diff --git a/arma/client/addons/mission_setup/XEH_preInit.sqf b/arma/client/addons/mission_setup/XEH_preInit.sqf new file mode 100644 index 0000000..1f72eca --- /dev/null +++ b/arma/client/addons/mission_setup/XEH_preInit.sqf @@ -0,0 +1,5 @@ +#include "script_component.hpp" + +PREP_RECOMPILE_START; +#include "XEH_PREP.hpp" +PREP_RECOMPILE_END; diff --git a/arma/client/addons/mission_setup/config.cpp b/arma/client/addons/mission_setup/config.cpp new file mode 100644 index 0000000..f003737 --- /dev/null +++ b/arma/client/addons/mission_setup/config.cpp @@ -0,0 +1,21 @@ +#include "script_component.hpp" + +class CfgPatches { + class ADDON { + author = AUTHOR; + authors[] = {"IDSolutions"}; + url = ECSTRING(main,url); + name = COMPONENT_NAME; + requiredVersion = REQUIRED_VERSION; + requiredAddons[] = { + "forge_client_main", + "forge_client_common" + }; + units[] = {}; + weapons[] = {}; + VERSION_CONFIG; + }; +}; + +#include "CfgEventHandlers.hpp" +#include "ui\RscMissionSetup.hpp" diff --git a/arma/client/addons/mission_setup/functions/fnc_handleUIEvents.sqf b/arma/client/addons/mission_setup/functions/fnc_handleUIEvents.sqf new file mode 100644 index 0000000..955fbda --- /dev/null +++ b/arma/client/addons/mission_setup/functions/fnc_handleUIEvents.sqf @@ -0,0 +1,76 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Handles JSON events from the framework mission setup browser UI. + * + * Arguments: + * 0: Browser control + * 1: Whether the event came from a confirm dialog + * 2: JSON event payload + * + * Return Value: + * Event handled + * + * Public: No + */ + +params [ + ["_control", controlNull, [controlNull]], + ["_isConfirmDialog", false, [false]], + ["_message", "", [""]] +]; + +if (_message isEqualTo "") exitWith { false }; + +private _alert = fromJSON _message; +if !(_alert isEqualType createHashMap) exitWith { false }; + +private _event = _alert getOrDefault ["event", ""]; +private _data = _alert getOrDefault ["data", createHashMap]; + +diag_log format ["[FORGE:Client:MissionSetup] Handling UI event: %1", _event]; + +private _send = { + params ["_eventName", "_payload"]; + + if (isNull _control) exitWith { false }; + + private _json = toJSON createHashMapFromArray [ + ["event", _eventName], + ["data", _payload] + ]; + _control ctrlWebBrowserAction ["ExecJS", format ["MissionSetupBridge.receive(%1)", _json]]; + true +}; + +switch (_event) do { + case "missionSetup::ready": { + if (isNil QGVAR(MissionSetupRepository)) then { call FUNC(initRepository); }; + ["missionSetup::hydrate", GVAR(MissionSetupRepository) call ["buildSetupPayload", []]] call _send; + }; + case "missionSetup::apply": { + if !(_data isEqualType createHashMap) exitWith { + ["missionSetup::error", createHashMapFromArray [["message", "Invalid mission setup payload."]]] call _send; + }; + + uiNamespace setVariable [QGVAR(MissionSetupHandled), true]; + [SRPC(task,requestApplyMissionSetupSettings), [_data, player]] call CFUNC(serverEvent); + closeDialog 1; + }; + case "missionSetup::cancel": { + uiNamespace setVariable [QGVAR(MissionSetupHandled), true]; + [SRPC(task,requestApplyMissionSetupSettings), [createHashMap, player]] call CFUNC(serverEvent); + closeDialog 1; + }; + case "missionSetup::close": { + uiNamespace setVariable [QGVAR(MissionSetupHandled), true]; + [SRPC(task,requestApplyMissionSetupSettings), [createHashMap, player]] call CFUNC(serverEvent); + closeDialog 1; + }; + default { + ["missionSetup::error", createHashMapFromArray [["message", format ["Unhandled setup event: %1", _event]]]] call _send; + }; +}; + +true diff --git a/arma/client/addons/mission_setup/functions/fnc_initRepository.sqf b/arma/client/addons/mission_setup/functions/fnc_initRepository.sqf new file mode 100644 index 0000000..037e7dd --- /dev/null +++ b/arma/client/addons/mission_setup/functions/fnc_initRepository.sqf @@ -0,0 +1,206 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Initializes the client mission setup repository. + * + * Arguments: + * None + * + * Return Value: + * Mission setup repository object + * + * Public: No + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(MissionSetupRepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "MissionSetupRepositoryBaseClass"], + ["getEnemyFactionOptions", compileFinal { + private _config = missionConfigFile >> "CfgEnemyFactions"; + if !(isClass _config) then { _config = configFile >> "CfgEnemyFactions"; }; + + private _allowedSides = getArray (_config >> "sides"); + if (_allowedSides isEqualTo []) then { _allowedSides = [0, 2]; }; + + private _denylist = getArray (_config >> "denylist"); + private _overridesConfig = _config >> "Overrides"; + + private _spawnableFactions = createHashMap; + { + if (getNumber (_x >> "scope") < 2) then { continue; }; + if !(configName _x isKindOf "CAManBase") then { continue; }; + + private _faction = getText (_x >> "faction"); + if (_faction isEqualTo "") then { continue; }; + + private _side = getNumber (_x >> "side"); + if !(_side in _allowedSides) then { continue; }; + + _spawnableFactions set [_faction, true]; + } forEach ("true" configClasses (configFile >> "CfgVehicles")); + + private _mappedFactions = createHashMap; + private _factionMapRoot = missionConfigFile >> "CfgFactionUnitMap"; + if !(isClass _factionMapRoot) then { _factionMapRoot = configFile >> "CfgFactionUnitMap"; }; + + { + private _unitsConfig = _x >> "Units"; + if !(isClass _unitsConfig) then { continue; }; + + private _hasUnits = false; + { + private _vehicle = getText (_x >> "vehicle"); + if (_vehicle isNotEqualTo "" && { isClass (configFile >> "CfgVehicles" >> _vehicle) }) exitWith { _hasUnits = true; }; + } forEach ("true" configClasses _unitsConfig); + + if (_hasUnits) then { _mappedFactions set [configName _x, true]; }; + } forEach ("true" configClasses _factionMapRoot); + + private _getFactionSideNumber = { + params ["_factionConfig"]; + + if (isNumber (_factionConfig >> "side")) exitWith { getNumber (_factionConfig >> "side") }; + switch (toUpperANSI getText (_factionConfig >> "side")) do { + case "0"; + case "EAST"; + case "OPFOR": { 0 }; + case "2"; + case "GUER"; + case "GUERRILA"; + case "GUERRILLA"; + case "INDEPENDENT"; + case "RESISTANCE": { 2 }; + default { -1 }; + }; + }; + + private _records = []; + private _dynamicIndex = 0; + { + private _faction = configName _x; + if (_faction isEqualTo "") then { continue; }; + if (_faction in _denylist) then { continue; }; + + private _side = [_x] call _getFactionSideNumber; + if !(_side in _allowedSides) then { continue; }; + if (!(_spawnableFactions getOrDefault [_faction, false]) && { + !(_mappedFactions getOrDefault [_faction, false]) + }) then { + continue; + }; + + private _override = _overridesConfig >> _faction; + private _display = getText (_x >> "displayName"); + private _order = 1000 + _dynamicIndex; + private _value = 1000 + _dynamicIndex; + + if (isClass _override) then { + private _overrideDisplay = getText (_override >> "display"); + if (_overrideDisplay isNotEqualTo "") then { _display = _overrideDisplay; }; + if (isNumber (_override >> "order")) then { _order = getNumber (_override >> "order"); }; + if (isNumber (_override >> "value")) then { _value = getNumber (_override >> "value"); }; + }; + if (_display isEqualTo "") then { _display = _faction; }; + + _records pushBack [_order, _display, _faction, _value]; + _dynamicIndex = _dynamicIndex + 1; + } forEach ("true" configClasses (configFile >> "CfgFactionClasses")); + + _records sort true; + + private _options = []; + { + _x params ["_order", "_display", "_faction", "_value"]; + _options pushBack [_faction, _display, _value]; + } forEach _records; + + if (_options isEqualTo []) then { + _options = [ + ["OPF_F", "CSAT", 0], + ["IND_G_F", "FIA", 6] + ]; + }; + + _options + }], + ["resolveEnemyFactionParam", compileFinal { + params [ + ["_value", 6, [0, ""]], + ["_fallback", "IND_G_F", [""]] + ]; + + if (_value isEqualType "") then { + if (_value isEqualTo "") exitWith { _fallback }; + if (isClass (configFile >> "CfgFactionClasses" >> _value)) exitWith { _value }; + _value = parseNumber _value; + }; + + private _faction = _fallback; + { + _x params ["_optionFaction", "_display", "_optionValue"]; + if (_optionValue isEqualTo _value) exitWith { _faction = _optionFaction; }; + } forEach (_self call ["getEnemyFactionOptions", []]); + + _faction + }], + ["buildSetupPayload", compileFinal { + private _missionConfig = missionConfigFile >> "CfgMissions"; + if !(isClass _missionConfig) then { _missionConfig = configFile >> "CfgMissions"; }; + + private _paramOrDefault = { + params ["_varName", "_default"]; + + private _value = missionNamespace getVariable [_varName, _default]; + if (_value isEqualType "") exitWith { parseNumber _value }; + _value + }; + + private _factions = []; + { + _x params ["_faction", "_display", "_value"]; + _factions pushBack createHashMapFromArray [ + ["faction", _faction], + ["display", _display], + ["value", _value] + ]; + } forEach (_self call ["getEnemyFactionOptions", []]); + + private _defaultFactionParam = GETMVAR(enemyFaction,6); + if (_defaultFactionParam isEqualTo 6) then { + private _paramValue = ["enemyFaction", -1] call BIS_fnc_getParamValue; + if (_paramValue isNotEqualTo -1) then { _defaultFactionParam = _paramValue; }; + }; + + private _defaultFaction = _self call ["resolveEnemyFactionParam", [_defaultFactionParam, "IND_G_F"]]; + private _hasDefaultFaction = false; + { + if ((_x getOrDefault ["faction", ""]) isEqualTo _defaultFaction) exitWith { _hasDefaultFaction = true; }; + } forEach _factions; + + if (!_hasDefaultFaction && { _factions isNotEqualTo [] }) then { + _defaultFaction = (_factions select 0) getOrDefault ["faction", _defaultFaction]; + }; + + createHashMapFromArray [ + ["factions", _factions], + ["settings", createHashMapFromArray [ + ["enemyFaction", _defaultFaction], + ["maxConcurrentMissions", ["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")] call _paramOrDefault], + ["missionInterval", ["missionInterval", getNumber (_missionConfig >> "missionInterval")] call _paramOrDefault], + ["locationReuseCooldown", ["locationReuseCooldown", getNumber (_missionConfig >> "locationReuseCooldown")] call _paramOrDefault], + ["moneyMin", ["moneyMin", 500] call _paramOrDefault], + ["moneyMax", ["moneyMax", 1000] call _paramOrDefault], + ["reputationMin", ["reputationMin", 25] call _paramOrDefault], + ["reputationMax", ["reputationMax", 100] call _paramOrDefault], + ["penaltyMin", ["penaltyMin", -5] call _paramOrDefault], + ["penaltyMax", ["penaltyMax", -25] call _paramOrDefault], + ["timeLimitMin", ["timeLimitMin", 600] call _paramOrDefault], + ["timeLimitMax", ["timeLimitMax", 900] call _paramOrDefault] + ]] + ] + }] +]; + +GVAR(MissionSetupRepository) = createHashMapObject [GVAR(MissionSetupRepositoryBaseClass)]; +GVAR(MissionSetupRepository) diff --git a/arma/client/addons/mission_setup/functions/fnc_openUI.sqf b/arma/client/addons/mission_setup/functions/fnc_openUI.sqf new file mode 100644 index 0000000..2bf4d70 --- /dev/null +++ b/arma/client/addons/mission_setup/functions/fnc_openUI.sqf @@ -0,0 +1,58 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Opens the framework mission setup UI. + * + * Arguments: + * None + * + * Return Value: + * UI opened + * + * Public: Yes + */ + +if !(hasInterface) exitWith { + diag_log "[FORGE:Client:MissionSetup] openUI skipped: no interface."; + false +}; +if !(isNull (uiNamespace getVariable ["RscMissionSetup", displayNull])) exitWith { + diag_log "[FORGE:Client:MissionSetup] openUI skipped: dialog already open."; + true +}; + +diag_log "[FORGE:Client:MissionSetup] Creating mission setup dialog."; + +private _display = createDialog ["RscMissionSetup", true]; +if (isNull _display) exitWith { + diag_log "[FORGE:Client:MissionSetup] createDialog returned null display."; + false +}; + +uiNamespace setVariable [QGVAR(MissionSetupHandled), false]; +_display displayAddEventHandler ["Unload", { + if !(uiNamespace getVariable [QGVAR(MissionSetupHandled), false]) then { + diag_log "[FORGE:Client:MissionSetup] Dialog unloaded before apply/cancel; applying default mission setup settings."; + [SRPC(task,requestApplyMissionSetupSettings), [createHashMap, player]] call CFUNC(serverEvent); + }; + + uiNamespace setVariable [QGVAR(MissionSetupHandled), nil]; +}]; + +private _control = _display displayCtrl 94011; +if (isNull _control) exitWith { + diag_log "[FORGE:Client:MissionSetup] Browser control 94011 not found. Closing dialog."; + closeDialog 1; + false +}; + +_control ctrlAddEventHandler ["JSDialog", { + params ["_control", "_isConfirmDialog", "_message"]; + [_control, _isConfirmDialog, _message] call FUNC(handleUIEvents); +}]; + +_control ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)]; +diag_log "[FORGE:Client:MissionSetup] Mission setup UI load requested."; + +true diff --git a/arma/client/addons/mission_setup/script_component.hpp b/arma/client/addons/mission_setup/script_component.hpp new file mode 100644 index 0000000..7959f6c --- /dev/null +++ b/arma/client/addons/mission_setup/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT mission_setup +#define COMPONENT_BEAUTIFIED Mission_Setup +#include "\forge\forge_client\addons\main\script_mod.hpp" + +// #define DEBUG_MODE_FULL +// #define DISABLE_COMPILE_CACHE +// #define ENABLE_PERFORMANCE_COUNTERS + +#include "\forge\forge_client\addons\main\script_macros.hpp" diff --git a/arma/client/addons/mission_setup/ui/RscMissionSetup.hpp b/arma/client/addons/mission_setup/ui/RscMissionSetup.hpp new file mode 100644 index 0000000..5da5b43 --- /dev/null +++ b/arma/client/addons/mission_setup/ui/RscMissionSetup.hpp @@ -0,0 +1,23 @@ +class RscText; + +class RscMissionSetup { + idd = 94010; + fadeIn = 0; + fadeOut = 0; + duration = 1e011; + onLoad = "uiNamespace setVariable ['RscMissionSetup', _this select 0]"; + onUnLoad = "uiNamespace setVariable ['RscMissionSetup', nil]"; + + class controlsBackground {}; + class controls { + class Browser: RscText { + type = 106; + idc = 94011; + x = "safeZoneXAbs + (safeZoneWAbs * 0.125)"; + y = "safeZoneY + (safeZoneH * 0.125)"; + w = "safeZoneWAbs * 0.75"; + h = "safeZoneH * 0.75"; + colorBackground[] = {0, 0, 0, 0}; + }; + }; +}; diff --git a/arma/client/addons/mission_setup/ui/_site/index.html b/arma/client/addons/mission_setup/ui/_site/index.html new file mode 100644 index 0000000..6310fb0 --- /dev/null +++ b/arma/client/addons/mission_setup/ui/_site/index.html @@ -0,0 +1 @@ +FORGE Mission Setup
diff --git a/arma/client/addons/mission_setup/ui/_site/mission-setup.css b/arma/client/addons/mission_setup/ui/_site/mission-setup.css new file mode 100644 index 0000000..ca93224 --- /dev/null +++ b/arma/client/addons/mission_setup/ui/_site/mission-setup.css @@ -0,0 +1,259 @@ +:root { + --bg-app: rgba(9, 12, 18, 0.88); + --surface: rgba(20, 24, 33, 0.9); + --border: rgba(255, 255, 255, 0.1); + --border-strong: rgba(255, 255, 255, 0.2); + --text-main: rgba(245, 248, 255, 0.92); + --text-muted: rgba(245, 248, 255, 0.62); + --text-subtle: rgba(245, 248, 255, 0.42); + --accent: rgba(104, 196, 255, 0.95); + --accent-soft: rgba(104, 196, 255, 0.13); + --accent-wash: rgba(41, 69, 93, 0.18); + --danger: rgba(255, 138, 128, 0.95); + --danger-bg: rgba(92, 18, 18, 0.78); + --shadow: 0 20px 60px rgba(0, 0, 0, 0.55); +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + width: 100%; + height: 100%; + margin: 0; +} + +body { + overflow: hidden; + font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; + color: var(--text-main); + background: + radial-gradient(circle at top left, var(--accent-wash), transparent 30%), + linear-gradient(180deg, rgba(9, 14, 20, 0.96), rgba(15, 22, 31, 0.98)), + var(--bg-app); + backdrop-filter: blur(16px); +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +.shell { + height: 100%; + display: grid; + grid-template-rows: auto 1fr auto; +} + +.titlebar { + min-height: 3.25rem; + padding: 0 1.6rem; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--border); + background: linear-gradient(90deg, rgba(16, 22, 31, 0.96), rgba(19, 26, 36, 0.94) 55%, rgba(15, 20, 28, 0.96)); +} + +.brand { + display: flex; + align-items: baseline; + gap: 0.8rem; +} + +.kicker { + color: var(--accent); + font-size: 0.76rem; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.title { + font-size: 1rem; + font-weight: 700; +} + +option { + background: rgb(20, 24, 33); + color: rgb(245, 248, 255); +} + +.close { + width: 2rem; + height: 2rem; + border: 1px solid var(--border); + background: rgba(255, 96, 96, 0.1); + color: rgba(255, 220, 220, 0.95); + font-size: 1.15rem; +} + +.content { + min-height: 0; + padding: 1.5rem; + overflow: auto; + display: flex; + align-items: center; +} + +.grid { + max-width: 78rem; + margin: 0 auto; + display: grid; + grid-template-columns: 1.1fr 0.9fr; + gap: 1rem; +} + +.panel { + min-width: 0; + border: 1px solid var(--border); + background: linear-gradient(180deg, rgba(23, 31, 40, 0.86), var(--surface) 9rem); + box-shadow: var(--shadow); +} + +.panel-head { + padding: 1.15rem 1.25rem; + border-bottom: 1px solid var(--border); +} + +.panel-head h1, +.panel-head h2 { + margin: 0.2rem 0 0; + font-size: 1.45rem; + letter-spacing: 0; +} + +.form { + padding: 1.25rem; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.field { + display: grid; + gap: 0.45rem; +} + +.wide { + grid-column: 1 / -1; +} + +label { + color: var(--text-subtle); + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.toggle { + min-height: 2.65rem; + padding: 0 0.85rem; + display: flex; + align-items: center; + gap: 0.65rem; + border: 1px solid var(--border); + background: rgba(24, 31, 40, 0.9); + color: var(--text-main); +} + +.toggle input { + width: 1rem; + min-height: 1rem; +} + +input, +select { + width: 100%; + min-height: 2.65rem; + padding: 0 0.85rem; + border: 1px solid var(--border); + background: rgba(24, 31, 40, 0.9); + color: var(--text-main); +} + +input:focus, +select:focus, +button:focus-visible { + outline: 2px solid rgba(104, 196, 255, 0.34); + outline-offset: 2px; +} + +.summary { + padding: 1.25rem; + display: grid; + gap: 0.8rem; +} + +.summary-row { + display: flex; + justify-content: space-between; + gap: 1rem; + padding-bottom: 0.8rem; + border-bottom: 1px solid var(--border); +} + +.summary-row span { + color: var(--text-muted); +} + +.summary-row strong { + text-align: right; +} + +.notice { + margin-top: 1rem; + padding: 0.85rem 1rem; + color: var(--danger); + background: var(--danger-bg); + border: 1px solid rgba(255, 107, 107, 0.38); +} + +.actions { + padding: 1rem 1.5rem; + display: flex; + justify-content: flex-end; + gap: 0.75rem; + border-top: 1px solid var(--border); + background: linear-gradient(90deg, rgba(11, 17, 24, 0.82), rgba(23, 31, 40, 0.86)); +} + +.btn { + min-height: 2.75rem; + padding: 0.72rem 1rem; + border: 1px solid var(--border-strong); + background: rgba(24, 31, 40, 0.9); + color: var(--text-main); + font-size: 0.82rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.btn.primary { + background: rgba(24, 86, 126, 0.95); + color: #ffffff; + border-color: rgba(104, 196, 255, 0.34); +} + +.btn.primary:hover { + background: rgba(32, 108, 156, 0.98); +} + +.btn.secondary { + background: rgba(255, 255, 255, 0.03); +} + +.btn.secondary:hover { + background: var(--accent-soft); + border-color: rgba(104, 196, 255, 0.28); +} diff --git a/arma/client/addons/mission_setup/ui/_site/mission-setup.js b/arma/client/addons/mission_setup/ui/_site/mission-setup.js new file mode 100644 index 0000000..b08a2fe --- /dev/null +++ b/arma/client/addons/mission_setup/ui/_site/mission-setup.js @@ -0,0 +1,250 @@ +(function () { + const state = { + factions: [], + settings: { + enemyFaction: "IND_G_F", + maxConcurrentMissions: 3, + missionInterval: 300, + locationReuseCooldown: 900, + moneyMin: 500, + moneyMax: 1000, + reputationMin: 25, + reputationMax: 100, + penaltyMin: -5, + penaltyMax: -25, + timeLimitMin: 600, + timeLimitMax: 900, + }, + error: "", + }; + + function send(event, data = {}) { + if (!window.A3API || typeof window.A3API.SendAlert !== "function") { + return false; + } + + window.A3API.SendAlert(JSON.stringify({ event, data })); + return true; + } + + function fieldNumber(id) { + const value = Number(document.getElementById(id)?.value || 0); + return Number.isFinite(value) ? value : 0; + } + + function readSettings() { + return { + enemyFaction: String(document.getElementById("enemyFaction")?.value || "IND_G_F"), + maxConcurrentMissions: fieldNumber("maxConcurrentMissions"), + missionInterval: fieldNumber("missionInterval"), + locationReuseCooldown: fieldNumber("locationReuseCooldown"), + moneyMin: fieldNumber("moneyMin"), + moneyMax: fieldNumber("moneyMax"), + reputationMin: fieldNumber("reputationMin"), + reputationMax: fieldNumber("reputationMax"), + penaltyMin: fieldNumber("penaltyMin"), + penaltyMax: fieldNumber("penaltyMax"), + timeLimitMin: fieldNumber("timeLimitMin"), + timeLimitMax: fieldNumber("timeLimitMax"), + }; + } + + function escapeHtml(value) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function apply() { + const settings = readSettings(); + if (settings.moneyMax < settings.moneyMin) { + state.error = "Money max must be greater than or equal to money min."; + render(); + return; + } + + if (settings.reputationMax < settings.reputationMin) { + state.error = "Reputation max must be greater than or equal to reputation min."; + render(); + return; + } + + if (settings.penaltyMin > 0 || settings.penaltyMax > 0) { + state.error = "Reputation hits must be zero or negative values."; + render(); + return; + } + + if (settings.timeLimitMax < settings.timeLimitMin) { + state.error = "Time limit max must be greater than or equal to time limit min."; + render(); + return; + } + + state.error = ""; + send("missionSetup::apply", settings); + } + + function close() { + send("missionSetup::cancel", {}); + } + + function option(faction) { + const selected = faction.faction === state.settings.enemyFaction ? " selected" : ""; + return ``; + } + + function render() { + const settings = state.settings; + const faction = state.factions.find((item) => item.faction === settings.enemyFaction); + const factionLabel = faction ? faction.display : settings.enemyFaction; + + document.getElementById("app").innerHTML = ` +
+
+
+ FORGE + Mission Setup +
+ +
+ +
+
+
+
+ Deployment Profile +

Operation Settings

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+ + +
+
+ `; + + document.querySelectorAll("input, select").forEach((input) => { + input.addEventListener("change", () => { + state.settings = readSettings(); + render(); + }); + }); + + document.querySelectorAll("[data-action='close']").forEach((button) => { + button.addEventListener("click", close); + }); + + document.querySelector("[data-action='apply']").addEventListener("click", apply); + } + + window.MissionSetupBridge = { + receive(payload) { + if (!payload || typeof payload !== "object") { + return false; + } + + if (payload.event === "missionSetup::hydrate") { + let factions = Array.isArray(payload.data?.factions) ? payload.data.factions : []; + const seen = new Set(); + factions = factions.filter((faction) => { + const key = ((faction.display || faction.faction) + "").toLowerCase().trim(); + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); + state.factions = factions; + state.settings = Object.assign({}, state.settings, payload.data?.settings || {}); + render(); + return true; + } + + if (payload.event === "missionSetup::error") { + state.error = String(payload.data?.message || "Mission setup failed."); + render(); + return true; + } + + return false; + }, + }; + + render(); + send("missionSetup::ready", { loaded: true }); +})(); diff --git a/arma/server/addons/cad/XEH_preInit.sqf b/arma/server/addons/cad/XEH_preInit.sqf index 6e561d8..fb63b0d 100644 --- a/arma/server/addons/cad/XEH_preInit.sqf +++ b/arma/server/addons/cad/XEH_preInit.sqf @@ -91,17 +91,13 @@ call FUNC(registerEventListeners); [CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent); }; - if !(isNil QEFUNC(task,requestMissionTask)) then { - _result = [_taskType, _metadata, _uid] call EFUNC(task,requestMissionTask); - } else { - if (isNil "forge_pmc_fnc_requestMissionTask") exitWith { - _result set ["message", "This mission does not expose dispatcher-generated tasks."]; - [CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent); - }; - - _result = [_taskType, _metadata, _uid] call forge_pmc_fnc_requestMissionTask; + if (isNil QEFUNC(task,requestMissionTask)) exitWith { + _result set ["message", "Framework generated mission requests are unavailable."]; + [CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent); }; + _result = [_taskType, _metadata, _uid] call EFUNC(task,requestMissionTask); + if !(_result getOrDefault ["success", false]) exitWith { [CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent); }; diff --git a/arma/server/addons/main/script_macros.hpp b/arma/server/addons/main/script_macros.hpp index 44efa49..d4fab1f 100644 --- a/arma/server/addons/main/script_macros.hpp +++ b/arma/server/addons/main/script_macros.hpp @@ -68,6 +68,7 @@ #define SETVAR(var1,var2,var3) var1 SETVAR_SYS(var2,var3) #define SETPVAR(var1,var2,var3) var1 SETPVAR_SYS(var2,var3) #define SETMVAR(var1,var2) missionNamespace SETVAR_SYS(var1,var2) +#define SETMPVAR(var1,var2) missionNamespace SETPVAR_SYS(var1,var2) #define SETUVAR(var1,var2) uiNamespace SETVAR_SYS(var1,var2) #define SETPRVAR(var1,var2) profileNamespace SETVAR_SYS(var1,var2) #define SETPAVAR(var1,var2) parsingNamespace SETVAR_SYS(var1,var2) diff --git a/arma/server/addons/task/CfgMissions.hpp b/arma/server/addons/task/CfgMissions.hpp index e109c35..ae42080 100644 --- a/arma/server/addons/task/CfgMissions.hpp +++ b/arma/server/addons/task/CfgMissions.hpp @@ -11,9 +11,9 @@ * * Generator behavior: * - maxConcurrentMissions and missionInterval are copied into - * forge_pmc_missionSettings by forge_pmc_fnc_setupMenu_applySettings. + * forge_server_task_missionSetup_settings by the framework mission setup service. * - Reward, reputation, penalty, and timeLimit ranges are read through - * forge_pmc_fnc_getMissionSettingRange so UI overrides and config fallbacks + * forge_server_task_fnc_getMissionSettingRange so UI overrides and config fallbacks * use the same path. */ class CfgMissions { diff --git a/arma/server/addons/task/XEH_PREP.hpp b/arma/server/addons/task/XEH_PREP.hpp index 7b60740..9af00b7 100644 --- a/arma/server/addons/task/XEH_PREP.hpp +++ b/arma/server/addons/task/XEH_PREP.hpp @@ -6,6 +6,7 @@ PREP(destroy); PREP(handler); PREP(hostage); PREP(hvt); +PREP(initMissionSetupService); PREP(makeCargo); PREP(makeHostage); PREP(makeHVT); diff --git a/arma/server/addons/task/XEH_postInit.sqf b/arma/server/addons/task/XEH_postInit.sqf index b9710e4..50d5f8b 100644 --- a/arma/server/addons/task/XEH_postInit.sqf +++ b/arma/server/addons/task/XEH_postInit.sqf @@ -1,11 +1,115 @@ #include "script_component.hpp" if (isNil QEGVAR(common,EventBus)) then { call EFUNC(common,eventBus); true }; +if (isNil QGVAR(MissionSetupService)) then { call FUNC(initMissionSetupService); }; + +[SRPC(task,requestOpenMissionSetup), { + params [ + ["_requester", objNull, [objNull]] + ]; + + private _notifyDenied = { + params [ + ["_unit", objNull, [objNull]], + ["_message", "", [""]] + ]; + + if (isNull _unit || { _message isEqualTo "" }) exitWith {}; + [CRPC(notifications,recieveNotification), ["warning", "Mission Setup", _message], _unit] call CFUNC(targetEvent); + }; + + if (isNull _requester) exitWith { + ["WARNING", "Mission setup open request ignored: requester was null."] call EFUNC(common,log); + }; + + private _requesterVar = toLowerANSI vehicleVarName _requester; + ["INFO", format [ + "Mission setup open requested. Requester=%1 VarName=%2 Enabled=%3 Applied=%4", + _requester, + _requesterVar, + GETGVAR(enableMissionSetup,false), + GETGVAR(missionSetup_settingsApplied,false) + ]] call EFUNC(common,log); + + if !(GETGVAR(enableMissionSetup,false)) exitWith { + ["INFO", "Mission setup open denied: framework mission setup is disabled."] call EFUNC(common,log); + [_requester, "Framework mission setup is disabled for this mission."] call _notifyDenied; + }; + if (GETGVAR(missionSetup_settingsApplied,false)) exitWith { + ["INFO", "Mission setup open denied: settings were already applied."] call EFUNC(common,log); + [_requester, "Mission setup has already been applied and cannot be reopened."] call _notifyDenied; + }; + + private _allowedVariables = GETGVAR(missionSetup_allowedUnitVariables,["ceo"]); + if !(_allowedVariables isEqualType []) then { _allowedVariables = ["ceo"]; }; + _allowedVariables = _allowedVariables apply { + if (_x isEqualType "") then { toLowerANSI _x } else { toLowerANSI str _x } + }; + + private _isSetupOperator = _requesterVar in _allowedVariables; + if !(_isSetupOperator) then { + { + private _unit = missionNamespace getVariable [_x, objNull]; + if (!isNull _unit && { _requester isEqualTo _unit }) exitWith { + _isSetupOperator = true; + }; + } forEach _allowedVariables; + }; + + if !(_isSetupOperator) exitWith { + ["INFO", format [ + "Mission setup open denied: requester is not an allowed setup operator. VarName=%1 Allowed=%2", + _requesterVar, + _allowedVariables + ]] call EFUNC(common,log); + [_requester, "You are not allowed to open mission setup."] call _notifyDenied; + }; + + ["INFO", format ["Mission setup open approved. Target=%1 VarName=%2", _requester, _requesterVar]] call EFUNC(common,log); + [CRPC(mission_setup,openMissionSetup), [], _requester] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); + +[SRPC(task,requestApplyMissionSetupSettings), { + params [ + ["_overrides", createHashMap, [createHashMap]], + ["_requester", objNull, [objNull]] + ]; + + private _allowedVariables = GETGVAR(missionSetup_allowedUnitVariables,["ceo"]); + if !(_allowedVariables isEqualType []) then { _allowedVariables = ["ceo"]; }; + _allowedVariables = _allowedVariables apply { + if (_x isEqualType "") then { toLowerANSI _x } else { toLowerANSI str _x } + }; + + private _requesterVar = toLowerANSI vehicleVarName _requester; + private _isSetupOperator = _requesterVar in _allowedVariables; + if !(_isSetupOperator) then { + { + private _unit = missionNamespace getVariable [_x, objNull]; + if (!isNull _unit && { _requester isEqualTo _unit }) exitWith { + _isSetupOperator = true; + }; + } forEach _allowedVariables; + }; + + if !(_isSetupOperator) exitWith { + ["WARNING", format [ + "Mission setup apply request denied. Requester=%1 VarName=%2", + _requester, + _requesterVar + ]] call EFUNC(common,log); + }; + + if (isNil QGVAR(MissionSetupService)) then { call FUNC(initMissionSetupService); }; + ["INFO", format ["Mission setup apply request accepted. Requester=%1 VarName=%2", _requester, _requesterVar]] call EFUNC(common,log); + GVAR(MissionSetupService) call ["apply", [_overrides]]; +}] call CFUNC(addEventHandler); + if (isNil QGVAR(TaskLifecycleEventLogTokens)) then { private _logTaskLifecycleEvent = { params ["_event"]; - if !(missionNamespace getVariable [QGVAR(enableEventLogs), false]) exitWith {}; + if !(GETGVAR(enableEventLogs,false)) exitWith {}; ["INFO", format [ "Task lifecycle event: %1 taskID=%2 taskType=%3 status=%4 participants=%5", @@ -20,7 +124,7 @@ if (isNil QGVAR(TaskLifecycleEventLogTokens)) then { private _logTaskRewardEvent = { params ["_event"]; - if !(missionNamespace getVariable [QGVAR(enableEventLogs), false]) exitWith {}; + if !(GETGVAR(enableEventLogs,false)) exitWith {}; ["INFO", format [ "Task reward event: %1 taskID=%2 success=%3 message=%4", @@ -62,7 +166,7 @@ if (isNil QGVAR(TaskNotificationEventTokens)) then { [CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent); } forEach _participantUids; - if (missionNamespace getVariable [QGVAR(enableEventLogs), false]) then { + if (GETGVAR(enableEventLogs,false)) then { ["INFO", format [ "Task notification event: taskID=%1 type=%2 recipients=%3 message=%4", _event getOrDefault ["taskID", ""], @@ -89,7 +193,7 @@ if (isNil QGVAR(TaskNotificationEventTokens)) then { [CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent); } forEach _memberUids; - if (missionNamespace getVariable [QGVAR(enableEventLogs), false]) then { + if (GETGVAR(enableEventLogs,false)) then { ["INFO", format [ "Task reward notification event: taskID=%1 type=%2 recipients=%3 message=%4", _event getOrDefault ["taskID", ""], diff --git a/arma/server/addons/task/XEH_preInit.sqf b/arma/server/addons/task/XEH_preInit.sqf index 9aea1c0..bf1872f 100644 --- a/arma/server/addons/task/XEH_preInit.sqf +++ b/arma/server/addons/task/XEH_preInit.sqf @@ -33,4 +33,5 @@ private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)]; [] call FUNC(DefendTaskBaseClass); call FUNC(initTaskStore); +call FUNC(initMissionSetupService); if !(isNil QGVAR(TaskStore)) then { GVAR(TaskStore) call ["resetMissionState", []]; }; diff --git a/arma/server/addons/task/functions/fnc_initMissionSetupService.sqf b/arma/server/addons/task/functions/fnc_initMissionSetupService.sqf new file mode 100644 index 0000000..994871a --- /dev/null +++ b/arma/server/addons/task/functions/fnc_initMissionSetupService.sqf @@ -0,0 +1,176 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Initializes the framework mission setup service. + * + * Arguments: + * None + * + * Return Value: + * Mission setup service object + * + * Public: No + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "MissionSetupServiceBaseClass"], + ["getMissionConfig", compileFinal { + private _missionConfig = missionConfigFile >> "CfgMissions"; + if !(isClass _missionConfig) then { + _missionConfig = configFile >> "CfgMissions"; + }; + _missionConfig + }], + ["numberOrDefault", compileFinal { + params ["_value", "_default"]; + + if (_value isEqualType "") exitWith { + private _parsed = parseNumber _value; + [_default, _parsed] select (_parsed isEqualType 0) + }; + + if (_value isEqualType 0) exitWith { _value }; + _default + }], + ["resolveFactionSide", compileFinal { + params [["_faction", "", [""]], ["_fallbackSide", east]]; + + private _cfgFaction = configFile >> "CfgFactionClasses" >> _faction; + if !(isClass _cfgFaction) exitWith { _fallbackSide }; + + private _sideNumber = -1; + if (isNumber (_cfgFaction >> "side")) then { + _sideNumber = getNumber (_cfgFaction >> "side"); + } else { + private _sideText = toUpperANSI getText (_cfgFaction >> "side"); + _sideNumber = switch (_sideText) do { + case "0"; + case "EAST"; + case "OPFOR": { 0 }; + case "2"; + case "GUER"; + case "GUERRILA"; + case "GUERRILLA"; + case "INDEPENDENT"; + case "RESISTANCE": { 2 }; + default { -1 }; + }; + }; + + switch (_sideNumber) do { + case 0: { east }; + case 2: { resistance }; + default { _fallbackSide }; + } + }], + ["apply", compileFinal { + if !(isServer) exitWith { false }; + + params [ + ["_overrides", createHashMap, [createHashMap]] + ]; + + private _missionConfig = _self call ["getMissionConfig", []]; + private _paramOrDefault = { + params ["_varName", "_default", "_overrides"]; + + if (_varName in _overrides) exitWith { + _overrides getOrDefault [_varName, _default] + }; + + missionNamespace getVariable [_varName, _default] + }; + + private _maxConcurrent = [ + ["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions"), _overrides] call _paramOrDefault, + 3 + ] call (_self get "numberOrDefault"); + private _interval = [ + ["missionInterval", getNumber (_missionConfig >> "missionInterval"), _overrides] call _paramOrDefault, + 300 + ] call (_self get "numberOrDefault"); + private _locationReuseCooldown = [ + ["locationReuseCooldown", getNumber (_missionConfig >> "locationReuseCooldown"), _overrides] call _paramOrDefault, + 900 + ] call (_self get "numberOrDefault"); + + private _moneyMin = [["moneyMin", 500, _overrides] call _paramOrDefault, 500] call (_self get "numberOrDefault"); + private _moneyMax = [["moneyMax", 1000, _overrides] call _paramOrDefault, 1000] call (_self get "numberOrDefault"); + private _repMin = [["reputationMin", 25, _overrides] call _paramOrDefault, 25] call (_self get "numberOrDefault"); + private _repMax = [["reputationMax", 100, _overrides] call _paramOrDefault, 100] call (_self get "numberOrDefault"); + private _penMin = [["penaltyMin", -5, _overrides] call _paramOrDefault, -5] call (_self get "numberOrDefault"); + private _penMax = [["penaltyMax", -25, _overrides] call _paramOrDefault, -25] call (_self get "numberOrDefault"); + private _timeMin = [["timeLimitMin", 600, _overrides] call _paramOrDefault, 600] call (_self get "numberOrDefault"); + private _timeMax = [["timeLimitMax", 900, _overrides] call _paramOrDefault, 900] call (_self get "numberOrDefault"); + + private _enemyFaction = _overrides getOrDefault [ + "enemyFaction", + GETMVAR(ENEMY_FACTION_STR,GETMVAR(enemyFaction,"IND_G_F")) + ]; + if !(_enemyFaction isEqualType "") then { _enemyFaction = str _enemyFaction; }; + if (_enemyFaction isEqualTo "") then { _enemyFaction = "IND_G_F"; }; + + _maxConcurrent = (_maxConcurrent max 1) min 50; + _interval = _interval max 1; + _locationReuseCooldown = _locationReuseCooldown max 0; + + _moneyMin = _moneyMin max 0; + _moneyMax = _moneyMax max _moneyMin; + + _repMin = _repMin max -100000; + _repMax = _repMax max _repMin; + + _penMin = _penMin min 0; + _penMax = _penMax min 0; + + _timeMin = _timeMin max 1; + _timeMax = _timeMax max _timeMin; + + private _settings = createHashMapFromArray [ + ["useMenuSettings", true], + ["maxConcurrentMissions", _maxConcurrent], + ["missionInterval", _interval], + ["locationReuseCooldown", _locationReuseCooldown], + ["moneyMin", _moneyMin], + ["moneyMax", _moneyMax], + ["reputationMin", _repMin], + ["reputationMax", _repMax], + ["penaltyMin", _penMin], + ["penaltyMax", _penMax], + ["timeLimitMin", _timeMin], + ["timeLimitMax", _timeMax], + ["enemyFaction", _enemyFaction] + ]; + + SETMPVAR(GVAR(missionSetup_settings),_settings); + SETMPVAR(GVAR(missionSetup_settingsApplied),true); + + private _side = _self call ["resolveFactionSide", [_enemyFaction, east]]; + ENEMY_SIDE = _side; + SETMPVAR(ENEMY_FACTION_STR,_enemyFaction); + publicVariable "ENEMY_SIDE"; + + ["INFO", format [ + "Framework mission setup applied. Faction=%1, Side=%2, MaxConcurrent=%3, Interval=%4", + _enemyFaction, + _side, + _maxConcurrent, + _interval + ]] call EFUNC(common,log); + + if !(isNil QEGVAR(common,EventBus)) then { + EGVAR(common,EventBus) call ["emit", [ + "mission.setup.applied", + createHashMapFromArray [["settings", _settings]], + createHashMapFromArray [["source", "task"]] + ]]; + }; + + true + }] +]; + +GVAR(MissionSetupService) = createHashMapObject [GVAR(MissionSetupServiceBaseClass)]; +GVAR(MissionSetupService) diff --git a/arma/server/addons/task/functions/fnc_missionManager.sqf b/arma/server/addons/task/functions/fnc_missionManager.sqf index 7b0777c..2b13665 100644 --- a/arma/server/addons/task/functions/fnc_missionManager.sqf +++ b/arma/server/addons/task/functions/fnc_missionManager.sqf @@ -20,25 +20,20 @@ if !(isServer) exitWith { false }; if !(isNil QGVAR(MissionManagerPFH)) exitWith { false }; if ( - !(missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) && - { !(isNil "forge_pmc_fnc_setupMenu_applySettings") } + (GETGVAR(enableMissionSetup,false)) && + { !(GETGVAR(missionSetup_settingsApplied,false)) } ) exitWith { - if !(missionNamespace getVariable [QGVAR(MissionManagerStartupPending), false]) then { - missionNamespace setVariable [QGVAR(MissionManagerStartupPending), true, true]; - ["INFO", "Mission manager startup deferred until mission setup settings are applied."] call EFUNC(common,log); + if !(GETGVAR(MissionManagerSetupPending,false)) then { + SETMPVAR(GVAR(MissionManagerSetupPending),true); + ["INFO", "Mission manager startup deferred until framework mission setup settings are applied."] call EFUNC(common,log); [] spawn { waitUntil { sleep 1; - (missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) || { time > 180 } + GETGVAR(missionSetup_settingsApplied,false) }; - if !(missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) then { - ["INFO", "Mission manager startup applying mission setup fallback settings after timeout."] call EFUNC(common,log); - [] call forge_pmc_fnc_setupMenu_applySettings; - }; - - missionNamespace setVariable [QGVAR(MissionManagerStartupPending), false, true]; + SETMPVAR(GVAR(MissionManagerSetupPending),false); call FUNC(missionManager); }; }; diff --git a/arma/server/addons/task/functions/generators/fnc_attackMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_attackMissionGenerator.sqf index 6cf7f2f..2d1f689 100644 --- a/arma/server/addons/task/functions/generators/fnc_attackMissionGenerator.sqf +++ b/arma/server/addons/task/functions/generators/fnc_attackMissionGenerator.sqf @@ -35,7 +35,7 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMissionInterval", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _interval = getNumber (_missionConfig >> "missionInterval"); if (_settings isEqualType createHashMap) then { _interval = _settings getOrDefault ["missionInterval", _interval]; @@ -45,7 +45,7 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMaxConcurrentMissions", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions"); if (_settings isEqualType createHashMap) then { _maxConcurrent = _settings getOrDefault ["maxConcurrentMissions", _maxConcurrent]; @@ -174,12 +174,12 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }; } forEach ("true" configClasses _aiGroupsConfig); - private _side = missionNamespace getVariable ["ENEMY_SIDE", east]; + private _side = GETMVAR(ENEMY_SIDE,east); private _sideText = str _side; private _group = createGroup _side; [] call FUNC(updateEnemyCountFromActivePlayers); - private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1]; + private _enemyMult = GETGVAR(enemyCountMultiplier,1); private _minUnitsBase = getNumber (_attackConfig >> "minUnits"); private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits"); private _patrolRadius = getNumber (_attackConfig >> "patrolRadius"); @@ -194,7 +194,7 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; }; private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1]; - private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]]; + private _enemyFaction = GETMVAR(ENEMY_FACTION_STR,GETMVAR(enemyFaction,"IND_G_F")); private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool); if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then { diff --git a/arma/server/addons/task/functions/generators/fnc_captureHvtMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_captureHvtMissionGenerator.sqf index f251b6a..fb30838 100644 --- a/arma/server/addons/task/functions/generators/fnc_captureHvtMissionGenerator.sqf +++ b/arma/server/addons/task/functions/generators/fnc_captureHvtMissionGenerator.sqf @@ -35,7 +35,7 @@ GVAR(CaptureHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray }], ["getMissionInterval", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _interval = getNumber (_missionConfig >> "missionInterval"); if (_settings isEqualType createHashMap) then { _interval = _settings getOrDefault ["missionInterval", _interval]; @@ -45,7 +45,7 @@ GVAR(CaptureHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray }], ["getMaxConcurrentMissions", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions"); if (_settings isEqualType createHashMap) then { _maxConcurrent = _settings getOrDefault ["maxConcurrentMissions", _maxConcurrent]; @@ -187,8 +187,8 @@ GVAR(CaptureHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray params [['_position', [0, 0, 0], [[]]], ["_buildingPositions", [], [[]]]]; private _hvtConfig = _self getOrDefault ["hvtConfig", configNull]; - private _side = missionNamespace getVariable ["ENEMY_SIDE", east]; - private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]]; + private _side = GETMVAR(ENEMY_SIDE,east); + private _enemyFaction = GETMVAR(ENEMY_FACTION_STR,GETMVAR(enemyFaction,"IND_G_F")); private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool); if (_unitPool isEqualTo []) exitWith { [] }; @@ -215,7 +215,7 @@ GVAR(CaptureHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray _leader setRank "LIEUTENANT"; [] call FUNC(updateEnemyCountFromActivePlayers); - private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1]; + private _enemyMult = GETGVAR(enemyCountMultiplier,1); private _escortCount = getNumber (_hvtConfig >> "escorts"); if (_escortCount < 0) then { _escortCount = 0; }; _escortCount = floor (_escortCount * _enemyMult); diff --git a/arma/server/addons/task/functions/generators/fnc_defendMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_defendMissionGenerator.sqf index fa839a5..308fad1 100644 --- a/arma/server/addons/task/functions/generators/fnc_defendMissionGenerator.sqf +++ b/arma/server/addons/task/functions/generators/fnc_defendMissionGenerator.sqf @@ -35,7 +35,7 @@ GVAR(DefendMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMissionInterval", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _interval = getNumber (_missionConfig >> "missionInterval"); if (_settings isEqualType createHashMap) then { _interval = _settings getOrDefault ["missionInterval", _interval]; @@ -45,7 +45,7 @@ GVAR(DefendMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMaxConcurrentMissions", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions"); if (_settings isEqualType createHashMap) then { _maxConcurrent = _settings getOrDefault ["maxConcurrentMissions", _maxConcurrent]; @@ -184,10 +184,10 @@ GVAR(DefendMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ } forEach ("true" configClasses _aiGroupsConfig); }; - private _side = missionNamespace getVariable ["ENEMY_SIDE", east]; + private _side = GETMVAR(ENEMY_SIDE,east); private _sideText = str _side; [] call FUNC(updateEnemyCountFromActivePlayers); - private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1]; + private _enemyMult = GETGVAR(enemyCountMultiplier,1); private _unitCountConfig = getArray (_defendConfig >> "unitsPerWave"); private _minUnits = _unitCountConfig select 0; private _maxUnits = _unitCountConfig select 1; @@ -199,7 +199,7 @@ GVAR(DefendMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; }; private _targetUnitCount = _minUnits + floor random ((_maxUnits - _minUnits) + 1); - private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]]; + private _enemyFaction = GETMVAR(ENEMY_FACTION_STR,GETMVAR(enemyFaction,"IND_G_F")); private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool); if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then { diff --git a/arma/server/addons/task/functions/generators/fnc_defuseMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_defuseMissionGenerator.sqf index 9e838ef..cd3a648 100644 --- a/arma/server/addons/task/functions/generators/fnc_defuseMissionGenerator.sqf +++ b/arma/server/addons/task/functions/generators/fnc_defuseMissionGenerator.sqf @@ -36,7 +36,7 @@ GVAR(DefuseMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMissionInterval", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _interval = getNumber (_missionConfig >> "missionInterval"); if (_settings isEqualType createHashMap) then { _interval = _settings getOrDefault ["missionInterval", _interval]; @@ -46,7 +46,7 @@ GVAR(DefuseMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMaxConcurrentMissions", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions"); if (_settings isEqualType createHashMap) then { _maxConcurrent = _settings getOrDefault ["maxConcurrentMissions", _maxConcurrent]; @@ -176,11 +176,11 @@ GVAR(DefuseMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }; } forEach ("true" configClasses _aiGroupsConfig); - private _side = missionNamespace getVariable ["ENEMY_SIDE", east]; + private _side = GETMVAR(ENEMY_SIDE,east); private _sideText = str _side; private _group = createGroup _side; [] call FUNC(updateEnemyCountFromActivePlayers); - private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1]; + private _enemyMult = GETGVAR(enemyCountMultiplier,1); private _minUnitsBase = getNumber (_attackConfig >> "minUnits"); private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits"); private _patrolRadius = getNumber (_attackConfig >> "patrolRadius"); @@ -196,7 +196,7 @@ GVAR(DefuseMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1]; diag_log format ["Defuse: Unit Count %1", _targetUnitCount]; - private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]]; + private _enemyFaction = GETMVAR(ENEMY_FACTION_STR,GETMVAR(enemyFaction,"IND_G_F")); private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool); if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then { diff --git a/arma/server/addons/task/functions/generators/fnc_deliveryMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_deliveryMissionGenerator.sqf index 8f5fd1d..4799f36 100644 --- a/arma/server/addons/task/functions/generators/fnc_deliveryMissionGenerator.sqf +++ b/arma/server/addons/task/functions/generators/fnc_deliveryMissionGenerator.sqf @@ -35,7 +35,7 @@ GVAR(DeliveryMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMissionInterval", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _interval = getNumber (_missionConfig >> "missionInterval"); if (_settings isEqualType createHashMap) then { _interval = _settings getOrDefault ["missionInterval", _interval]; @@ -45,7 +45,7 @@ GVAR(DeliveryMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMaxConcurrentMissions", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions"); if (_settings isEqualType createHashMap) then { _maxConcurrent = _settings getOrDefault ["maxConcurrentMissions", _maxConcurrent]; @@ -201,13 +201,13 @@ GVAR(DeliveryMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ if ("CargoSpawn" in allMapMarkers) exitWith { getMarkerPos "CargoSpawn" }; - private _cargoSpawn = missionNamespace getVariable ["CargoSpawn", objNull]; + private _cargoSpawn = GETMVAR(CargoSpawn,objNull); if (_cargoSpawn isEqualType "" && { _cargoSpawn in allMapMarkers }) exitWith { getMarkerPos _cargoSpawn }; if (_cargoSpawn isEqualType objNull && { !(isNull _cargoSpawn) }) exitWith { getPosATL _cargoSpawn }; if ("ExtZone" in allMapMarkers) exitWith { getMarkerPos "ExtZone" }; - private _extZone = missionNamespace getVariable ["ExtZone", objNull]; + private _extZone = GETMVAR(ExtZone,objNull); if (_extZone isEqualType "" && { _extZone in allMapMarkers }) exitWith { getMarkerPos _extZone }; if (_extZone isEqualType objNull && { !(isNull _extZone) }) exitWith { getPosATL _extZone }; diff --git a/arma/server/addons/task/functions/generators/fnc_destroyMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_destroyMissionGenerator.sqf index 320bb2a..aea4d0e 100644 --- a/arma/server/addons/task/functions/generators/fnc_destroyMissionGenerator.sqf +++ b/arma/server/addons/task/functions/generators/fnc_destroyMissionGenerator.sqf @@ -36,7 +36,7 @@ GVAR(DestroyMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMissionInterval", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _interval = getNumber (_missionConfig >> "missionInterval"); if (_settings isEqualType createHashMap) then { _interval = _settings getOrDefault ["missionInterval", _interval]; @@ -46,7 +46,7 @@ GVAR(DestroyMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMaxConcurrentMissions", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions"); if (_settings isEqualType createHashMap) then { _maxConcurrent = _settings getOrDefault ["maxConcurrentMissions", _maxConcurrent]; @@ -179,11 +179,11 @@ GVAR(DestroyMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }; } forEach ("true" configClasses _aiGroupsConfig); - private _side = missionNamespace getVariable ["ENEMY_SIDE", east]; + private _side = GETMVAR(ENEMY_SIDE,east); private _sideText = str _side; private _group = createGroup _side; [] call FUNC(updateEnemyCountFromActivePlayers); - private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1]; + private _enemyMult = GETGVAR(enemyCountMultiplier,1); private _minUnitsBase = getNumber (_attackConfig >> "minUnits"); private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits"); private _patrolRadius = getNumber (_attackConfig >> "patrolRadius"); @@ -198,7 +198,7 @@ GVAR(DestroyMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; }; private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1]; - private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]]; + private _enemyFaction = GETMVAR(ENEMY_FACTION_STR,GETMVAR(enemyFaction,"IND_G_F")); private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool); if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then { diff --git a/arma/server/addons/task/functions/generators/fnc_hostageMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_hostageMissionGenerator.sqf index e81b6ac..796dd68 100644 --- a/arma/server/addons/task/functions/generators/fnc_hostageMissionGenerator.sqf +++ b/arma/server/addons/task/functions/generators/fnc_hostageMissionGenerator.sqf @@ -36,7 +36,7 @@ GVAR(HostageMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMissionInterval", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _interval = getNumber (_missionConfig >> "missionInterval"); if (_settings isEqualType createHashMap) then { _interval = _settings getOrDefault ["missionInterval", _interval]; @@ -46,7 +46,7 @@ GVAR(HostageMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMaxConcurrentMissions", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions"); if (_settings isEqualType createHashMap) then { _maxConcurrent = _settings getOrDefault ["maxConcurrentMissions", _maxConcurrent]; @@ -200,11 +200,11 @@ GVAR(HostageMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }; } forEach ("true" configClasses _aiGroupsConfig); - private _side = missionNamespace getVariable ["ENEMY_SIDE", east]; + private _side = GETMVAR(ENEMY_SIDE,east); private _sideText = str _side; private _group = createGroup _side; [] call FUNC(updateEnemyCountFromActivePlayers); - private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1]; + private _enemyMult = GETGVAR(enemyCountMultiplier,1); private _minUnitsBase = getNumber (_attackConfig >> "minUnits"); private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits"); private _patrolRadius = getNumber (_attackConfig >> "patrolRadius"); @@ -219,7 +219,7 @@ GVAR(HostageMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; }; private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1]; - private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]]; + private _enemyFaction = GETMVAR(ENEMY_FACTION_STR,GETMVAR(enemyFaction,"IND_G_F")); private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool); if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then { @@ -335,11 +335,11 @@ GVAR(HostageMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ } forEach ("true" configClasses _aiGroupsConfig); }; - private _side = missionNamespace getVariable ["ENEMY_SIDE", east]; + private _side = GETMVAR(ENEMY_SIDE,east); private _sideText = str _side; private _group = createGroup _side; - private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]]; + private _enemyFaction = GETMVAR(ENEMY_FACTION_STR,GETMVAR(enemyFaction,"IND_G_F")); private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool); if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then { diff --git a/arma/server/addons/task/functions/generators/fnc_hvtMissionGenerator.sqf b/arma/server/addons/task/functions/generators/fnc_hvtMissionGenerator.sqf index 05a7eb9..56446fc 100644 --- a/arma/server/addons/task/functions/generators/fnc_hvtMissionGenerator.sqf +++ b/arma/server/addons/task/functions/generators/fnc_hvtMissionGenerator.sqf @@ -35,7 +35,7 @@ GVAR(KillHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMissionInterval", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _interval = getNumber (_missionConfig >> "missionInterval"); if (_settings isEqualType createHashMap) then { _interval = _settings getOrDefault ["missionInterval", _interval]; @@ -45,7 +45,7 @@ GVAR(KillHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ }], ["getMaxConcurrentMissions", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; - private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _settings = GETGVAR(missionSetup_settings,createHashMap); private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions"); if (_settings isEqualType createHashMap) then { _maxConcurrent = _settings getOrDefault ["maxConcurrentMissions", _maxConcurrent]; @@ -187,8 +187,8 @@ GVAR(KillHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ params [['_position', [0, 0, 0], [[]]], ["_buildingPositions", [], [[]]]]; private _hvtConfig = _self getOrDefault ["hvtConfig", configNull]; - private _side = missionNamespace getVariable ["ENEMY_SIDE", east]; - private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]]; + private _side = GETMVAR(ENEMY_SIDE,east); + private _enemyFaction = GETMVAR(ENEMY_FACTION_STR,GETMVAR(enemyFaction,"IND_G_F")); private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool); if (_unitPool isEqualTo []) exitWith { [] }; @@ -215,7 +215,7 @@ GVAR(KillHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [ _leader setRank "LIEUTENANT"; [] call FUNC(updateEnemyCountFromActivePlayers); - private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1]; + private _enemyMult = GETGVAR(enemyCountMultiplier,1); private _escortCount = getNumber (_hvtConfig >> "escorts"); if (_escortCount < 0) then { _escortCount = 0; }; _escortCount = floor (_escortCount * _enemyMult); diff --git a/arma/server/addons/task/functions/helpers/fnc_getEnemyFactionUnitPool.sqf b/arma/server/addons/task/functions/helpers/fnc_getEnemyFactionUnitPool.sqf index f9f2131..c730f19 100644 --- a/arma/server/addons/task/functions/helpers/fnc_getEnemyFactionUnitPool.sqf +++ b/arma/server/addons/task/functions/helpers/fnc_getEnemyFactionUnitPool.sqf @@ -18,8 +18,8 @@ */ params [ - ["_faction", missionNamespace getVariable ["ENEMY_FACTION_STR", "IND_G_F"], [""]], - ["_fallbackSide", missionNamespace getVariable ["ENEMY_SIDE", east], [east]], + ["_faction", GETMVAR(ENEMY_FACTION_STR,"IND_G_F"), [""]], + ["_fallbackSide", GETMVAR(ENEMY_SIDE,east), [east]], ["_allowSideFallback", true, [false]] ]; diff --git a/arma/server/addons/task/functions/helpers/fnc_getMissionSettingRange.sqf b/arma/server/addons/task/functions/helpers/fnc_getMissionSettingRange.sqf index ca27e8c..2db4ab0 100644 --- a/arma/server/addons/task/functions/helpers/fnc_getMissionSettingRange.sqf +++ b/arma/server/addons/task/functions/helpers/fnc_getMissionSettingRange.sqf @@ -28,9 +28,7 @@ params [ ]; private _rangeConfig = _config; -{ - _rangeConfig = _rangeConfig >> _x; -} forEach _path; +{ _rangeConfig = _rangeConfig >> _x; } forEach _path; private _range = getArray _rangeConfig; private _fallbackMin = _fallback param [0, 0, [0]]; @@ -39,7 +37,7 @@ private _fallbackMax = _fallback param [1, _fallbackMin, [0]]; private _min = _range param [0, _fallbackMin, [0]]; private _max = _range param [1, _fallbackMax, [0]]; -private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; +private _settings = GETGVAR(missionSetup_settings,createHashMap); if (_settings isEqualType createHashMap) then { _min = _settings getOrDefault [_minKey, _min]; _max = _settings getOrDefault [_maxKey, _max]; diff --git a/arma/server/addons/task/functions/helpers/fnc_updateEnemyCountFromActivePlayers.sqf b/arma/server/addons/task/functions/helpers/fnc_updateEnemyCountFromActivePlayers.sqf index 0e50256..8034785 100644 --- a/arma/server/addons/task/functions/helpers/fnc_updateEnemyCountFromActivePlayers.sqf +++ b/arma/server/addons/task/functions/helpers/fnc_updateEnemyCountFromActivePlayers.sqf @@ -16,18 +16,16 @@ if !(isServer) exitWith { 1 }; -private _table = missionNamespace getVariable [ - "forge_pmc_enemyCountMultiplierTable", - [ - [1, 2, 0.75], - [3, 6, 1.0], - [7, 10, 1.25], - [11, 19, 1.5] - ] +private _defaultTable = [ + [1, 2, 0.75], + [3, 6, 1.0], + [7, 10, 1.25], + [11, 19, 1.5] ]; +private _table = GETGVAR(enemyCountMultiplierTable,_defaultTable); -private _minMultiplier = missionNamespace getVariable ["forge_pmc_enemyCountMultiplierMin", 0.5]; -private _maxMultiplier = missionNamespace getVariable ["forge_pmc_enemyCountMultiplierMax", 2.0]; +private _minMultiplier = GETGVAR(enemyCountMultiplierMin,0.5); +private _maxMultiplier = GETGVAR(enemyCountMultiplierMax,2.0); private _activeCount = { (isPlayer _x) && { alive _x } @@ -45,8 +43,8 @@ private _multiplier = 1; _multiplier = (_multiplier max _minMultiplier) min _maxMultiplier; -missionNamespace setVariable ["forge_pmc_activePlayerCount", _activeCountSafe, true]; -missionNamespace setVariable ["forge_pmc_enemyCountMultiplier", _multiplier, true]; +SETMPVAR(GVAR(activePlayerCount),_activeCountSafe); +SETMPVAR(GVAR(enemyCountMultiplier),_multiplier); ["INFO", format [ "Mission enemy scaling updated. ActivePlayers=%1, Multiplier=%2", diff --git a/arma/server/addons/task/initSettings.inc.sqf b/arma/server/addons/task/initSettings.inc.sqf index b86599c..b37a297 100644 --- a/arma/server/addons/task/initSettings.inc.sqf +++ b/arma/server/addons/task/initSettings.inc.sqf @@ -4,6 +4,12 @@ _category, false, true ] call CBA_fnc_addSetting; +[ + QGVAR(enableMissionSetup), "CHECKBOX", + [LSTRING(enableMissionSetup), LSTRING(enableMissionSetupTooltip)], + _category, false, true +] call CBA_fnc_addSetting; + [ QGVAR(enableEventLogs), "CHECKBOX", [LSTRING(enableEventLogs), LSTRING(enableEventLogsTooltip)], diff --git a/arma/server/addons/task/stringtable.xml b/arma/server/addons/task/stringtable.xml index 43df877..e687be3 100644 --- a/arma/server/addons/task/stringtable.xml +++ b/arma/server/addons/task/stringtable.xml @@ -16,5 +16,11 @@ Enable Task Generator + + Enable Mission Setup UI + + + Open the framework mission setup UI for the configured setup operator before generated missions start. + diff --git a/docs/CAD_USAGE_GUIDE.md b/docs/CAD_USAGE_GUIDE.md index 062e40f..aa8e1f3 100644 --- a/docs/CAD_USAGE_GUIDE.md +++ b/docs/CAD_USAGE_GUIDE.md @@ -85,9 +85,13 @@ Generated mission requests are controlled by the server CBA setting is disabled, and server-side request handling rejects any manual request. The framework-owned request entry point is -`forge_server_task_fnc_requestMissionTask`. Server CAD calls that first and only -falls back to a mission-local `forge_pmc_fnc_requestMissionTask` when the -framework entry point is unavailable. +`forge_server_task_fnc_requestMissionTask`. Server CAD uses this framework +handler directly; it does not call mission-local generator functions. + +Custom mission generators can still create CAD-visible tasks directly by +registering task catalog entries and task statuses. See +[Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md) for the supported +integration path and the current generated-task provider limitation. ## Submit a Support Request diff --git a/docs/CLIENT_CAD_USAGE_GUIDE.md b/docs/CLIENT_CAD_USAGE_GUIDE.md index 61c6134..2531959 100644 --- a/docs/CLIENT_CAD_USAGE_GUIDE.md +++ b/docs/CLIENT_CAD_USAGE_GUIDE.md @@ -107,6 +107,11 @@ older payload compatibility, but any hydrate payload that includes request control, which is how `forge_server_task_enableGenerator = false` is surfaced client-side. +Custom mission generators can still publish tasks into CAD by using the server +task catalog. The generated-task dropdown itself currently needs a framework +provider extension point before custom providers can replace the built-in list +cleanly. See [Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md). + ## Authorization Notes Only dispatcher sessions can enter dispatch mode. If the hydrated session is @@ -116,4 +121,5 @@ not a dispatcher, the bridge forces the UI back to operations mode. - [CAD Usage Guide](./CAD_USAGE_GUIDE.md) - [Task Usage Guide](./TASK_USAGE_GUIDE.md) +- [Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md) - [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) diff --git a/docs/CUSTOM_MISSION_GENERATORS.md b/docs/CUSTOM_MISSION_GENERATORS.md new file mode 100644 index 0000000..45b5d97 --- /dev/null +++ b/docs/CUSTOM_MISSION_GENERATORS.md @@ -0,0 +1,379 @@ +# Custom Mission Generators + +Forge can be used as a complete out-of-box PMC mission framework, or as a +foundation that communities build on top of. Custom mission generators should +integrate through the same task, CAD, and event surfaces that the built-in +mission manager uses. + +This guide documents the supported integration path today and calls out the +current CAD generated-task provider limitation that should be addressed by a +small framework extension point. + +## Recommended Architecture + +Keep custom generation split into three layers: + +| Layer | Responsibility | +| --- | --- | +| Generator | Select a mission type, position, entities, rewards, timing, and ownership metadata. | +| Task registration | Create a CAD-visible Forge task catalog entry and BIS map task. | +| Mission runtime | Own custom win/loss logic, cleanup, and task status transitions. | + +Use Forge systems for persistence-adjacent state, dispatch visibility, group +assignment, notifications, ownership, rewards, and client refresh behavior. +Keep mission-specific spawning and objective logic in the mission or community +addon. + +## Disable Built-In Generation + +The built-in timer-driven generator is controlled by the server CBA setting: + +```sqf +forge_server_task_enableGenerator = false; +``` + +When disabled, Forge does not run timer-based generated missions and CAD +hydrates no built-in generated task types. + +This does not prevent custom code from creating CAD-visible tasks directly. +It only disables the built-in generator request list and the framework-owned +manual request entry point. + +The mission setup UI does not override this setting. Generated mission +enablement is mission/server policy and should stay in CBA settings until a +provider selection extension point exists. + +## Framework Mission Setup UI + +Forge includes an optional framework-level mission setup UI in +`arma/client/addons/mission_setup`. Enable it with the server CBA setting: + +```sqf +forge_server_task_enableMissionSetup = true; +``` + +When enabled, the UI opens for the setup operator before the mission manager +starts. By default, the operator is the player whose Eden variable name is +`ceo`. Missions can override the allowed unit variable names before client +post-init completes: + +```sqf +missionNamespace setVariable [ + "forge_server_task_missionSetup_allowedUnitVariables", + ["ceo", "mission_admin"], + true +]; +``` + +The UI configures: + +- opposing faction +- max concurrent generated missions +- mission interval +- location reuse cooldown +- funds, reputation, penalty, and time limit ranges + +Applying the UI writes framework-prefixed setup state: + +```sqf +forge_server_task_missionSetup_settings +forge_server_task_missionSetup_settingsApplied +``` + +The server also publishes the selected opposing faction and side for generated +mission runtime code: + +```sqf +ENEMY_FACTION_STR +ENEMY_SIDE +``` + +When settings are applied, Forge emits the EventBus event +`mission.setup.applied` with the applied settings in the event payload. + +The mission manager waits until setup settings are applied. There is no timeout +fallback. If the operator presses Cancel, X, or Escape, Forge applies default +settings from CBA, mission parameters, and `CfgMissions`, then starts normally. + +After setup settings have been applied, the setup UI cannot be reopened. The +actor interaction entry is hidden once clients receive the public applied flag, +and direct or stale open requests receive a notification explaining that setup +has already been applied. + +## CAD-Visible Task Contract + +CAD reads assignable contracts from `TaskStore.getActiveTaskCatalog`. A custom +task appears in CAD when it has: + +- a task catalog entry +- a task status of `available`, `assigned`, or `active` +- a stable `taskID` or `taskId` +- display fields such as `title`, `description`, `type`, and `position` + +The easiest supported path is to call `forge_server_task_fnc_startTask` from +server-side mission code: + +```sqf +[ + "attack", + "custom_attack_01", + getMarkerPos "custom_attack_area", + "Raid the Checkpoint", + "Clear the checkpoint and secure the site.", + createHashMapFromArray [ + ["targets", [target_1, target_2, target_3]] + ], + createHashMapFromArray [ + ["limitSuccess", 3], + ["limitFail", 0], + ["funds", 25000], + ["ratingSuccess", 10], + ["ratingFail", -5], + ["timeLimit", 1200] + ], + 0, + "", + "custom_generator" +] call forge_server_task_fnc_startTask; +``` + +`startTask` registers entities, creates the BIS task, upserts the Forge task +catalog entry, sets the initial task status, and dispatches the matching Forge +task flow. + +## Custom Runtime Tasks + +If a community generator has its own objective logic and does not use a built-in +Forge task flow, register the catalog entry and status directly: + +```sqf +private _taskID = "pvp_supply_drop_01"; +private _entry = createHashMapFromArray [ + ["taskID", _taskID], + ["taskId", _taskID], + ["type", "pvp_supply_drop"], + ["taskType", "custom"], + ["title", "Contest the Supply Drop"], + ["description", "Secure the marked drop zone before the opposing team."], + ["position", getMarkerPos "supply_drop_zone"], + ["accepted", false], + ["requesterUid", ""], + ["orgID", "default"], + ["source", "custom_generator"] +]; + +"forge_server" callExtension ["task:catalog:upsert", [ + _taskID, + toJSON _entry +]]; + +"forge_server" callExtension ["task:status:set", [ + _taskID, + "available" +]]; +``` + +Create a BIS task separately if players should see it in the vanilla map task +tab: + +```sqf +[ + west, + _taskID, + ["Secure the supply drop.", "Supply Drop", "custom"], + getMarkerPos "supply_drop_zone", + "CREATED", + 1, + true, + "container" +] call BIS_fnc_taskCreate; +``` + +When custom objective logic completes, set the task status: + +```sqf +"forge_server" callExtension ["task:status:set", [_taskID, "succeeded"]]; +// or +"forge_server" callExtension ["task:status:set", [_taskID, "failed"]]; +``` + +Use `task:clear` or `task:catalog:delete` when the custom runtime fully owns +cleanup and the contract should leave CAD. + +## CAD Assignment Lifecycle + +CAD assignment and task execution are intentionally separate. + +| Phase | Task status | Owner | +| --- | --- | --- | +| Created and visible | `available` | No group reservation yet. | +| Dispatcher assigns | `assigned` | CAD reserves the task for a group. | +| Group leader acknowledges | `active` | Task ownership is accepted for the acknowledging player/org. | +| Runtime finishes | `succeeded` or `failed` | CAD refreshes and removes completed active contracts. | + +Custom task logic should account for this lifecycle. If the task should not +start until the assigned group leader accepts it, wait for `active` status: + +```sqf +waitUntil { + sleep 2; + private _statusResult = "forge_server" callExtension ["task:status:get", [_taskID]]; + private _status = fromJSON (_statusResult select 0); + _status isEqualTo "active" +}; +``` + +If a group declines the assignment, CAD returns the task to `available`. + +## EventBus Integration + +The server EventBus is an in-process SQF event system. Initialize it if needed: + +```sqf +if (isNil "forge_server_common_EventBus") then { + call forge_server_common_fnc_eventBus; +}; +``` + +Subscribe to CAD and task lifecycle events: + +```sqf +private _token = forge_server_common_EventBus call ["on", [ + "cad.assignment.acknowledged", + { + params ["_event"]; + private _taskID = _event getOrDefault ["taskID", ""]; + private _assignment = _event getOrDefault ["assignment", createHashMap]; + diag_log format [ + "[CustomGenerator] Task %1 acknowledged by group %2", + _taskID, + _assignment getOrDefault ["groupId", ""] + ]; + }, + "custom_generator.assignment" +]]; +``` + +Remove a listener when it is no longer needed: + +```sqf +forge_server_common_EventBus call ["off", [_token]]; +``` + +Useful CAD events: + +| Event | When it fires | +| --- | --- | +| `cad.assignment.assigned` | Dispatcher assigns a task or order. | +| `cad.assignment.acknowledged` | Group leader accepts an assignment. | +| `cad.assignment.declined` | Group leader declines an assignment. | +| `cad.assignment.closed` | Dispatch order is closed. | +| `cad.request.submitted` | Support request is submitted. | +| `cad.request.closed` | Support request is closed. | +| `cad.group.updated` | Group status or role changes. | + +Useful task events: + +| Event | When it fires | +| --- | --- | +| `task.created` | Task catalog entry is registered through TaskStore. | +| `task.started` | Task status transitions to active/started. | +| `task.completed` | Task succeeds. | +| `task.failed` | Task fails. | +| `task.cleared` | Task state is cleared. | +| `task.reward.applied` | Task reward mutation succeeds. | +| `task.rating.applied` | Rating/earnings outcome succeeds. | +| `task.notification.requested` | Task participant notification is requested. | + +CAD already listens to task and CAD events and globally invalidates CAD state +when relevant changes occur. Custom generators usually only need to emit task +status changes through TaskStore or extension commands; CAD refresh follows +from the existing listeners. + +## Generated Task Dropdown Limitation + +The current CAD generated-task dropdown is owned by the framework task mission +manager. CAD hydrates `generatedTaskTypes` from the built-in manager when +`forge_server_task_enableGenerator` is enabled. When that setting is disabled, +the generated-task request control is disabled. + +The current CAD request handler calls `forge_server_task_fnc_requestMissionTask` +directly. It no longer falls back to mission-local generator request functions, +so third-party generated-task providers should create CAD-visible tasks directly +until a framework provider extension point is added. + +Until a provider extension point is added, use one of these supported patterns: + +1. Run custom generators from mission/server code and create CAD-visible tasks + directly. +2. Use CAD support requests or dispatch orders to let players request custom + work, then have mission code convert approved requests into tasks. +3. Keep the built-in generator enabled only if the community intentionally + wants the framework dropdown and request handler. + +## Planned Provider Extension Point + +A future code change should make CAD generator providers explicit. The desired +shape is: + +- built-in Forge provider remains the default out-of-box behavior +- mission/community providers can supply their own `generatedTaskTypes` +- mission/community providers can handle generated-task requests +- disabling the built-in provider does not disable custom providers +- mission designers or developers can select or toggle the active generator + provider when a mission includes custom generators +- a framework-hosted mission setup UI can display the active provider and, when + supported by the mission, allow choosing between built-in and custom + providers + +Candidate SQF hooks: + +```sqf +forge_custom_fnc_getGeneratedTaskTypes +forge_custom_fnc_requestMissionTask +``` + +or mission namespace variables: + +```sqf +missionNamespace setVariable ["forge_generatorProvider_getTypes", { + [ + createHashMapFromArray [["value", "supply_drop"], ["label", "Supply Drop"]], + createHashMapFromArray [["value", "pvp_hold"], ["label", "PvP Hold Area"]] + ] +}]; + +missionNamespace setVariable ["forge_generatorProvider_requestTask", { + params ["_taskType", "_metadata", "_requesterUid"]; + createHashMapFromArray [ + ["success", true], + ["message", "Generated custom task."], + ["taskID", "custom_task_01"], + ["taskType", _taskType] + ] +}]; +``` + +The exact API should be implemented in the framework code before communities +depend on it. + +Implementation note: the provider selection should be separate from +`forge_server_task_enableGenerator`. That CBA setting should continue to gate +the built-in Forge generator, while a new provider option can decide whether +CAD/manual requests use the built-in provider, a custom provider, both, or no +provider at all. + +## Validation Checklist + +For each custom generator: + +1. Disable the built-in generator if it should not run. +2. Generate or place task entities on the server. +3. Register a task catalog entry with stable `taskID` and display fields. +4. Set task status to `available`. +5. Confirm the task appears in CAD. +6. Assign it to a group from CAD. +7. Acknowledge and decline from the group leader UI. +8. Confirm custom logic waits for `active` if needed. +9. Set `succeeded` or `failed` when the objective resolves. +10. Confirm CAD refreshes and rewards or cleanup behave as expected. diff --git a/docs/MISSION_DESIGNER_GUIDE.md b/docs/MISSION_DESIGNER_GUIDE.md index 52db45d..9d0882f 100644 --- a/docs/MISSION_DESIGNER_GUIDE.md +++ b/docs/MISSION_DESIGNER_GUIDE.md @@ -310,6 +310,11 @@ interaction that calls: Tasks show in CAD only when they are created through a CAD-compatible task creation path. +Mission or community-owned generators can also create CAD-visible tasks by +using the task catalog/status contract. See +[Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md) before replacing +the built-in generated mission flow. + ## CEO and Dispatch Slots Forge grants dispatch-board permissions from the player's Eden unit variable @@ -747,6 +752,23 @@ The generated mission system supports `attack`, `defend`, `defuse`, `forge_server_task_enableGenerator` CBA setting gates both timer-based generation and CAD dispatcher-requested generation. +The optional framework mission setup UI lets the setup operator choose runtime +tuning such as opposing faction, mission cap, interval, location cooldown, +reward ranges, reputation ranges, penalty ranges, and time limits. It does not +enable or disable generated missions; use the CBA setting for that policy. + +If mission setup is enabled, the mission manager waits until the setup operator +applies settings. Cancel, X, and Escape apply default values from CBA, mission +parameters, and `CfgMissions`. There is no timeout that auto-applies defaults. +After settings are applied, the setup UI cannot be reopened. + +Future custom-generator support should add an explicit provider option so +mission designers or developers can select or toggle a mission/community-owned +generator without relying on mission-local fallback functions. Until then, +custom generators should create CAD-visible tasks directly through the task +catalog/status contract described in +[Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md). + The dynamic mission generator avoids rectangle and ellipse area markers whose marker name or marker text starts with `blklist`. diff --git a/docs/MODULE_REFERENCE.md b/docs/MODULE_REFERENCE.md index b369a80..a4f7d76 100644 --- a/docs/MODULE_REFERENCE.md +++ b/docs/MODULE_REFERENCE.md @@ -27,6 +27,7 @@ docs/ Framework-level documentation | CAD | Dispatch requests, assignments, orders, activity stream, profiles, groups, and hydrated dispatcher views. | `arma/client/addons/cad` | `arma/server/addons/cad` | `lib/models/src/cad.rs`, `lib/services/src/cad.rs` | `cad:*` | | Garage | Player vehicle storage with plate IDs, fuel, damage, and hit point state. | `arma/client/addons/garage` | `arma/server/addons/garage` | `lib/models/src/garage.rs`, `lib/services/src/garage.rs` | `garage:*`, `garage:hot:*` | | Locker | Player item storage keyed by classname with category and amount. | `arma/client/addons/locker` | `arma/server/addons/locker` | `lib/models/src/locker.rs`, `lib/services/src/locker.rs` | `locker:*`, `locker:hot:*` | +| Mission Setup | Optional pre-start setup UI for generated mission tuning, opposing faction selection, reward ranges, and mission manager defaults. Generator enablement remains a CBA setting. | `arma/client/addons/mission_setup` | `arma/server/addons/task` | task runtime settings in `missionNamespace` | EventBus `mission.setup.applied` | | Organization | Player organizations, membership, treasury, credit lines, shared assets, and fleet data. | `arma/client/addons/org` | `arma/server/addons/org` | `lib/models/src/org.rs`, `lib/services/src/org.rs` | `org:*`, `org:hot:*` | | Phone | Contacts, messages, and email state. | `arma/client/addons/phone` | `arma/server/addons/phone` | `lib/models/src/phone.rs`, `lib/services/src/phone.rs` | `phone:*` | | Store | Storefront entity setup, catalog hydration, checkout workflows, and checkout charging integration. | `arma/client/addons/store` | `arma/server/addons/store` | `lib/models/src/store.rs`, `lib/services/src/store.rs` | `store:checkout` | @@ -38,6 +39,7 @@ Server and extension guides: [Actor](./ACTOR_USAGE_GUIDE.md), [Bank](./BANK_USAGE_GUIDE.md), [CAD](./CAD_USAGE_GUIDE.md), +[Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md), [Economy](./ECONOMY_USAGE_GUIDE.md), [Garage](./GARAGE_USAGE_GUIDE.md), [Locker](./LOCKER_USAGE_GUIDE.md), diff --git a/docs/README.md b/docs/README.md index d72af8a..25c5aa4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -51,6 +51,9 @@ Players also load `@forge_client` for player-facing UI. See - [Mission Designer Guide](./MISSION_DESIGNER_GUIDE.md): how to place Eden objects, garage markers, and CAD-compatible task modules for playable missions. +- [Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md): how communities + and mission developers can create CAD-visible custom generated missions on + top of Forge. - [Player Guide](./PLAYER_GUIDE.md): how players use CAD, phone, bank, store, locker, garage, and economy services during missions. - [SurrealDB Setup](./surrealdb-setup.md): where to get SurrealDB or @@ -61,6 +64,7 @@ Players also load `@forge_client` for player-facing UI. See - [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md) - [Bank Usage Guide](./BANK_USAGE_GUIDE.md) - [CAD Usage Guide](./CAD_USAGE_GUIDE.md) +- [Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md) - [Economy Usage Guide](./ECONOMY_USAGE_GUIDE.md) - [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md) - [ICOM Usage Guide](./ICOM_USAGE_GUIDE.md) diff --git a/docs/TASK_USAGE_GUIDE.md b/docs/TASK_USAGE_GUIDE.md index 68c8f09..7cfe7b2 100644 --- a/docs/TASK_USAGE_GUIDE.md +++ b/docs/TASK_USAGE_GUIDE.md @@ -144,6 +144,11 @@ The dynamic mission manager can also generate attack, defend, defuse, delivery, destroy, hostage, HVT kill, and HVT capture tasks from config. That is system-generated content rather than a hand-authored task creation path. +Communities can disable the built-in generator and create CAD-visible tasks +from their own mission or server code. See +[Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md) for the custom +generator integration contract. + ## Generated Mission Configuration Mission designers should define `class CfgMissions` in the mission folder, such @@ -181,6 +186,22 @@ for generated missions. When disabled, timer-based generation does not run, CAD hydrates no generated task types, and manual dispatcher requests are rejected server-side. +The mission setup UI does not enable or disable generated missions. It applies +runtime tuning such as faction, caps, intervals, reward ranges, rating ranges, +penalties, and time limits. Generator enablement remains controlled by the CBA +setting above. + +When `forge_server_task_enableMissionSetup` is enabled, the mission manager +waits for setup settings before starting. There is no timeout auto-apply. +Pressing Cancel, X, or Escape applies default values from CBA, mission +parameters, and `CfgMissions`. + +Planned custom-generator work should add an explicit provider option for +mission designers or developers who want to select or toggle a custom mission +generator. That provider option should be separate from the built-in generator +CBA gate so disabling Forge's built-in generator does not prevent custom +providers from publishing CAD-visible work. + ## CAD Compatibility CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must diff --git a/docus/content/1.getting-started/0.index.md b/docus/content/1.getting-started/0.index.md index 411c915..196507d 100644 --- a/docus/content/1.getting-started/0.index.md +++ b/docus/content/1.getting-started/0.index.md @@ -73,6 +73,16 @@ npm run build:webui playable missions. ::: + :::u-page-card + --- + icon: i-lucide-waypoints + title: Custom Mission Generators + to: /getting-started/custom-mission-generators + --- + Create CAD-visible custom generated missions and understand the current + provider extension point. + ::: + :::u-page-card --- icon: i-lucide-user-round-check diff --git a/docus/content/1.getting-started/2.module-reference.md b/docus/content/1.getting-started/2.module-reference.md index 599ace6..6e17b26 100644 --- a/docus/content/1.getting-started/2.module-reference.md +++ b/docus/content/1.getting-started/2.module-reference.md @@ -28,6 +28,7 @@ docs/ Framework-level documentation | CAD | Dispatch requests, assignments, orders, activity stream, profiles, groups, and hydrated dispatcher views. | `arma/client/addons/cad` | `arma/server/addons/cad` | `lib/models/src/cad.rs`, `lib/services/src/cad.rs` | `cad:*` | | Garage | Player vehicle storage with plate IDs, fuel, damage, and hit point state. | `arma/client/addons/garage` | `arma/server/addons/garage` | `lib/models/src/garage.rs`, `lib/services/src/garage.rs` | `garage:*`, `garage:hot:*` | | Locker | Player item storage keyed by classname with category and amount. | `arma/client/addons/locker` | `arma/server/addons/locker` | `lib/models/src/locker.rs`, `lib/services/src/locker.rs` | `locker:*`, `locker:hot:*` | +| Mission Setup | Optional pre-start setup UI for generated mission tuning, opposing faction selection, reward ranges, and mission manager defaults. Generator enablement remains a CBA setting. | `arma/client/addons/mission_setup` | `arma/server/addons/task` | task runtime settings in `missionNamespace` | EventBus `mission.setup.applied` | | Organization | Player organizations, membership, treasury, credit lines, shared assets, and fleet data. | `arma/client/addons/org` | `arma/server/addons/org` | `lib/models/src/org.rs`, `lib/services/src/org.rs` | `org:*`, `org:hot:*` | | Phone | Contacts, messages, and email state. | `arma/client/addons/phone` | `arma/server/addons/phone` | `lib/models/src/phone.rs`, `lib/services/src/phone.rs` | `phone:*` | | Store | Storefront entity setup, catalog hydration, checkout workflows, and checkout charging integration. | `arma/client/addons/store` | `arma/server/addons/store` | `lib/models/src/store.rs`, `lib/services/src/store.rs` | `store:checkout` | @@ -39,6 +40,7 @@ Server and extension guides: [Actor](/server-modules/actor), [Bank](/server-modules/bank), [CAD](/server-modules/cad), +[Custom Mission Generators](/getting-started/custom-mission-generators), [Economy](/server-modules/economy), [Garage](/server-modules/garage), [Locker](/server-modules/locker), diff --git a/docus/content/1.getting-started/4.git-workflow.md b/docus/content/1.getting-started/4.git-workflow.md deleted file mode 100644 index 4f3e87b..0000000 --- a/docus/content/1.getting-started/4.git-workflow.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: "Git Workflow" -description: "This repository uses `master` as the clean framework branch. Mission folders are kept off `master` so the framework can be versioned without bundling local test missions or playable mission copies." ---- - -## Workflow Helper - -The repository includes a small helper for the common branch checks and branch -switching commands: - -```powershell -npm run workflow -- status -npm run workflow -- doctor -npm run workflow -- switch dev -npm run workflow -- switch missions -npm run workflow -- start-feature cad-task-request -npm run workflow -- release-check -``` - -The helper refuses branch switches and feature branch creation when the working -tree has uncommitted changes. Use the manual Git commands below when you need -more control. - -## Branch Roles - -- `master`: framework source, addon code, Rust extension code, docs, tooling, - and release tags. -- `missions/local-mission-copies`: local mission folders used for testing and - mission iteration. This branch is not pushed unless intentionally needed. -- `archive/pre-v0.1-history`: read-only archive of the previous full `master` - history before the `v0.1.0` baseline cleanup. - -## Daily Framework Work - -Start from the clean framework branch. - -```powershell -git switch master -git pull -git status --short --branch -``` - -Create a short-lived feature branch for framework work. - -```powershell -git switch -c feature/garage-marker-selection -``` - -Make the change, validate it, then commit. - -```powershell -git status --short --branch -git add arma/client/addons/garage/functions/fnc_initContextService.sqf -git commit -m "Improve garage spawn marker selection" -``` - -Merge the work back into `master`. Squash merges keep future `master` history -compact. - -```powershell -git switch master -git merge --squash feature/garage-marker-selection -git commit -m "Improve garage spawn marker selection" -git push -``` - -Remove the local feature branch when it is no longer needed. - -```powershell -git branch -D feature/garage-marker-selection -``` - -## Mission Work - -Switch to the local mission branch before editing mission folders. - -```powershell -git switch missions/local-mission-copies -git status --short --branch -``` - -Mission folders currently tracked on that branch: - -```text -arma/forge_framework.Malden -arma/forge_pmc_simulator.Tanoa -arma/forge_pmc_simulator_v2.Tanoa -``` - -Commit mission-only changes on the mission branch. - -```powershell -git add arma/forge_pmc_simulator.Tanoa -git commit -m "Update PMC simulator mission setup" -``` - -Do not merge the mission branch into `master`. If a mission change becomes -framework code, copy only the reusable files or logic onto a framework feature -branch created from `master`. - -Example: - -```powershell -git switch master -git switch -c feature/cad-on-demand-task-request - -# Bring over only the framework files needed from the mission branch. -git checkout missions/local-mission-copies -- arma/client/addons/cad/functions/fnc_initUIBridge.sqf -git checkout missions/local-mission-copies -- arma/server/addons/cad/XEH_preInit.sqf - -git add arma/client/addons/cad/functions/fnc_initUIBridge.sqf arma/server/addons/cad/XEH_preInit.sqf -git commit -m "Add CAD on-demand mission task request bridge" -``` - -## Release Versioning - -Use tags to mark framework releases. - -Version guideline: - -- Patch, such as `v0.1.1`: fixes and small compatible changes. -- Minor, such as `v0.2.0`: new modules or features. -- Major, such as `v1.0.0`: stable release line or breaking changes. - -Create a release tag from `master`. - -```powershell -git switch master -git pull -git status --short --branch -git tag -a v0.1.1 -m "v0.1.1" -git push origin master -git push origin v0.1.1 -``` - -## Safety Checks - -Before committing on `master`, check that no mission folders are staged. - -```powershell -git status --short --branch -``` - -On `master`, these paths should not appear: - -```text -arma/forge_framework.Malden -arma/forge_pmc_simulator.Tanoa -arma/forge_pmc_simulator_v2.Tanoa -``` - -If mission files appear while on `master`, stop and switch to the mission -branch before continuing. - -```powershell -git switch missions/local-mission-copies -``` - diff --git a/docus/content/1.getting-started/5.mission-designer.md b/docus/content/1.getting-started/4.mission-designer.md similarity index 93% rename from docus/content/1.getting-started/5.mission-designer.md rename to docus/content/1.getting-started/4.mission-designer.md index 2bb9b85..6e7707b 100644 --- a/docus/content/1.getting-started/5.mission-designer.md +++ b/docus/content/1.getting-started/4.mission-designer.md @@ -7,6 +7,17 @@ This guide focuses on editor placement and mission validation. Framework internals, extension commands, and persistence details are covered in the developer-oriented module guides. +## Required Forge Addons + +Forge missions that place Forge task modules or shared Forge vehicle classes +must depend on `@forge_mod`. This addon is loaded by both clients and servers +and provides the mission-facing config classes, including `forge_mod_task`. + +Do not make missions depend on `@forge_server` for Eden module classes. +`@forge_server` remains server-only and owns the runtime task handlers. A +mission that lists `forge_server_task` in `requiredAddons` will force clients to +install the server-only mod. + ## Core Rule Most Forge systems become available to players through nearby Eden objects. @@ -150,15 +161,12 @@ Minimum Eden setup: Transport nodes are generic paid travel points. They can represent ferries, airports, bus stops, teleport terminals, or any other mission transport system. -The framework owns the menu, billing, cargo scan, and movement logic. +The framework owns the menu, billing, cargo scan, and movement logic. The +mission only needs placed objects and optional arrival markers. -![Eden transport location one](images/eden/transport_loc_1.jpg) +![Placeholder: Eden transport node object placement](images/eden/transport_node_obj.svg) -![Eden transport location two](images/eden/transport_loc_2.jpg) - -![Eden transport node object placement](images/eden/transport_obj_1.jpg) - -![Eden transport node variable name](images/eden/transport_obj_1_var.jpg) +![Placeholder: Eden transport node variable name](images/eden/transport_node_var.svg) Place transport node objects with these variable names: @@ -170,7 +178,7 @@ transport_2 transport_10 ``` -Place arrival markers with matching suffixes: +Place optional arrival markers with matching suffixes: ```text transport_arrival @@ -180,9 +188,7 @@ transport_arrival_2 transport_arrival_10 ``` -![Eden transport arrival marker placement](images/eden/transport_arrival_mrkr.jpg) - -![Eden transport arrival marker variable name](images/eden/transport_arrival_mrkr_var.jpg) +![Placeholder: Eden transport arrival marker placement](images/eden/transport_arrival_marker.svg) Objects that should be excluded from the nearby cargo scan, such as the actual boat or transport vehicle used as set dressing, should use: @@ -195,9 +201,7 @@ transport_vehicle_2 transport_vehicle_10 ``` -![Eden transport vehicle exclusion object placement](images/eden/transport_veh_obj.jpg) - -![Eden transport vehicle exclusion object variable name](images/eden/transport_veh_obj_var.jpg) +![Placeholder: Eden transport vehicle exclusion object variable name](images/eden/transport_vehicle_var.svg) Minimum Eden setup: @@ -306,6 +310,11 @@ interaction that calls: Tasks show in CAD only when they are created through a CAD-compatible task creation path. +Mission or community-owned generators can also create CAD-visible tasks by +using the task catalog/status contract. See +[Custom Mission Generators](/getting-started/custom-mission-generators) before replacing +the built-in generated mission flow. + ## CEO and Dispatch Slots Forge grants dispatch-board permissions from the player's Eden unit variable @@ -743,6 +752,23 @@ The generated mission system supports `attack`, `defend`, `defuse`, `forge_server_task_enableGenerator` CBA setting gates both timer-based generation and CAD dispatcher-requested generation. +The optional framework mission setup UI lets the setup operator choose runtime +tuning such as opposing faction, mission cap, interval, location cooldown, +reward ranges, reputation ranges, penalty ranges, and time limits. It does not +enable or disable generated missions; use the CBA setting for that policy. + +If mission setup is enabled, the mission manager waits until the setup operator +applies settings. Cancel, X, and Escape apply default values from CBA, mission +parameters, and `CfgMissions`. There is no timeout that auto-applies defaults. +After settings are applied, the setup UI cannot be reopened. + +Future custom-generator support should add an explicit provider option so +mission designers or developers can select or toggle a mission/community-owned +generator without relying on mission-local fallback functions. Until then, +custom generators should create CAD-visible tasks directly through the task +catalog/status contract described in +[Custom Mission Generators](/getting-started/custom-mission-generators). + The dynamic mission generator avoids rectangle and ellipse area markers whose marker name or marker text starts with `blklist`. diff --git a/docus/content/1.getting-started/6.player-guide.md b/docus/content/1.getting-started/5.player-guide.md similarity index 100% rename from docus/content/1.getting-started/6.player-guide.md rename to docus/content/1.getting-started/5.player-guide.md diff --git a/docus/content/1.getting-started/7.surrealdb-setup.md b/docus/content/1.getting-started/6.surrealdb-setup.md similarity index 100% rename from docus/content/1.getting-started/7.surrealdb-setup.md rename to docus/content/1.getting-started/6.surrealdb-setup.md diff --git a/docus/content/1.getting-started/7.custom-mission-generators.md b/docus/content/1.getting-started/7.custom-mission-generators.md new file mode 100644 index 0000000..26e0a78 --- /dev/null +++ b/docus/content/1.getting-started/7.custom-mission-generators.md @@ -0,0 +1,377 @@ +--- +title: "Custom Mission Generators" +description: "Forge can be used as a complete out-of-box PMC mission framework, or as a foundation that communities build on top of. Custom mission generators should integrate through the same task, CAD, and event surfaces that the built-in mission manager uses." +--- + +This guide documents the supported integration path today and calls out the +current CAD generated-task provider limitation that should be addressed by a +small framework extension point. + +## Recommended Architecture + +Keep custom generation split into three layers: + +| Layer | Responsibility | +| --- | --- | +| Generator | Select a mission type, position, entities, rewards, timing, and ownership metadata. | +| Task registration | Create a CAD-visible Forge task catalog entry and BIS map task. | +| Mission runtime | Own custom win/loss logic, cleanup, and task status transitions. | + +Use Forge systems for persistence-adjacent state, dispatch visibility, group +assignment, notifications, ownership, rewards, and client refresh behavior. +Keep mission-specific spawning and objective logic in the mission or community +addon. + +## Disable Built-In Generation + +The built-in timer-driven generator is controlled by the server CBA setting: + +```sqf +forge_server_task_enableGenerator = false; +``` + +When disabled, Forge does not run timer-based generated missions and CAD +hydrates no built-in generated task types. + +This does not prevent custom code from creating CAD-visible tasks directly. +It only disables the built-in generator request list and the framework-owned +manual request entry point. + +The mission setup UI does not override this setting. Generated mission +enablement is mission/server policy and should stay in CBA settings until a +provider selection extension point exists. + +## Framework Mission Setup UI + +Forge includes an optional framework-level mission setup UI in +`arma/client/addons/mission_setup`. Enable it with the server CBA setting: + +```sqf +forge_server_task_enableMissionSetup = true; +``` + +When enabled, the UI opens for the setup operator before the mission manager +starts. By default, the operator is the player whose Eden variable name is +`ceo`. Missions can override the allowed unit variable names before client +post-init completes: + +```sqf +missionNamespace setVariable [ + "forge_server_task_missionSetup_allowedUnitVariables", + ["ceo", "mission_admin"], + true +]; +``` + +The UI configures: + +- opposing faction +- max concurrent generated missions +- mission interval +- location reuse cooldown +- funds, reputation, penalty, and time limit ranges + +Applying the UI writes framework-prefixed setup state: + +```sqf +forge_server_task_missionSetup_settings +forge_server_task_missionSetup_settingsApplied +``` + +The server also publishes the selected opposing faction and side for generated +mission runtime code: + +```sqf +ENEMY_FACTION_STR +ENEMY_SIDE +``` + +When settings are applied, Forge emits the EventBus event +`mission.setup.applied` with the applied settings in the event payload. + +The mission manager waits until setup settings are applied. There is no timeout +fallback. If the operator presses Cancel, X, or Escape, Forge applies default +settings from CBA, mission parameters, and `CfgMissions`, then starts normally. + +After setup settings have been applied, the setup UI cannot be reopened. The +actor interaction entry is hidden once clients receive the public applied flag, +and direct or stale open requests receive a notification explaining that setup +has already been applied. + +## CAD-Visible Task Contract + +CAD reads assignable contracts from `TaskStore.getActiveTaskCatalog`. A custom +task appears in CAD when it has: + +- a task catalog entry +- a task status of `available`, `assigned`, or `active` +- a stable `taskID` or `taskId` +- display fields such as `title`, `description`, `type`, and `position` + +The easiest supported path is to call `forge_server_task_fnc_startTask` from +server-side mission code: + +```sqf +[ + "attack", + "custom_attack_01", + getMarkerPos "custom_attack_area", + "Raid the Checkpoint", + "Clear the checkpoint and secure the site.", + createHashMapFromArray [ + ["targets", [target_1, target_2, target_3]] + ], + createHashMapFromArray [ + ["limitSuccess", 3], + ["limitFail", 0], + ["funds", 25000], + ["ratingSuccess", 10], + ["ratingFail", -5], + ["timeLimit", 1200] + ], + 0, + "", + "custom_generator" +] call forge_server_task_fnc_startTask; +``` + +`startTask` registers entities, creates the BIS task, upserts the Forge task +catalog entry, sets the initial task status, and dispatches the matching Forge +task flow. + +## Custom Runtime Tasks + +If a community generator has its own objective logic and does not use a built-in +Forge task flow, register the catalog entry and status directly: + +```sqf +private _taskID = "pvp_supply_drop_01"; +private _entry = createHashMapFromArray [ + ["taskID", _taskID], + ["taskId", _taskID], + ["type", "pvp_supply_drop"], + ["taskType", "custom"], + ["title", "Contest the Supply Drop"], + ["description", "Secure the marked drop zone before the opposing team."], + ["position", getMarkerPos "supply_drop_zone"], + ["accepted", false], + ["requesterUid", ""], + ["orgID", "default"], + ["source", "custom_generator"] +]; + +"forge_server" callExtension ["task:catalog:upsert", [ + _taskID, + toJSON _entry +]]; + +"forge_server" callExtension ["task:status:set", [ + _taskID, + "available" +]]; +``` + +Create a BIS task separately if players should see it in the vanilla map task +tab: + +```sqf +[ + west, + _taskID, + ["Secure the supply drop.", "Supply Drop", "custom"], + getMarkerPos "supply_drop_zone", + "CREATED", + 1, + true, + "container" +] call BIS_fnc_taskCreate; +``` + +When custom objective logic completes, set the task status: + +```sqf +"forge_server" callExtension ["task:status:set", [_taskID, "succeeded"]]; +// or +"forge_server" callExtension ["task:status:set", [_taskID, "failed"]]; +``` + +Use `task:clear` or `task:catalog:delete` when the custom runtime fully owns +cleanup and the contract should leave CAD. + +## CAD Assignment Lifecycle + +CAD assignment and task execution are intentionally separate. + +| Phase | Task status | Owner | +| --- | --- | --- | +| Created and visible | `available` | No group reservation yet. | +| Dispatcher assigns | `assigned` | CAD reserves the task for a group. | +| Group leader acknowledges | `active` | Task ownership is accepted for the acknowledging player/org. | +| Runtime finishes | `succeeded` or `failed` | CAD refreshes and removes completed active contracts. | + +Custom task logic should account for this lifecycle. If the task should not +start until the assigned group leader accepts it, wait for `active` status: + +```sqf +waitUntil { + sleep 2; + private _statusResult = "forge_server" callExtension ["task:status:get", [_taskID]]; + private _status = fromJSON (_statusResult select 0); + _status isEqualTo "active" +}; +``` + +If a group declines the assignment, CAD returns the task to `available`. + +## EventBus Integration + +The server EventBus is an in-process SQF event system. Initialize it if needed: + +```sqf +if (isNil "forge_server_common_EventBus") then { + call forge_server_common_fnc_eventBus; +}; +``` + +Subscribe to CAD and task lifecycle events: + +```sqf +private _token = forge_server_common_EventBus call ["on", [ + "cad.assignment.acknowledged", + { + params ["_event"]; + private _taskID = _event getOrDefault ["taskID", ""]; + private _assignment = _event getOrDefault ["assignment", createHashMap]; + diag_log format [ + "[CustomGenerator] Task %1 acknowledged by group %2", + _taskID, + _assignment getOrDefault ["groupId", ""] + ]; + }, + "custom_generator.assignment" +]]; +``` + +Remove a listener when it is no longer needed: + +```sqf +forge_server_common_EventBus call ["off", [_token]]; +``` + +Useful CAD events: + +| Event | When it fires | +| --- | --- | +| `cad.assignment.assigned` | Dispatcher assigns a task or order. | +| `cad.assignment.acknowledged` | Group leader accepts an assignment. | +| `cad.assignment.declined` | Group leader declines an assignment. | +| `cad.assignment.closed` | Dispatch order is closed. | +| `cad.request.submitted` | Support request is submitted. | +| `cad.request.closed` | Support request is closed. | +| `cad.group.updated` | Group status or role changes. | + +Useful task events: + +| Event | When it fires | +| --- | --- | +| `task.created` | Task catalog entry is registered through TaskStore. | +| `task.started` | Task status transitions to active/started. | +| `task.completed` | Task succeeds. | +| `task.failed` | Task fails. | +| `task.cleared` | Task state is cleared. | +| `task.reward.applied` | Task reward mutation succeeds. | +| `task.rating.applied` | Rating/earnings outcome succeeds. | +| `task.notification.requested` | Task participant notification is requested. | + +CAD already listens to task and CAD events and globally invalidates CAD state +when relevant changes occur. Custom generators usually only need to emit task +status changes through TaskStore or extension commands; CAD refresh follows +from the existing listeners. + +## Generated Task Dropdown Limitation + +The current CAD generated-task dropdown is owned by the framework task mission +manager. CAD hydrates `generatedTaskTypes` from the built-in manager when +`forge_server_task_enableGenerator` is enabled. When that setting is disabled, +the generated-task request control is disabled. + +The current CAD request handler calls `forge_server_task_fnc_requestMissionTask` +directly. It no longer falls back to mission-local generator request functions, +so third-party generated-task providers should create CAD-visible tasks directly +until a framework provider extension point is added. + +Until a provider extension point is added, use one of these supported patterns: + +1. Run custom generators from mission/server code and create CAD-visible tasks + directly. +2. Use CAD support requests or dispatch orders to let players request custom + work, then have mission code convert approved requests into tasks. +3. Keep the built-in generator enabled only if the community intentionally + wants the framework dropdown and request handler. + +## Planned Provider Extension Point + +A future code change should make CAD generator providers explicit. The desired +shape is: + +- built-in Forge provider remains the default out-of-box behavior +- mission/community providers can supply their own `generatedTaskTypes` +- mission/community providers can handle generated-task requests +- disabling the built-in provider does not disable custom providers +- mission designers or developers can select or toggle the active generator + provider when a mission includes custom generators +- a framework-hosted mission setup UI can display the active provider and, when + supported by the mission, allow choosing between built-in and custom + providers + +Candidate SQF hooks: + +```sqf +forge_custom_fnc_getGeneratedTaskTypes +forge_custom_fnc_requestMissionTask +``` + +or mission namespace variables: + +```sqf +missionNamespace setVariable ["forge_generatorProvider_getTypes", { + [ + createHashMapFromArray [["value", "supply_drop"], ["label", "Supply Drop"]], + createHashMapFromArray [["value", "pvp_hold"], ["label", "PvP Hold Area"]] + ] +}]; + +missionNamespace setVariable ["forge_generatorProvider_requestTask", { + params ["_taskType", "_metadata", "_requesterUid"]; + createHashMapFromArray [ + ["success", true], + ["message", "Generated custom task."], + ["taskID", "custom_task_01"], + ["taskType", _taskType] + ] +}]; +``` + +The exact API should be implemented in the framework code before communities +depend on it. + +Implementation note: the provider selection should be separate from +`forge_server_task_enableGenerator`. That CBA setting should continue to gate +the built-in Forge generator, while a new provider option can decide whether +CAD/manual requests use the built-in provider, a custom provider, both, or no +provider at all. + +## Validation Checklist + +For each custom generator: + +1. Disable the built-in generator if it should not run. +2. Generate or place task entities on the server. +3. Register a task catalog entry with stable `taskID` and display fields. +4. Set task status to `available`. +5. Confirm the task appears in CAD. +6. Assign it to a group from CAD. +7. Acknowledge and decline from the group leader UI. +8. Confirm custom logic waits for `active` if needed. +9. Set `succeeded` or `failed` when the objective resolves. +10. Confirm CAD refreshes and rewards or cleanup behave as expected. diff --git a/docus/content/3.server-modules/11.task.md b/docus/content/3.server-modules/11.task.md index 4842b23..16de70b 100644 --- a/docus/content/3.server-modules/11.task.md +++ b/docus/content/3.server-modules/11.task.md @@ -143,6 +143,11 @@ The dynamic mission manager can also generate attack, defend, defuse, delivery, destroy, hostage, HVT kill, and HVT capture tasks from config. That is system-generated content rather than a hand-authored task creation path. +Communities can disable the built-in generator and create CAD-visible tasks +from their own mission or server code. See +[Custom Mission Generators](/getting-started/custom-mission-generators) for the custom +generator integration contract. + ## Generated Mission Configuration Mission designers should define `class CfgMissions` in the mission folder, such @@ -180,6 +185,22 @@ for generated missions. When disabled, timer-based generation does not run, CAD hydrates no generated task types, and manual dispatcher requests are rejected server-side. +The mission setup UI does not enable or disable generated missions. It applies +runtime tuning such as faction, caps, intervals, reward ranges, rating ranges, +penalties, and time limits. Generator enablement remains controlled by the CBA +setting above. + +When `forge_server_task_enableMissionSetup` is enabled, the mission manager +waits for setup settings before starting. There is no timeout auto-apply. +Pressing Cancel, X, or Escape applies default values from CBA, mission +parameters, and `CfgMissions`. + +Planned custom-generator work should add an explicit provider option for +mission designers or developers who want to select or toggle a custom mission +generator. That provider option should be separate from the built-in generator +CBA gate so disabling Forge's built-in generator does not prevent custom +providers from publishing CAD-visible work. + ## CAD Compatibility CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must diff --git a/docus/content/3.server-modules/3.cad.md b/docus/content/3.server-modules/3.cad.md index 3580a10..e5b9c28 100644 --- a/docus/content/3.server-modules/3.cad.md +++ b/docus/content/3.server-modules/3.cad.md @@ -83,9 +83,13 @@ Generated mission requests are controlled by the server CBA setting is disabled, and server-side request handling rejects any manual request. The framework-owned request entry point is -`forge_server_task_fnc_requestMissionTask`. Server CAD calls that first and only -falls back to a mission-local `forge_pmc_fnc_requestMissionTask` when the -framework entry point is unavailable. +`forge_server_task_fnc_requestMissionTask`. Server CAD uses this framework +handler directly; it does not call mission-local generator functions. + +Custom mission generators can still create CAD-visible tasks directly by +registering task catalog entries and task statuses. See +[Custom Mission Generators](/getting-started/custom-mission-generators) for the supported +integration path and the current generated-task provider limitation. ## Submit a Support Request diff --git a/docus/content/4.client-addons/5.cad.md b/docus/content/4.client-addons/5.cad.md index b1f9dcb..3abdb14 100644 --- a/docus/content/4.client-addons/5.cad.md +++ b/docus/content/4.client-addons/5.cad.md @@ -106,6 +106,11 @@ older payload compatibility, but any hydrate payload that includes request control, which is how `forge_server_task_enableGenerator = false` is surfaced client-side. +Custom mission generators can still publish tasks into CAD by using the server +task catalog. The generated-task dropdown itself currently needs a framework +provider extension point before custom providers can replace the built-in list +cleanly. See [Custom Mission Generators](/getting-started/custom-mission-generators). + ## Authorization Notes Only dispatcher sessions can enter dispatch mode. If the hydrated session is @@ -115,4 +120,5 @@ not a dispatcher, the bridge forces the UI back to operations mode. - [CAD Usage Guide](/server-modules/cad) - [Task Usage Guide](/server-modules/task) +- [Custom Mission Generators](/getting-started/custom-mission-generators) - [Client Common Usage Guide](/client-addons/common) diff --git a/docus/content/index.md b/docus/content/index.md index dee8f26..99dd92d 100644 --- a/docus/content/index.md +++ b/docus/content/index.md @@ -10,8 +10,8 @@ Forge Framework Documentation #description Forge is a persistent Arma 3 framework that combines SQF addons, a Rust -`arma-rs` extension, SurrealDB persistence, shared domain crates, and -browser-backed player interfaces. +`arma-rs` extension, SurrealDB persistence, shared domain crates, a shared +mission config addon, and browser-backed player interfaces. Use these docs to understand the runtime architecture, extension API surface, server gameplay modules, and client addon integration patterns. @@ -20,6 +20,10 @@ Server owners and developers must start SurrealDB and place a matching `config.toml` beside `forge_server_x64.dll` before launching a Forge-enabled server or local multiplayer test. +Forge missions require `@forge_mod` for shared mission-facing config classes. +Servers also load `@forge_server` as a server-only runtime mod, and players +load `@forge_client` for client UI. + #links :::u-button --- diff --git a/tools/sync-docus-docs.mjs b/tools/sync-docus-docs.mjs index 7abd9c6..ad21db2 100644 --- a/tools/sync-docus-docs.mjs +++ b/tools/sync-docus-docs.mjs @@ -31,6 +31,10 @@ const generatedPages = [ source: 'docs/surrealdb-setup.md', target: '1.getting-started/6.surrealdb-setup.md' }, + { + source: 'docs/CUSTOM_MISSION_GENERATORS.md', + target: '1.getting-started/7.custom-mission-generators.md' + }, { source: 'arma/server/docs/README.md', target: '2.server-extension/0.index.md' @@ -450,6 +454,16 @@ npm run build:webui playable missions. ::: + :::u-page-card + --- + icon: i-lucide-waypoints + title: Custom Mission Generators + to: /getting-started/custom-mission-generators + --- + Create CAD-visible custom generated missions and understand the current + provider extension point. + ::: + :::u-page-card --- icon: i-lucide-user-round-check