- add the imported server task addon to the current framework with task ownership, task catalog, mission-manager attack generation, org-owned reward routing, participant notifications, and reputation syncing - restructure org persistence so core org data, assets, fleet, and members are handled through the current Redis/extension model with matching Rust repository and service updates - wire the client CAD addon into the framework, actor device action, shared web UI bridge pattern, and task listing/acceptance flow - add a source-driven CAD web UI layout with ui.config.mjs and extend the shared web UI builder to support custom HTML template pages for multi-surface UIs
370 lines
14 KiB
Plaintext
370 lines
14 KiB
Plaintext
#include "..\script_component.hpp"
|
|
|
|
/*
|
|
* Author: IDSolutions
|
|
* Manages attack-only dynamic mission generation.
|
|
*
|
|
* Arguments:
|
|
* None
|
|
*
|
|
* Return Value:
|
|
* None
|
|
*
|
|
* Example:
|
|
* [] call forge_server_task_fnc_missionManager
|
|
*
|
|
* Public: No
|
|
*/
|
|
|
|
#pragma hemtt ignore_variables ["_self"]
|
|
GVAR(MissionManagerBaseClass) = compileFinal createHashMapFromArray [
|
|
["#type", "MissionManagerBaseClass"],
|
|
["#create", compileFinal {
|
|
private _missionConfig = missionConfigFile >> "CfgMissions";
|
|
_self set ["missionConfig", _missionConfig];
|
|
_self set ["locationsConfig", (_missionConfig >> "Locations")];
|
|
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
|
|
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
|
|
_self set ["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")];
|
|
_self set ["missionInterval", getNumber (_missionConfig >> "missionInterval")];
|
|
_self set ["recentLocationRegistry", createHashMap];
|
|
_self set ["activeMissionRegistry", createHashMap];
|
|
}],
|
|
["getMissionInterval", compileFinal {
|
|
private _interval = _self getOrDefault ["missionInterval", 300];
|
|
if (_interval <= 0) then { _interval = 300; };
|
|
_interval
|
|
}],
|
|
["getMaxConcurrentMissions", compileFinal {
|
|
private _maxConcurrent = _self getOrDefault ["maxConcurrentMissions", 1];
|
|
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
|
|
_maxConcurrent
|
|
}],
|
|
["getLocationReuseCooldown", compileFinal {
|
|
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
|
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
|
|
if (_cooldown <= 0) then { _cooldown = 900; };
|
|
_cooldown
|
|
}],
|
|
["getActiveMissionIds", compileFinal {
|
|
private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
|
|
keys _activeMissionRegistry
|
|
}],
|
|
["getActiveLocationKeys", compileFinal {
|
|
private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
|
|
private _locationKeys = [];
|
|
{
|
|
private _locationKey = _y getOrDefault ["locationKey", ""];
|
|
if (_locationKey isNotEqualTo "") then {
|
|
_locationKeys pushBackUnique _locationKey;
|
|
};
|
|
} forEach _activeMissionRegistry;
|
|
_locationKeys
|
|
}],
|
|
["buildAttackSpawnPosition", compileFinal {
|
|
params [["_locationConfig", configNull, [configNull]]];
|
|
|
|
if (isNull _locationConfig) exitWith { [0, 0, 0] };
|
|
|
|
private _center = getArray (_locationConfig >> "position");
|
|
private _radius = getNumber (_locationConfig >> "radius");
|
|
if (_radius <= 0) exitWith { _center };
|
|
|
|
private _spawnPosition = +_center;
|
|
private _attempts = 0;
|
|
while { _attempts < 8 } do {
|
|
private _angle = random 360;
|
|
private _distance = (_radius * 0.2) + random (_radius * 0.65);
|
|
private _candidate = [
|
|
(_center # 0) + ((sin _angle) * _distance),
|
|
(_center # 1) + ((cos _angle) * _distance),
|
|
_center param [2, 0]
|
|
];
|
|
|
|
if !(surfaceIsWater _candidate) exitWith {
|
|
_spawnPosition = _candidate;
|
|
};
|
|
|
|
_attempts = _attempts + 1;
|
|
};
|
|
|
|
_spawnPosition
|
|
}],
|
|
["selectAttackLocation", compileFinal {
|
|
private _locationsConfig = _self getOrDefault ["locationsConfig", configNull];
|
|
private _locations = [];
|
|
private _recentLocationRegistry = _self getOrDefault ["recentLocationRegistry", createHashMap];
|
|
private _activeLocationKeys = _self call ["getActiveLocationKeys", []];
|
|
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
|
|
private _now = serverTime;
|
|
|
|
{
|
|
private _locationKey = configName _x;
|
|
private _lastUsed = _recentLocationRegistry getOrDefault [_locationKey, -1];
|
|
private _isCoolingDown = (_lastUsed >= 0) && { (_now - _lastUsed) < _reuseCooldown };
|
|
|
|
if (
|
|
"attack" in getArray (_x >> "suitable")
|
|
&& { !(_locationKey in _activeLocationKeys) }
|
|
&& { !_isCoolingDown }
|
|
) then {
|
|
_locations pushBack _x;
|
|
};
|
|
} forEach ("true" configClasses _locationsConfig);
|
|
|
|
if (_locations isEqualTo []) then {
|
|
{
|
|
if ("attack" in getArray (_x >> "suitable") && { !(configName _x in _activeLocationKeys) }) then {
|
|
_locations pushBack _x;
|
|
};
|
|
} forEach ("true" configClasses _locationsConfig);
|
|
};
|
|
|
|
if (_locations isEqualTo []) exitWith { createHashMap };
|
|
|
|
private _location = selectRandom _locations;
|
|
createHashMapFromArray [
|
|
["config", _location],
|
|
["key", configName _location],
|
|
["position", _self call ["buildAttackSpawnPosition", [_location]]]
|
|
]
|
|
}],
|
|
["spawnAttackGroup", compileFinal {
|
|
params [["_position", [0, 0, 0], [[]]]];
|
|
|
|
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
|
|
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
|
|
private _groups = [];
|
|
{
|
|
if ("attack" in getArray (_x >> "suitable")) then {
|
|
_groups pushBack _x;
|
|
};
|
|
} forEach ("true" configClasses _aiGroupsConfig);
|
|
|
|
if (_groups isEqualTo []) exitWith { grpNull };
|
|
|
|
private _groupConfig = selectRandom _groups;
|
|
private _side = getText (_groupConfig >> "side");
|
|
private _group = createGroup (call compile _side);
|
|
private _minUnits = getNumber (_attackConfig >> "minUnits");
|
|
private _maxUnits = getNumber (_attackConfig >> "maxUnits");
|
|
if (_minUnits <= 0) then { _minUnits = 4; };
|
|
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
|
|
private _targetUnitCount = floor random [ _minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1 ];
|
|
|
|
private _unitPool = [];
|
|
{
|
|
if ((getText (_x >> "side")) isNotEqualTo _side) then { continue; };
|
|
|
|
{
|
|
_unitPool pushBack createHashMapFromArray [
|
|
["vehicle", getText (_x >> "vehicle")],
|
|
["rank", getText (_x >> "rank")],
|
|
["position", getArray (_x >> "position")]
|
|
];
|
|
} forEach ("true" configClasses (_x >> "Units"));
|
|
} forEach _groups;
|
|
|
|
if (_unitPool isEqualTo []) exitWith {
|
|
deleteGroup _group;
|
|
grpNull
|
|
};
|
|
|
|
private _leaderPool = _unitPool select {
|
|
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
|
|
};
|
|
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
|
|
|
|
private _spawnDefs = [selectRandom _leaderPool];
|
|
for "_i" from 1 to (_targetUnitCount - 1) do {
|
|
_spawnDefs pushBack (selectRandom _unitPool);
|
|
};
|
|
|
|
{
|
|
private _unitClass = _x getOrDefault ["vehicle", ""];
|
|
if (_unitClass isEqualTo "") then { continue; };
|
|
|
|
private _unitOffset = +(_x getOrDefault ["position", [0, 0, 0]]);
|
|
if (count _unitOffset < 3) then { _unitOffset resize 3; };
|
|
_unitOffset set [0, (_unitOffset # 0) + (random 6 - 3)];
|
|
_unitOffset set [1, (_unitOffset # 1) + (random 6 - 3)];
|
|
|
|
private _unit = _group createUnit [_unitClass, _position vectorAdd _unitOffset, [], 0, "NONE"];
|
|
_unit setRank (_x getOrDefault ["rank", "PRIVATE"]);
|
|
} forEach _spawnDefs;
|
|
|
|
_group
|
|
}],
|
|
["rollRewards", compileFinal {
|
|
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
|
|
private _equipmentRewards = [];
|
|
private _supplyRewards = [];
|
|
private _weaponRewards = [];
|
|
private _vehicleRewards = [];
|
|
private _specialRewards = [];
|
|
|
|
{
|
|
private _category = _x;
|
|
{
|
|
_x params ["_item", "_chance"];
|
|
if (random 1 < _chance) then {
|
|
switch (_category) do {
|
|
case "equipment": { _equipmentRewards pushBack _item; };
|
|
case "supplies": { _supplyRewards pushBack _item; };
|
|
case "weapons": { _weaponRewards pushBack _item; };
|
|
case "vehicles": { _vehicleRewards pushBack _item; };
|
|
case "special": { _specialRewards pushBack _item; };
|
|
};
|
|
};
|
|
} forEach (getArray (_attackConfig >> "Rewards" >> _category));
|
|
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
|
|
|
|
createHashMapFromArray [
|
|
["equipment", _equipmentRewards],
|
|
["supplies", _supplyRewards],
|
|
["weapons", _weaponRewards],
|
|
["vehicles", _vehicleRewards],
|
|
["special", _specialRewards]
|
|
]
|
|
}],
|
|
["createAttackTask", compileFinal {
|
|
params [
|
|
["_taskID", "", [""]],
|
|
["_position", [0, 0, 0], [[]]],
|
|
["_locationConfig", configNull, [configNull]]
|
|
];
|
|
|
|
if (_taskID isEqualTo "" || { isNull _locationConfig }) exitWith { false };
|
|
|
|
private _locationKey = configName _locationConfig;
|
|
private _locationType = getText (_locationConfig >> "type");
|
|
if (_locationType isEqualTo "") then { _locationType = "area"; };
|
|
|
|
[
|
|
west,
|
|
_taskID,
|
|
[
|
|
format ["Eliminate hostile forces operating near %1.", _locationKey],
|
|
format ["Attack: %1", _locationKey],
|
|
_locationType
|
|
],
|
|
_position,
|
|
"CREATED",
|
|
1,
|
|
true,
|
|
"attack"
|
|
] call BFUNC(taskCreate);
|
|
|
|
true
|
|
}],
|
|
["startAttackMission", compileFinal {
|
|
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
|
|
private _locationData = _self call ["selectAttackLocation"];
|
|
if (_locationData isEqualTo createHashMap) exitWith { "" };
|
|
|
|
private _location = _locationData getOrDefault ["config", configNull];
|
|
private _locationKey = _locationData getOrDefault ["key", ""];
|
|
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
|
|
private _group = _self call ["spawnAttackGroup", [_position]];
|
|
if (isNull _group) exitWith { "" };
|
|
|
|
private _units = units _group;
|
|
if (_units isEqualTo []) exitWith {
|
|
deleteGroup _group;
|
|
""
|
|
};
|
|
|
|
private _taskID = format ["task_attack_%1", round (diag_tickTime * 1000)];
|
|
{
|
|
[_x, _taskID] call FUNC(makeTarget);
|
|
} forEach _units;
|
|
|
|
_self call ["createAttackTask", [_taskID, _position, _location]];
|
|
GVAR(TaskStore) call ["registerTaskCatalogEntry", [_taskID, createHashMapFromArray [
|
|
["type", "attack"],
|
|
["title", format ["Attack: %1", _locationKey]],
|
|
["description", format ["Eliminate hostile forces operating near %1.", _locationKey]],
|
|
["position", _position],
|
|
["locationKey", _locationKey],
|
|
["accepted", false],
|
|
["requesterUid", ""],
|
|
["orgID", "default"],
|
|
["source", "mission_manager"]
|
|
]]];
|
|
|
|
private _rewardRange = getArray (_attackConfig >> "Rewards" >> "money");
|
|
private _reputationRange = getArray (_attackConfig >> "Rewards" >> "reputation");
|
|
private _penaltyRange = getArray (_attackConfig >> "penalty");
|
|
private _timeRange = getArray (_attackConfig >> "timeLimit");
|
|
private _rewards = _self call ["rollRewards"];
|
|
|
|
private _params = [
|
|
_taskID,
|
|
0,
|
|
count _units,
|
|
_rewardRange call BFUNC(randomNum),
|
|
_penaltyRange call BFUNC(randomNum),
|
|
_reputationRange call BFUNC(randomNum),
|
|
false,
|
|
false,
|
|
_timeRange call BFUNC(randomNum),
|
|
_rewards get "equipment",
|
|
_rewards get "supplies",
|
|
_rewards get "weapons",
|
|
_rewards get "vehicles",
|
|
_rewards get "special"
|
|
];
|
|
|
|
private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
|
|
_activeMissionRegistry set [_taskID, createHashMapFromArray [
|
|
["locationKey", _locationKey],
|
|
["startedAt", serverTime]
|
|
]];
|
|
_self set ["activeMissionRegistry", _activeMissionRegistry];
|
|
|
|
["attack", _params, 0, ""] spawn FUNC(handler);
|
|
_taskID
|
|
}],
|
|
["completeMission", compileFinal {
|
|
params [["_taskID", "", [""]]];
|
|
|
|
if (_taskID isEqualTo "") exitWith { false };
|
|
|
|
private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
|
|
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
|
|
private _locationKey = _missionRecord getOrDefault ["locationKey", ""];
|
|
|
|
_activeMissionRegistry deleteAt _taskID;
|
|
_self set ["activeMissionRegistry", _activeMissionRegistry];
|
|
|
|
if (_locationKey isNotEqualTo "") then {
|
|
private _recentLocationRegistry = _self getOrDefault ["recentLocationRegistry", createHashMap];
|
|
_recentLocationRegistry set [_locationKey, serverTime];
|
|
_self set ["recentLocationRegistry", _recentLocationRegistry];
|
|
};
|
|
|
|
true
|
|
}]
|
|
];
|
|
|
|
GVAR(MissionManager) = createHashMapObject [GVAR(MissionManagerBaseClass)];
|
|
|
|
[{
|
|
{
|
|
private _status = GVAR(TaskStore) call ["getTaskStatus", [_x]];
|
|
if (_status in ["succeeded", "failed"]) then {
|
|
GVAR(MissionManager) call ["completeMission", [_x]];
|
|
GVAR(TaskStore) call ["clearTaskStatus", [_x]];
|
|
};
|
|
} forEach (GVAR(MissionManager) call ["getActiveMissionIds", []]);
|
|
|
|
if (count (GVAR(MissionManager) call ["getActiveMissionIds", []]) >= (GVAR(MissionManager) call ["getMaxConcurrentMissions", []])) exitWith {};
|
|
|
|
private _taskID = GVAR(MissionManager) call ["startAttackMission", []];
|
|
if (_taskID isEqualTo "") exitWith {
|
|
["WARNING", "Mission manager failed to start an attack mission."] call EFUNC(common,log);
|
|
};
|
|
|
|
["INFO", format ["Mission manager started attack mission %1.", _taskID]] call EFUNC(common,log);
|
|
}, GVAR(MissionManager) call ["getMissionInterval", []], []] call CFUNC(addPerFrameHandler);
|