Integrate framework mission setup and generators

This commit is contained in:
Jacob Schmidt 2026-05-31 17:09:56 -05:00
parent 623f690a82
commit d4d1f251c4
58 changed files with 2249 additions and 266 deletions

View File

@ -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 {

View File

@ -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];

View File

@ -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",

View File

@ -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)

View File

@ -0,0 +1 @@
forge\forge_client\addons\mission_setup

View 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));
};
};

View File

@ -0,0 +1,3 @@
PREP(handleUIEvents);
PREP(initRepository);
PREP(openUI);

View 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);

View File

@ -0,0 +1,5 @@
#include "script_component.hpp"
PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;

View 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"

View File

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

View File

@ -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)

View 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

View 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"

View 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};
};
};
};

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

View 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);
}

View 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
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 });
})();

View File

@ -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);
};

View File

@ -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)

View File

@ -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 {

View File

@ -6,6 +6,7 @@ PREP(destroy);
PREP(handler);
PREP(hostage);
PREP(hvt);
PREP(initMissionSetupService);
PREP(makeCargo);
PREP(makeHostage);
PREP(makeHVT);

View File

@ -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", ""],

View File

@ -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", []]; };

View File

@ -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)

View File

@ -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);
};
};

View File

@ -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 {

View File

@ -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);

View File

@ -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 {

View File

@ -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 {

View File

@ -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 };

View File

@ -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 {

View File

@ -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 {

View File

@ -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);

View File

@ -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]]
];

View File

@ -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];

View File

@ -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",

View File

@ -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)],

View File

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

View File

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

View File

@ -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)

View 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.

View File

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

View File

@ -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),

View File

@ -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)

View File

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

View File

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

View File

@ -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),

View File

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

View File

@ -7,6 +7,17 @@ This guide focuses on editor placement and mission validation. Framework
internals, extension commands, and persistence details are covered in the
developer-oriented module guides.
## Required Forge Addons
Forge missions that place Forge task modules or shared Forge vehicle classes
must depend on `@forge_mod`. This addon is loaded by both clients and servers
and provides the mission-facing config classes, including `forge_mod_task`.
Do not make missions depend on `@forge_server` for Eden module classes.
`@forge_server` remains server-only and owns the runtime task handlers. A
mission that lists `forge_server_task` in `requiredAddons` will force clients to
install the server-only mod.
## Core Rule
Most Forge systems become available to players through nearby Eden objects.
@ -150,15 +161,12 @@ Minimum Eden setup:
Transport nodes are generic paid travel points. They can represent ferries,
airports, bus stops, teleport terminals, or any other mission transport system.
The framework owns the menu, billing, cargo scan, and movement logic.
The framework owns the menu, billing, cargo scan, and movement logic. The
mission only needs placed objects and optional arrival markers.
![Eden transport location one](images/eden/transport_loc_1.jpg)
![Placeholder: Eden transport node object placement](images/eden/transport_node_obj.svg)
![Eden transport location two](images/eden/transport_loc_2.jpg)
![Eden transport node object placement](images/eden/transport_obj_1.jpg)
![Eden transport node variable name](images/eden/transport_obj_1_var.jpg)
![Placeholder: Eden transport node variable name](images/eden/transport_node_var.svg)
Place transport node objects with these variable names:
@ -170,7 +178,7 @@ transport_2
transport_10
```
Place arrival markers with matching suffixes:
Place optional arrival markers with matching suffixes:
```text
transport_arrival
@ -180,9 +188,7 @@ transport_arrival_2
transport_arrival_10
```
![Eden transport arrival marker placement](images/eden/transport_arrival_mrkr.jpg)
![Eden transport arrival marker variable name](images/eden/transport_arrival_mrkr_var.jpg)
![Placeholder: Eden transport arrival marker placement](images/eden/transport_arrival_marker.svg)
Objects that should be excluded from the nearby cargo scan, such as the actual
boat or transport vehicle used as set dressing, should use:
@ -195,9 +201,7 @@ transport_vehicle_2
transport_vehicle_10
```
![Eden transport vehicle exclusion object placement](images/eden/transport_veh_obj.jpg)
![Eden transport vehicle exclusion object variable name](images/eden/transport_veh_obj_var.jpg)
![Placeholder: Eden transport vehicle exclusion object variable name](images/eden/transport_vehicle_var.svg)
Minimum Eden setup:
@ -306,6 +310,11 @@ interaction that calls:
Tasks show in CAD only when they are created through a CAD-compatible task
creation path.
Mission or community-owned generators can also create CAD-visible tasks by
using the task catalog/status contract. See
[Custom Mission Generators](/getting-started/custom-mission-generators) before replacing
the built-in generated mission flow.
## CEO and Dispatch Slots
Forge grants dispatch-board permissions from the player's Eden unit variable
@ -743,6 +752,23 @@ The generated mission system supports `attack`, `defend`, `defuse`,
`forge_server_task_enableGenerator` CBA setting gates both timer-based generation and
CAD dispatcher-requested generation.
The optional framework mission setup UI lets the setup operator choose runtime
tuning such as opposing faction, mission cap, interval, location cooldown,
reward ranges, reputation ranges, penalty ranges, and time limits. It does not
enable or disable generated missions; use the CBA setting for that policy.
If mission setup is enabled, the mission manager waits until the setup operator
applies settings. Cancel, X, and Escape apply default values from CBA, mission
parameters, and `CfgMissions`. There is no timeout that auto-applies defaults.
After settings are applied, the setup UI cannot be reopened.
Future custom-generator support should add an explicit provider option so
mission designers or developers can select or toggle a mission/community-owned
generator without relying on mission-local fallback functions. Until then,
custom generators should create CAD-visible tasks directly through the task
catalog/status contract described in
[Custom Mission Generators](/getting-started/custom-mission-generators).
The dynamic mission generator avoids rectangle and ellipse area markers whose
marker name or marker text starts with `blklist`.

View 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.

View File

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

View File

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

View File

@ -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)

View File

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

View File

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