Separate CAD task assignment from task acceptance
- Reserve tasks as `available`/`assigned` before leader acknowledgement - Update CAD and task lifecycle handling and docs to reflect the new flow - Remove startup heartbeat reset and reset task backend explicitly in preInit
This commit is contained in:
parent
e49b4e4dd9
commit
9b31184f0c
@ -48,6 +48,16 @@ The addon listens to and emits events through the event bus:
|
|||||||
|
|
||||||
Successful mutations may invalidate CAD state globally so clients refresh their views.
|
Successful mutations may invalidate CAD state globally so clients refresh their views.
|
||||||
|
|
||||||
|
## Contract Lifecycle
|
||||||
|
CAD assignment and task acceptance are intentionally separate. Dispatch
|
||||||
|
assignment reserves a contract for a group and marks the CAD assignment
|
||||||
|
`assigned`; it does not start task logic. The assigned group leader must
|
||||||
|
acknowledge the assignment before the task is accepted and ownership is bound.
|
||||||
|
If the leader declines, CAD removes the assignment and the contract returns to
|
||||||
|
the open board. Task status follows the same lifecycle: `available` on
|
||||||
|
creation, `assigned` after dispatch assignment, and `active` after
|
||||||
|
acknowledgement.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
CAD hydrate payloads include active task catalog entries from `TaskStore` and
|
CAD hydrate payloads include assignable task catalog entries from `TaskStore` and
|
||||||
organization context from `ActorStore`.
|
organization context from `ActorStore`.
|
||||||
|
|||||||
@ -50,9 +50,10 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
if ((_y getOrDefault ["state", ""]) isNotEqualTo "acknowledged") then { continue; };
|
if ((_y getOrDefault ["state", ""]) isNotEqualTo "acknowledged") then { continue; };
|
||||||
if ((_y getOrDefault ["acknowledgedByUid", ""]) isEqualTo "") then { continue; };
|
if ((_y getOrDefault ["acknowledgedByUid", ""]) isEqualTo "") then { continue; };
|
||||||
if ((_dispatchOrderRegistry getOrDefault [_x, createHashMap]) isNotEqualTo createHashMap) then { continue; };
|
if ((_dispatchOrderRegistry getOrDefault [_x, createHashMap]) isNotEqualTo createHashMap) then { continue; };
|
||||||
if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; };
|
if !((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) in ["assigned", "active"]) then { continue; };
|
||||||
|
|
||||||
EGVAR(task,TaskStore) call ["bindTaskOwnership", [_x, _y getOrDefault ["acknowledgedByUid", ""]]];
|
EGVAR(task,TaskStore) call ["acceptTask", [_x, _y getOrDefault ["acknowledgedByUid", ""]]];
|
||||||
|
EGVAR(task,TaskStore) call ["setTaskStatus", [_x, "active"]];
|
||||||
} forEach _assignmentRegistry;
|
} forEach _assignmentRegistry;
|
||||||
|
|
||||||
_self set ["ownershipHydrated", true];
|
_self set ["ownershipHydrated", true];
|
||||||
@ -77,7 +78,7 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _status = EGVAR(task,TaskStore) call ["getTaskStatus", [_x]];
|
private _status = EGVAR(task,TaskStore) call ["getTaskStatus", [_x]];
|
||||||
if !(_status in ["active", ""]) then {
|
if !(_status in ["available", "assigned", "active", ""]) then {
|
||||||
_keysToRemove pushBack _x;
|
_keysToRemove pushBack _x;
|
||||||
};
|
};
|
||||||
} forEach _assignmentRegistry;
|
} forEach _assignmentRegistry;
|
||||||
@ -145,7 +146,7 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
|
|
||||||
private _dispatchOrder = +(_dispatchOrderRegistry getOrDefault [_x, createHashMap]);
|
private _dispatchOrder = +(_dispatchOrderRegistry getOrDefault [_x, createHashMap]);
|
||||||
if (_dispatchOrder isEqualTo createHashMap) then {
|
if (_dispatchOrder isEqualTo createHashMap) then {
|
||||||
if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; };
|
if !((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) in ["assigned", "active"]) then { continue; };
|
||||||
_taskID = _x;
|
_taskID = _x;
|
||||||
} else {
|
} else {
|
||||||
_taskID = _dispatchOrder getOrDefault ["title", _x];
|
_taskID = _dispatchOrder getOrDefault ["title", _x];
|
||||||
@ -235,8 +236,8 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
private _dispatchOrderRegistry = _state getOrDefault ["dispatchOrders", createHashMap];
|
private _dispatchOrderRegistry = _state getOrDefault ["dispatchOrders", createHashMap];
|
||||||
private _isDispatchOrder = (_dispatchOrderRegistry getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap;
|
private _isDispatchOrder = (_dispatchOrderRegistry getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap;
|
||||||
|
|
||||||
if (!_isDispatchOrder && { (EGVAR(task,TaskStore) call ["getTaskStatus", [_taskID]]) isNotEqualTo "active" }) exitWith {
|
if (!_isDispatchOrder && { (EGVAR(task,TaskStore) call ["getTaskStatus", [_taskID]]) isNotEqualTo "available" }) exitWith {
|
||||||
_result set ["message", "Task is no longer active."];
|
_result set ["message", "Task is no longer available."];
|
||||||
_result
|
_result
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -300,23 +301,15 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
_activityRepository call ["appendEntry", [_activityEntry]];
|
_activityRepository call ["appendEntry", [_activityEntry]];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!_isDispatchOrder) then {
|
||||||
|
EGVAR(task,TaskStore) call ["setTaskStatus", [_taskID, "assigned"]];
|
||||||
|
};
|
||||||
|
|
||||||
_result set ["success", true];
|
_result set ["success", true];
|
||||||
_result set ["message", _assignData getOrDefault ["message", ["Task assigned.", "Dispatch order assigned."] select _isDispatchOrder]];
|
_result set ["message", _assignData getOrDefault ["message", ["Task assigned.", "Dispatch order assigned."] select _isDispatchOrder]];
|
||||||
_result set ["assignment", _assignment];
|
_result set ["assignment", _assignment];
|
||||||
_result set ["leaderUid", _leaderUid];
|
_result set ["leaderUid", _leaderUid];
|
||||||
_result set ["isDispatchOrder", _isDispatchOrder];
|
_result set ["isDispatchOrder", _isDispatchOrder];
|
||||||
if (!_isDispatchOrder && { !(isNil QEGVAR(task,TaskStore)) }) then {
|
|
||||||
private _acceptResult = EGVAR(task,TaskStore) call ["acceptTask", [_taskID, _leaderUid]];
|
|
||||||
if !(_acceptResult getOrDefault ["success", false]) then {
|
|
||||||
["WARNING", format [
|
|
||||||
"CAD assigned task %1 to group %2 but could not mark it accepted for leader %3: %4",
|
|
||||||
_taskID,
|
|
||||||
_groupID,
|
|
||||||
_leaderUid,
|
|
||||||
_acceptResult getOrDefault ["message", "Unknown error."]
|
|
||||||
]] call EFUNC(common,log);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (_isDispatchOrder) then {
|
if (_isDispatchOrder) then {
|
||||||
_result set ["order", +(_dispatchOrderRegistry getOrDefault [_taskID, createHashMap])];
|
_result set ["order", +(_dispatchOrderRegistry getOrDefault [_taskID, createHashMap])];
|
||||||
};
|
};
|
||||||
@ -524,16 +517,18 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
switch (_transition) do {
|
switch (_transition) do {
|
||||||
case "acknowledge": {
|
case "acknowledge": {
|
||||||
if (!_isDispatchOrder) then {
|
if (!_isDispatchOrder) then {
|
||||||
private _bindResult = EGVAR(task,TaskStore) call ["bindTaskOwnership", [_taskID, _requesterUid]];
|
private _acceptResult = EGVAR(task,TaskStore) call ["acceptTask", [_taskID, _requesterUid]];
|
||||||
if !(_bindResult getOrDefault ["success", false]) exitWith {
|
if !(_acceptResult getOrDefault ["success", false]) exitWith {
|
||||||
_result set ["message", _bindResult getOrDefault ["message", "Failed to bind task ownership."]];
|
_result set ["message", _acceptResult getOrDefault ["message", "Failed to accept task."]];
|
||||||
_result
|
_result
|
||||||
};
|
};
|
||||||
|
EGVAR(task,TaskStore) call ["setTaskStatus", [_taskID, "active"]];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
case "decline": {
|
case "decline": {
|
||||||
if (!_isDispatchOrder) then {
|
if (!_isDispatchOrder) then {
|
||||||
EGVAR(task,TaskStore) call ["releaseTaskOwnership", [_taskID]];
|
EGVAR(task,TaskStore) call ["releaseTaskOwnership", [_taskID]];
|
||||||
|
EGVAR(task,TaskStore) call ["setTaskStatus", [_taskID, "available"]];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -100,7 +100,11 @@ system-generated content rather than a hand-authored task creation path.
|
|||||||
|
|
||||||
### CAD Compatibility
|
### CAD Compatibility
|
||||||
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
|
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
|
||||||
have a catalog entry and active task status before CAD can show and assign it.
|
have a catalog entry and a task status of `available`, `assigned`, or `active`
|
||||||
|
before CAD can show it.
|
||||||
|
CAD assignment reserves a task for a group, but task logic waits until the
|
||||||
|
assigned group leader acknowledges the assignment. Declined assignments return
|
||||||
|
to the open CAD board.
|
||||||
|
|
||||||
CAD-compatible creation paths:
|
CAD-compatible creation paths:
|
||||||
- Eden modules: compatible because they delegate to `fnc_startTask.sqf`
|
- Eden modules: compatible because they delegate to `fnc_startTask.sqf`
|
||||||
@ -111,11 +115,11 @@ CAD-compatible creation paths:
|
|||||||
|
|
||||||
Limited or incompatible paths:
|
Limited or incompatible paths:
|
||||||
- `fnc_handler.sqf`: only compatible if a catalog entry was already registered
|
- `fnc_handler.sqf`: only compatible if a catalog entry was already registered
|
||||||
elsewhere. The handler sets active status and ownership, but it does not
|
elsewhere. The handler sets available status and ownership, but it does not
|
||||||
create the BIS task shown in the map task tab or upsert the catalog entry
|
create the BIS task shown in the map task tab or upsert the catalog entry
|
||||||
- direct task function calls: not CAD-compatible by default. They bypass
|
- direct task function calls: not CAD-compatible by default. They bypass
|
||||||
`fnc_startTask.sqf` and usually do not register the task catalog entry or
|
`fnc_startTask.sqf` and usually do not register the task catalog entry or
|
||||||
active status that CAD hydrates from. They also only call
|
available/assigned/active status that CAD hydrates from. They also only call
|
||||||
`BIS_fnc_taskSetState` at completion/failure; they do not create the BIS task
|
`BIS_fnc_taskSetState` at completion/failure; they do not create the BIS task
|
||||||
first
|
first
|
||||||
|
|
||||||
|
|||||||
@ -1,77 +0,0 @@
|
|||||||
#include "..\script_component.hpp"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Author: IDSolutions
|
|
||||||
* Registers Entity and starts heartbeat
|
|
||||||
*
|
|
||||||
* Arguments:
|
|
||||||
* 0: The entity <OBJECT>
|
|
||||||
* 1: Type of the entity <STRING>
|
|
||||||
* 2: The countdown timer <NUMBER>
|
|
||||||
*
|
|
||||||
* Return Value:
|
|
||||||
* None
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* [_entity, "entity_type", 30] spawn FUNC(heartBeat);
|
|
||||||
*
|
|
||||||
* Public: Yes
|
|
||||||
*/
|
|
||||||
|
|
||||||
params [["_entity", nil, [objNull, 0, [], sideUnknown, grpNull, ""]], ["_typeOf", "", [""]], ["_time", 0, [0]]];
|
|
||||||
|
|
||||||
private _nearPlayers = [];
|
|
||||||
|
|
||||||
switch (_typeOf) do {
|
|
||||||
case "hostage": {
|
|
||||||
_entity setCaptive true;
|
|
||||||
_entity enableAIFeature ["MOVE", false];
|
|
||||||
_entity playMove "acts_executionvictim_loop";
|
|
||||||
|
|
||||||
waitUntil {
|
|
||||||
sleep 1;
|
|
||||||
_nearPlayers = allPlayers inAreaArray [ASLToAGL getPosASL _entity, 2, 2, 0, false, 2];
|
|
||||||
count _nearPlayers > 0
|
|
||||||
};
|
|
||||||
|
|
||||||
private _nearPlayer = _nearPlayers select 0;
|
|
||||||
|
|
||||||
[_entity] joinSilent (group _nearPlayer);
|
|
||||||
|
|
||||||
// Keep rescued hostages protected while they follow the player group.
|
|
||||||
_entity setCaptive true;
|
|
||||||
_entity enableAIFeature ["MOVE", true];
|
|
||||||
_entity playMove "acts_executionvictim_unbow";
|
|
||||||
};
|
|
||||||
case "hvt": {
|
|
||||||
waitUntil {
|
|
||||||
sleep 1;
|
|
||||||
_nearPlayers = allPlayers inAreaArray [ASLToAGL getPosASL _entity, 2, 2, 0, false, 2];
|
|
||||||
count _nearPlayers > 0
|
|
||||||
};
|
|
||||||
|
|
||||||
_entity setCaptive true;
|
|
||||||
doStop _entity;
|
|
||||||
};
|
|
||||||
case "ied": {
|
|
||||||
private _taskID = _entity getVariable ["assignedTask", ""];
|
|
||||||
if (_taskID isNotEqualTo "") then {
|
|
||||||
waitUntil {
|
|
||||||
sleep 1;
|
|
||||||
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
while { alive _entity && _time > 0} do {
|
|
||||||
if (_time > 10) then { _entity say3D "FORGE_timerBeep" };
|
|
||||||
if (_time <= 10 && _time > 5) then { _entity say3D "FORGE_timerBeepShort" };
|
|
||||||
if (_time <= 5) then { _entity say3D "FORGE_timerEnd" };
|
|
||||||
if (_time <= 0) exitWith { _entity setDamage 1 };
|
|
||||||
|
|
||||||
_time = _time -1;
|
|
||||||
sleep 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (alive _entity && _time <= 0) then { _entity setDamage 1 };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -71,7 +71,7 @@ if (_taskID isNotEqualTo "") then {
|
|||||||
]] call EFUNC(common,log);
|
]] call EFUNC(common,log);
|
||||||
};
|
};
|
||||||
|
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "active"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "available"]];
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (_taskType) do {
|
switch (_taskType) do {
|
||||||
|
|||||||
@ -5,9 +5,9 @@
|
|||||||
* Initializes the task store for task entity tracking, participant
|
* Initializes the task store for task entity tracking, participant
|
||||||
* contribution tracking, and task outcome application.
|
* contribution tracking, and task outcome application.
|
||||||
*
|
*
|
||||||
* Task metadata is extension-backed but intentionally transient. The
|
* Task metadata is extension-backed but intentionally transient. The task
|
||||||
* task backend is reset when this store is created so task/catalog/status
|
* backend is reset explicitly from task preInit so task/catalog/status state
|
||||||
* state starts clean for each server or mission lifecycle.
|
* starts clean before mission setup repopulates contracts.
|
||||||
*
|
*
|
||||||
* Arguments:
|
* Arguments:
|
||||||
* None
|
* None
|
||||||
@ -37,7 +37,6 @@ GVAR(TaskStore) = createHashMapObject [[
|
|||||||
["targets", createHashMap]
|
["targets", createHashMap]
|
||||||
]];
|
]];
|
||||||
|
|
||||||
_self call ["resetMissionState", []];
|
|
||||||
}],
|
}],
|
||||||
["resetMissionState", compileFinal {
|
["resetMissionState", compileFinal {
|
||||||
_self set ["participantRegistry", createHashMap];
|
_self set ["participantRegistry", createHashMap];
|
||||||
@ -62,6 +61,7 @@ GVAR(TaskStore) = createHashMapObject [[
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
["INFO", "Task backend state reset for mission lifecycle."] call EFUNC(common,log);
|
||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["callTaskStateEnvelope", compileFinal {
|
["callTaskStateEnvelope", compileFinal {
|
||||||
|
|||||||
@ -126,6 +126,14 @@ private _result = "forge_server" callExtension ["cad:orders:create_from_context"
|
|||||||
|
|
||||||
## Assignment Workflow
|
## Assignment Workflow
|
||||||
|
|
||||||
|
Task contracts have two separate phases. Dispatch assignment reserves a
|
||||||
|
contract for a group and sets the CAD assignment state to `assigned`, but it
|
||||||
|
does not accept or start the task. The assigned group leader must acknowledge
|
||||||
|
the assignment before task ownership is bound and task logic starts. If the
|
||||||
|
leader declines, the CAD assignment is removed and the contract returns to the
|
||||||
|
open board. Task status follows the same lifecycle: `available` on creation,
|
||||||
|
`assigned` after dispatch assignment, and `active` after acknowledgement.
|
||||||
|
|
||||||
```sqf
|
```sqf
|
||||||
private _assignment = createHashMapFromArray [
|
private _assignment = createHashMapFromArray [
|
||||||
["groupId", "bravo"],
|
["groupId", "bravo"],
|
||||||
|
|||||||
@ -145,7 +145,12 @@ system-generated content rather than a hand-authored task creation path.
|
|||||||
## CAD Compatibility
|
## CAD Compatibility
|
||||||
|
|
||||||
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
|
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
|
||||||
have a catalog entry and active task status before CAD can show and assign it.
|
have a catalog entry and a task status of `available`, `assigned`, or `active`
|
||||||
|
before CAD can show it.
|
||||||
|
CAD assignment only reserves a task for a group. The task is accepted and task
|
||||||
|
logic starts after the assigned group leader acknowledges the assignment. If
|
||||||
|
the leader declines, the CAD assignment is removed and the task returns to the
|
||||||
|
open contract board.
|
||||||
|
|
||||||
CAD-compatible creation paths:
|
CAD-compatible creation paths:
|
||||||
|
|
||||||
@ -159,7 +164,7 @@ CAD-compatible creation paths:
|
|||||||
Limited or incompatible paths:
|
Limited or incompatible paths:
|
||||||
|
|
||||||
- `forge_server_task_fnc_handler`: only compatible if a catalog entry was
|
- `forge_server_task_fnc_handler`: only compatible if a catalog entry was
|
||||||
already registered elsewhere. The handler sets active status and ownership,
|
already registered elsewhere. The handler sets available status and ownership,
|
||||||
but it does not create the BIS task shown in the map task tab or upsert the
|
but it does not create the BIS task shown in the map task tab or upsert the
|
||||||
catalog entry.
|
catalog entry.
|
||||||
- Direct task function calls: not CAD-compatible by default. They bypass
|
- Direct task function calls: not CAD-compatible by default. They bypass
|
||||||
@ -220,7 +225,8 @@ Use these rules for every Forge task:
|
|||||||
- `CBRNZone`
|
- `CBRNZone`
|
||||||
3. Prefer `RECTANGLE` or `ELLIPSE` markers with real size.
|
3. Prefer `RECTANGLE` or `ELLIPSE` markers with real size.
|
||||||
4. Set success and fail limits explicitly instead of relying on defaults.
|
4. Set success and fail limits explicitly instead of relying on defaults.
|
||||||
5. If a task uses a timer, the countdown now waits until the task is assigned.
|
5. If a task uses a timer, the countdown now waits until the assigned group
|
||||||
|
leader acknowledges the task.
|
||||||
6. Grouping modules such as `Explosive Entities`, `Protected Entities`,
|
6. Grouping modules such as `Explosive Entities`, `Protected Entities`,
|
||||||
`Cargo`, `Hostages`, and `Shooters` should be synced to real world objects,
|
`Cargo`, `Hostages`, and `Shooters` should be synced to real world objects,
|
||||||
not other logic modules.
|
not other logic modules.
|
||||||
@ -368,7 +374,8 @@ Notes:
|
|||||||
|
|
||||||
- Hostages and shooters are filtered to real units only.
|
- Hostages and shooters are filtered to real units only.
|
||||||
- Hostages are protected immediately on task registration to avoid startup race conditions.
|
- Hostages are protected immediately on task registration to avoid startup race conditions.
|
||||||
- The hostage timer now waits until the task is assigned before counting down.
|
- The hostage timer now waits until the assigned group leader acknowledges the
|
||||||
|
task before counting down.
|
||||||
- `ExtZone` is checked with `inArea`, so it must be an area marker.
|
- `ExtZone` is checked with `inArea`, so it must be an area marker.
|
||||||
|
|
||||||
### HVT Task
|
### HVT Task
|
||||||
@ -395,7 +402,8 @@ Notes:
|
|||||||
|
|
||||||
- Capture mode uses `ExtZone` with `inArea`, so use an area marker.
|
- Capture mode uses `ExtZone` with `inArea`, so use an area marker.
|
||||||
- Elimination mode does not require an extraction zone.
|
- Elimination mode does not require an extraction zone.
|
||||||
- The HVT timer now waits until the task is assigned before counting down.
|
- The HVT timer now waits until the assigned group leader acknowledges the task
|
||||||
|
before counting down.
|
||||||
|
|
||||||
### Defend Task
|
### Defend Task
|
||||||
|
|
||||||
|
|||||||
@ -144,7 +144,12 @@ system-generated content rather than a hand-authored task creation path.
|
|||||||
## CAD Compatibility
|
## CAD Compatibility
|
||||||
|
|
||||||
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
|
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
|
||||||
have a catalog entry and active task status before CAD can show and assign it.
|
have a catalog entry and a task status of `available`, `assigned`, or `active`
|
||||||
|
before CAD can show it.
|
||||||
|
CAD assignment only reserves a task for a group. The task is accepted and task
|
||||||
|
logic starts after the assigned group leader acknowledges the assignment. If
|
||||||
|
the leader declines, the CAD assignment is removed and the task returns to the
|
||||||
|
open contract board.
|
||||||
|
|
||||||
CAD-compatible creation paths:
|
CAD-compatible creation paths:
|
||||||
|
|
||||||
@ -158,7 +163,7 @@ CAD-compatible creation paths:
|
|||||||
Limited or incompatible paths:
|
Limited or incompatible paths:
|
||||||
|
|
||||||
- `forge_server_task_fnc_handler`: only compatible if a catalog entry was
|
- `forge_server_task_fnc_handler`: only compatible if a catalog entry was
|
||||||
already registered elsewhere. The handler sets active status and ownership,
|
already registered elsewhere. The handler sets available status and ownership,
|
||||||
but it does not create the BIS task shown in the map task tab or upsert the
|
but it does not create the BIS task shown in the map task tab or upsert the
|
||||||
catalog entry.
|
catalog entry.
|
||||||
- Direct task function calls: not CAD-compatible by default. They bypass
|
- Direct task function calls: not CAD-compatible by default. They bypass
|
||||||
@ -219,7 +224,8 @@ Use these rules for every Forge task:
|
|||||||
- `CBRNZone`
|
- `CBRNZone`
|
||||||
3. Prefer `RECTANGLE` or `ELLIPSE` markers with real size.
|
3. Prefer `RECTANGLE` or `ELLIPSE` markers with real size.
|
||||||
4. Set success and fail limits explicitly instead of relying on defaults.
|
4. Set success and fail limits explicitly instead of relying on defaults.
|
||||||
5. If a task uses a timer, the countdown now waits until the task is assigned.
|
5. If a task uses a timer, the countdown now waits until the assigned group
|
||||||
|
leader acknowledges the task.
|
||||||
6. Grouping modules such as `Explosive Entities`, `Protected Entities`,
|
6. Grouping modules such as `Explosive Entities`, `Protected Entities`,
|
||||||
`Cargo`, `Hostages`, and `Shooters` should be synced to real world objects,
|
`Cargo`, `Hostages`, and `Shooters` should be synced to real world objects,
|
||||||
not other logic modules.
|
not other logic modules.
|
||||||
@ -367,7 +373,8 @@ Notes:
|
|||||||
|
|
||||||
- Hostages and shooters are filtered to real units only.
|
- Hostages and shooters are filtered to real units only.
|
||||||
- Hostages are protected immediately on task registration to avoid startup race conditions.
|
- Hostages are protected immediately on task registration to avoid startup race conditions.
|
||||||
- The hostage timer now waits until the task is assigned before counting down.
|
- The hostage timer now waits until the assigned group leader acknowledges the
|
||||||
|
task before counting down.
|
||||||
- `ExtZone` is checked with `inArea`, so it must be an area marker.
|
- `ExtZone` is checked with `inArea`, so it must be an area marker.
|
||||||
|
|
||||||
### HVT Task
|
### HVT Task
|
||||||
@ -394,7 +401,8 @@ Notes:
|
|||||||
|
|
||||||
- Capture mode uses `ExtZone` with `inArea`, so use an area marker.
|
- Capture mode uses `ExtZone` with `inArea`, so use an area marker.
|
||||||
- Elimination mode does not require an extraction zone.
|
- Elimination mode does not require an extraction zone.
|
||||||
- The HVT timer now waits until the task is assigned before counting down.
|
- The HVT timer now waits until the assigned group leader acknowledges the task
|
||||||
|
before counting down.
|
||||||
|
|
||||||
### Defend Task
|
### Defend Task
|
||||||
|
|
||||||
|
|||||||
@ -124,6 +124,14 @@ private _result = "forge_server" callExtension ["cad:orders:create_from_context"
|
|||||||
|
|
||||||
## Assignment Workflow
|
## Assignment Workflow
|
||||||
|
|
||||||
|
Task contracts have two separate phases. Dispatch assignment reserves a
|
||||||
|
contract for a group and sets the CAD assignment state to `assigned`, but it
|
||||||
|
does not accept or start the task. The assigned group leader must acknowledge
|
||||||
|
the assignment before task ownership is bound and task logic starts. If the
|
||||||
|
leader declines, the CAD assignment is removed and the contract returns to the
|
||||||
|
open board. Task status follows the same lifecycle: `available` on creation,
|
||||||
|
`assigned` after dispatch assignment, and `active` after acknowledgement.
|
||||||
|
|
||||||
```sqf
|
```sqf
|
||||||
private _assignment = createHashMapFromArray [
|
private _assignment = createHashMapFromArray [
|
||||||
["groupId", "bravo"],
|
["groupId", "bravo"],
|
||||||
|
|||||||
@ -49,7 +49,7 @@ impl<R: TaskRepository> TaskStateService<R> {
|
|||||||
let mut active_entries = Vec::new();
|
let mut active_entries = Vec::new();
|
||||||
|
|
||||||
for (task_id, status) in active_statuses {
|
for (task_id, status) in active_statuses {
|
||||||
if status != "active" {
|
if !matches!(status.as_str(), "available" | "assigned" | "active") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,8 +129,11 @@ impl<R: TaskRepository> TaskStateService<R> {
|
|||||||
return Err("Missing task ID or requester UID.".to_string());
|
return Err("Missing task ID or requester UID.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.get_status(entry_id.clone())? != "active" {
|
if !matches!(
|
||||||
return Err("Task is no longer active.".to_string());
|
self.get_status(entry_id.clone())?.as_str(),
|
||||||
|
"assigned" | "active"
|
||||||
|
) {
|
||||||
|
return Err("Task is not assigned or active.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(existing) = self.repository.get_ownership(&entry_id)?
|
if let Some(existing) = self.repository.get_ownership(&entry_id)?
|
||||||
@ -381,9 +384,21 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_active_catalog_only_returns_active_entries() {
|
fn list_active_catalog_returns_assignable_and_active_entries() {
|
||||||
let service = TaskStateService::new(InMemoryTaskRepository::new());
|
let service = TaskStateService::new(InMemoryTaskRepository::new());
|
||||||
|
|
||||||
|
service
|
||||||
|
.upsert_catalog_entry(
|
||||||
|
"task-available".to_string(),
|
||||||
|
r#"{"title":"Available"}"#.to_string(),
|
||||||
|
)
|
||||||
|
.expect("available catalog upsert should succeed");
|
||||||
|
service
|
||||||
|
.upsert_catalog_entry(
|
||||||
|
"task-assigned".to_string(),
|
||||||
|
r#"{"title":"Assigned"}"#.to_string(),
|
||||||
|
)
|
||||||
|
.expect("assigned catalog upsert should succeed");
|
||||||
service
|
service
|
||||||
.upsert_catalog_entry(
|
.upsert_catalog_entry(
|
||||||
"task-active".to_string(),
|
"task-active".to_string(),
|
||||||
@ -393,6 +408,12 @@ mod tests {
|
|||||||
service
|
service
|
||||||
.upsert_catalog_entry("task-done".to_string(), r#"{"title":"Done"}"#.to_string())
|
.upsert_catalog_entry("task-done".to_string(), r#"{"title":"Done"}"#.to_string())
|
||||||
.expect("done catalog upsert should succeed");
|
.expect("done catalog upsert should succeed");
|
||||||
|
service
|
||||||
|
.set_status("task-available".to_string(), "available".to_string())
|
||||||
|
.expect("available status update should succeed");
|
||||||
|
service
|
||||||
|
.set_status("task-assigned".to_string(), "assigned".to_string())
|
||||||
|
.expect("assigned status update should succeed");
|
||||||
service
|
service
|
||||||
.set_status("task-active".to_string(), "active".to_string())
|
.set_status("task-active".to_string(), "active".to_string())
|
||||||
.expect("active status update should succeed");
|
.expect("active status update should succeed");
|
||||||
@ -404,10 +425,14 @@ mod tests {
|
|||||||
.list_active_catalog()
|
.list_active_catalog()
|
||||||
.expect("active catalog should build");
|
.expect("active catalog should build");
|
||||||
|
|
||||||
assert_eq!(active_catalog.len(), 1);
|
let task_ids: Vec<_> = active_catalog
|
||||||
assert_eq!(
|
.iter()
|
||||||
active_catalog[0].get("taskId").and_then(Value::as_str),
|
.filter_map(|entry| entry.get("taskId").and_then(Value::as_str))
|
||||||
Some("task-active")
|
.collect();
|
||||||
);
|
|
||||||
|
assert_eq!(active_catalog.len(), 3);
|
||||||
|
assert!(task_ids.contains(&"task-available"));
|
||||||
|
assert!(task_ids.contains(&"task-assigned"));
|
||||||
|
assert!(task_ids.contains(&"task-active"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user