diff --git a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf index 4c16639..e79305e 100644 --- a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf @@ -79,14 +79,32 @@ switch (_event) do { hint "Transport destination is no longer available."; }; + private _transportSetting = { + params [["_name", "", [""]], ["_default", 0, [0]]]; + + private _configDefault = _default; + private _missionConfig = missionConfigFile >> "CfgMissions"; + if !(isClass _missionConfig) then { _missionConfig = configFile >> "CfgMissions"; }; + private _serviceConfig = _missionConfig >> "ServicePricing"; + if (isNumber (_serviceConfig >> _name)) then { + _configDefault = getNumber (_serviceConfig >> _name); + }; + + private _paramValue = [_name, _configDefault] call BIS_fnc_getParamValue; + private _value = missionNamespace getVariable [_name, _paramValue]; + if (_value isEqualType "") exitWith { (parseNumber _value) max 0 }; + if (_value isEqualType 0) exitWith { _value max 0 }; + _configDefault + }; + private _options = createHashMapFromArray [ ["label", _data getOrDefault ["label", "Transport"]], ["nodePrefix", _data getOrDefault ["nodePrefix", "transport"]], ["vehiclePrefix", _data getOrDefault ["vehiclePrefix", "transport_vehicle"]], ["arrivalPrefix", _data getOrDefault ["arrivalPrefix", "transport_arrival"]], ["maxIndexedNodes", _data getOrDefault ["maxIndexedNodes", 10]], - ["baseFare", _data getOrDefault ["baseFare", 100]], - ["pricePerKm", _data getOrDefault ["pricePerKm", 50]], + ["baseFare", _data getOrDefault ["baseFare", ["transportBaseFare", 100] call _transportSetting]], + ["pricePerKm", _data getOrDefault ["pricePerKm", ["transportPricePerKm", 50] call _transportSetting]], ["cargoRadius", _data getOrDefault ["cargoRadius", 25]], ["includeCargo", _data getOrDefault ["includeCargo", true]] ]; diff --git a/arma/client/addons/actor/functions/fnc_initRepository.sqf b/arma/client/addons/actor/functions/fnc_initRepository.sqf index 7df756f..535ea06 100644 --- a/arma/client/addons/actor/functions/fnc_initRepository.sqf +++ b/arma/client/addons/actor/functions/fnc_initRepository.sqf @@ -142,8 +142,25 @@ GVAR(ActorRepositoryBaseClass) = compileFinal createHashMapFromArray [ if (_isTransport) then { private _fromTransportNode = _x; private _maxIndexedNodes = _x getVariable ["transportMaxIndexedNodes", 10]; - private _baseFare = _x getVariable ["transportBaseFare", 100]; - private _pricePerKm = _x getVariable ["transportPricePerKm", 50]; + private _transportSetting = { + params [["_name", "", [""]], ["_default", 0, [0]]]; + + private _configDefault = _default; + private _missionConfig = missionConfigFile >> "CfgMissions"; + if !(isClass _missionConfig) then { _missionConfig = configFile >> "CfgMissions"; }; + private _serviceConfig = _missionConfig >> "ServicePricing"; + if (isNumber (_serviceConfig >> _name)) then { + _configDefault = getNumber (_serviceConfig >> _name); + }; + + private _paramValue = [_name, _configDefault] call BIS_fnc_getParamValue; + private _value = missionNamespace getVariable [_name, _paramValue]; + if (_value isEqualType "") exitWith { (parseNumber _value) max 0 }; + if (_value isEqualType 0) exitWith { _value max 0 }; + _configDefault + }; + private _baseFare = _x getVariable ["transportBaseFare", ["transportBaseFare", 100] call _transportSetting]; + private _pricePerKm = _x getVariable ["transportPricePerKm", ["transportPricePerKm", 50] call _transportSetting]; private _vehiclePrefix = _x getVariable ["transportVehiclePrefix", format ["%1_vehicle", _transportPrefix]]; private _arrivalPrefix = _x getVariable ["transportArrivalPrefix", format ["%1_arrival", _transportPrefix]]; private _nodeNames = [_transportPrefix]; diff --git a/arma/client/addons/mission_setup/functions/fnc_initRepository.sqf b/arma/client/addons/mission_setup/functions/fnc_initRepository.sqf index dcb79ea..3af8bb9 100644 --- a/arma/client/addons/mission_setup/functions/fnc_initRepository.sqf +++ b/arma/client/addons/mission_setup/functions/fnc_initRepository.sqf @@ -151,10 +151,21 @@ GVAR(MissionSetupRepositoryBaseClass) = compileFinal createHashMapFromArray [ private _paramOrDefault = { params ["_varName", "_default"]; - private _value = missionNamespace getVariable [_varName, _default]; + private _paramValue = [_varName, _default] call BIS_fnc_getParamValue; + private _value = missionNamespace getVariable [_varName, _paramValue]; if (_value isEqualType "") exitWith { parseNumber _value }; _value }; + private _serviceDefault = { + params ["_varName", "_default"]; + + private _serviceConfig = _missionConfig >> "ServicePricing"; + if (isNumber (_serviceConfig >> _varName)) exitWith { + getNumber (_serviceConfig >> _varName) + }; + + _default + }; private _factions = []; { @@ -197,6 +208,13 @@ GVAR(MissionSetupRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["penaltyMax", ["penaltyMax", -25] call _paramOrDefault], ["timeLimitMin", ["timeLimitMin", 600] call _paramOrDefault], ["timeLimitMax", ["timeLimitMax", 900] call _paramOrDefault], + ["medicalSpawnCost", ["medicalSpawnCost", ["medicalSpawnCost", 100] call _serviceDefault] call _paramOrDefault], + ["medicalHealCost", ["medicalHealCost", ["medicalHealCost", 100] call _serviceDefault] call _paramOrDefault], + ["serviceRepairCost", ["serviceRepairCost", ["serviceRepairCost", 500] call _serviceDefault] call _paramOrDefault], + ["serviceRearmCost", ["serviceRearmCost", ["serviceRearmCost", 500] call _serviceDefault] call _paramOrDefault], + ["fuelCost", ["fuelCost", ["fuelCost", 5] call _serviceDefault] call _paramOrDefault], + ["transportBaseFare", ["transportBaseFare", ["transportBaseFare", 100] call _serviceDefault] call _paramOrDefault], + ["transportPricePerKm", ["transportPricePerKm", ["transportPricePerKm", 50] call _serviceDefault] call _paramOrDefault], ["generatorProvider", GETMVAR(forge_server_task_generatorProvider,"builtin")] ]] ] diff --git a/arma/client/addons/mission_setup/ui/_site/mission-setup.css b/arma/client/addons/mission_setup/ui/_site/mission-setup.css index 11b59b5..c8f77a2 100644 --- a/arma/client/addons/mission_setup/ui/_site/mission-setup.css +++ b/arma/client/addons/mission_setup/ui/_site/mission-setup.css @@ -54,8 +54,8 @@ button { } .titlebar { - min-height: 2.75rem; - padding: 0 1.35rem; + min-height: 2.5rem; + padding: 0 1.1rem; display: flex; align-items: center; justify-content: space-between; @@ -98,18 +98,19 @@ option { .content { min-height: 0; - padding: 1rem 1.25rem; + padding: 0.75rem 1rem; overflow: hidden; display: flex; align-items: center; } .grid { - max-width: 78rem; + width: min(94rem, 100%); + max-width: 94rem; margin: 0 auto; display: grid; - grid-template-columns: 1.1fr 0.9fr; - gap: 1rem; + grid-template-columns: minmax(28rem, 1.35fr) minmax(18rem, 0.8fr) minmax(20rem, 0.85fr); + gap: 0.75rem; } .panel { @@ -120,22 +121,26 @@ option { } .panel-head { - padding: 0.85rem 1rem; + padding: 0.65rem 0.85rem; border-bottom: 1px solid var(--border); } .panel-head h1, .panel-head h2 { margin: 0.2rem 0 0; - font-size: 1.18rem; + font-size: 1.02rem; letter-spacing: 0; } .form { - padding: 0.9rem 1rem 1rem; + padding: 0.7rem 0.85rem 0.85rem; display: grid; grid-template-columns: 1fr 1fr; - gap: 0.68rem; + gap: 0.5rem; +} + +.form.compact { + gap: 0.55rem; } .field { @@ -149,7 +154,7 @@ option { label { color: var(--text-subtle); - font-size: 0.68rem; + font-size: 0.62rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; @@ -172,8 +177,8 @@ label { } .provider-toggle { - min-height: 2.25rem; - padding: 0 0.75rem; + min-height: 2rem; + padding: 0 0.65rem; display: grid; grid-template-columns: auto 1fr; align-items: center; @@ -249,8 +254,8 @@ label { input, select { width: 100%; - min-height: 2.25rem; - padding: 0 0.75rem; + min-height: 2rem; + padding: 0 0.65rem; border: 1px solid var(--border); background: rgba(24, 31, 40, 0.9); color: var(--text-main); @@ -264,16 +269,16 @@ button:focus-visible { } .summary { - padding: 0.9rem 1rem 1rem; + padding: 0.7rem 0.85rem 0.85rem; display: grid; - gap: 0.55rem; + gap: 0.42rem; } .summary-row { display: flex; justify-content: space-between; gap: 1rem; - padding-bottom: 0.55rem; + padding-bottom: 0.42rem; border-bottom: 1px solid var(--border); } @@ -294,7 +299,7 @@ button:focus-visible { } .actions { - padding: 0.75rem 1.25rem; + padding: 0.6rem 1rem; display: flex; justify-content: flex-end; gap: 0.75rem; @@ -303,7 +308,7 @@ button:focus-visible { } .btn { - min-height: 2.25rem; + min-height: 2rem; padding: 0.55rem 0.9rem; border: 1px solid var(--border-strong); background: rgba(24, 31, 40, 0.9); diff --git a/arma/client/addons/mission_setup/ui/_site/mission-setup.js b/arma/client/addons/mission_setup/ui/_site/mission-setup.js index 448476b..9128328 100644 --- a/arma/client/addons/mission_setup/ui/_site/mission-setup.js +++ b/arma/client/addons/mission_setup/ui/_site/mission-setup.js @@ -14,6 +14,13 @@ penaltyMax: -25, timeLimitMin: 600, timeLimitMax: 900, + medicalSpawnCost: 100, + medicalHealCost: 100, + serviceRepairCost: 500, + serviceRearmCost: 500, + fuelCost: 5, + transportBaseFare: 100, + transportPricePerKm: 50, generatorProvider: "builtin", }, error: "", @@ -47,6 +54,13 @@ penaltyMax: fieldNumber("penaltyMax"), timeLimitMin: fieldNumber("timeLimitMin"), timeLimitMax: fieldNumber("timeLimitMax"), + medicalSpawnCost: fieldNumber("medicalSpawnCost"), + medicalHealCost: fieldNumber("medicalHealCost"), + serviceRepairCost: fieldNumber("serviceRepairCost"), + serviceRearmCost: fieldNumber("serviceRearmCost"), + fuelCost: fieldNumber("fuelCost"), + transportBaseFare: fieldNumber("transportBaseFare"), + transportPricePerKm: fieldNumber("transportPricePerKm"), generatorProvider: document.getElementById("generatorProviderCustom")?.checked ? "custom" : "builtin", }; } @@ -86,6 +100,21 @@ return; } + const costFields = [ + settings.medicalSpawnCost, + settings.medicalHealCost, + settings.serviceRepairCost, + settings.serviceRearmCost, + settings.fuelCost, + settings.transportBaseFare, + settings.transportPricePerKm, + ]; + if (costFields.some((value) => value < 0)) { + state.error = "Service pricing cannot use negative values."; + render(); + return; + } + state.error = ""; send("missionSetup::apply", settings); } @@ -186,6 +215,43 @@ + + diff --git a/arma/server/addons/actor/README.md b/arma/server/addons/actor/README.md index 6f8c2a8..5bdb52e 100644 --- a/arma/server/addons/actor/README.md +++ b/arma/server/addons/actor/README.md @@ -25,13 +25,38 @@ life state, phone number, email, organization, and holster state. ## Runtime Behavior - Missing persistent actors can be created from live player snapshots. -- Newly created actors receive a Field Commander job orientation email, two +- Newly created actors receive their starting loadout from mission + `CfgStartingEquipment`, plus a Field Commander job orientation email, two Field Commander text messages, and a `$2,000` starting credit in their bank account. - Hot actor reads are migrated and hydrated before use. - `saveHotState` in the main addon snapshots and saves actor state on player disconnect and mission end. +## Starting Equipment +Missions can include `CfgStartingEquipment.hpp` from `description.ext` to +override starter actor gear without recompiling the addon or extension. + +```cpp +class CfgStartingEquipment { + loadout[] = { + {}, + {}, + {}, + {"U_BG_Guerrilla_6_1", {{"FirstAidKit", 2}}}, + {}, + {}, + "H_Cap_blk_ION", + "", + {}, + {"ItemMap", "ItemGPS", "ItemRadio", "ItemCompass", "ItemWatch", ""} + }; +}; +``` + +The Rust actor model no longer hardcodes a starter loadout. SQF supplies the +mission-configured loadout when it creates a missing actor record. + ## Event Surface The addon handles server events for actor init, get, set, multi-set, save, and remove requests, then replies to the requesting player through client actor RPCs. diff --git a/arma/server/addons/actor/functions/fnc_initActorStore.sqf b/arma/server/addons/actor/functions/fnc_initActorStore.sqf index 8454b1b..e2ba8de 100644 --- a/arma/server/addons/actor/functions/fnc_initActorStore.sqf +++ b/arma/server/addons/actor/functions/fnc_initActorStore.sqf @@ -4,7 +4,7 @@ * File: fnc_initActorStore.sqf * Author: IDSolutions * Date: 2025-12-17 - * Last Update: 2026-05-16 + * Last Update: 2026-06-03 * Public: Yes * * Description: @@ -25,12 +25,23 @@ #pragma hemtt ignore_variables ["_self"] GVAR(ActorModel) = compileFinal createHashMapObject [[ ["#type", "ActorModel"], + ["getStartingConfig", compileFinal { + missionConfigFile >> "CfgStartingEquipment" + }], + ["getDefaultLoadout", compileFinal { + private _config = _self call ["getStartingConfig", []]; + private _loadoutConfig = _config >> "loadout"; + + if (isArray _loadoutConfig) exitWith { getArray _loadoutConfig }; + + [[],[],[],["U_BG_Guerrilla_6_1",[["FirstAidKit", 2]]],[],[],"H_Cap_blk_ION","",[],["ItemMap","ItemGPS","ItemRadio","ItemCompass","ItemWatch",""]] + }], ["defaults", compileFinal { private _actor = createHashMap; _actor set ["uid", ""]; _actor set ["name", ""]; - _actor set ["loadout", [[],[],[],["U_BG_Guerrilla_6_1",[["FirstAidKit", 2]]],[],[],"H_Cap_blk_ION","",[],["ItemMap","ItemGPS","ItemRadio","ItemCompass","ItemWatch",""]]]; + _actor set ["loadout", _self call ["getDefaultLoadout", []]]; _actor set ["position", [0,0,0]]; _actor set ["direction", 0]; _actor set ["stance", "STAND"]; diff --git a/arma/server/addons/economy/README.md b/arma/server/addons/economy/README.md index 14f7331..3736f39 100644 --- a/arma/server/addons/economy/README.md +++ b/arma/server/addons/economy/README.md @@ -9,6 +9,21 @@ inventory handling. Current stores cover fuel tracking, medical service behavior, and service charges such as repairs and rearming. +## Configurable Prices +Service prices are read dynamically from mission namespace values so the +framework mission setup UI can override them at startup. If the UI is cancelled +or unavailable, mission `Params` with matching names are used as the backup; +if no param is defined, `CfgMissions >> ServicePricing` provides the fallback. + +Supported setting names: +- `medicalSpawnCost` - best-effort medical respawn charge; default `100` +- `medicalHealCost` - heal charge; default `100` +- `serviceRepairCost` - default repair service charge; default `500` +- `serviceRearmCost` - default rearm service charge; default `500` +- `fuelCost` - refuel price per liter; default `5` +- `transportBaseFare` - transport fare base price; default `100` +- `transportPricePerKm` - transport distance price; default `50` + ## Dependencies - `forge_server_main` - `forge_server_common` for logging, formatting, and player lookup @@ -23,7 +38,8 @@ Note: Bank and Org are runtime-only dependencies (not compile-time requiredAddon totals, charges the player's organization through `OrgStore`, syncs the org patch, and rolls fuel back to the starting level when organization funds cannot cover the refuel. -- `fnc_initMEconomyStore.sqf` manages medical spawn occupancy, healing charges, +- `fnc_initMEconomyStore.sqf` manages medical spawn occupancy, medical spawn + billing, healing charges, respawn placement, death inventory handling, and body-bag transfer. Medical charges use player bank/cash first, then organization funds with repayable member debt only when the player cannot cover the service. diff --git a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf index 2e6824d..27bed70 100644 --- a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf +++ b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf @@ -32,6 +32,23 @@ GVAR(FEconomyStore) = createHashMapObject [[ ["INFO", "Fuel Store Initialized!", nil, nil] call EFUNC(common,log); }], + ["numberSetting", { + params [["_name", "", [""]], ["_default", 0, [0]]]; + + private _configDefault = _default; + private _missionConfig = missionConfigFile >> "CfgMissions"; + if !(isClass _missionConfig) then { _missionConfig = configFile >> "CfgMissions"; }; + private _serviceConfig = _missionConfig >> "ServicePricing"; + if (isNumber (_serviceConfig >> _name)) then { + _configDefault = getNumber (_serviceConfig >> _name); + }; + + private _paramValue = [_name, _configDefault] call BIS_fnc_getParamValue; + private _value = missionNamespace getVariable [_name, _paramValue]; + if (_value isEqualType "") exitWith { (parseNumber _value) max 0 }; + if (_value isEqualType 0) exitWith { _value max 0 }; + _configDefault + }], ["start", { params ["_source", "_target", "_unit"]; @@ -100,7 +117,7 @@ GVAR(FEconomyStore) = createHashMapObject [[ if (_fuelCapacity <= 0) then { _fuelCapacity = 100; }; private _totalLiters = _missingFuel * _fuelCapacity; - private _totalCost = _totalLiters * GVAR(FuelCost); + private _totalCost = _totalLiters * (_self call ["numberSetting", ["fuelCost", GVAR(FuelCost)]]); private _chargeResult = GVAR(SEconomyStore) call ["chargeOrg", [_unit, _totalCost, "Refueling"]]; if !(_chargeResult getOrDefault ["success", false]) exitWith { _self call ["notify", [_unit, "danger", "Refueling", _chargeResult getOrDefault ["message", "Organization funds cannot cover this refuel. Refueling was not completed."]]]; @@ -130,7 +147,7 @@ GVAR(FEconomyStore) = createHashMapObject [[ private _player = [_uid] call EFUNC(common,getPlayer); private _totalLiters = GETVAR(_target,liters,0); - private _totalCost = _totalLiters * GVAR(FuelCost); + private _totalCost = _totalLiters * (_self call ["numberSetting", ["fuelCost", GVAR(FuelCost)]]); private _formattedTotalCost = [_totalCost] call EFUNC(common,formatNumber); private _formattedTotalLiters = _totalLiters toFixed 2; diff --git a/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf index 9024a48..bf0f97b 100644 --- a/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf +++ b/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf @@ -4,7 +4,7 @@ * File: fnc_initMEconomyStore.sqf * Author: IDSolutions * Date: 2025-12-20 - * Last Update: 2026-05-15 + * Last Update: 2026-06-03 * Public: No * * Description: @@ -30,8 +30,26 @@ GVAR(MEconomyStore) = createHashMapObject [[ _self set ["mSpawns", createHashMap]; GVAR(occupancyTriggers) = []; + GVAR(SpawnCost) = 100; ["INFO", "Medical Store Initialized!", nil, nil] call EFUNC(common,log); }], + ["numberSetting", { + params [["_name", "", [""]], ["_default", 0, [0]]]; + + private _configDefault = _default; + private _missionConfig = missionConfigFile >> "CfgMissions"; + if !(isClass _missionConfig) then { _missionConfig = configFile >> "CfgMissions"; }; + private _serviceConfig = _missionConfig >> "ServicePricing"; + if (isNumber (_serviceConfig >> _name)) then { + _configDefault = getNumber (_serviceConfig >> _name); + }; + + private _paramValue = [_name, _configDefault] call BIS_fnc_getParamValue; + private _value = missionNamespace getVariable [_name, _paramValue]; + if (_value isEqualType "") exitWith { (parseNumber _value) max 0 }; + if (_value isEqualType 0) exitWith { _value max 0 }; + _configDefault + }], ["init", { private _mSpawns = (_self get "mSpawns"); private _prefix = "med_spawn"; @@ -166,40 +184,61 @@ GVAR(MEconomyStore) = createHashMapObject [[ _result set ["message", ""]; _result }], - ["onHealed", { - params [["_unit", objNull, [objNull]]]; - - if (isNull _unit) exitWith { ["WARNING", format ["Invalid unit provided: %1", (name _unit)], nil, nil] call EFUNC(common,log); }; + ["chargeMedicalService", { + params [ + ["_unit", objNull, [objNull]], + ["_amount", 0, [0]], + ["_serviceLabel", "Medical service", [""]], + ["_requirePayment", true, [true]] + ]; + if (isNull _unit) exitWith { + ["WARNING", format ["Invalid unit provided: %1", (name _unit)], nil, nil] call EFUNC(common,log); + false + }; private _uid = getPlayerUID _unit; - if (_uid isEqualTo "") exitWith { ["WARNING", "Unable to charge medical service for unit without UID.", nil, nil] call EFUNC(common,log); }; + if (_uid isEqualTo "") exitWith { + ["WARNING", "Unable to charge medical service for unit without UID.", nil, nil] call EFUNC(common,log); + !_requirePayment + }; - private _healCost = 100; + if (_amount <= 0) exitWith { true }; - private _personalCharge = _self call ["chargePlayer", [_uid, _healCost]]; + private _personalCharge = _self call ["chargePlayer", [_uid, _amount]]; if (_personalCharge getOrDefault ["success", false]) exitWith { private _sourceLabel = ["cash", "bank"] select ((_personalCharge getOrDefault ["source", "bank"]) isEqualTo "bank"); - _self call ["notify", [_unit, "info", "Medical Billing", format ["Medical service charged $%1 from your %2.", [_healCost] call EFUNC(common,formatNumber), _sourceLabel]]]; - [CRPC(actor,onActorHealed), [], _unit] call CFUNC(targetEvent); + _self call ["notify", [_unit, "info", "Medical Billing", format ["%1 charged $%2 from your %3.", _serviceLabel, [_amount] call EFUNC(common,formatNumber), _sourceLabel]]]; + true }; if !(_personalCharge getOrDefault ["fallbackEligible", false]) exitWith { private _message = _personalCharge getOrDefault ["message", "Personal funds could not be charged for medical service."]; _self call ["notify", [_unit, "danger", "Medical Billing", _message]]; + !_requirePayment }; if (isNil QGVAR(SEconomyStore)) exitWith { ["ERROR", "Service economy store unavailable for medical organization fallback charge.", nil, nil] call EFUNC(common,log); _self call ["notify", [_unit, "danger", "Medical Billing", "Organization billing is unavailable. Medical service cannot complete."]]; + !_requirePayment }; - private _chargeResult = GVAR(SEconomyStore) call ["chargeOrg", [_unit, _healCost, "Medical", true]]; + private _chargeResult = GVAR(SEconomyStore) call ["chargeOrg", [_unit, _amount, "Medical", true]]; if !(_chargeResult getOrDefault ["success", false]) exitWith { private _message = _chargeResult getOrDefault ["message", "Organization funds cannot cover this medical service."]; _self call ["notify", [_unit, "danger", "Medical Billing", _message]]; + !_requirePayment }; - _self call ["notify", [_unit, "info", "Medical Billing", format ["Personal funds could not cover medical service. Organization charged $%1; repay it through your organization credit line.", [_healCost] call EFUNC(common,formatNumber)]]]; + _self call ["notify", [_unit, "info", "Medical Billing", format ["Personal funds could not cover %1. Organization charged $%2; repay it through your organization credit line.", _serviceLabel, [_amount] call EFUNC(common,formatNumber)]]]; + true + }], + ["onHealed", { + params [["_unit", objNull, [objNull]]]; + + private _healCost = _self call ["numberSetting", ["medicalHealCost", 100]]; + if !(_self call ["chargeMedicalService", [_unit, _healCost, "Medical service", true]]) exitWith {}; + [CRPC(actor,onActorHealed), [], _unit] call CFUNC(targetEvent); }], ["onRespawn", { @@ -214,6 +253,8 @@ GVAR(MEconomyStore) = createHashMapObject [[ deleteVehicle _corpse; private _player = [_uid] call EFUNC(common,getPlayer); + private _spawnCost = _self call ["numberSetting", ["medicalSpawnCost", GVAR(SpawnCost)]]; + _self call ["chargeMedicalService", [_player, _spawnCost, "Medical spawn", false]]; [CRPC(actor,onActorRespawn), [_loadout, _medSpawnPos, _medSpawnDir], _player] call CFUNC(targetEvent); }], ["onKilled", { diff --git a/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf index 3cd86d3..c99d4d5 100644 --- a/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf +++ b/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf @@ -30,6 +30,23 @@ GVAR(SEconomyStore) = createHashMapObject [[ GVAR(ServiceRearmCost) = 500; ["INFO", "Service Store Initialized!", nil, nil] call EFUNC(common,log); }], + ["numberSetting", { + params [["_name", "", [""]], ["_default", 0, [0]]]; + + private _configDefault = _default; + private _missionConfig = missionConfigFile >> "CfgMissions"; + if !(isClass _missionConfig) then { _missionConfig = configFile >> "CfgMissions"; }; + private _serviceConfig = _missionConfig >> "ServicePricing"; + if (isNumber (_serviceConfig >> _name)) then { + _configDefault = getNumber (_serviceConfig >> _name); + }; + + private _paramValue = [_name, _configDefault] call BIS_fnc_getParamValue; + private _value = missionNamespace getVariable [_name, _paramValue]; + if (_value isEqualType "") exitWith { (parseNumber _value) max 0 }; + if (_value isEqualType 0) exitWith { _value max 0 }; + _configDefault + }], ["notify", { params [["_unit", objNull, [objNull]], ["_type", "info", [""]], ["_title", "Service", [""]], ["_message", "", [""]]]; @@ -148,7 +165,7 @@ GVAR(SEconomyStore) = createHashMapObject [[ if (isNull _target || { isNull _unit }) exitWith { false }; - private _repairCost = [_cost, GVAR(ServiceRepairCost)] select (_cost < 0); + private _repairCost = [_cost, _self call ["numberSetting", ["serviceRepairCost", GVAR(ServiceRepairCost)]]] select (_cost < 0); private _charge = _self call ["chargeOrg", [_unit, _repairCost, "Repair"]]; if !(_charge getOrDefault ["success", false]) exitWith { _self call ["notify", [_unit, "danger", "Repair", _charge getOrDefault ["message", "Organization funds cannot cover this repair."]]]; @@ -164,7 +181,7 @@ GVAR(SEconomyStore) = createHashMapObject [[ if (isNull _target || { isNull _unit }) exitWith { false }; - private _rearmCost = [_cost, GVAR(ServiceRearmCost)] select (_cost < 0); + private _rearmCost = [_cost, _self call ["numberSetting", ["serviceRearmCost", GVAR(ServiceRearmCost)]]] select (_cost < 0); private _charge = _self call ["chargeOrg", [_unit, _rearmCost, "Rearm"]]; if !(_charge getOrDefault ["success", false]) exitWith { _self call ["notify", [_unit, "danger", "Rearm", _charge getOrDefault ["message", "Organization funds cannot cover this rearm."]]]; diff --git a/arma/server/addons/garage/README.md b/arma/server/addons/garage/README.md index 9eece37..3950994 100644 --- a/arma/server/addons/garage/README.md +++ b/arma/server/addons/garage/README.md @@ -34,3 +34,26 @@ Garage listens for sync events through the event bus: - `notification.requested` - storage and vehicle modification alerts The store module emits these events when granting vehicles; garage applies the changes to player state. + +## Starting Unlocks +Missions can include `CfgStartingEquipment.hpp` from `description.ext` to +configure initial virtual garage unlocks for new players. + +```cpp +class CfgStartingEquipment { + class Unlocks { + class Garage { + cars[] = {"B_Quadbike_01_F"}; + armor[] = {}; + helis[] = {}; + planes[] = {}; + naval[] = {}; + other[] = {}; + }; + }; +}; +``` + +The extension virtual garage default is intentionally empty. The server addon +seeds `CfgStartingEquipment` unlocks only when a player does not already have a +persistent owner-scoped garage record. diff --git a/arma/server/addons/garage/functions/fnc_initVGStore.sqf b/arma/server/addons/garage/functions/fnc_initVGStore.sqf index be3d649..42d381c 100644 --- a/arma/server/addons/garage/functions/fnc_initVGStore.sqf +++ b/arma/server/addons/garage/functions/fnc_initVGStore.sqf @@ -24,15 +24,28 @@ #pragma hemtt ignore_variables ["_self"] GVAR(VGarageModel) = compileFinal createHashMapObject [[ ["#type", "VGarageModel"], + ["getStartingUnlocksConfig", compileFinal { + missionConfigFile >> "CfgStartingEquipment" >> "Unlocks" >> "Garage" + }], + ["getStartingUnlocks", compileFinal { + params [["_category", "", [""]], ["_fallback", [], [[]]]]; + + private _config = _self call ["getStartingUnlocksConfig", []]; + private _categoryConfig = _config >> _category; + + if (isArray _categoryConfig) exitWith { getArray _categoryConfig }; + + +_fallback + }], ["defaults", compileFinal { private _vGarage = createHashMap; - _vGarage set ["armor", []]; - _vGarage set ["cars", ["B_Quadbike_01_F"]]; - _vGarage set ["helis", []]; - _vGarage set ["naval", []]; - _vGarage set ["other", []]; - _vGarage set ["planes", []]; + _vGarage set ["armor", _self call ["getStartingUnlocks", ["armor", []]]]; + _vGarage set ["cars", _self call ["getStartingUnlocks", ["cars", ["B_Quadbike_01_F"]]]]; + _vGarage set ["helis", _self call ["getStartingUnlocks", ["helis", []]]]; + _vGarage set ["naval", _self call ["getStartingUnlocks", ["naval", []]]]; + _vGarage set ["other", _self call ["getStartingUnlocks", ["other", []]]]; + _vGarage set ["planes", _self call ["getStartingUnlocks", ["planes", []]]]; _vGarage }] @@ -71,17 +84,46 @@ GVAR(VGBaseStore) = compileFinal ([ private _command = ["owned:garage:hot:fetch", "owned:garage:hot:init"] select _initialize; _self call ["callHotVGarage", [_command, [_uid]]] }], + ["isPersistentVGarageInitialized", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { false }; + + ["owned:garage:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + _isSuccess && { _result isEqualTo "true" } + }], + ["seedStartingUnlocks", compileFinal { + params [["_uid", "", [""]], ["_garage", createHashMap, [createHashMap]]]; + + if (_uid isEqualTo "" || { _garage isEqualTo createHashMap }) exitWith { _garage }; + + private _defaults = GVAR(VGarageModel) call ["defaults", []]; + private _seeded = +_garage; + { + _seeded set [_x, +_y]; + } forEach _defaults; + + private _updated = _self call ["callHotVGarage", ["owned:garage:hot:override", [_uid, toJSON _seeded]]]; + if (_updated isEqualTo createHashMap) exitWith { _seeded }; + + _self call ["callHotVGarage", ["owned:garage:hot:save", [_uid]]]; + _updated + }], ["init", compileFinal { params [["_uid", "", [""]]]; private _player = [_uid] call EFUNC(common,getPlayer); if (isNull _player) exitWith { createHashMap }; + private _hasPersistentGarage = _self call ["isPersistentVGarageInitialized", [_uid]]; private _garage = _self call ["loadHotVGarage", [_uid, true]]; if (_garage isEqualTo createHashMap) then { _garage = GVAR(VGarageModel) call ["defaults", []]; ["ERROR", format ["Failed to initialize virtual garage for %1! Using fallback virtual garage.", _uid]] call EFUNC(common,log); }; + if !(_hasPersistentGarage) then { + _garage = _self call ["seedStartingUnlocks", [_uid, _garage]]; + }; [CRPC(garage,responseInitVG), [_garage], _player] call CFUNC(targetEvent); _garage diff --git a/arma/server/addons/locker/README.md b/arma/server/addons/locker/README.md index 1a82b3d..e4757dc 100644 --- a/arma/server/addons/locker/README.md +++ b/arma/server/addons/locker/README.md @@ -35,3 +35,24 @@ Locker listens for sync events through the event bus: - `notification.requested` - storage and item modification alerts The store module emits these events when granting items; locker applies the changes to player state. + +## Starting Unlocks +Missions can include `CfgStartingEquipment.hpp` from `description.ext` to +configure initial virtual arsenal unlocks for new players. + +```cpp +class CfgStartingEquipment { + class Unlocks { + class Locker { + items[] = {"FirstAidKit", "ItemMap", "ItemCompass"}; + weapons[] = {"hgun_P07_F"}; + magazines[] = {"16Rnd_9x21_Mag"}; + backpacks[] = {}; + }; + }; +}; +``` + +The extension virtual locker default is intentionally empty. The server addon +seeds `CfgStartingEquipment` unlocks only when a player does not already have a +persistent owner-scoped locker record. diff --git a/arma/server/addons/locker/functions/fnc_initVAStore.sqf b/arma/server/addons/locker/functions/fnc_initVAStore.sqf index 542bb6a..e8b1e62 100644 --- a/arma/server/addons/locker/functions/fnc_initVAStore.sqf +++ b/arma/server/addons/locker/functions/fnc_initVAStore.sqf @@ -24,13 +24,26 @@ #pragma hemtt ignore_variables ["_self"] GVAR(VArsenalModel) = compileFinal createHashMapObject [[ ["#type", "VArsenalModel"], + ["getStartingUnlocksConfig", compileFinal { + missionConfigFile >> "CfgStartingEquipment" >> "Unlocks" >> "Locker" + }], + ["getStartingUnlocks", compileFinal { + params [["_category", "", [""]], ["_fallback", [], [[]]]]; + + private _config = _self call ["getStartingUnlocksConfig", []]; + private _categoryConfig = _config >> _category; + + if (isArray _categoryConfig) exitWith { getArray _categoryConfig }; + + +_fallback + }], ["defaults", compileFinal { private _vArsenal = createHashMap; - _vArsenal set ["backpacks", ["B_AssaultPack_rgr"]]; - _vArsenal set ["items", ["FirstAidKit", "G_Combat", "H_Cap_blk_ION", "H_HelmetB", "ItemCompass", "ItemGPS", "ItemMap", "ItemRadio", "ItemWatch", "U_BG_Guerrilla_6_1", "V_TacVest_oli", "ACE_EarPlugs"]]; - _vArsenal set ["magazines", ["16Rnd_9x21_Mag", "30Rnd_65x39_caseless_black_mag", "Chemlight_blue", "Chemlight_green", "Chemlight_red", "Chemlight_yellow", "HandGrenade", "SmokeShell", "SmokeShellBlue", "SmokeShellGreen", "SmokeShellOrange", "SmokeShellPurple", "SmokeShellRed", "SmokeShellYellow"]]; - _vArsenal set ["weapons", ["arifle_MX_F", "hgun_P07_F"]]; + _vArsenal set ["backpacks", _self call ["getStartingUnlocks", ["backpacks", ["B_AssaultPack_rgr"]]]]; + _vArsenal set ["items", _self call ["getStartingUnlocks", ["items", ["FirstAidKit", "G_Combat", "H_Cap_blk_ION", "H_HelmetB", "ItemCompass", "ItemGPS", "ItemMap", "ItemRadio", "ItemWatch", "U_BG_Guerrilla_6_1", "V_TacVest_oli", "ACE_EarPlugs"]]]]; + _vArsenal set ["magazines", _self call ["getStartingUnlocks", ["magazines", ["16Rnd_9x21_Mag", "30Rnd_65x39_caseless_black_mag", "Chemlight_blue", "Chemlight_green", "Chemlight_red", "Chemlight_yellow", "HandGrenade", "SmokeShell", "SmokeShellBlue", "SmokeShellGreen", "SmokeShellOrange", "SmokeShellPurple", "SmokeShellRed", "SmokeShellYellow"]]]]; + _vArsenal set ["weapons", _self call ["getStartingUnlocks", ["weapons", ["arifle_MX_F", "hgun_P07_F"]]]]; _vArsenal }] @@ -69,17 +82,46 @@ GVAR(VABaseStore) = compileFinal ([ private _command = ["owned:locker:hot:fetch", "owned:locker:hot:init"] select _initialize; _self call ["callHotVArsenal", [_command, [_uid]]] }], + ["isPersistentVArsenalInitialized", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { false }; + + ["owned:locker:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + _isSuccess && { _result isEqualTo "true" } + }], + ["seedStartingUnlocks", compileFinal { + params [["_uid", "", [""]], ["_arsenal", createHashMap, [createHashMap]]]; + + if (_uid isEqualTo "" || { _arsenal isEqualTo createHashMap }) exitWith { _arsenal }; + + private _defaults = GVAR(VArsenalModel) call ["defaults", []]; + private _seeded = +_arsenal; + { + _seeded set [_x, +_y]; + } forEach _defaults; + + private _updated = _self call ["callHotVArsenal", ["owned:locker:hot:override", [_uid, toJSON _seeded]]]; + if (_updated isEqualTo createHashMap) exitWith { _seeded }; + + _self call ["callHotVArsenal", ["owned:locker:hot:save", [_uid]]]; + _updated + }], ["init", compileFinal { params [["_uid", "", [""]]]; private _player = [_uid] call EFUNC(common,getPlayer); if (isNull _player) exitWith { createHashMap }; + private _hasPersistentArsenal = _self call ["isPersistentVArsenalInitialized", [_uid]]; private _arsenal = _self call ["loadHotVArsenal", [_uid, true]]; if (_arsenal isEqualTo createHashMap) then { _arsenal = GVAR(VArsenalModel) call ["defaults", []]; ["ERROR", format ["Failed to initialize virtual arsenal for %1! Using fallback virtual arsenal.", _uid]] call EFUNC(common,log); }; + if !(_hasPersistentArsenal) then { + _arsenal = _self call ["seedStartingUnlocks", [_uid, _arsenal]]; + }; [CRPC(locker,responseInitVA), [_arsenal], _player] call CFUNC(targetEvent); _arsenal diff --git a/arma/server/addons/store/README.md b/arma/server/addons/store/README.md index 04b481a..84313ae 100644 --- a/arma/server/addons/store/README.md +++ b/arma/server/addons/store/README.md @@ -34,6 +34,22 @@ generated catalog without changing the addon. ```cpp class CfgStore { mode = "allowlist"; // dynamic, allowlist, or denylist + modMode = "dynamic"; // dynamic, allowlist, or denylist + mods[] = {}; // ModSources class names used when modMode is not dynamic + + class ModSources { + class rhs { + patches[] = {"rhs_main", "rhsusf_main"}; + addons[] = {"rhs_", "rhsusf_", "rhsgref_", "rhsafrf_"}; + prefixes[] = {"rhs_", "rhsusf_", "rhsgref_", "rhsafrf_"}; + }; + + class ace3 { + patches[] = {"ace_main"}; + addons[] = {"ace_"}; + prefixes[] = {"ace_"}; + }; + }; class Categories { primary[] = {"arifle_MX_F", "arifle_MXC_F"}; @@ -56,6 +72,13 @@ listed for the requested category. `denylist` removes listed classnames from the generated category. Overrides are applied server-side, so checkout validation uses the same prices and descriptions the UI displays. +`modMode` applies before category filtering. `allowlist` only keeps generated +entries that match one of the configured `mods[]`; `denylist` removes matching +entries. Each `ModSources` child can define `patches[]` to detect whether the +mod is loaded, `addons[]` for config source addon/source mod names or classname +prefixes, and `prefixes[]` for classname prefixes. If a mod source defines no +patches, it is treated as available and only the source/prefix checks are used. + `units[]` follows the same `dynamic`, `allowlist`, and `denylist` behavior as item and vehicle categories. Unit purchases are immediate spawn grants, not durable virtual garage unlocks. diff --git a/arma/server/addons/store/functions/fnc_initCatalogService.sqf b/arma/server/addons/store/functions/fnc_initCatalogService.sqf index 12587c4..35d1e45 100644 --- a/arma/server/addons/store/functions/fnc_initCatalogService.sqf +++ b/arma/server/addons/store/functions/fnc_initCatalogService.sqf @@ -28,6 +28,132 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ _mode }], + ["getMissionStoreModMode", compileFinal { + private _storeConfig = _self call ["getMissionStoreConfig", []]; + private _mode = toLowerANSI getText (_storeConfig >> "modMode"); + + if !(_mode in ["allowlist", "denylist", "dynamic"]) then { _mode = "dynamic"; }; + + _mode + }], + ["getMissionStoreModList", compileFinal { + private _storeConfig = _self call ["getMissionStoreConfig", []]; + private _mods = []; + + if (isArray (_storeConfig >> "mods")) then { + _mods = getArray (_storeConfig >> "mods"); + }; + + _mods apply { + private _modID = ""; + if (_x isEqualType "") then { + _modID = _x; + } else { + _modID = str _x; + }; + + toLowerANSI _modID + } + }], + ["getMissionStoreModSourceValues", compileFinal { + params [["_modID", "", [""]], ["_key", "", [""]]]; + + private _storeConfig = _self call ["getMissionStoreConfig", []]; + private _sourceConfig = _storeConfig >> "ModSources" >> _modID; + private _values = []; + + if (isArray (_sourceConfig >> _key)) then { + _values = getArray (_sourceConfig >> _key); + }; + + _values apply { + if (_x isEqualType "") then { + _x + } else { + str _x + } + } + }], + ["isMissionStoreModLoaded", compileFinal { + params [["_modID", "", [""]]]; + + private _patches = _self call ["getMissionStoreModSourceValues", [_modID, "patches"]]; + if (_patches isEqualTo []) exitWith { true }; + + private _loaded = false; + { + if (isClass (configFile >> "CfgPatches" >> _x)) exitWith { _loaded = true; }; + } forEach _patches; + + _loaded + }], + ["doesValueMatchAnyPrefix", compileFinal { + params [["_value", "", [""]], ["_prefixes", [], [[]]]]; + + private _normalizedValue = toLowerANSI _value; + private _matches = false; + { + private _prefix = toLowerANSI _x; + if (_prefix isEqualTo "") then { continue; }; + if ((_normalizedValue select [0, count _prefix]) isEqualTo _prefix) exitWith { _matches = true; }; + } forEach _prefixes; + + _matches + }], + ["doesItemMatchMissionStoreMod", compileFinal { + params [["_item", createHashMap, [createHashMap]], ["_modID", "", [""]]]; + + if (_item isEqualTo createHashMap || { _modID isEqualTo "" }) exitWith { false }; + if !(_self call ["isMissionStoreModLoaded", [_modID]]) exitWith { false }; + + private _className = _item getOrDefault ["className", ""]; + private _sourceAddons = (_item getOrDefault ["sourceAddons", []]) apply { toLowerANSI _x }; + private _sourceMod = _item getOrDefault ["sourceMod", ""]; + private _addons = (_self call ["getMissionStoreModSourceValues", [_modID, "addons"]]) apply { toLowerANSI _x }; + private _prefixes = (_self call ["getMissionStoreModSourceValues", [_modID, "prefixes"]]) apply { toLowerANSI _x }; + private _sourceModLower = toLowerANSI _sourceMod; + + if (_sourceModLower in _addons) exitWith { true }; + private _sourceAddonMatched = false; + { + if (_x in _addons) exitWith { _sourceAddonMatched = true; }; + if (_self call ["doesValueMatchAnyPrefix", [_x, _addons]]) exitWith { _sourceAddonMatched = true; }; + } forEach _sourceAddons; + if (_sourceAddonMatched) exitWith { true }; + if (_self call ["doesValueMatchAnyPrefix", [_className, _addons + _prefixes]]) exitWith { true }; + if (_self call ["doesValueMatchAnyPrefix", [_sourceMod, _addons]]) exitWith { true }; + + false + }], + ["doesItemMatchMissionStoreMods", compileFinal { + params [["_item", createHashMap, [createHashMap]], ["_mods", [], [[]]]]; + + private _matches = false; + { + if (_self call ["doesItemMatchMissionStoreMod", [_item, _x]]) exitWith { _matches = true; }; + } forEach _mods; + + _matches + }], + ["applyMissionStoreModFilter", compileFinal { + params [["_items", [], [[]]]]; + + private _mode = _self call ["getMissionStoreModMode", []]; + private _mods = _self call ["getMissionStoreModList", []]; + if (_mode isEqualTo "dynamic" || { _mods isEqualTo [] }) exitWith { +_items }; + + switch (_mode) do { + case "allowlist": { + _items select { _self call ["doesItemMatchMissionStoreMods", [_x, _mods]] } + }; + case "denylist": { + _items select { !(_self call ["doesItemMatchMissionStoreMods", [_x, _mods]]) } + }; + default { + +_items + }; + } + }], ["getMissionStoreCategoryList", compileFinal { params [["_category", "", [""]]]; @@ -93,16 +219,16 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ private _mode = _self call ["getMissionStoreMode", []]; private _classNames = _self call ["getMissionStoreCategoryList", [_category]]; - private _filteredItems = +_items; + private _filteredItems = _self call ["applyMissionStoreModFilter", [_items]]; switch (_mode) do { case "allowlist": { - _filteredItems = _items select { + _filteredItems = _filteredItems select { (toLowerANSI (_x getOrDefault ["className", ""])) in _classNames }; }; case "denylist": { - _filteredItems = _items select { + _filteredItems = _filteredItems select { !((toLowerANSI (_x getOrDefault ["className", ""])) in _classNames) }; }; @@ -175,6 +301,8 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ private _className = configName _cfg; private _displayName = getText (_cfg >> "displayName"); + private _sourceAddons = configSourceAddonList _cfg; + private _sourceMod = configSourceMod _cfg; private _picture = getText (_cfg >> _imageField); if (_picture isEqualTo "" && { _imageField isNotEqualTo "picture" }) then { _picture = getText (_cfg >> "picture"); @@ -190,7 +318,9 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ ["price", _self call ["formatCurrency", [_priceValue]]], ["priceValue", _priceValue], ["image", _picture], - ["type", _typeLabel] + ["type", _typeLabel], + ["sourceAddons", _sourceAddons], + ["sourceMod", _sourceMod] ] }], ["appendCfgWeaponsByItemInfoType", compileFinal { diff --git a/arma/server/addons/task/CfgMissions.hpp b/arma/server/addons/task/CfgMissions.hpp index ae42080..f470909 100644 --- a/arma/server/addons/task/CfgMissions.hpp +++ b/arma/server/addons/task/CfgMissions.hpp @@ -12,6 +12,9 @@ * Generator behavior: * - maxConcurrentMissions and missionInterval are copied into * forge_server_task_missionSetup_settings by the framework mission setup service. + * - ServicePricing values are copied to missionNamespace by the framework + * mission setup service so economy and transport stores can read UI or + * mission-param overrides at runtime. * - Reward, reputation, penalty, and timeLimit ranges are read through * forge_server_task_fnc_getMissionSettingRange so UI overrides and config fallbacks * use the same path. @@ -26,6 +29,18 @@ class CfgMissions { // Seconds before a generated mission location can be reused. locationReuseCooldown = 900; + // Economy and service defaults. Mission Params with matching names override + // these values before the setup UI opens; submitted UI values override both. + class ServicePricing { + medicalSpawnCost = 100; + medicalHealCost = 100; + serviceRepairCost = 500; + serviceRearmCost = 500; + fuelCost = 5; + transportBaseFare = 100; + transportPricePerKm = 50; + }; + // Enemy faction selection is ultimately exported to ENEMY_FACTION_STR and // ENEMY_SIDE for server-side generators. class EnemyFactionConfig { diff --git a/arma/server/addons/task/functions/fnc_initMissionSetupService.sqf b/arma/server/addons/task/functions/fnc_initMissionSetupService.sqf index 5313ad7..068c040 100644 --- a/arma/server/addons/task/functions/fnc_initMissionSetupService.sqf +++ b/arma/server/addons/task/functions/fnc_initMissionSetupService.sqf @@ -80,7 +80,18 @@ GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [ _overrides getOrDefault [_varName, _default] }; - missionNamespace getVariable [_varName, _default] + private _paramValue = [_varName, _default] call BIS_fnc_getParamValue; + missionNamespace getVariable [_varName, _paramValue] + }; + private _serviceDefault = { + params ["_varName", "_default"]; + + private _serviceConfig = _missionConfig >> "ServicePricing"; + if (isNumber (_serviceConfig >> _varName)) exitWith { + getNumber (_serviceConfig >> _varName) + }; + + _default }; private _maxConcurrent = [ @@ -104,6 +115,13 @@ GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [ 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 _medicalSpawnCost = [["medicalSpawnCost", ["medicalSpawnCost", 100] call _serviceDefault, _overrides] call _paramOrDefault, 100] call (_self get "numberOrDefault"); + private _medicalHealCost = [["medicalHealCost", ["medicalHealCost", 100] call _serviceDefault, _overrides] call _paramOrDefault, 100] call (_self get "numberOrDefault"); + private _serviceRepairCost = [["serviceRepairCost", ["serviceRepairCost", 500] call _serviceDefault, _overrides] call _paramOrDefault, 500] call (_self get "numberOrDefault"); + private _serviceRearmCost = [["serviceRearmCost", ["serviceRearmCost", 500] call _serviceDefault, _overrides] call _paramOrDefault, 500] call (_self get "numberOrDefault"); + private _fuelCost = [["fuelCost", ["fuelCost", 5] call _serviceDefault, _overrides] call _paramOrDefault, 5] call (_self get "numberOrDefault"); + private _transportBaseFare = [["transportBaseFare", ["transportBaseFare", 100] call _serviceDefault, _overrides] call _paramOrDefault, 100] call (_self get "numberOrDefault"); + private _transportPricePerKm = [["transportPricePerKm", ["transportPricePerKm", 50] call _serviceDefault, _overrides] call _paramOrDefault, 50] call (_self get "numberOrDefault"); private _generatorProvider = _overrides getOrDefault ["generatorProvider", GETGVAR(generatorProvider,"builtin")]; if !(_generatorProvider isEqualType "") then { _generatorProvider = str _generatorProvider; }; _generatorProvider = toLowerANSI _generatorProvider; @@ -131,6 +149,13 @@ GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [ _timeMin = _timeMin max 1; _timeMax = _timeMax max _timeMin; + _medicalSpawnCost = _medicalSpawnCost max 0; + _medicalHealCost = _medicalHealCost max 0; + _serviceRepairCost = _serviceRepairCost max 0; + _serviceRearmCost = _serviceRearmCost max 0; + _fuelCost = _fuelCost max 0; + _transportBaseFare = _transportBaseFare max 0; + _transportPricePerKm = _transportPricePerKm max 0; private _settings = createHashMapFromArray [ ["useMenuSettings", true], @@ -145,6 +170,13 @@ GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [ ["penaltyMax", _penMax], ["timeLimitMin", _timeMin], ["timeLimitMax", _timeMax], + ["medicalSpawnCost", _medicalSpawnCost], + ["medicalHealCost", _medicalHealCost], + ["serviceRepairCost", _serviceRepairCost], + ["serviceRearmCost", _serviceRearmCost], + ["fuelCost", _fuelCost], + ["transportBaseFare", _transportBaseFare], + ["transportPricePerKm", _transportPricePerKm], ["enemyFaction", _enemyFaction], ["generatorProvider", _generatorProvider] ]; @@ -152,6 +184,17 @@ GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [ SETMPVAR(GVAR(missionSetup_settings),_settings); SETMPVAR(GVAR(missionSetup_settingsApplied),true); SETMPVAR(GVAR(generatorProvider),_generatorProvider); + { + missionNamespace setVariable [_x, _settings getOrDefault [_x, 0], true]; + } forEach [ + "medicalSpawnCost", + "medicalHealCost", + "serviceRepairCost", + "serviceRearmCost", + "fuelCost", + "transportBaseFare", + "transportPricePerKm" + ]; private _side = _self call ["resolveFactionSide", [_enemyFaction, east]]; ENEMY_SIDE = _side; diff --git a/arma/server/addons/task/functions/fnc_initTaskStore.sqf b/arma/server/addons/task/functions/fnc_initTaskStore.sqf index 6b01de5..4c0dbb3 100644 --- a/arma/server/addons/task/functions/fnc_initTaskStore.sqf +++ b/arma/server/addons/task/functions/fnc_initTaskStore.sqf @@ -56,6 +56,9 @@ GVAR(TaskStore) = createHashMapObject [[ ["isTaskCompleted", compileFinal { GVAR(TaskCatalogStore) call ["isTaskCompleted", _this] }], + ["isTerminalStatus", compileFinal { + GVAR(TaskCatalogStore) call ["isTerminalStatus", _this] + }], ["areTaskPrerequisitesSatisfied", compileFinal { GVAR(TaskCatalogStore) call ["areTaskPrerequisitesSatisfied", _this] }], diff --git a/arma/server/addons/task/functions/objects/fnc_AttackTaskBaseClass.sqf b/arma/server/addons/task/functions/objects/fnc_AttackTaskBaseClass.sqf index 9487a72..e968d93 100644 --- a/arma/server/addons/task/functions/objects/fnc_AttackTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/objects/fnc_AttackTaskBaseClass.sqf @@ -96,10 +96,10 @@ GVAR(AttackTaskBaseClass) merge [createHashMapFromArray [ waitUntil { sleep 1; - GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + !(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] } }; - true + _self call ["isTaskStoreOpen", []] }], ["tick", compileFinal { private _startedAt = _self getOrDefault ["startedAt", -1]; @@ -139,7 +139,7 @@ GVAR(AttackTaskBaseClass) merge [createHashMapFromArray [ _self call ["refreshTargetsFromStore", []]; private _targets = _self getOrDefault ["targets", []]; GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]]; - count _targets > 0 + !(_self call ["isTaskStoreOpen", []]) || { count _targets > 0 } }; } else { waitUntil { @@ -148,10 +148,20 @@ GVAR(AttackTaskBaseClass) merge [createHashMapFromArray [ }; }; - _self call ["waitForAssignment", []]; + if !(_self call ["isTaskStoreOpen", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before targets registered."]]; + _self call ["cleanup", []]; + false + }; + + if !(_self call ["waitForAssignment", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before assignment."]]; + _self call ["cleanup", []]; + false + }; _self call ["markActive", []]; - while { (_self call ["getStatus", []]) isEqualTo "active" } do { + while { _self call ["isTaskLoopActive", []] } do { private _targets = _self getOrDefault ["targets", []]; if (_useTaskStore) then { @@ -186,10 +196,8 @@ GVAR(AttackTaskBaseClass) merge [createHashMapFromArray [ sleep 1; }; - if ((_self call ["getStatus", []]) isEqualTo "failed") then { - private _targets = _self getOrDefault ["targets", []]; - { deleteVehicle _x } forEach _targets; - + private _finalStatus = _self call ["getStatus", []]; + if (_finalStatus isEqualTo "failed") then { if (_useTaskStore) then { [_taskID, "FAILED"] call BFUNC(taskSetState); GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]]; @@ -202,10 +210,9 @@ GVAR(AttackTaskBaseClass) merge [createHashMapFromArray [ }; if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); }; - } else { - private _targets = _self getOrDefault ["targets", []]; - { deleteVehicle _x } forEach _targets; + }; + if (_finalStatus isEqualTo "succeeded") then { if (_useTaskStore) then { [_taskID, "SUCCEEDED"] call BFUNC(taskSetState); GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]]; diff --git a/arma/server/addons/task/functions/objects/fnc_CargoEntityController.sqf b/arma/server/addons/task/functions/objects/fnc_CargoEntityController.sqf index 1b615db..ecbd2ed 100644 --- a/arma/server/addons/task/functions/objects/fnc_CargoEntityController.sqf +++ b/arma/server/addons/task/functions/objects/fnc_CargoEntityController.sqf @@ -38,6 +38,8 @@ GVAR(CargoEntityController) merge [createHashMapFromArray [ private _taskID = _unit getVariable ["assignedTask", _unit getVariable [QGVAR(assignedTask), ""]]; if (_taskID isEqualTo "") exitWith {}; + private _taskStatus = GVAR(TaskStore) call ["getTaskStatus", [_taskID]]; + if (GVAR(TaskStore) call ["isTerminalStatus", [_taskStatus]]) exitWith {}; if (_unit getVariable [QGVAR(cargoDamageWarned), false]) exitWith {}; _unit setVariable [QGVAR(cargoDamageWarned), true]; @@ -70,7 +72,13 @@ GVAR(CargoEntityController) merge [createHashMapFromArray [ waitUntil { sleep 1; private _entity = _self getOrDefault ["entity", objNull]; - isNull _entity || { !alive _entity } || { damage _entity >= (_self getOrDefault ["damageThreshold", 0.7]) } + !(_self call ["isAssignedTaskOpen", []]) || { isNull _entity } || { !alive _entity } || { damage _entity >= (_self getOrDefault ["damageThreshold", 0.7]) } + }; + + if !(_self call ["isAssignedTaskOpen", []]) exitWith { + _self call ["markAborted", []]; + _self call ["cleanup", []]; + false }; _self call ["markFinished", []]; diff --git a/arma/server/addons/task/functions/objects/fnc_DefendTaskBaseClass.sqf b/arma/server/addons/task/functions/objects/fnc_DefendTaskBaseClass.sqf index 6e29abe..0837891 100644 --- a/arma/server/addons/task/functions/objects/fnc_DefendTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/objects/fnc_DefendTaskBaseClass.sqf @@ -51,10 +51,10 @@ GVAR(DefendTaskBaseClass) merge [createHashMapFromArray [ waitUntil { sleep 1; - GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + !(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] } }; - true + _self call ["isTaskStoreOpen", []] }], ["countBluforInZone", compileFinal { private _defenseZone = _self getOrDefault ["defenseZone", ""]; @@ -68,6 +68,7 @@ GVAR(DefendTaskBaseClass) merge [createHashMapFromArray [ waitUntil { sleep 1; _self call ["trackParticipants", []]; + if !(_self call ["isTaskStoreOpen", []]) exitWith { true }; private _ready = (_self call ["countBluforInZone", []]) >= _minBlufor; if (_ready) then { @@ -82,7 +83,7 @@ GVAR(DefendTaskBaseClass) merge [createHashMapFromArray [ _ready }; - true + _self call ["isTaskStoreOpen", []] }], ["tick", compileFinal { private _taskID = _self getOrDefault ["taskID", ""]; @@ -186,10 +187,18 @@ GVAR(DefendTaskBaseClass) merge [createHashMapFromArray [ false }; - _self call ["waitForAssignment", []]; - _self call ["waitForDefenseStart", []]; + if !(_self call ["waitForAssignment", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before assignment."]]; + _self call ["cleanup", []]; + false + }; + if !(_self call ["waitForDefenseStart", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before defense started."]]; + _self call ["cleanup", []]; + false + }; - while { (_self call ["getStatus", []]) isEqualTo "active" } do { + while { _self call ["isTaskLoopActive", []] } do { _self call ["trackParticipants", []]; private _snapshot = _self call ["tick", []]; @@ -204,9 +213,12 @@ GVAR(DefendTaskBaseClass) merge [createHashMapFromArray [ sleep 1; }; - if ((_self call ["getStatus", []]) isEqualTo "failed") then { + private _finalStatus = _self call ["getStatus", []]; + if (_finalStatus isEqualTo "failed") then { _self call ["handleFailureOutcome", []]; - } else { + }; + + if (_finalStatus isEqualTo "succeeded") then { _self call ["handleSuccessOutcome", []]; }; diff --git a/arma/server/addons/task/functions/objects/fnc_DefenseEnemyController.sqf b/arma/server/addons/task/functions/objects/fnc_DefenseEnemyController.sqf index 8e54c7b..e2253a3 100644 --- a/arma/server/addons/task/functions/objects/fnc_DefenseEnemyController.sqf +++ b/arma/server/addons/task/functions/objects/fnc_DefenseEnemyController.sqf @@ -42,7 +42,13 @@ GVAR(DefenseEnemyController) merge [createHashMapFromArray [ _self call ["markActive", []]; waitUntil { sleep 1; - !(_self call ["isEntityUsable", []]) + !(_self call ["isAssignedTaskOpen", []]) || { !(_self call ["isEntityUsable", []]) } + }; + + if !(_self call ["isAssignedTaskOpen", []]) exitWith { + _self call ["markAborted", []]; + _self call ["cleanup", []]; + false }; _self call ["markFinished", []]; diff --git a/arma/server/addons/task/functions/objects/fnc_DefuseTaskBaseClass.sqf b/arma/server/addons/task/functions/objects/fnc_DefuseTaskBaseClass.sqf index b024e60..3f3e3b0 100644 --- a/arma/server/addons/task/functions/objects/fnc_DefuseTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/objects/fnc_DefuseTaskBaseClass.sqf @@ -103,7 +103,7 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [ waitUntil { sleep 1; _self call ["refreshEntitiesFromStore", []]; - count (_self getOrDefault ["ieds", []]) > 0 + !(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["ieds", []]) > 0 } }; } else { waitUntil { @@ -112,6 +112,8 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [ }; }; + if !(_self call ["isTaskStoreOpen", []]) exitWith { false }; + true }], ["waitForAssignment", compileFinal { @@ -121,10 +123,10 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [ waitUntil { sleep 1; - GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + !(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] } }; - true + _self call ["isTaskStoreOpen", []] }], ["startIedControllers", compileFinal { if ((_self getOrDefault ["iedControllers", []]) isNotEqualTo []) exitWith { true }; @@ -194,15 +196,10 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [ }], ["handleFailureOutcome", compileFinal { private _taskID = _self getOrDefault ["taskID", ""]; - private _ieds = _self getOrDefault ["ieds", []]; - private _protected = _self getOrDefault ["protected", []]; private _rewardData = _self getOrDefault ["rewardData", createHashMap]; private _ratingFail = _rewardData getOrDefault ["ratingFail", 0]; private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false]; - { deleteVehicle _x } forEach _ieds; - { deleteVehicle _x } forEach _protected; - if (_self getOrDefault ["useTaskStore", false]) then { [_taskID, "FAILED"] call BFUNC(taskSetState); GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]]; @@ -219,16 +216,11 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [ }], ["handleSuccessOutcome", compileFinal { private _taskID = _self getOrDefault ["taskID", ""]; - private _ieds = _self getOrDefault ["ieds", []]; - private _protected = _self getOrDefault ["protected", []]; private _rewardData = _self getOrDefault ["rewardData", createHashMap]; private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0]; private _funds = _rewardData getOrDefault ["funds", 0]; private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false]; - { deleteVehicle _x } forEach _ieds; - { deleteVehicle _x } forEach _protected; - if (_self getOrDefault ["useTaskStore", false]) then { [_taskID, "SUCCEEDED"] call BFUNC(taskSetState); GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]]; @@ -245,12 +237,20 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [ true }], ["runLoop", compileFinal { - _self call ["waitForRequiredEntities", []]; - _self call ["waitForAssignment", []]; + if !(_self call ["waitForRequiredEntities", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before required entities registered."]]; + _self call ["cleanup", []]; + false + }; + if !(_self call ["waitForAssignment", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before assignment."]]; + _self call ["cleanup", []]; + false + }; _self call ["startIedControllers", []]; _self call ["markActive", []]; - while { (_self call ["getStatus", []]) isEqualTo "active" } do { + while { _self call ["isTaskLoopActive", []] } do { _self call ["trackParticipants", []]; private _snapshot = _self call ["tick", []]; @@ -265,9 +265,12 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [ sleep 1; }; - if ((_self call ["getStatus", []]) isEqualTo "failed") then { + private _finalStatus = _self call ["getStatus", []]; + if (_finalStatus isEqualTo "failed") then { _self call ["handleFailureOutcome", []]; - } else { + }; + + if (_finalStatus isEqualTo "succeeded") then { _self call ["handleSuccessOutcome", []]; }; diff --git a/arma/server/addons/task/functions/objects/fnc_DeliveryTaskBaseClass.sqf b/arma/server/addons/task/functions/objects/fnc_DeliveryTaskBaseClass.sqf index 15272dc..233739b 100644 --- a/arma/server/addons/task/functions/objects/fnc_DeliveryTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/objects/fnc_DeliveryTaskBaseClass.sqf @@ -57,7 +57,7 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [ sleep 1; _self call ["refreshEntitiesFromStore", []]; _self call ["trackParticipants", []]; - count (_self getOrDefault ["cargo", []]) > 0 + !(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["cargo", []]) > 0 } }; } else { waitUntil { @@ -66,6 +66,8 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [ }; }; + if !(_self call ["isTaskStoreOpen", []]) exitWith { false }; + private _cargo = _self getOrDefault ["cargo", []]; private _taskParams = _self getOrDefault ["taskParams", createHashMap]; private _requiredDelivered = _taskParams getOrDefault ["limitSuccess", -1]; @@ -85,10 +87,10 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [ waitUntil { sleep 1; - GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + !(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] } }; - true + _self call ["isTaskStoreOpen", []] }], ["countDeliveredCargo", compileFinal { private _deliveryZone = _self getOrDefault ["deliveryZone", ""]; @@ -126,13 +128,10 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [ }], ["handleFailureOutcome", compileFinal { private _taskID = _self getOrDefault ["taskID", ""]; - private _cargo = _self getOrDefault ["cargo", []]; private _rewardData = _self getOrDefault ["rewardData", createHashMap]; private _ratingFail = _rewardData getOrDefault ["ratingFail", 0]; private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false]; - { deleteVehicle _x } forEach _cargo; - if (_self getOrDefault ["useTaskStore", false]) then { [_taskID, "FAILED"] call BFUNC(taskSetState); GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]]; @@ -149,14 +148,11 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [ }], ["handleSuccessOutcome", compileFinal { private _taskID = _self getOrDefault ["taskID", ""]; - private _cargo = _self getOrDefault ["cargo", []]; private _rewardData = _self getOrDefault ["rewardData", createHashMap]; private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0]; private _funds = _rewardData getOrDefault ["funds", 0]; private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false]; - { deleteVehicle _x } forEach _cargo; - if (_self getOrDefault ["useTaskStore", false]) then { [_taskID, "SUCCEEDED"] call BFUNC(taskSetState); GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]]; @@ -173,11 +169,19 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [ true }], ["runLoop", compileFinal { - _self call ["waitForRequiredEntities", []]; - _self call ["waitForAssignment", []]; + if !(_self call ["waitForRequiredEntities", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before required entities registered."]]; + _self call ["cleanup", []]; + false + }; + if !(_self call ["waitForAssignment", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before assignment."]]; + _self call ["cleanup", []]; + false + }; _self call ["markActive", []]; - while { (_self call ["getStatus", []]) isEqualTo "active" } do { + while { _self call ["isTaskLoopActive", []] } do { _self call ["trackParticipants", []]; private _snapshot = _self call ["tick", []]; @@ -192,9 +196,12 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [ sleep 1; }; - if ((_self call ["getStatus", []]) isEqualTo "failed") then { + private _finalStatus = _self call ["getStatus", []]; + if (_finalStatus isEqualTo "failed") then { _self call ["handleFailureOutcome", []]; - } else { + }; + + if (_finalStatus isEqualTo "succeeded") then { _self call ["handleSuccessOutcome", []]; }; diff --git a/arma/server/addons/task/functions/objects/fnc_DestroyTaskBaseClass.sqf b/arma/server/addons/task/functions/objects/fnc_DestroyTaskBaseClass.sqf index 29dd41f..53f2e42 100644 --- a/arma/server/addons/task/functions/objects/fnc_DestroyTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/objects/fnc_DestroyTaskBaseClass.sqf @@ -52,7 +52,7 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [ sleep 1; _self call ["refreshEntitiesFromStore", []]; _self call ["trackParticipants", []]; - count (_self getOrDefault ["targets", []]) > 0 + !(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["targets", []]) > 0 } }; } else { waitUntil { @@ -61,6 +61,8 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [ }; }; + if !(_self call ["isTaskStoreOpen", []]) exitWith { false }; + private _targets = _self getOrDefault ["targets", []]; private _taskParams = _self getOrDefault ["taskParams", createHashMap]; private _requiredDestroyed = _taskParams getOrDefault ["limitSuccess", -1]; @@ -76,10 +78,10 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [ waitUntil { sleep 1; - GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + !(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] } }; - true + _self call ["isTaskStoreOpen", []] }], ["countDestroyedTargets", compileFinal { private _targets = _self getOrDefault ["targets", []]; @@ -106,13 +108,10 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [ }], ["handleFailureOutcome", compileFinal { private _taskID = _self getOrDefault ["taskID", ""]; - private _targets = _self getOrDefault ["targets", []]; private _rewardData = _self getOrDefault ["rewardData", createHashMap]; private _ratingFail = _rewardData getOrDefault ["ratingFail", 0]; private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false]; - { deleteVehicle _x } forEach _targets; - if (_self getOrDefault ["useTaskStore", false]) then { [_taskID, "FAILED"] call BFUNC(taskSetState); GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]]; @@ -129,14 +128,11 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [ }], ["handleSuccessOutcome", compileFinal { private _taskID = _self getOrDefault ["taskID", ""]; - private _targets = _self getOrDefault ["targets", []]; private _rewardData = _self getOrDefault ["rewardData", createHashMap]; private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0]; private _funds = _rewardData getOrDefault ["funds", 0]; private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false]; - { deleteVehicle _x } forEach _targets; - if (_self getOrDefault ["useTaskStore", false]) then { [_taskID, "SUCCEEDED"] call BFUNC(taskSetState); GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]]; @@ -153,11 +149,19 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [ true }], ["runLoop", compileFinal { - _self call ["waitForRequiredEntities", []]; - _self call ["waitForAssignment", []]; + if !(_self call ["waitForRequiredEntities", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before required entities registered."]]; + _self call ["cleanup", []]; + false + }; + if !(_self call ["waitForAssignment", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before assignment."]]; + _self call ["cleanup", []]; + false + }; _self call ["markActive", []]; - while { (_self call ["getStatus", []]) isEqualTo "active" } do { + while { _self call ["isTaskLoopActive", []] } do { _self call ["trackParticipants", []]; private _snapshot = _self call ["tick", []]; @@ -172,9 +176,12 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [ sleep 1; }; - if ((_self call ["getStatus", []]) isEqualTo "failed") then { + private _finalStatus = _self call ["getStatus", []]; + if (_finalStatus isEqualTo "failed") then { _self call ["handleFailureOutcome", []]; - } else { + }; + + if (_finalStatus isEqualTo "succeeded") then { _self call ["handleSuccessOutcome", []]; }; diff --git a/arma/server/addons/task/functions/objects/fnc_EntityControllerBaseClass.sqf b/arma/server/addons/task/functions/objects/fnc_EntityControllerBaseClass.sqf index 88cf092..ecbd083 100644 --- a/arma/server/addons/task/functions/objects/fnc_EntityControllerBaseClass.sqf +++ b/arma/server/addons/task/functions/objects/fnc_EntityControllerBaseClass.sqf @@ -76,6 +76,20 @@ GVAR(EntityControllerBaseClass) = createHashMapFromArray [ private _entity = _self getOrDefault ["entity", objNull]; !isNull _entity && { alive _entity } }], + ["isTerminalStatus", compileFinal { + params [["_status", "", [""]]]; + + (toLowerANSI _status) in ["failed", "succeeded"] + }], + ["isAssignedTaskOpen", compileFinal { + private _taskID = _self getOrDefault ["taskID", ""]; + if (_taskID isEqualTo "" || { isNil QGVAR(TaskStore) }) exitWith { true }; + + private _status = GVAR(TaskStore) call ["getTaskStatus", [_taskID]]; + if (_status isEqualTo "") exitWith { true }; + + !(_self call ["isTerminalStatus", [_status]]) + }], ["assignTaskVariable", compileFinal { private _entity = _self getOrDefault ["entity", objNull]; private _taskID = _self getOrDefault ["taskID", ""]; diff --git a/arma/server/addons/task/functions/objects/fnc_HVTEntityController.sqf b/arma/server/addons/task/functions/objects/fnc_HVTEntityController.sqf index cd758c8..b6e69fe 100644 --- a/arma/server/addons/task/functions/objects/fnc_HVTEntityController.sqf +++ b/arma/server/addons/task/functions/objects/fnc_HVTEntityController.sqf @@ -52,12 +52,19 @@ GVAR(HVTEntityController) merge [createHashMapFromArray [ private _capturer = objNull; waitUntil { sleep 1; + if !(_self call ["isAssignedTaskOpen", []]) exitWith { true }; if !(_self call ["isEntityUsable", []]) exitWith { true }; _capturer = _self call ["findNearbyCapturer", []]; !isNull _capturer }; + if !(_self call ["isAssignedTaskOpen", []]) exitWith { + _self call ["markAborted", []]; + _self call ["cleanup", []]; + false + }; + if !(_self call ["isEntityUsable", []]) exitWith { _self call ["markAborted", []]; _self call ["cleanup", []]; diff --git a/arma/server/addons/task/functions/objects/fnc_HVTTaskBaseClass.sqf b/arma/server/addons/task/functions/objects/fnc_HVTTaskBaseClass.sqf index 142d9a0..2adc369 100644 --- a/arma/server/addons/task/functions/objects/fnc_HVTTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/objects/fnc_HVTTaskBaseClass.sqf @@ -70,7 +70,7 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [ sleep 1; _self call ["refreshEntitiesFromStore", []]; _self call ["trackParticipants", []]; - count (_self getOrDefault ["hvts", []]) > 0 + !(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["hvts", []]) > 0 } }; } else { waitUntil { @@ -79,6 +79,8 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [ }; }; + if !(_self call ["isTaskStoreOpen", []]) exitWith { false }; + private _hvts = _self getOrDefault ["hvts", []]; private _taskParams = _self getOrDefault ["taskParams", createHashMap]; private _required = _taskParams getOrDefault ["limitSuccess", -1]; @@ -122,10 +124,10 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [ waitUntil { sleep 1; - GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + !(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] } }; - true + _self call ["isTaskStoreOpen", []] }], ["tick", compileFinal { private _startedAt = _self getOrDefault ["startedAt", -1]; @@ -161,13 +163,10 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [ }], ["handleFailureOutcome", compileFinal { private _taskID = _self getOrDefault ["taskID", ""]; - private _hvts = _self getOrDefault ["hvts", []]; private _rewardData = _self getOrDefault ["rewardData", createHashMap]; private _ratingFail = _rewardData getOrDefault ["ratingFail", 0]; private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false]; - { deleteVehicle _x } forEach _hvts; - if (_self getOrDefault ["useTaskStore", false]) then { [_taskID, "FAILED"] call BFUNC(taskSetState); GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]]; @@ -184,14 +183,11 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [ }], ["handleSuccessOutcome", compileFinal { private _taskID = _self getOrDefault ["taskID", ""]; - private _hvts = _self getOrDefault ["hvts", []]; private _rewardData = _self getOrDefault ["rewardData", createHashMap]; private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0]; private _funds = _rewardData getOrDefault ["funds", 0]; private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false]; - { deleteVehicle _x } forEach _hvts; - if (_self getOrDefault ["useTaskStore", false]) then { [_taskID, "SUCCEEDED"] call BFUNC(taskSetState); GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]]; @@ -208,12 +204,20 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [ true }], ["runLoop", compileFinal { - _self call ["waitForRequiredEntities", []]; - _self call ["waitForAssignment", []]; + if !(_self call ["waitForRequiredEntities", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before required entities registered."]]; + _self call ["cleanup", []]; + false + }; + if !(_self call ["waitForAssignment", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before assignment."]]; + _self call ["cleanup", []]; + false + }; _self call ["startHvtControllers", []]; _self call ["markActive", []]; - while { (_self call ["getStatus", []]) isEqualTo "active" } do { + while { _self call ["isTaskLoopActive", []] } do { _self call ["trackParticipants", []]; private _snapshot = _self call ["tick", []]; @@ -228,9 +232,12 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [ sleep 1; }; - if ((_self call ["getStatus", []]) isEqualTo "failed") then { + private _finalStatus = _self call ["getStatus", []]; + if (_finalStatus isEqualTo "failed") then { _self call ["handleFailureOutcome", []]; - } else { + }; + + if (_finalStatus isEqualTo "succeeded") then { _self call ["handleSuccessOutcome", []]; }; diff --git a/arma/server/addons/task/functions/objects/fnc_HostageEntityController.sqf b/arma/server/addons/task/functions/objects/fnc_HostageEntityController.sqf index 0d70b34..ad42be7 100644 --- a/arma/server/addons/task/functions/objects/fnc_HostageEntityController.sqf +++ b/arma/server/addons/task/functions/objects/fnc_HostageEntityController.sqf @@ -103,12 +103,19 @@ GVAR(HostageEntityController) merge [createHashMapFromArray [ waitUntil { sleep 1; + if !(_self call ["isAssignedTaskOpen", []]) exitWith { true }; if (isNull _entity || { !alive _entity }) exitWith { true }; _rescuer = _self call ["findNearbyRescuer", []]; !isNull _rescuer }; + if !(_self call ["isAssignedTaskOpen", []]) exitWith { + _self call ["markAborted", []]; + _self call ["cleanup", []]; + false + }; + if (isNull _entity || { !alive _entity }) exitWith { _self call ["markAborted", []]; _self call ["cleanup", []]; diff --git a/arma/server/addons/task/functions/objects/fnc_HostageTaskBaseClass.sqf b/arma/server/addons/task/functions/objects/fnc_HostageTaskBaseClass.sqf index c8193aa..5ef7c03 100644 --- a/arma/server/addons/task/functions/objects/fnc_HostageTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/objects/fnc_HostageTaskBaseClass.sqf @@ -150,14 +150,15 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [ waitUntil { sleep 1; _self call ["refreshEntitiesFromStore", []]; - count (_self getOrDefault ["hostages", []]) > 0 + !(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["hostages", []]) > 0 } }; + if !(_self call ["isTaskStoreOpen", []]) exitWith { false }; waitUntil { sleep 1; _self call ["refreshEntitiesFromStore", []]; _self call ["trackParticipants", []]; - count (_self getOrDefault ["shooters", []]) > 0 + !(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["shooters", []]) > 0 } }; } else { waitUntil { @@ -171,6 +172,8 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [ }; }; + if !(_self call ["isTaskStoreOpen", []]) exitWith { false }; + private _hostages = _self getOrDefault ["hostages", []]; private _taskParams = _self getOrDefault ["taskParams", createHashMap]; private _requiredRescues = _taskParams getOrDefault ["limitSuccess", -1]; @@ -190,10 +193,10 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [ waitUntil { sleep 1; - GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + !(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] } }; - true + _self call ["isTaskStoreOpen", []] }], ["countFreedHostages", compileFinal { private _playerGroups = allPlayers apply { group _x }; @@ -290,11 +293,9 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [ sleep 5; }; - { deleteVehicle _x } forEach _hostages; - { deleteVehicle _x } forEach _shooters; - if (_useTaskStore) then { [_taskID, "FAILED"] call BFUNC(taskSetState); + GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]]; sleep 1; @@ -308,17 +309,12 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [ }], ["handleSuccessOutcome", compileFinal { private _taskID = _self getOrDefault ["taskID", ""]; - private _hostages = _self getOrDefault ["hostages", []]; - private _shooters = _self getOrDefault ["shooters", []]; private _rewardData = _self getOrDefault ["rewardData", createHashMap]; private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0]; private _funds = _rewardData getOrDefault ["funds", 0]; private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false]; private _useTaskStore = _self getOrDefault ["useTaskStore", false]; - { deleteVehicle _x } forEach _hostages; - { deleteVehicle _x } forEach _shooters; - if (_useTaskStore) then { [_taskID, "SUCCEEDED"] call BFUNC(taskSetState); GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]]; @@ -335,12 +331,20 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [ true }], ["runLoop", compileFinal { - _self call ["waitForRequiredEntities", []]; - _self call ["waitForAssignment", []]; + if !(_self call ["waitForRequiredEntities", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before required entities registered."]]; + _self call ["cleanup", []]; + false + }; + if !(_self call ["waitForAssignment", []]) exitWith { + _self call ["markAborted", ["Task reached terminal status before assignment."]]; + _self call ["cleanup", []]; + false + }; _self call ["startHostageControllers", []]; _self call ["markActive", []]; - while { (_self call ["getStatus", []]) isEqualTo "active" } do { + while { _self call ["isTaskLoopActive", []] } do { _self call ["trackParticipants", []]; private _snapshot = _self call ["tick", []]; @@ -355,9 +359,12 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [ sleep 1; }; - if ((_self call ["getStatus", []]) isEqualTo "failed") then { + private _finalStatus = _self call ["getStatus", []]; + if (_finalStatus isEqualTo "failed") then { _self call ["handleFailureOutcome", []]; - } else { + }; + + if (_finalStatus isEqualTo "succeeded") then { _self call ["handleSuccessOutcome", []]; }; diff --git a/arma/server/addons/task/functions/objects/fnc_IEDEntityController.sqf b/arma/server/addons/task/functions/objects/fnc_IEDEntityController.sqf index 2613431..b46926d 100644 --- a/arma/server/addons/task/functions/objects/fnc_IEDEntityController.sqf +++ b/arma/server/addons/task/functions/objects/fnc_IEDEntityController.sqf @@ -29,10 +29,10 @@ GVAR(IEDEntityController) merge [createHashMapFromArray [ waitUntil { sleep 1; - GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + !(_self call ["isAssignedTaskOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] } }; - true + _self call ["isAssignedTaskOpen", []] }], ["playCountdownSound", compileFinal { params [["_timeRemaining", 0, [0]]]; @@ -67,20 +67,35 @@ GVAR(IEDEntityController) merge [createHashMapFromArray [ false }; - _self call ["waitForAssignment", []]; + if !(_self call ["waitForAssignment", []]) exitWith { + _self call ["markAborted", []]; + _self call ["cleanup", []]; + false + }; + if !(_self call ["isAssignedTaskOpen", []]) exitWith { + _self call ["markAborted", []]; + _self call ["cleanup", []]; + false + }; _self call ["markActive", []]; - while { (_self call ["isEntityUsable", []]) && { _countdown > 0 } } do { + while { (_self call ["isAssignedTaskOpen", []]) && { (_self call ["isEntityUsable", []]) && { _countdown > 0 } } } do { _self call ["playCountdownSound", [_countdown]]; _countdown = _countdown - 1; _self set ["countdown", _countdown]; sleep 1; }; - if ((_self call ["isEntityUsable", []]) && { _countdown <= 0 }) then { + if ((_self call ["isAssignedTaskOpen", []]) && { (_self call ["isEntityUsable", []]) && { _countdown <= 0 } }) then { _self call ["detonate", []]; }; + if !(_self call ["isAssignedTaskOpen", []]) exitWith { + _self call ["markAborted", []]; + _self call ["cleanup", []]; + false + }; + _self call ["markFinished", []]; _self call ["cleanup", []]; true diff --git a/arma/server/addons/task/functions/objects/fnc_ProtectedEntityController.sqf b/arma/server/addons/task/functions/objects/fnc_ProtectedEntityController.sqf index 5de2f15..1fc71dd 100644 --- a/arma/server/addons/task/functions/objects/fnc_ProtectedEntityController.sqf +++ b/arma/server/addons/task/functions/objects/fnc_ProtectedEntityController.sqf @@ -31,7 +31,13 @@ GVAR(ProtectedEntityController) merge [createHashMapFromArray [ _self call ["markActive", []]; waitUntil { sleep 1; - !(_self call ["isEntityUsable", []]) + !(_self call ["isAssignedTaskOpen", []]) || { !(_self call ["isEntityUsable", []]) } + }; + + if !(_self call ["isAssignedTaskOpen", []]) exitWith { + _self call ["markAborted", []]; + _self call ["cleanup", []]; + false }; _self call ["markFinished", []]; diff --git a/arma/server/addons/task/functions/objects/fnc_ShooterEntityController.sqf b/arma/server/addons/task/functions/objects/fnc_ShooterEntityController.sqf index 06a8fdc..d644f03 100644 --- a/arma/server/addons/task/functions/objects/fnc_ShooterEntityController.sqf +++ b/arma/server/addons/task/functions/objects/fnc_ShooterEntityController.sqf @@ -31,7 +31,13 @@ GVAR(ShooterEntityController) merge [createHashMapFromArray [ _self call ["markActive", []]; waitUntil { sleep 1; - !(_self call ["isEntityUsable", []]) + !(_self call ["isAssignedTaskOpen", []]) || { !(_self call ["isEntityUsable", []]) } + }; + + if !(_self call ["isAssignedTaskOpen", []]) exitWith { + _self call ["markAborted", []]; + _self call ["cleanup", []]; + false }; _self call ["markFinished", []]; diff --git a/arma/server/addons/task/functions/objects/fnc_TargetEntityController.sqf b/arma/server/addons/task/functions/objects/fnc_TargetEntityController.sqf index bb965e9..300d483 100644 --- a/arma/server/addons/task/functions/objects/fnc_TargetEntityController.sqf +++ b/arma/server/addons/task/functions/objects/fnc_TargetEntityController.sqf @@ -31,7 +31,13 @@ GVAR(TargetEntityController) merge [createHashMapFromArray [ _self call ["markActive", []]; waitUntil { sleep 1; - !(_self call ["isEntityUsable", []]) + !(_self call ["isAssignedTaskOpen", []]) || { !(_self call ["isEntityUsable", []]) } + }; + + if !(_self call ["isAssignedTaskOpen", []]) exitWith { + _self call ["markAborted", []]; + _self call ["cleanup", []]; + false }; _self call ["markFinished", []]; diff --git a/arma/server/addons/task/functions/objects/fnc_TaskCatalogStore.sqf b/arma/server/addons/task/functions/objects/fnc_TaskCatalogStore.sqf index ec0ee4c..b719c5a 100644 --- a/arma/server/addons/task/functions/objects/fnc_TaskCatalogStore.sqf +++ b/arma/server/addons/task/functions/objects/fnc_TaskCatalogStore.sqf @@ -123,6 +123,11 @@ GVAR(TaskCatalogStore) = createHashMapObject [[ (_self call ["getTaskStatus", [_taskID]]) isEqualTo "succeeded" }], + ["isTerminalStatus", compileFinal { + params [["_status", "", [""]]]; + + (toLowerANSI _status) in ["failed", "succeeded"] + }], ["areTaskPrerequisitesSatisfied", compileFinal { params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]]; @@ -359,6 +364,28 @@ GVAR(TaskCatalogStore) = createHashMapObject [[ if (_taskID isEqualTo "" || { _status isEqualTo "" }) exitWith { false }; private _normalizedStatus = toLowerANSI _status; + private _currentStatus = toLowerANSI (_self call ["getTaskStatus", [_taskID]]); + private _currentIsTerminal = _self call ["isTerminalStatus", [_currentStatus]]; + private _nextIsTerminal = _self call ["isTerminalStatus", [_normalizedStatus]]; + + if (_currentIsTerminal && { _currentStatus isNotEqualTo _normalizedStatus }) exitWith { + ["WARNING", format [ + "Task status transition blocked for %1: terminal status %2 cannot be changed to %3 without clearing task state first.", + _taskID, + _currentStatus, + _normalizedStatus + ]] call EFUNC(common,log); + false + }; + + if (_currentIsTerminal && { _nextIsTerminal }) exitWith { + if (_normalizedStatus isEqualTo "succeeded") then { + _self call ["markTaskCompleted", [_taskID]]; + _self call ["unlockDependentTasks", [_taskID]]; + }; + true + }; + private _runtimeCatalogRegistry = _self getOrDefault ["runtimeCatalogRegistry", createHashMap]; private _runtimeEntry = +(_runtimeCatalogRegistry getOrDefault [_taskID, createHashMap]); if (_runtimeEntry isNotEqualTo createHashMap) then { diff --git a/arma/server/addons/task/functions/objects/fnc_TaskInstanceBaseClass.sqf b/arma/server/addons/task/functions/objects/fnc_TaskInstanceBaseClass.sqf index f035f79..983169f 100644 --- a/arma/server/addons/task/functions/objects/fnc_TaskInstanceBaseClass.sqf +++ b/arma/server/addons/task/functions/objects/fnc_TaskInstanceBaseClass.sqf @@ -83,6 +83,50 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [ ["getStatus", compileFinal { _self getOrDefault ["status", "created"] }], + ["isTerminalStatus", compileFinal { + params [["_status", "", [""]]]; + + (toLowerANSI _status) in ["failed", "succeeded"] + }], + ["getStoreStatus", compileFinal { + private _taskID = _self getOrDefault ["taskID", ""]; + if (_taskID isEqualTo "" || { !(_self getOrDefault ["useTaskStore", false]) } || { isNil QGVAR(TaskStore) }) exitWith { "" }; + + GVAR(TaskStore) call ["getTaskStatus", [_taskID]] + }], + ["canTransitionToTerminal", compileFinal { + params [["_nextStatus", "", [""]]]; + + private _normalizedNext = toLowerANSI _nextStatus; + if !(_self call ["isTerminalStatus", [_normalizedNext]]) exitWith { true }; + + private _currentStatus = toLowerANSI (_self getOrDefault ["status", "created"]); + if ((_self call ["isTerminalStatus", [_currentStatus]]) && { _currentStatus isNotEqualTo _normalizedNext }) exitWith { false }; + + private _storeStatus = toLowerANSI (_self call ["getStoreStatus", []]); + if ((_self call ["isTerminalStatus", [_storeStatus]]) && { _storeStatus isNotEqualTo _normalizedNext }) exitWith { false }; + + true + }], + ["isTaskLoopActive", compileFinal { + if ((_self call ["getStatus", []]) isNotEqualTo "active") exitWith { false }; + + private _storeStatus = toLowerANSI (_self call ["getStoreStatus", []]); + if (_storeStatus isEqualTo "") exitWith { true }; + + if (_self call ["isTerminalStatus", [_storeStatus]]) exitWith { + _self call ["markAborted", [format ["Task store reached terminal status '%1'.", _storeStatus]]]; + false + }; + + true + }], + ["isTaskStoreOpen", compileFinal { + private _storeStatus = toLowerANSI (_self call ["getStoreStatus", []]); + if (_storeStatus isEqualTo "") exitWith { true }; + + !(_self call ["isTerminalStatus", [_storeStatus]]) + }], ["getRewardData", compileFinal { _self getOrDefault ["rewardData", createHashMap] }], @@ -162,6 +206,8 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [ ["markSucceeded", compileFinal { params [["_resultSnapshot", createHashMap, [createHashMap]]]; + if !(_self call ["canTransitionToTerminal", ["succeeded"]]) exitWith { false }; + _self set ["status", "succeeded"]; _self set ["finishedAt", serverTime]; _self set ["resultSnapshot", _resultSnapshot]; @@ -173,6 +219,8 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [ ["markFailed", compileFinal { params [["_reason", "", [""]], ["_resultSnapshot", createHashMap, [createHashMap]]]; + if !(_self call ["canTransitionToTerminal", ["failed"]]) exitWith { false }; + _self set ["status", "failed"]; _self set ["finishedAt", serverTime]; _self set ["failureReason", _reason]; @@ -182,6 +230,14 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [ }; true }], + ["markAborted", compileFinal { + params [["_reason", "", [""]]]; + + _self set ["status", "aborted"]; + _self set ["finishedAt", serverTime]; + _self set ["failureReason", _reason]; + true + }], ["cleanup", compileFinal { _self call ["unregisterInstance", []] }], diff --git a/arma/server/addons/transport/functions/fnc_initTransportService.sqf b/arma/server/addons/transport/functions/fnc_initTransportService.sqf index e2c352b..ff3e36a 100644 --- a/arma/server/addons/transport/functions/fnc_initTransportService.sqf +++ b/arma/server/addons/transport/functions/fnc_initTransportService.sqf @@ -39,6 +39,23 @@ GVAR(TransportServiceBase) = compileFinal createHashMapFromArray [ ["INFO", "Transport Service Initialized!"] call EFUNC(common,log); true }], + ["numberSetting", compileFinal { + params [["_name", "", [""]], ["_default", 0, [0]]]; + + private _configDefault = _default; + private _missionConfig = missionConfigFile >> "CfgMissions"; + if !(isClass _missionConfig) then { _missionConfig = configFile >> "CfgMissions"; }; + private _serviceConfig = _missionConfig >> "ServicePricing"; + if (isNumber (_serviceConfig >> _name)) then { + _configDefault = getNumber (_serviceConfig >> _name); + }; + + private _paramValue = [_name, _configDefault] call BIS_fnc_getParamValue; + private _value = missionNamespace getVariable [_name, _paramValue]; + if (_value isEqualType "") exitWith { (parseNumber _value) max 0 }; + if (_value isEqualType 0) exitWith { _value max 0 }; + _configDefault + }], ["notify", compileFinal { params [["_unit", objNull, [objNull]], ["_type", "info", [""]], ["_title", "Transport", [""]], ["_message", "", [""]]]; @@ -120,8 +137,10 @@ GVAR(TransportServiceBase) = compileFinal createHashMapFromArray [ ["getCost", compileFinal { params [["_fromNode", objNull, [objNull]], ["_toNode", objNull, [objNull]], ["_options", createHashMap, [createHashMap]]]; - private _baseFare = _options getOrDefault ["baseFare", _self getOrDefault ["baseFare", 100]]; - private _pricePerKm = _options getOrDefault ["pricePerKm", _self getOrDefault ["pricePerKm", 50]]; + private _baseFareDefault = _self call ["numberSetting", ["transportBaseFare", _self getOrDefault ["baseFare", 100]]]; + private _pricePerKmDefault = _self call ["numberSetting", ["transportPricePerKm", _self getOrDefault ["pricePerKm", 50]]]; + private _baseFare = _options getOrDefault ["baseFare", _baseFareDefault]; + private _pricePerKm = _options getOrDefault ["pricePerKm", _pricePerKmDefault]; private _distanceMeters = _fromNode distance2D _toNode; round (_baseFare + ((_distanceMeters / 1000) * _pricePerKm)) diff --git a/docs/ECONOMY_USAGE_GUIDE.md b/docs/ECONOMY_USAGE_GUIDE.md index eefa8cc..35226b0 100644 --- a/docs/ECONOMY_USAGE_GUIDE.md +++ b/docs/ECONOMY_USAGE_GUIDE.md @@ -29,6 +29,10 @@ calculates missing fuel from the vehicle config `fuelCapacity`, charges the player's organization, and fills the vehicle only after the organization charge succeeds. +The refuel price per liter is controlled by `fuelCost`. The mission setup UI +can override it at startup; otherwise a mission `Params` entry named +`fuelCost` or `CfgMissions >> ServicePricing >> fuelCost` is used. + ## Repair Repair is organization-funded. @@ -45,6 +49,9 @@ The target is only repaired after the organization charge succeeds. The client garage UI forwards selected nearby vehicle repair requests through the same event. +The default repair charge is controlled by `serviceRepairCost`. A direct +service event can still pass a concrete `_cost` to override that request. + ## Rearm Rearm is organization-funded. @@ -63,6 +70,9 @@ turrets, so the service broadcasts the ammo reset after billing succeeds. The client garage UI forwards selected nearby vehicle rearm requests through the same event. +The default rearm charge is controlled by `serviceRearmCost`. A direct service +event can still pass a concrete `_cost` to override that request. + ## Medical Medical is player-funded first. @@ -79,6 +89,15 @@ The heal only completes after one of those charges succeeds. If personal billing is unavailable, the heal does not fall back to organization funds because the server cannot verify that the player is unable to cover the fee. +Medical pricing uses: + +- `medicalHealCost` for heal billing. +- `medicalSpawnCost` for medical respawn billing. Respawn billing is + best-effort so a failed charge does not block the respawn flow. + +Both values can be set in the mission setup UI, mission `Params`, or +`CfgMissions >> ServicePricing`. + ## Medical Debt Repayment Medical fallback debt uses the existing organization credit-line repayment diff --git a/docs/MISSION_DESIGNER_GUIDE.md b/docs/MISSION_DESIGNER_GUIDE.md index 8876901..f2caa5d 100644 --- a/docs/MISSION_DESIGNER_GUIDE.md +++ b/docs/MISSION_DESIGNER_GUIDE.md @@ -755,14 +755,21 @@ 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, time limits, and a generator -provider preference. It does not enable or disable generated missions; use the -CBA setting for that policy. +provider preference. It also exposes service pricing for medical spawn, heal, +repair, rearm, refuel, and transport defaults. 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. +Service pricing fallback values live under `CfgMissions >> ServicePricing`. +Mission `Params` with matching names, such as `medicalHealCost`, +`serviceRepairCost`, `serviceRearmCost`, `fuelCost`, `transportBaseFare`, and +`transportPricePerKm`, are read before the setup UI hydrates so mission makers +can keep a non-UI backup. + The setup UI stores the provider preference as `builtin` or `custom`. CAD/manual generated task requests use the task provider registry and route to the selected provider. Custom generators should register a provider or create CAD-visible diff --git a/docs/TASK_USAGE_GUIDE.md b/docs/TASK_USAGE_GUIDE.md index d4c1a36..cff8546 100644 --- a/docs/TASK_USAGE_GUIDE.md +++ b/docs/TASK_USAGE_GUIDE.md @@ -188,14 +188,21 @@ 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, time limits, and a generator provider preference. Generator -enablement remains controlled by the CBA setting above. +penalties, time limits, service pricing, and a generator provider preference. +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`. +Service price defaults are stored in `CfgMissions >> ServicePricing`. Mission +`Params` with matching names override those defaults before the UI opens, and +submitted UI values override both. The supported names are +`medicalSpawnCost`, `medicalHealCost`, `serviceRepairCost`, +`serviceRearmCost`, `fuelCost`, `transportBaseFare`, and +`transportPricePerKm`. + The setup UI stores the provider preference in `forge_server_task_generatorProvider` as `builtin` or `custom`. CAD/manual generated task requests use the task provider registry and route to the selected diff --git a/docs/TRANSPORT_SERVICE_GUIDE.md b/docs/TRANSPORT_SERVICE_GUIDE.md index f4301f2..860b4b0 100644 --- a/docs/TRANSPORT_SERVICE_GUIDE.md +++ b/docs/TRANSPORT_SERVICE_GUIDE.md @@ -89,6 +89,12 @@ nearby vehicles, ships, aircraft, and player units. The scan ignores: Use `transport_vehicle*` names for the actual boat, ferry, aircraft, or set dressing object that should not be moved as cargo. +## Pricing + +Default transport pricing comes from the mission setup UI or matching mission +`Params` entries named `transportBaseFare` and `transportPricePerKm`. If neither +is set, `CfgMissions >> ServicePricing` provides the fallback. + ## Optional Per-Node Overrides The default naming convention should cover normal missions. If a specific @@ -108,7 +114,8 @@ this setVariable ["transportCargoRadius", 25, true]; this setVariable ["transportIncludeCargo", true, true]; ``` -Only use overrides when the default `transport*` convention is not appropriate. +Only use overrides when the default `transport*` convention or mission-level +pricing is not appropriate. ## Reference Images diff --git a/lib/models/src/actor.rs b/lib/models/src/actor.rs index 51e8b53..5a9da7a 100644 --- a/lib/models/src/actor.rs +++ b/lib/models/src/actor.rs @@ -1,7 +1,4 @@ -use arma_rs::{ - FromArma, IntoArma, - loadout::{AssignedItems, InventoryItem, Loadout as ArmaLoadout}, -}; +use arma_rs::{FromArma, IntoArma, loadout::Loadout as ArmaLoadout}; use forge_shared::{ ActorValidationError, arma_value_to_json, generate_email, generate_phone_number, }; @@ -128,26 +125,7 @@ impl Actor { } fn default_loadout_json() -> serde_json::Value { - let mut loadout = ArmaLoadout::default(); - - let uniform = loadout.uniform_mut(); - uniform.set_class("U_BG_Guerrilla_6_1".to_string()); - - let uniform_items = uniform.items_mut().unwrap(); - uniform_items.push(InventoryItem::new_item("FirstAidKit".to_string(), 1)); - - loadout.set_headgear("H_Cap_blk_ION".to_string()); - - let mut items = AssignedItems::default(); - items.set_map("ItemMap".to_string()); - items.set_terminal("ItemGPS".to_string()); - items.set_radio("ItemRadio".to_string()); - items.set_compass("ItemCompass".to_string()); - items.set_watch("ItemWatch".to_string()); - loadout.set_assigned_items(items); - - let arma_value = loadout.to_arma(); - arma_value_to_json(&arma_value) + serde_json::Value::Array(Vec::new()) } pub fn get_loadout(&self) -> Result { diff --git a/lib/models/src/v_garage.rs b/lib/models/src/v_garage.rs index dcb528b..6c834e7 100644 --- a/lib/models/src/v_garage.rs +++ b/lib/models/src/v_garage.rs @@ -23,12 +23,8 @@ pub struct VGarage { impl VGarage { pub fn new() -> Self { - Self::default_unlocks() - } - - fn default_unlocks() -> Self { Self { - cars: vec!["B_Quadbike_01_F".to_string()], + cars: Vec::new(), armor: Vec::new(), helis: Vec::new(), planes: Vec::new(), diff --git a/lib/models/src/v_locker.rs b/lib/models/src/v_locker.rs index f9f66a3..e50751a 100644 --- a/lib/models/src/v_locker.rs +++ b/lib/models/src/v_locker.rs @@ -19,43 +19,11 @@ pub struct VLocker { impl VLocker { pub fn new() -> Self { - Self::default_unlocks() - } - - fn default_unlocks() -> Self { Self { - items: vec![ - "FirstAidKit".to_string(), - "G_Combat".to_string(), - "H_Cap_blk_ION".to_string(), - "H_HelmetB".to_string(), - "ACE_EarPlugs".to_string(), - "ItemCompass".to_string(), - "ItemGPS".to_string(), - "ItemMap".to_string(), - "ItemRadio".to_string(), - "ItemWatch".to_string(), - "U_BG_Guerrilla_6_1".to_string(), - "V_TacVest_oli".to_string(), - ], - weapons: vec!["arifle_MX_F".to_string(), "hgun_P07_F".to_string()], - magazines: vec![ - "16Rnd_9x21_Mag".to_string(), - "30Rnd_65x39_caseless_black_mag".to_string(), - "Chemlight_blue".to_string(), - "Chemlight_green".to_string(), - "Chemlight_red".to_string(), - "Chemlight_yellow".to_string(), - "HandGrenade".to_string(), - "SmokeShell".to_string(), - "SmokeShellBlue".to_string(), - "SmokeShellGreen".to_string(), - "SmokeShellOrange".to_string(), - "SmokeShellPurple".to_string(), - "SmokeShellRed".to_string(), - "SmokeShellYellow".to_string(), - ], - backpacks: vec!["B_AssaultPack_rgr".to_string()], + items: Vec::new(), + weapons: Vec::new(), + magazines: Vec::new(), + backpacks: Vec::new(), } }