Integrate framework mission setup and generators
This commit is contained in:
parent
623f690a82
commit
d4d1f251c4
@ -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 {
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
1
arma/client/addons/mission_setup/$PBOPREFIX$
Normal file
1
arma/client/addons/mission_setup/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\mission_setup
|
||||
11
arma/client/addons/mission_setup/CfgEventHandlers.hpp
Normal file
11
arma/client/addons/mission_setup/CfgEventHandlers.hpp
Normal file
@ -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));
|
||||
};
|
||||
};
|
||||
3
arma/client/addons/mission_setup/XEH_PREP.hpp
Normal file
3
arma/client/addons/mission_setup/XEH_PREP.hpp
Normal file
@ -0,0 +1,3 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initRepository);
|
||||
PREP(openUI);
|
||||
19
arma/client/addons/mission_setup/XEH_postInitClient.sqf
Normal file
19
arma/client/addons/mission_setup/XEH_postInitClient.sqf
Normal file
@ -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);
|
||||
5
arma/client/addons/mission_setup/XEH_preInit.sqf
Normal file
5
arma/client/addons/mission_setup/XEH_preInit.sqf
Normal file
@ -0,0 +1,5 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
PREP_RECOMPILE_START;
|
||||
#include "XEH_PREP.hpp"
|
||||
PREP_RECOMPILE_END;
|
||||
21
arma/client/addons/mission_setup/config.cpp
Normal file
21
arma/client/addons/mission_setup/config.cpp
Normal file
@ -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"
|
||||
@ -0,0 +1,76 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Handles JSON events from the framework mission setup browser UI.
|
||||
*
|
||||
* Arguments:
|
||||
* 0: Browser control <CONTROL>
|
||||
* 1: Whether the event came from a confirm dialog <BOOL>
|
||||
* 2: JSON event payload <STRING>
|
||||
*
|
||||
* Return Value:
|
||||
* Event handled <BOOL>
|
||||
*
|
||||
* 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
|
||||
@ -0,0 +1,206 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Initializes the client mission setup repository.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Mission setup repository object <HASHMAP 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)
|
||||
58
arma/client/addons/mission_setup/functions/fnc_openUI.sqf
Normal file
58
arma/client/addons/mission_setup/functions/fnc_openUI.sqf
Normal file
@ -0,0 +1,58 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Opens the framework mission setup UI.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* UI opened <BOOL>
|
||||
*
|
||||
* 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
|
||||
9
arma/client/addons/mission_setup/script_component.hpp
Normal file
9
arma/client/addons/mission_setup/script_component.hpp
Normal file
@ -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"
|
||||
23
arma/client/addons/mission_setup/ui/RscMissionSetup.hpp
Normal file
23
arma/client/addons/mission_setup/ui/RscMissionSetup.hpp
Normal file
@ -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};
|
||||
};
|
||||
};
|
||||
};
|
||||
1
arma/client/addons/mission_setup/ui/_site/index.html
Normal file
1
arma/client/addons/mission_setup/ui/_site/index.html
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>FORGE Mission Setup</title><script>window.ForgeSiteConfig={addonName:"mission_setup",logLabel:"Mission Setup UI",styles:["mission-setup.css"],commonScripts:[],scripts:["mission-setup.js"]},function(){const e="../../../common/ui/_site/forge-site-loader.js";("undefined"!=typeof A3API&&A3API&&"function"==typeof A3API.RequestFile?A3API.RequestFile("forge\\forge_client\\addons\\common\\ui\\_site\\forge-site-loader.js"):fetch(e).then(o=>{if(!o.ok)throw new Error("Failed to load "+e);return o.text()})).then(function(e){const o=document.createElement("script");o.text=e,document.head.appendChild(o)}).catch(e=>{console.error("[Mission Setup UI] Failed to load Forge site loader.",e)})}()</script></head><body><div id="app"></div></body></html>
|
||||
259
arma/client/addons/mission_setup/ui/_site/mission-setup.css
Normal file
259
arma/client/addons/mission_setup/ui/_site/mission-setup.css
Normal file
@ -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);
|
||||
}
|
||||
250
arma/client/addons/mission_setup/ui/_site/mission-setup.js
Normal file
250
arma/client/addons/mission_setup/ui/_site/mission-setup.js
Normal file
@ -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, """)
|
||||
.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 `<option value="${escapeHtml(faction.faction)}"${selected}>${escapeHtml(faction.display)}</option>`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="shell">
|
||||
<header class="titlebar">
|
||||
<div class="brand">
|
||||
<span class="kicker">FORGE</span>
|
||||
<span class="title">Mission Setup</span>
|
||||
</div>
|
||||
<button class="close" type="button" aria-label="Close" data-action="close">x</button>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<div class="grid">
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="kicker">Deployment Profile</span>
|
||||
<h1>Operation Settings</h1>
|
||||
</div>
|
||||
<div class="form">
|
||||
<div class="field">
|
||||
<label for="enemyFaction">Opposing Faction</label>
|
||||
<select id="enemyFaction">${state.factions.map(option).join("")}</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="locationReuseCooldown">Location Cooldown</label>
|
||||
<input id="locationReuseCooldown" type="number" min="0" step="60" value="${settings.locationReuseCooldown}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="maxConcurrentMissions">Concurrent Missions</label>
|
||||
<input id="maxConcurrentMissions" type="number" min="1" max="50" value="${settings.maxConcurrentMissions}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="missionInterval">Mission Interval</label>
|
||||
<input id="missionInterval" type="number" min="1" step="30" value="${settings.missionInterval}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="moneyMin">Min Funds</label>
|
||||
<input id="moneyMin" type="number" min="0" step="100" value="${settings.moneyMin}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="moneyMax">Max Funds</label>
|
||||
<input id="moneyMax" type="number" min="0" step="100" value="${settings.moneyMax}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="reputationMin">Min Rating</label>
|
||||
<input id="reputationMin" type="number" step="1" value="${settings.reputationMin}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="reputationMax">Max Rating</label>
|
||||
<input id="reputationMax" type="number" step="1" value="${settings.reputationMax}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="penaltyMin">Min Rep Hit</label>
|
||||
<input id="penaltyMin" type="number" max="0" step="1" value="${settings.penaltyMin}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="penaltyMax">Max Rep Hit</label>
|
||||
<input id="penaltyMax" type="number" max="0" step="1" value="${settings.penaltyMax}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="timeLimitMin">Min Time</label>
|
||||
<input id="timeLimitMin" type="number" min="1" step="60" value="${settings.timeLimitMin}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="timeLimitMax">Max Time</label>
|
||||
<input id="timeLimitMax" type="number" min="1" step="60" value="${settings.timeLimitMax}" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="kicker">Current Selection</span>
|
||||
<h2>Generator Runtime</h2>
|
||||
</div>
|
||||
<div class="summary">
|
||||
<div class="summary-row"><span>Faction</span><strong>${escapeHtml(factionLabel)}</strong></div>
|
||||
<div class="summary-row"><span>Mission Cap</span><strong>${settings.maxConcurrentMissions}</strong></div>
|
||||
<div class="summary-row"><span>Interval</span><strong>${settings.missionInterval}s</strong></div>
|
||||
<div class="summary-row"><span>Location Cooldown</span><strong>${settings.locationReuseCooldown}s</strong></div>
|
||||
<div class="summary-row"><span>Reward Range</span><strong>$${Number(settings.moneyMin).toLocaleString()} - $${Number(settings.moneyMax).toLocaleString()}</strong></div>
|
||||
<div class="summary-row"><span>Reputation</span><strong>${settings.reputationMin} - ${settings.reputationMax}</strong></div>
|
||||
<div class="summary-row"><span>Reputation Hit</span><strong>${settings.penaltyMin} to ${settings.penaltyMax}</strong></div>
|
||||
<div class="summary-row"><span>Time Limit</span><strong>${settings.timeLimitMin}s - ${settings.timeLimitMax}s</strong></div>
|
||||
${state.error ? `<div class="notice">${state.error}</div>` : ""}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="actions">
|
||||
<button class="btn secondary" type="button" data-action="close">Cancel</button>
|
||||
<button class="btn primary" type="button" data-action="apply">Apply Settings</button>
|
||||
</footer>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 });
|
||||
})();
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -6,6 +6,7 @@ PREP(destroy);
|
||||
PREP(handler);
|
||||
PREP(hostage);
|
||||
PREP(hvt);
|
||||
PREP(initMissionSetupService);
|
||||
PREP(makeCargo);
|
||||
PREP(makeHostage);
|
||||
PREP(makeHVT);
|
||||
|
||||
@ -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", ""],
|
||||
|
||||
@ -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", []]; };
|
||||
|
||||
@ -0,0 +1,176 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Initializes the framework mission setup service.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Mission setup service object <HASHMAP 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)
|
||||
@ -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);
|
||||
};
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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]]
|
||||
];
|
||||
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)],
|
||||
|
||||
@ -16,5 +16,11 @@
|
||||
<Key ID="STR_forge_server_task_enableGeneratorTooltip">
|
||||
<English>Enable Task Generator</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_server_task_enableMissionSetup">
|
||||
<English>Enable Mission Setup UI</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_server_task_enableMissionSetupTooltip">
|
||||
<English>Open the framework mission setup UI for the configured setup operator before generated missions start.</English>
|
||||
</Key>
|
||||
</Package>
|
||||
</Project>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
379
docs/CUSTOM_MISSION_GENERATORS.md
Normal file
379
docs/CUSTOM_MISSION_GENERATORS.md
Normal file
@ -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.
|
||||
@ -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`.
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
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
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
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
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
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`.
|
||||
|
||||
377
docus/content/1.getting-started/7.custom-mission-generators.md
Normal file
377
docus/content/1.getting-started/7.custom-mission-generators.md
Normal file
@ -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.
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
---
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user