forge/docs/CUSTOM_MISSION_GENERATORS.md
2026-05-31 17:14:47 -05:00

380 lines
12 KiB
Markdown

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