Compare commits
5 Commits
d4d1f251c4
...
4f54edf467
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f54edf467 | ||
|
|
d61cb86d3a | ||
|
|
bfb317eb5c | ||
|
|
623f718caf | ||
|
|
6229f56ba4 |
@ -79,14 +79,31 @@ switch (_event) do {
|
|||||||
hint "Transport destination is no longer available.";
|
hint "Transport destination is no longer available.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _transportSetting = {
|
||||||
|
params [["_name", "", [""]], ["_default", 0, [0]]];
|
||||||
|
|
||||||
|
private _configDefault = _default;
|
||||||
|
private _serviceConfig = missionConfigFile >> "CfgServicePricing";
|
||||||
|
if !(isClass _serviceConfig) then { _serviceConfig = configFile >> "CfgServicePricing"; };
|
||||||
|
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 [
|
private _options = createHashMapFromArray [
|
||||||
["label", _data getOrDefault ["label", "Transport"]],
|
["label", _data getOrDefault ["label", "Transport"]],
|
||||||
["nodePrefix", _data getOrDefault ["nodePrefix", "transport"]],
|
["nodePrefix", _data getOrDefault ["nodePrefix", "transport"]],
|
||||||
["vehiclePrefix", _data getOrDefault ["vehiclePrefix", "transport_vehicle"]],
|
["vehiclePrefix", _data getOrDefault ["vehiclePrefix", "transport_vehicle"]],
|
||||||
["arrivalPrefix", _data getOrDefault ["arrivalPrefix", "transport_arrival"]],
|
["arrivalPrefix", _data getOrDefault ["arrivalPrefix", "transport_arrival"]],
|
||||||
["maxIndexedNodes", _data getOrDefault ["maxIndexedNodes", 10]],
|
["maxIndexedNodes", _data getOrDefault ["maxIndexedNodes", 10]],
|
||||||
["baseFare", _data getOrDefault ["baseFare", 100]],
|
["baseFare", _data getOrDefault ["baseFare", ["transportBaseFare", 100] call _transportSetting]],
|
||||||
["pricePerKm", _data getOrDefault ["pricePerKm", 50]],
|
["pricePerKm", _data getOrDefault ["pricePerKm", ["transportPricePerKm", 50] call _transportSetting]],
|
||||||
["cargoRadius", _data getOrDefault ["cargoRadius", 25]],
|
["cargoRadius", _data getOrDefault ["cargoRadius", 25]],
|
||||||
["includeCargo", _data getOrDefault ["includeCargo", true]]
|
["includeCargo", _data getOrDefault ["includeCargo", true]]
|
||||||
];
|
];
|
||||||
|
|||||||
@ -142,8 +142,24 @@ GVAR(ActorRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
if (_isTransport) then {
|
if (_isTransport) then {
|
||||||
private _fromTransportNode = _x;
|
private _fromTransportNode = _x;
|
||||||
private _maxIndexedNodes = _x getVariable ["transportMaxIndexedNodes", 10];
|
private _maxIndexedNodes = _x getVariable ["transportMaxIndexedNodes", 10];
|
||||||
private _baseFare = _x getVariable ["transportBaseFare", 100];
|
private _transportSetting = {
|
||||||
private _pricePerKm = _x getVariable ["transportPricePerKm", 50];
|
params [["_name", "", [""]], ["_default", 0, [0]]];
|
||||||
|
|
||||||
|
private _configDefault = _default;
|
||||||
|
private _serviceConfig = missionConfigFile >> "CfgServicePricing";
|
||||||
|
if !(isClass _serviceConfig) then { _serviceConfig = configFile >> "CfgServicePricing"; };
|
||||||
|
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 _vehiclePrefix = _x getVariable ["transportVehiclePrefix", format ["%1_vehicle", _transportPrefix]];
|
||||||
private _arrivalPrefix = _x getVariable ["transportArrivalPrefix", format ["%1_arrival", _transportPrefix]];
|
private _arrivalPrefix = _x getVariable ["transportArrivalPrefix", format ["%1_arrival", _transportPrefix]];
|
||||||
private _nodeNames = [_transportPrefix];
|
private _nodeNames = [_transportPrefix];
|
||||||
|
|||||||
@ -151,10 +151,22 @@ GVAR(MissionSetupRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
private _paramOrDefault = {
|
private _paramOrDefault = {
|
||||||
params ["_varName", "_default"];
|
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 };
|
if (_value isEqualType "") exitWith { parseNumber _value };
|
||||||
_value
|
_value
|
||||||
};
|
};
|
||||||
|
private _serviceDefault = {
|
||||||
|
params ["_varName", "_default"];
|
||||||
|
|
||||||
|
private _serviceConfig = missionConfigFile >> "CfgServicePricing";
|
||||||
|
if !(isClass _serviceConfig) then { _serviceConfig = configFile >> "CfgServicePricing"; };
|
||||||
|
if (isNumber (_serviceConfig >> _varName)) exitWith {
|
||||||
|
getNumber (_serviceConfig >> _varName)
|
||||||
|
};
|
||||||
|
|
||||||
|
_default
|
||||||
|
};
|
||||||
|
|
||||||
private _factions = [];
|
private _factions = [];
|
||||||
{
|
{
|
||||||
@ -196,7 +208,15 @@ GVAR(MissionSetupRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
["penaltyMin", ["penaltyMin", -5] call _paramOrDefault],
|
["penaltyMin", ["penaltyMin", -5] call _paramOrDefault],
|
||||||
["penaltyMax", ["penaltyMax", -25] call _paramOrDefault],
|
["penaltyMax", ["penaltyMax", -25] call _paramOrDefault],
|
||||||
["timeLimitMin", ["timeLimitMin", 600] call _paramOrDefault],
|
["timeLimitMin", ["timeLimitMin", 600] call _paramOrDefault],
|
||||||
["timeLimitMax", ["timeLimitMax", 900] 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")]
|
||||||
]]
|
]]
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
|
|||||||
@ -54,8 +54,8 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.titlebar {
|
.titlebar {
|
||||||
min-height: 3.25rem;
|
min-height: 2.5rem;
|
||||||
padding: 0 1.6rem;
|
padding: 0 1.1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -98,18 +98,19 @@ option {
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 1.5rem;
|
padding: 0.75rem 1rem;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
max-width: 78rem;
|
width: min(94rem, 100%);
|
||||||
|
max-width: 94rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.1fr 0.9fr;
|
grid-template-columns: minmax(28rem, 1.35fr) minmax(18rem, 0.8fr) minmax(20rem, 0.85fr);
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
@ -120,36 +121,46 @@ option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-head {
|
.panel-head {
|
||||||
padding: 1.15rem 1.25rem;
|
padding: 0.65rem 0.85rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-head h1,
|
.panel-head h1,
|
||||||
.panel-head h2 {
|
.panel-head h2 {
|
||||||
margin: 0.2rem 0 0;
|
margin: 0.2rem 0 0;
|
||||||
font-size: 1.45rem;
|
font-size: 1.02rem;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
padding: 1.25rem;
|
padding: 0.7rem 0.85rem 0.85rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 1rem;
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form.compact {
|
||||||
|
gap: 0.55rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.45rem;
|
gap: 0.28rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wide {
|
.wide {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timer-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.95fr) minmax(0, 0.95fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
color: var(--text-subtle);
|
color: var(--text-subtle);
|
||||||
font-size: 0.78rem;
|
font-size: 0.62rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@ -171,16 +182,95 @@ label {
|
|||||||
min-height: 1rem;
|
min-height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
.provider-toggle {
|
||||||
select {
|
min-height: 2rem;
|
||||||
width: 100%;
|
padding: 0 0.65rem;
|
||||||
min-height: 2.65rem;
|
display: grid;
|
||||||
padding: 0 0.85rem;
|
grid-template-columns: auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: rgba(24, 31, 40, 0.9);
|
background: rgba(24, 31, 40, 0.9);
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.provider-toggle input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
width: 2.6rem;
|
||||||
|
height: 1.35rem;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch::after {
|
||||||
|
content: "";
|
||||||
|
width: 0.9rem;
|
||||||
|
height: 0.9rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.16rem;
|
||||||
|
left: 0.18rem;
|
||||||
|
background: rgba(245, 248, 255, 0.86);
|
||||||
|
transition: transform 120ms ease, background 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-toggle input:checked + .switch {
|
||||||
|
background: rgba(24, 86, 126, 0.95);
|
||||||
|
border-color: rgba(104, 196, 255, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-toggle input:checked + .switch::after {
|
||||||
|
transform: translateX(1.2rem);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-toggle:focus-within {
|
||||||
|
outline: 2px solid rgba(104, 196, 255, 0.34);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-copy {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-copy strong,
|
||||||
|
.provider-copy small {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-copy strong {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-copy small {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 2rem;
|
||||||
|
padding: 0 0.65rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: rgba(24, 31, 40, 0.9);
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
input:focus,
|
input:focus,
|
||||||
select:focus,
|
select:focus,
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
@ -189,16 +279,16 @@ button:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
padding: 1.25rem;
|
padding: 0.7rem 0.85rem 0.85rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.8rem;
|
gap: 0.42rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-row {
|
.summary-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding-bottom: 0.8rem;
|
padding-bottom: 0.42rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,7 +309,7 @@ button:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
padding: 1rem 1.5rem;
|
padding: 0.6rem 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@ -228,8 +318,8 @@ button:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
min-height: 2.75rem;
|
min-height: 2rem;
|
||||||
padding: 0.72rem 1rem;
|
padding: 0.55rem 0.9rem;
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
background: rgba(24, 31, 40, 0.9);
|
background: rgba(24, 31, 40, 0.9);
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
|
|||||||
@ -12,8 +12,17 @@
|
|||||||
reputationMax: 100,
|
reputationMax: 100,
|
||||||
penaltyMin: -5,
|
penaltyMin: -5,
|
||||||
penaltyMax: -25,
|
penaltyMax: -25,
|
||||||
|
timeLimitEnabled: true,
|
||||||
timeLimitMin: 600,
|
timeLimitMin: 600,
|
||||||
timeLimitMax: 900,
|
timeLimitMax: 900,
|
||||||
|
medicalSpawnCost: 100,
|
||||||
|
medicalHealCost: 100,
|
||||||
|
serviceRepairCost: 500,
|
||||||
|
serviceRearmCost: 500,
|
||||||
|
fuelCost: 5,
|
||||||
|
transportBaseFare: 100,
|
||||||
|
transportPricePerKm: 50,
|
||||||
|
generatorProvider: "builtin",
|
||||||
},
|
},
|
||||||
error: "",
|
error: "",
|
||||||
};
|
};
|
||||||
@ -33,6 +42,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readSettings() {
|
function readSettings() {
|
||||||
|
const timeLimitEnabled = document.getElementById("timeLimitEnabled")?.checked !== false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enemyFaction: String(document.getElementById("enemyFaction")?.value || "IND_G_F"),
|
enemyFaction: String(document.getElementById("enemyFaction")?.value || "IND_G_F"),
|
||||||
maxConcurrentMissions: fieldNumber("maxConcurrentMissions"),
|
maxConcurrentMissions: fieldNumber("maxConcurrentMissions"),
|
||||||
@ -44,8 +55,17 @@
|
|||||||
reputationMax: fieldNumber("reputationMax"),
|
reputationMax: fieldNumber("reputationMax"),
|
||||||
penaltyMin: fieldNumber("penaltyMin"),
|
penaltyMin: fieldNumber("penaltyMin"),
|
||||||
penaltyMax: fieldNumber("penaltyMax"),
|
penaltyMax: fieldNumber("penaltyMax"),
|
||||||
timeLimitMin: fieldNumber("timeLimitMin"),
|
timeLimitEnabled,
|
||||||
timeLimitMax: fieldNumber("timeLimitMax"),
|
timeLimitMin: timeLimitEnabled ? fieldNumber("timeLimitMin") : 0,
|
||||||
|
timeLimitMax: timeLimitEnabled ? fieldNumber("timeLimitMax") : 0,
|
||||||
|
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",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +78,16 @@
|
|||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSettings(settings) {
|
||||||
|
const next = Object.assign({}, settings);
|
||||||
|
next.timeLimitMin = Number(next.timeLimitMin || 0);
|
||||||
|
next.timeLimitMax = Number(next.timeLimitMax || 0);
|
||||||
|
if (typeof next.timeLimitEnabled !== "boolean") {
|
||||||
|
next.timeLimitEnabled = next.timeLimitMax > 0;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
function apply() {
|
function apply() {
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
if (settings.moneyMax < settings.moneyMin) {
|
if (settings.moneyMax < settings.moneyMin) {
|
||||||
@ -78,8 +108,31 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.timeLimitMax < settings.timeLimitMin) {
|
if (settings.timeLimitEnabled) {
|
||||||
state.error = "Time limit max must be greater than or equal to time limit min.";
|
if (settings.timeLimitMin < 1 || settings.timeLimitMax < 1) {
|
||||||
|
state.error = "Time limits must be positive seconds when task timers are enabled.";
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.timeLimitMax < settings.timeLimitMin) {
|
||||||
|
state.error = "Time limit max must be greater than or equal to time limit min.";
|
||||||
|
render();
|
||||||
|
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();
|
render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -101,6 +154,17 @@
|
|||||||
const settings = state.settings;
|
const settings = state.settings;
|
||||||
const faction = state.factions.find((item) => item.faction === settings.enemyFaction);
|
const faction = state.factions.find((item) => item.faction === settings.enemyFaction);
|
||||||
const factionLabel = faction ? faction.display : settings.enemyFaction;
|
const factionLabel = faction ? faction.display : settings.enemyFaction;
|
||||||
|
const generatorProviderLabel = settings.generatorProvider === "custom" ? "Custom" : "Built-in";
|
||||||
|
const generatorProviderChecked = settings.generatorProvider === "custom" ? " checked" : "";
|
||||||
|
const timeLimitEnabled = settings.timeLimitEnabled !== false;
|
||||||
|
const timeLimitChecked = timeLimitEnabled ? " checked" : "";
|
||||||
|
const timeLimitDisabled = timeLimitEnabled ? "" : " disabled";
|
||||||
|
const timeLimitLabel = timeLimitEnabled ? "Enabled" : "No Limit";
|
||||||
|
const timeLimitMinValue = timeLimitEnabled ? settings.timeLimitMin : 600;
|
||||||
|
const timeLimitMaxValue = timeLimitEnabled ? settings.timeLimitMax : 900;
|
||||||
|
const timeLimitSummary = timeLimitEnabled
|
||||||
|
? `${settings.timeLimitMin}s - ${settings.timeLimitMax}s`
|
||||||
|
: "No limit";
|
||||||
|
|
||||||
document.getElementById("app").innerHTML = `
|
document.getElementById("app").innerHTML = `
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
@ -120,7 +184,7 @@
|
|||||||
<h1>Operation Settings</h1>
|
<h1>Operation Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<div class="field">
|
<div class="field wide">
|
||||||
<label for="enemyFaction">Opposing Faction</label>
|
<label for="enemyFaction">Opposing Faction</label>
|
||||||
<select id="enemyFaction">${state.factions.map(option).join("")}</select>
|
<select id="enemyFaction">${state.factions.map(option).join("")}</select>
|
||||||
</div>
|
</div>
|
||||||
@ -128,6 +192,17 @@
|
|||||||
<label for="locationReuseCooldown">Location Cooldown</label>
|
<label for="locationReuseCooldown">Location Cooldown</label>
|
||||||
<input id="locationReuseCooldown" type="number" min="0" step="60" value="${settings.locationReuseCooldown}" />
|
<input id="locationReuseCooldown" type="number" min="0" step="60" value="${settings.locationReuseCooldown}" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="generatorProviderCustom">Mission Generator</label>
|
||||||
|
<label class="provider-toggle" for="generatorProviderCustom">
|
||||||
|
<input id="generatorProviderCustom" type="checkbox"${generatorProviderChecked} />
|
||||||
|
<span class="switch" aria-hidden="true"></span>
|
||||||
|
<span class="provider-copy">
|
||||||
|
<strong>${generatorProviderLabel}</strong>
|
||||||
|
<small>Mission Generators</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="maxConcurrentMissions">Concurrent Missions</label>
|
<label for="maxConcurrentMissions">Concurrent Missions</label>
|
||||||
<input id="maxConcurrentMissions" type="number" min="1" max="50" value="${settings.maxConcurrentMissions}" />
|
<input id="maxConcurrentMissions" type="number" min="1" max="50" value="${settings.maxConcurrentMissions}" />
|
||||||
@ -160,17 +235,67 @@
|
|||||||
<label for="penaltyMax">Max Rep Hit</label>
|
<label for="penaltyMax">Max Rep Hit</label>
|
||||||
<input id="penaltyMax" type="number" max="0" step="1" value="${settings.penaltyMax}" />
|
<input id="penaltyMax" type="number" max="0" step="1" value="${settings.penaltyMax}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="timer-row wide">
|
||||||
<label for="timeLimitMin">Min Time</label>
|
<div class="field">
|
||||||
<input id="timeLimitMin" type="number" min="1" step="60" value="${settings.timeLimitMin}" />
|
<label for="timeLimitEnabled">Task Timer</label>
|
||||||
</div>
|
<label class="provider-toggle" for="timeLimitEnabled">
|
||||||
<div class="field">
|
<input id="timeLimitEnabled" type="checkbox"${timeLimitChecked} />
|
||||||
<label for="timeLimitMax">Max Time</label>
|
<span class="switch" aria-hidden="true"></span>
|
||||||
<input id="timeLimitMax" type="number" min="1" step="60" value="${settings.timeLimitMax}" />
|
<span class="provider-copy">
|
||||||
|
<strong>${timeLimitLabel}</strong>
|
||||||
|
<small>Time Limits</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="timeLimitMin">Min Time</label>
|
||||||
|
<input id="timeLimitMin" type="number" min="1" step="60" value="${timeLimitMinValue}"${timeLimitDisabled} />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="timeLimitMax">Max Time</label>
|
||||||
|
<input id="timeLimitMax" type="number" min="1" step="60" value="${timeLimitMaxValue}"${timeLimitDisabled} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<aside class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<span class="kicker">Service Pricing</span>
|
||||||
|
<h2>Economy Settings</h2>
|
||||||
|
</div>
|
||||||
|
<div class="form compact">
|
||||||
|
<div class="field">
|
||||||
|
<label for="medicalSpawnCost">Medical Respawn</label>
|
||||||
|
<input id="medicalSpawnCost" type="number" min="0" step="50" value="${settings.medicalSpawnCost}" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="medicalHealCost">Medical Heal</label>
|
||||||
|
<input id="medicalHealCost" type="number" min="0" step="50" value="${settings.medicalHealCost}" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="serviceRepairCost">Repair</label>
|
||||||
|
<input id="serviceRepairCost" type="number" min="0" step="50" value="${settings.serviceRepairCost}" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="serviceRearmCost">Rearm</label>
|
||||||
|
<input id="serviceRearmCost" type="number" min="0" step="50" value="${settings.serviceRearmCost}" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="fuelCost">Fuel / Liter</label>
|
||||||
|
<input id="fuelCost" type="number" min="0" step="1" value="${settings.fuelCost}" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="transportBaseFare">Transport Base</label>
|
||||||
|
<input id="transportBaseFare" type="number" min="0" step="25" value="${settings.transportBaseFare}" />
|
||||||
|
</div>
|
||||||
|
<div class="field wide">
|
||||||
|
<label for="transportPricePerKm">Transport / KM</label>
|
||||||
|
<input id="transportPricePerKm" type="number" min="0" step="25" value="${settings.transportPricePerKm}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<aside class="panel">
|
<aside class="panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<span class="kicker">Current Selection</span>
|
<span class="kicker">Current Selection</span>
|
||||||
@ -178,13 +303,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<div class="summary-row"><span>Faction</span><strong>${escapeHtml(factionLabel)}</strong></div>
|
<div class="summary-row"><span>Faction</span><strong>${escapeHtml(factionLabel)}</strong></div>
|
||||||
|
<div class="summary-row"><span>Generator</span><strong>${generatorProviderLabel}</strong></div>
|
||||||
<div class="summary-row"><span>Mission Cap</span><strong>${settings.maxConcurrentMissions}</strong></div>
|
<div class="summary-row"><span>Mission Cap</span><strong>${settings.maxConcurrentMissions}</strong></div>
|
||||||
<div class="summary-row"><span>Interval</span><strong>${settings.missionInterval}s</strong></div>
|
<div class="summary-row"><span>Interval</span><strong>${settings.missionInterval}s</strong></div>
|
||||||
<div class="summary-row"><span>Location Cooldown</span><strong>${settings.locationReuseCooldown}s</strong></div>
|
<div class="summary-row"><span>Location Cooldown</span><strong>${settings.locationReuseCooldown}s</strong></div>
|
||||||
<div class="summary-row"><span>Reward Range</span><strong>$${Number(settings.moneyMin).toLocaleString()} - $${Number(settings.moneyMax).toLocaleString()}</strong></div>
|
<div class="summary-row"><span>Reward Range</span><strong>$${Number(settings.moneyMin).toLocaleString()} - $${Number(settings.moneyMax).toLocaleString()}</strong></div>
|
||||||
<div class="summary-row"><span>Reputation</span><strong>${settings.reputationMin} - ${settings.reputationMax}</strong></div>
|
<div class="summary-row"><span>Reputation</span><strong>${settings.reputationMin} - ${settings.reputationMax}</strong></div>
|
||||||
<div class="summary-row"><span>Reputation Hit</span><strong>${settings.penaltyMin} to ${settings.penaltyMax}</strong></div>
|
<div class="summary-row"><span>Reputation Hit</span><strong>${settings.penaltyMin} to ${settings.penaltyMax}</strong></div>
|
||||||
<div class="summary-row"><span>Time Limit</span><strong>${settings.timeLimitMin}s - ${settings.timeLimitMax}s</strong></div>
|
<div class="summary-row"><span>Time Limit</span><strong>${timeLimitSummary}</strong></div>
|
||||||
|
<div class="summary-row"><span>Repair / Rearm</span><strong>$${Number(settings.serviceRepairCost).toLocaleString()} / $${Number(settings.serviceRearmCost).toLocaleString()}</strong></div>
|
||||||
|
<div class="summary-row"><span>Fuel</span><strong>$${Number(settings.fuelCost).toLocaleString()} / L</strong></div>
|
||||||
|
<div class="summary-row"><span>Medical Billing</span><strong>$${Number(settings.medicalSpawnCost).toLocaleString()} respawn / $${Number(settings.medicalHealCost).toLocaleString()} heal</strong></div>
|
||||||
${state.error ? `<div class="notice">${state.error}</div>` : ""}
|
${state.error ? `<div class="notice">${state.error}</div>` : ""}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@ -230,7 +359,7 @@
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
state.factions = factions;
|
state.factions = factions;
|
||||||
state.settings = Object.assign({}, state.settings, payload.data?.settings || {});
|
state.settings = normalizeSettings(Object.assign({}, state.settings, payload.data?.settings || {}));
|
||||||
render();
|
render();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -571,7 +571,7 @@ ${scopeSelector} .store-toast.is-error {
|
|||||||
{ className: "filter-placeholder" },
|
{ className: "filter-placeholder" },
|
||||||
selectedPaymentSource
|
selectedPaymentSource
|
||||||
? selectedPaymentSource.label
|
? selectedPaymentSource.label
|
||||||
: "Cash",
|
: "Select Payment",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -645,7 +645,7 @@ ${scopeSelector} .store-toast.is-error {
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ className: "footer-copy" },
|
{ className: "footer-copy" },
|
||||||
"Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.",
|
"Uniforms, protective gear, weapon slots, vehicles, units, ammunition groups, and general support inventory.",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
h(
|
h(
|
||||||
|
|||||||
@ -300,15 +300,13 @@ ${scopeSelector} .cart-empty {
|
|||||||
getters.getPaymentSourceById(
|
getters.getPaymentSourceById(
|
||||||
storeConfig,
|
storeConfig,
|
||||||
state.selectedPaymentSource,
|
state.selectedPaymentSource,
|
||||||
) ||
|
) || null;
|
||||||
paymentSources[0] ||
|
|
||||||
null;
|
|
||||||
const availablePaymentSourceCount = paymentSources.filter(
|
const availablePaymentSourceCount = paymentSources.filter(
|
||||||
(source) => source.enabled !== false,
|
(source) => source.enabled !== false,
|
||||||
).length;
|
).length;
|
||||||
const selectedPaymentLabel = selectedPaymentSource
|
const selectedPaymentLabel = selectedPaymentSource
|
||||||
? selectedPaymentSource.label
|
? selectedPaymentSource.label
|
||||||
: "Unavailable";
|
: "Select Payment";
|
||||||
const selectedPaymentBalance = selectedPaymentSource
|
const selectedPaymentBalance = selectedPaymentSource
|
||||||
? Number(selectedPaymentSource.balance || 0)
|
? Number(selectedPaymentSource.balance || 0)
|
||||||
: 0;
|
: 0;
|
||||||
@ -392,12 +390,20 @@ ${scopeSelector} .cart-empty {
|
|||||||
"select",
|
"select",
|
||||||
{
|
{
|
||||||
className: "payment-source-select",
|
className: "payment-source-select",
|
||||||
value: state.selectedPaymentSource,
|
value: state.selectedPaymentSource || "",
|
||||||
onChange: (event) =>
|
onChange: (event) =>
|
||||||
actions.selectPaymentSource(
|
actions.selectPaymentSource(
|
||||||
event.target.value,
|
event.target.value,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
h(
|
||||||
|
"option",
|
||||||
|
{
|
||||||
|
value: "",
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
"Select Payment",
|
||||||
|
),
|
||||||
paymentSources.map((source) =>
|
paymentSources.map((source) =>
|
||||||
h(
|
h(
|
||||||
"option",
|
"option",
|
||||||
@ -467,7 +473,28 @@ ${scopeSelector} .cart-empty {
|
|||||||
: "Unavailable",
|
: "Unavailable",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: "payment-source-meta",
|
||||||
|
},
|
||||||
|
h(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
className: "payment-source-label",
|
||||||
|
},
|
||||||
|
"Select Payment",
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
className: "payment-source-state",
|
||||||
|
},
|
||||||
|
availablePaymentSourceCount > 0
|
||||||
|
? "Required"
|
||||||
|
: "Unavailable",
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
h(
|
h(
|
||||||
@ -630,6 +657,7 @@ ${scopeSelector} .cart-empty {
|
|||||||
className: "store-btn store-btn-primary",
|
className: "store-btn store-btn-primary",
|
||||||
disabled:
|
disabled:
|
||||||
summary.lineCount === 0 ||
|
summary.lineCount === 0 ||
|
||||||
|
!selectedPaymentSource ||
|
||||||
state.isCheckingOut,
|
state.isCheckingOut,
|
||||||
onClick: () => actions.requestCheckout(),
|
onClick: () => actions.requestCheckout(),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -81,6 +81,7 @@
|
|||||||
{ id: "ammo", label: "Ammo" },
|
{ id: "ammo", label: "Ammo" },
|
||||||
{ id: "misc", label: "Misc" },
|
{ id: "misc", label: "Misc" },
|
||||||
{ id: "vehicles", label: "Vehicles" },
|
{ id: "vehicles", label: "Vehicles" },
|
||||||
|
{ id: "units", label: "Units" },
|
||||||
],
|
],
|
||||||
vehicleCards: [
|
vehicleCards: [
|
||||||
{ id: "cars", label: "Cars" },
|
{ id: "cars", label: "Cars" },
|
||||||
@ -113,6 +114,7 @@
|
|||||||
planes: [],
|
planes: [],
|
||||||
naval: [],
|
naval: [],
|
||||||
other: [],
|
other: [],
|
||||||
|
units: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -112,8 +112,8 @@
|
|||||||
return {
|
return {
|
||||||
eyebrow: "Supply Categories",
|
eyebrow: "Supply Categories",
|
||||||
title: "Procurement Dashboard",
|
title: "Procurement Dashboard",
|
||||||
copy: "Choose a category to enter the exchange. Weapons and vehicles open a second tier, while the other departments display placeholder product inventory inside the new runtime/store architecture.",
|
copy: "Choose a category to enter the exchange. Weapons and vehicles open a second tier, while the other departments display live product inventory inside the runtime store architecture.",
|
||||||
badge: "8 Categories",
|
badge: "11 Categories",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
const payload = {
|
const payload = {
|
||||||
items: [],
|
items: [],
|
||||||
vehicles: [],
|
vehicles: [],
|
||||||
|
units: [],
|
||||||
totalPrice,
|
totalPrice,
|
||||||
paymentMethod,
|
paymentMethod,
|
||||||
};
|
};
|
||||||
@ -57,6 +58,20 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (normalizedItem.entryKind === "unit") {
|
||||||
|
for (
|
||||||
|
let index = 0;
|
||||||
|
index < normalizedItem.quantity;
|
||||||
|
index += 1
|
||||||
|
) {
|
||||||
|
payload.units.push({
|
||||||
|
classname: normalizedItem.classname,
|
||||||
|
category: "units",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
payload.items.push({
|
payload.items.push({
|
||||||
classname: normalizedItem.classname,
|
classname: normalizedItem.classname,
|
||||||
category: normalizedItem.category,
|
category: normalizedItem.category,
|
||||||
|
|||||||
@ -58,7 +58,7 @@
|
|||||||
[this.getIsCheckingOut, this.setIsCheckingOut] =
|
[this.getIsCheckingOut, this.setIsCheckingOut] =
|
||||||
createSignal(false);
|
createSignal(false);
|
||||||
[this.getSelectedPaymentSource, this.setSelectedPaymentSource] =
|
[this.getSelectedPaymentSource, this.setSelectedPaymentSource] =
|
||||||
createSignal("cash");
|
createSignal("");
|
||||||
}
|
}
|
||||||
|
|
||||||
resetToCategories() {
|
resetToCategories() {
|
||||||
@ -191,23 +191,9 @@
|
|||||||
const currentSource = String(
|
const currentSource = String(
|
||||||
this.getSelectedPaymentSource() || "",
|
this.getSelectedPaymentSource() || "",
|
||||||
).trim();
|
).trim();
|
||||||
const defaultSource = String(
|
|
||||||
storeConfig?.defaultPaymentSource || "",
|
|
||||||
).trim();
|
|
||||||
const sourceIds = paymentSources.map((source) =>
|
const sourceIds = paymentSources.map((source) =>
|
||||||
String(source?.id || "").trim(),
|
String(source?.id || "").trim(),
|
||||||
);
|
);
|
||||||
const enabledSource = paymentSources.find(
|
|
||||||
(source) => source && source.enabled !== false,
|
|
||||||
);
|
|
||||||
const defaultAvailable =
|
|
||||||
defaultSource && sourceIds.includes(defaultSource)
|
|
||||||
? paymentSources.find(
|
|
||||||
(source) =>
|
|
||||||
String(source?.id || "").trim() ===
|
|
||||||
defaultSource,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
currentSource &&
|
currentSource &&
|
||||||
@ -221,19 +207,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defaultAvailable && defaultAvailable.enabled !== false) {
|
this.setSelectedPaymentSource("");
|
||||||
this.setSelectedPaymentSource(defaultSource);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enabledSource) {
|
|
||||||
this.setSelectedPaymentSource(
|
|
||||||
String(enabledSource.id || "cash"),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setSelectedPaymentSource(defaultSource || "cash");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateToBreadcrumb(target) {
|
navigateToBreadcrumb(target) {
|
||||||
|
|||||||
@ -25,13 +25,38 @@ life state, phone number, email, organization, and holster state.
|
|||||||
|
|
||||||
## Runtime Behavior
|
## Runtime Behavior
|
||||||
- Missing persistent actors can be created from live player snapshots.
|
- 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
|
Field Commander text messages, and a `$2,000` starting credit in their bank
|
||||||
account.
|
account.
|
||||||
- Hot actor reads are migrated and hydrated before use.
|
- Hot actor reads are migrated and hydrated before use.
|
||||||
- `saveHotState` in the main addon snapshots and saves actor state on player
|
- `saveHotState` in the main addon snapshots and saves actor state on player
|
||||||
disconnect and mission end.
|
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
|
## Event Surface
|
||||||
The addon handles server events for actor init, get, set, multi-set, save, and
|
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.
|
remove requests, then replies to the requesting player through client actor RPCs.
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* File: fnc_initActorStore.sqf
|
* File: fnc_initActorStore.sqf
|
||||||
* Author: IDSolutions
|
* Author: IDSolutions
|
||||||
* Date: 2025-12-17
|
* Date: 2025-12-17
|
||||||
* Last Update: 2026-05-16
|
* Last Update: 2026-06-03
|
||||||
* Public: Yes
|
* Public: Yes
|
||||||
*
|
*
|
||||||
* Description:
|
* Description:
|
||||||
@ -25,12 +25,23 @@
|
|||||||
#pragma hemtt ignore_variables ["_self"]
|
#pragma hemtt ignore_variables ["_self"]
|
||||||
GVAR(ActorModel) = compileFinal createHashMapObject [[
|
GVAR(ActorModel) = compileFinal createHashMapObject [[
|
||||||
["#type", "ActorModel"],
|
["#type", "ActorModel"],
|
||||||
|
["getStartingConfig", compileFinal {
|
||||||
|
missionConfigFile >> "CfgStartingEquipment"
|
||||||
|
}],
|
||||||
|
["getDefaultLoadout", compileFinal {
|
||||||
|
private _config = _self call ["getStartingConfig", []];
|
||||||
|
private _loadoutConfig = _config >> "loadout";
|
||||||
|
|
||||||
|
if (isArray _loadoutConfig) exitWith { getArray _loadoutConfig };
|
||||||
|
|
||||||
|
[[],[],["hgun_P07_F","","","",["16Rnd_9x21_Mag",4,17],[],""],["U_BG_Guerrilla_6_1",[["FirstAidKit", 2],["ACE_EarPlugs",1]]],["V_Rangemaster_belt",[["16Rnd_9x21_Mag",4]]],[],"H_Cap_blk_ION","",["Binocular","","","",[],[],""],["ItemMap","ItemGPS","ItemRadio","ItemCompass","ItemWatch",""]]
|
||||||
|
}],
|
||||||
["defaults", compileFinal {
|
["defaults", compileFinal {
|
||||||
private _actor = createHashMap;
|
private _actor = createHashMap;
|
||||||
|
|
||||||
_actor set ["uid", ""];
|
_actor set ["uid", ""];
|
||||||
_actor set ["name", ""];
|
_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 ["position", [0,0,0]];
|
||||||
_actor set ["direction", 0];
|
_actor set ["direction", 0];
|
||||||
_actor set ["stance", "STAND"];
|
_actor set ["stance", "STAND"];
|
||||||
@ -105,13 +116,11 @@ GVAR(ActorModel) = compileFinal createHashMapObject [[
|
|||||||
}]
|
}]
|
||||||
]];
|
]];
|
||||||
|
|
||||||
GVAR(ActorBaseStore) = compileFinal ([
|
GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
|
||||||
EGVAR(common,BaseStore),
|
["#base", EGVAR(common,BaseStore)],
|
||||||
createHashMapFromArray [
|
|
||||||
["#type", "ActorBaseStore"],
|
["#type", "ActorBaseStore"],
|
||||||
["#create", compileFinal {
|
["#create", compileFinal {
|
||||||
["INFO", "Actor Store Initialized!"] call EFUNC(common,log);
|
["INFO", "Actor Store Initialized!"] call EFUNC(common,log);
|
||||||
true
|
|
||||||
}],
|
}],
|
||||||
["cacheActor", compileFinal {
|
["cacheActor", compileFinal {
|
||||||
params [["_uid", "", [""]], ["_actor", createHashMap, [createHashMap]]];
|
params [["_uid", "", [""]], ["_actor", createHashMap, [createHashMap]]];
|
||||||
@ -563,13 +572,7 @@ GVAR(ActorBaseStore) = compileFinal ([
|
|||||||
|
|
||||||
_self call ["override", [_uid, _finalActor, false]]
|
_self call ["override", [_uid, _finalActor, false]]
|
||||||
}]
|
}]
|
||||||
]] call {
|
];
|
||||||
params ["_base", "_child"];
|
|
||||||
|
|
||||||
private _merged = +_base;
|
GVAR(ActorStore) = createHashMapObject [GVAR(ActorBaseStore)];
|
||||||
{ _merged set [_x, _y]; } forEach _child;
|
GVAR(ActorStore)
|
||||||
_merged
|
|
||||||
});
|
|
||||||
|
|
||||||
GVAR(ActorStore) = createHashMapObject [GVAR(ActorBaseStore), []];
|
|
||||||
true
|
|
||||||
|
|||||||
@ -91,12 +91,12 @@ call FUNC(registerEventListeners);
|
|||||||
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
|
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isNil QEFUNC(task,requestMissionTask)) exitWith {
|
if (isNil QEGVAR(task,MissionGeneratorProviderRegistry)) exitWith {
|
||||||
_result set ["message", "Framework generated mission requests are unavailable."];
|
_result set ["message", "Generated mission provider registry is unavailable."];
|
||||||
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
|
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
_result = [_taskType, _metadata, _uid] call EFUNC(task,requestMissionTask);
|
_result = EGVAR(task,MissionGeneratorProviderRegistry) call ["requestMissionTask", [_taskType, _metadata, _uid]];
|
||||||
|
|
||||||
if !(_result getOrDefault ["success", false]) exitWith {
|
if !(_result getOrDefault ["success", false]) exitWith {
|
||||||
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
|
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
|
||||||
|
|||||||
@ -300,31 +300,10 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
private _permissionService = _self get "PermissionService";
|
private _permissionService = _self get "PermissionService";
|
||||||
private _groupRepository = _self get "GroupRepository";
|
private _groupRepository = _self get "GroupRepository";
|
||||||
private _generatedTaskTypes = [];
|
private _generatedTaskTypes = [];
|
||||||
if (missionNamespace getVariable [QEGVAR(task,enableGenerator), false]) then {
|
if !(isNil QEGVAR(task,MissionGeneratorProviderRegistry)) then {
|
||||||
_generatedTaskTypes = [
|
_generatedTaskTypes = EGVAR(task,MissionGeneratorProviderRegistry) call ["getGeneratedTaskTypes", []];
|
||||||
createHashMapFromArray [["value", "attack"], ["label", "Attack"]],
|
|
||||||
createHashMapFromArray [["value", "defend"], ["label", "Defend"]],
|
|
||||||
createHashMapFromArray [["value", "defuse"], ["label", "Defuse"]],
|
|
||||||
createHashMapFromArray [["value", "delivery"], ["label", "Delivery"]],
|
|
||||||
createHashMapFromArray [["value", "destroy"], ["label", "Destroy"]],
|
|
||||||
createHashMapFromArray [["value", "hostage"], ["label", "Hostage"]],
|
|
||||||
createHashMapFromArray [["value", "hvtkill"], ["label", "Kill HVT"]],
|
|
||||||
createHashMapFromArray [["value", "hvtcapture"], ["label", "Capture HVT"]]
|
|
||||||
];
|
|
||||||
["INFO", "CAD hydrate using framework generator fallback type list while checking task mission manager."] call EFUNC(common,log);
|
|
||||||
|
|
||||||
if (isNil QEGVAR(task,MissionManager) && { !(isNil QEFUNC(task,missionManager)) }) then {
|
|
||||||
call EFUNC(task,missionManager);
|
|
||||||
};
|
|
||||||
|
|
||||||
if !(isNil QEGVAR(task,MissionManager)) then {
|
|
||||||
_generatedTaskTypes = EGVAR(task,MissionManager) call ["getGeneratedTaskTypes", []];
|
|
||||||
["INFO", format ["CAD hydrate using task mission manager generated types: %1", _generatedTaskTypes apply { _x getOrDefault ["value", ""] }]] call EFUNC(common,log);
|
|
||||||
} else {
|
|
||||||
["INFO", "CAD hydrate task mission manager is not ready; sending fallback generated task types."] call EFUNC(common,log);
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
["INFO", "CAD hydrate generated task types disabled by forge_server_task_enableGenerator."] call EFUNC(common,log);
|
["INFO", "CAD hydrate generated task types unavailable because the task provider registry is not ready."] call EFUNC(common,log);
|
||||||
};
|
};
|
||||||
|
|
||||||
private _groupID = _groupRepository call ["getPlayerGroupId", [_uid]];
|
private _groupID = _groupRepository call ["getPlayerGroupId", [_uid]];
|
||||||
|
|||||||
16
arma/server/addons/economy/CfgServicePricing.hpp
Normal file
16
arma/server/addons/economy/CfgServicePricing.hpp
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* Framework fallback prices for service/economy interactions.
|
||||||
|
*
|
||||||
|
* Mission-local CfgServicePricing overrides this config. Mission Params with
|
||||||
|
* matching names override config defaults before the setup UI opens, and
|
||||||
|
* submitted setup UI values override both.
|
||||||
|
*/
|
||||||
|
class CfgServicePricing {
|
||||||
|
medicalSpawnCost = 100;
|
||||||
|
medicalHealCost = 100;
|
||||||
|
serviceRepairCost = 500;
|
||||||
|
serviceRearmCost = 500;
|
||||||
|
fuelCost = 5;
|
||||||
|
transportBaseFare = 100;
|
||||||
|
transportPricePerKm = 50;
|
||||||
|
};
|
||||||
@ -9,6 +9,21 @@ inventory handling.
|
|||||||
Current stores cover fuel tracking, medical service behavior, and service
|
Current stores cover fuel tracking, medical service behavior, and service
|
||||||
charges such as repairs and rearming.
|
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, `CfgServicePricing` 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
|
## Dependencies
|
||||||
- `forge_server_main`
|
- `forge_server_main`
|
||||||
- `forge_server_common` for logging, formatting, and player lookup
|
- `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
|
totals, charges the player's organization through `OrgStore`, syncs the org
|
||||||
patch, and rolls fuel back to the starting level when organization funds
|
patch, and rolls fuel back to the starting level when organization funds
|
||||||
cannot cover the refuel.
|
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
|
respawn placement, death inventory handling, and body-bag transfer. Medical
|
||||||
charges use player bank/cash first, then organization funds with repayable
|
charges use player bank/cash first, then organization funds with repayable
|
||||||
member debt only when the player cannot cover the service.
|
member debt only when the player cannot cover the service.
|
||||||
|
|||||||
@ -18,3 +18,4 @@ class CfgPatches {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#include "CfgEventHandlers.hpp"
|
#include "CfgEventHandlers.hpp"
|
||||||
|
#include "CfgServicePricing.hpp"
|
||||||
|
|||||||
@ -32,6 +32,22 @@ GVAR(FEconomyStore) = createHashMapObject [[
|
|||||||
|
|
||||||
["INFO", "Fuel Store Initialized!", nil, nil] call EFUNC(common,log);
|
["INFO", "Fuel Store Initialized!", nil, nil] call EFUNC(common,log);
|
||||||
}],
|
}],
|
||||||
|
["numberSetting", {
|
||||||
|
params [["_name", "", [""]], ["_default", 0, [0]]];
|
||||||
|
|
||||||
|
private _configDefault = _default;
|
||||||
|
private _serviceConfig = missionConfigFile >> "CfgServicePricing";
|
||||||
|
if !(isClass _serviceConfig) then { _serviceConfig = configFile >> "CfgServicePricing"; };
|
||||||
|
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", {
|
["start", {
|
||||||
params ["_source", "_target", "_unit"];
|
params ["_source", "_target", "_unit"];
|
||||||
|
|
||||||
@ -100,7 +116,7 @@ GVAR(FEconomyStore) = createHashMapObject [[
|
|||||||
if (_fuelCapacity <= 0) then { _fuelCapacity = 100; };
|
if (_fuelCapacity <= 0) then { _fuelCapacity = 100; };
|
||||||
|
|
||||||
private _totalLiters = _missingFuel * _fuelCapacity;
|
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"]];
|
private _chargeResult = GVAR(SEconomyStore) call ["chargeOrg", [_unit, _totalCost, "Refueling"]];
|
||||||
if !(_chargeResult getOrDefault ["success", false]) exitWith {
|
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."]]];
|
_self call ["notify", [_unit, "danger", "Refueling", _chargeResult getOrDefault ["message", "Organization funds cannot cover this refuel. Refueling was not completed."]]];
|
||||||
@ -130,7 +146,7 @@ GVAR(FEconomyStore) = createHashMapObject [[
|
|||||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||||
|
|
||||||
private _totalLiters = GETVAR(_target,liters,0);
|
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 _formattedTotalCost = [_totalCost] call EFUNC(common,formatNumber);
|
||||||
private _formattedTotalLiters = _totalLiters toFixed 2;
|
private _formattedTotalLiters = _totalLiters toFixed 2;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* File: fnc_initMEconomyStore.sqf
|
* File: fnc_initMEconomyStore.sqf
|
||||||
* Author: IDSolutions
|
* Author: IDSolutions
|
||||||
* Date: 2025-12-20
|
* Date: 2025-12-20
|
||||||
* Last Update: 2026-05-15
|
* Last Update: 2026-06-03
|
||||||
* Public: No
|
* Public: No
|
||||||
*
|
*
|
||||||
* Description:
|
* Description:
|
||||||
@ -30,8 +30,25 @@ GVAR(MEconomyStore) = createHashMapObject [[
|
|||||||
_self set ["mSpawns", createHashMap];
|
_self set ["mSpawns", createHashMap];
|
||||||
|
|
||||||
GVAR(occupancyTriggers) = [];
|
GVAR(occupancyTriggers) = [];
|
||||||
|
GVAR(SpawnCost) = 100;
|
||||||
["INFO", "Medical Store Initialized!", nil, nil] call EFUNC(common,log);
|
["INFO", "Medical Store Initialized!", nil, nil] call EFUNC(common,log);
|
||||||
}],
|
}],
|
||||||
|
["numberSetting", {
|
||||||
|
params [["_name", "", [""]], ["_default", 0, [0]]];
|
||||||
|
|
||||||
|
private _configDefault = _default;
|
||||||
|
private _serviceConfig = missionConfigFile >> "CfgServicePricing";
|
||||||
|
if !(isClass _serviceConfig) then { _serviceConfig = configFile >> "CfgServicePricing"; };
|
||||||
|
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", {
|
["init", {
|
||||||
private _mSpawns = (_self get "mSpawns");
|
private _mSpawns = (_self get "mSpawns");
|
||||||
private _prefix = "med_spawn";
|
private _prefix = "med_spawn";
|
||||||
@ -166,40 +183,61 @@ GVAR(MEconomyStore) = createHashMapObject [[
|
|||||||
_result set ["message", ""];
|
_result set ["message", ""];
|
||||||
_result
|
_result
|
||||||
}],
|
}],
|
||||||
["onHealed", {
|
["chargeMedicalService", {
|
||||||
params [["_unit", objNull, [objNull]]];
|
params [
|
||||||
|
["_unit", objNull, [objNull]],
|
||||||
if (isNull _unit) exitWith { ["WARNING", format ["Invalid unit provided: %1", (name _unit)], nil, nil] call EFUNC(common,log); };
|
["_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;
|
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 {
|
if (_personalCharge getOrDefault ["success", false]) exitWith {
|
||||||
private _sourceLabel = ["cash", "bank"] select ((_personalCharge getOrDefault ["source", "bank"]) isEqualTo "bank");
|
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]]];
|
_self call ["notify", [_unit, "info", "Medical Billing", format ["%1 charged $%2 from your %3.", _serviceLabel, [_amount] call EFUNC(common,formatNumber), _sourceLabel]]];
|
||||||
[CRPC(actor,onActorHealed), [], _unit] call CFUNC(targetEvent);
|
true
|
||||||
};
|
};
|
||||||
|
|
||||||
if !(_personalCharge getOrDefault ["fallbackEligible", false]) exitWith {
|
if !(_personalCharge getOrDefault ["fallbackEligible", false]) exitWith {
|
||||||
private _message = _personalCharge getOrDefault ["message", "Personal funds could not be charged for medical service."];
|
private _message = _personalCharge getOrDefault ["message", "Personal funds could not be charged for medical service."];
|
||||||
_self call ["notify", [_unit, "danger", "Medical Billing", _message]];
|
_self call ["notify", [_unit, "danger", "Medical Billing", _message]];
|
||||||
|
!_requirePayment
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isNil QGVAR(SEconomyStore)) exitWith {
|
if (isNil QGVAR(SEconomyStore)) exitWith {
|
||||||
["ERROR", "Service economy store unavailable for medical organization fallback charge.", nil, nil] call EFUNC(common,log);
|
["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."]];
|
_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 {
|
if !(_chargeResult getOrDefault ["success", false]) exitWith {
|
||||||
private _message = _chargeResult getOrDefault ["message", "Organization funds cannot cover this medical service."];
|
private _message = _chargeResult getOrDefault ["message", "Organization funds cannot cover this medical service."];
|
||||||
_self call ["notify", [_unit, "danger", "Medical Billing", _message]];
|
_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);
|
[CRPC(actor,onActorHealed), [], _unit] call CFUNC(targetEvent);
|
||||||
}],
|
}],
|
||||||
["onRespawn", {
|
["onRespawn", {
|
||||||
@ -214,6 +252,8 @@ GVAR(MEconomyStore) = createHashMapObject [[
|
|||||||
deleteVehicle _corpse;
|
deleteVehicle _corpse;
|
||||||
|
|
||||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
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);
|
[CRPC(actor,onActorRespawn), [_loadout, _medSpawnPos, _medSpawnDir], _player] call CFUNC(targetEvent);
|
||||||
}],
|
}],
|
||||||
["onKilled", {
|
["onKilled", {
|
||||||
|
|||||||
@ -30,6 +30,22 @@ GVAR(SEconomyStore) = createHashMapObject [[
|
|||||||
GVAR(ServiceRearmCost) = 500;
|
GVAR(ServiceRearmCost) = 500;
|
||||||
["INFO", "Service Store Initialized!", nil, nil] call EFUNC(common,log);
|
["INFO", "Service Store Initialized!", nil, nil] call EFUNC(common,log);
|
||||||
}],
|
}],
|
||||||
|
["numberSetting", {
|
||||||
|
params [["_name", "", [""]], ["_default", 0, [0]]];
|
||||||
|
|
||||||
|
private _configDefault = _default;
|
||||||
|
private _serviceConfig = missionConfigFile >> "CfgServicePricing";
|
||||||
|
if !(isClass _serviceConfig) then { _serviceConfig = configFile >> "CfgServicePricing"; };
|
||||||
|
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", {
|
["notify", {
|
||||||
params [["_unit", objNull, [objNull]], ["_type", "info", [""]], ["_title", "Service", [""]], ["_message", "", [""]]];
|
params [["_unit", objNull, [objNull]], ["_type", "info", [""]], ["_title", "Service", [""]], ["_message", "", [""]]];
|
||||||
|
|
||||||
@ -148,7 +164,7 @@ GVAR(SEconomyStore) = createHashMapObject [[
|
|||||||
|
|
||||||
if (isNull _target || { isNull _unit }) exitWith { false };
|
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"]];
|
private _charge = _self call ["chargeOrg", [_unit, _repairCost, "Repair"]];
|
||||||
if !(_charge getOrDefault ["success", false]) exitWith {
|
if !(_charge getOrDefault ["success", false]) exitWith {
|
||||||
_self call ["notify", [_unit, "danger", "Repair", _charge getOrDefault ["message", "Organization funds cannot cover this repair."]]];
|
_self call ["notify", [_unit, "danger", "Repair", _charge getOrDefault ["message", "Organization funds cannot cover this repair."]]];
|
||||||
@ -164,7 +180,7 @@ GVAR(SEconomyStore) = createHashMapObject [[
|
|||||||
|
|
||||||
if (isNull _target || { isNull _unit }) exitWith { false };
|
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"]];
|
private _charge = _self call ["chargeOrg", [_unit, _rearmCost, "Rearm"]];
|
||||||
if !(_charge getOrDefault ["success", false]) exitWith {
|
if !(_charge getOrDefault ["success", false]) exitWith {
|
||||||
_self call ["notify", [_unit, "danger", "Rearm", _charge getOrDefault ["message", "Organization funds cannot cover this rearm."]]];
|
_self call ["notify", [_unit, "danger", "Rearm", _charge getOrDefault ["message", "Organization funds cannot cover this rearm."]]];
|
||||||
|
|||||||
@ -34,3 +34,26 @@ Garage listens for sync events through the event bus:
|
|||||||
- `notification.requested` - storage and vehicle modification alerts
|
- `notification.requested` - storage and vehicle modification alerts
|
||||||
|
|
||||||
The store module emits these events when granting vehicles; garage applies the changes to player state.
|
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.
|
||||||
|
|||||||
@ -24,15 +24,28 @@
|
|||||||
#pragma hemtt ignore_variables ["_self"]
|
#pragma hemtt ignore_variables ["_self"]
|
||||||
GVAR(VGarageModel) = compileFinal createHashMapObject [[
|
GVAR(VGarageModel) = compileFinal createHashMapObject [[
|
||||||
["#type", "VGarageModel"],
|
["#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 {
|
["defaults", compileFinal {
|
||||||
private _vGarage = createHashMap;
|
private _vGarage = createHashMap;
|
||||||
|
|
||||||
_vGarage set ["armor", []];
|
_vGarage set ["armor", _self call ["getStartingUnlocks", ["armor", []]]];
|
||||||
_vGarage set ["cars", ["B_Quadbike_01_F"]];
|
_vGarage set ["cars", _self call ["getStartingUnlocks", ["cars", ["B_Quadbike_01_F"]]]];
|
||||||
_vGarage set ["helis", []];
|
_vGarage set ["helis", _self call ["getStartingUnlocks", ["helis", []]]];
|
||||||
_vGarage set ["naval", []];
|
_vGarage set ["naval", _self call ["getStartingUnlocks", ["naval", []]]];
|
||||||
_vGarage set ["other", []];
|
_vGarage set ["other", _self call ["getStartingUnlocks", ["other", []]]];
|
||||||
_vGarage set ["planes", []];
|
_vGarage set ["planes", _self call ["getStartingUnlocks", ["planes", []]]];
|
||||||
|
|
||||||
_vGarage
|
_vGarage
|
||||||
}]
|
}]
|
||||||
@ -71,17 +84,46 @@ GVAR(VGBaseStore) = compileFinal ([
|
|||||||
private _command = ["owned:garage:hot:fetch", "owned:garage:hot:init"] select _initialize;
|
private _command = ["owned:garage:hot:fetch", "owned:garage:hot:init"] select _initialize;
|
||||||
_self call ["callHotVGarage", [_command, [_uid]]]
|
_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 {
|
["init", compileFinal {
|
||||||
params [["_uid", "", [""]]];
|
params [["_uid", "", [""]]];
|
||||||
|
|
||||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||||
if (isNull _player) exitWith { createHashMap };
|
if (isNull _player) exitWith { createHashMap };
|
||||||
|
|
||||||
|
private _hasPersistentGarage = _self call ["isPersistentVGarageInitialized", [_uid]];
|
||||||
private _garage = _self call ["loadHotVGarage", [_uid, true]];
|
private _garage = _self call ["loadHotVGarage", [_uid, true]];
|
||||||
if (_garage isEqualTo createHashMap) then {
|
if (_garage isEqualTo createHashMap) then {
|
||||||
_garage = GVAR(VGarageModel) call ["defaults", []];
|
_garage = GVAR(VGarageModel) call ["defaults", []];
|
||||||
["ERROR", format ["Failed to initialize virtual garage for %1! Using fallback virtual garage.", _uid]] call EFUNC(common,log);
|
["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);
|
[CRPC(garage,responseInitVG), [_garage], _player] call CFUNC(targetEvent);
|
||||||
_garage
|
_garage
|
||||||
|
|||||||
@ -35,3 +35,24 @@ Locker listens for sync events through the event bus:
|
|||||||
- `notification.requested` - storage and item modification alerts
|
- `notification.requested` - storage and item modification alerts
|
||||||
|
|
||||||
The store module emits these events when granting items; locker applies the changes to player state.
|
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.
|
||||||
|
|||||||
@ -24,13 +24,26 @@
|
|||||||
#pragma hemtt ignore_variables ["_self"]
|
#pragma hemtt ignore_variables ["_self"]
|
||||||
GVAR(VArsenalModel) = compileFinal createHashMapObject [[
|
GVAR(VArsenalModel) = compileFinal createHashMapObject [[
|
||||||
["#type", "VArsenalModel"],
|
["#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 {
|
["defaults", compileFinal {
|
||||||
private _vArsenal = createHashMap;
|
private _vArsenal = createHashMap;
|
||||||
|
|
||||||
_vArsenal set ["backpacks", ["B_AssaultPack_rgr"]];
|
_vArsenal set ["backpacks", _self call ["getStartingUnlocks", ["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 ["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", ["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 ["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", ["arifle_MX_F", "hgun_P07_F"]];
|
_vArsenal set ["weapons", _self call ["getStartingUnlocks", ["weapons", ["arifle_MX_F", "hgun_P07_F"]]]];
|
||||||
|
|
||||||
_vArsenal
|
_vArsenal
|
||||||
}]
|
}]
|
||||||
@ -69,17 +82,46 @@ GVAR(VABaseStore) = compileFinal ([
|
|||||||
private _command = ["owned:locker:hot:fetch", "owned:locker:hot:init"] select _initialize;
|
private _command = ["owned:locker:hot:fetch", "owned:locker:hot:init"] select _initialize;
|
||||||
_self call ["callHotVArsenal", [_command, [_uid]]]
|
_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 {
|
["init", compileFinal {
|
||||||
params [["_uid", "", [""]]];
|
params [["_uid", "", [""]]];
|
||||||
|
|
||||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||||
if (isNull _player) exitWith { createHashMap };
|
if (isNull _player) exitWith { createHashMap };
|
||||||
|
|
||||||
|
private _hasPersistentArsenal = _self call ["isPersistentVArsenalInitialized", [_uid]];
|
||||||
private _arsenal = _self call ["loadHotVArsenal", [_uid, true]];
|
private _arsenal = _self call ["loadHotVArsenal", [_uid, true]];
|
||||||
if (_arsenal isEqualTo createHashMap) then {
|
if (_arsenal isEqualTo createHashMap) then {
|
||||||
_arsenal = GVAR(VArsenalModel) call ["defaults", []];
|
_arsenal = GVAR(VArsenalModel) call ["defaults", []];
|
||||||
["ERROR", format ["Failed to initialize virtual arsenal for %1! Using fallback virtual arsenal.", _uid]] call EFUNC(common,log);
|
["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);
|
[CRPC(locker,responseInitVA), [_arsenal], _player] call CFUNC(targetEvent);
|
||||||
_arsenal
|
_arsenal
|
||||||
|
|||||||
@ -4,10 +4,10 @@ PREP_RECOMPILE_START;
|
|||||||
#include "XEH_PREP.hpp"
|
#include "XEH_PREP.hpp"
|
||||||
PREP_RECOMPILE_END;
|
PREP_RECOMPILE_END;
|
||||||
|
|
||||||
GVAR(PlayerBootstrapRegistry) = createHashMap;
|
|
||||||
|
|
||||||
if (isServer) then { "forge_server" callExtension ["surreal:reconnect", []]; };
|
if (isServer) then { "forge_server" callExtension ["surreal:reconnect", []]; };
|
||||||
|
|
||||||
|
GVAR(PlayerBootstrapRegistry) = createHashMap;
|
||||||
|
|
||||||
["forge_icom_event", {
|
["forge_icom_event", {
|
||||||
params [["_event", "", [""]], ["_data", createHashMap, [createHashMap]]];
|
params [["_event", "", [""]], ["_data", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
|
|||||||
@ -19,10 +19,83 @@ extension owns authoritative checkout calculation through `store:checkout`.
|
|||||||
- `fnc_initStore.sqf` marks editor-placed store objects with `isStore = true`.
|
- `fnc_initStore.sqf` marks editor-placed store objects with `isStore = true`.
|
||||||
- `fnc_initCatalogService.sqf` scans live Arma config categories, builds
|
- `fnc_initCatalogService.sqf` scans live Arma config categories, builds
|
||||||
catalog responses, resolves checkout entries, and calculates authoritative
|
catalog responses, resolves checkout entries, and calculates authoritative
|
||||||
catalog prices.
|
catalog prices. It also applies the optional mission `CfgStore` filter and
|
||||||
|
overrides before payloads or checkout validation use catalog entries.
|
||||||
- `fnc_initStorefrontStore.sqf` builds hydrate payloads, validates checkout
|
- `fnc_initStorefrontStore.sqf` builds hydrate payloads, validates checkout
|
||||||
requests, calls `store:checkout`, syncs client patches, and coordinates
|
requests, calls `store:checkout`, syncs client patches, and coordinates
|
||||||
related bank/org persistence.
|
related bank/org persistence. Purchased units are fulfilled by spawning the
|
||||||
|
granted unit classes at discovered `unit_spawn` markers after the backend
|
||||||
|
charge succeeds.
|
||||||
|
|
||||||
|
## Mission Catalog Filter
|
||||||
|
Missions can include `CfgStore.hpp` from `description.ext` to control the
|
||||||
|
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"};
|
||||||
|
cars[] = {"B_MRAP_01_F"};
|
||||||
|
units[] = {"B_Soldier_F"};
|
||||||
|
};
|
||||||
|
|
||||||
|
class Overrides {
|
||||||
|
class arifle_MX_F {
|
||||||
|
price = 2500;
|
||||||
|
displayName = "MX Rifle";
|
||||||
|
description = "Approved PMC service rifle.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`dynamic` keeps the full generated catalog. `allowlist` only shows classnames
|
||||||
|
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. `dynamic` means no mod-source
|
||||||
|
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 exact config source addon/source mod names, and `prefixes[]` for classname,
|
||||||
|
source addon, or source mod 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.
|
||||||
|
|
||||||
|
The filter is currently global for the mission. Revisit per-store profile
|
||||||
|
support if individual vendors need different inventories.
|
||||||
|
|
||||||
|
## Unit Spawn Markers
|
||||||
|
Purchased units spawn at mission markers named `unit_spawn`, `unit_spawn_1`,
|
||||||
|
`unit_spawn_2`, and so on. The store resolves the closest initialized store
|
||||||
|
object to the requesting player, scans `allMapMarkers` at fulfillment time, and
|
||||||
|
uses the closest matching marker within 25 meters of that store.
|
||||||
|
|
||||||
|
If no matching marker exists within 25 meters, the store falls back to spawning
|
||||||
|
units around the store object. If no store object can be resolved, it falls back
|
||||||
|
to the requesting player so checkout still completes.
|
||||||
|
|
||||||
## Editor Entities
|
## Editor Entities
|
||||||
`fnc_initStore` matches non-null mission namespace objects whose variable names
|
`fnc_initStore` matches non-null mission namespace objects whose variable names
|
||||||
@ -31,8 +104,8 @@ contain `store`, mirroring the garage entity initialization pattern.
|
|||||||
## Checkout Flow
|
## Checkout Flow
|
||||||
Store checkout can charge cash, bank balance, organization funds, or approved
|
Store checkout can charge cash, bank balance, organization funds, or approved
|
||||||
credit lines depending on the hydrated session context. Checkout results can
|
credit lines depending on the hydrated session context. Checkout results can
|
||||||
grant locker assets, organization assets, and fleet vehicles through the
|
grant locker assets, organization assets, fleet vehicles, and immediate unit
|
||||||
related domain stores.
|
spawns through the related domain stores and Arma server runtime.
|
||||||
|
|
||||||
Checkout results emit notifications and syncs through the event bus:
|
Checkout results emit notifications and syncs through the event bus:
|
||||||
- `notification.requested` - receipt and transaction alerts
|
- `notification.requested` - receipt and transaction alerts
|
||||||
|
|||||||
@ -17,6 +17,226 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
_self set ["catalogCache", createHashMap];
|
_self set ["catalogCache", createHashMap];
|
||||||
["INFO", "Store catalog service initialized!"] call EFUNC(common,log);
|
["INFO", "Store catalog service initialized!"] call EFUNC(common,log);
|
||||||
}],
|
}],
|
||||||
|
["getMissionStoreConfig", compileFinal {
|
||||||
|
missionConfigFile >> "CfgStore"
|
||||||
|
}],
|
||||||
|
["getMissionStoreMode", compileFinal {
|
||||||
|
private _storeConfig = _self call ["getMissionStoreConfig", []];
|
||||||
|
private _mode = toLowerANSI getText (_storeConfig >> "mode");
|
||||||
|
|
||||||
|
if !(_mode in ["allowlist", "denylist", "dynamic"]) then { _mode = "dynamic"; };
|
||||||
|
|
||||||
|
_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 _matchPrefixes = _addons + _prefixes;
|
||||||
|
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, _matchPrefixes]]) exitWith { _sourceAddonMatched = true; };
|
||||||
|
} forEach _sourceAddons;
|
||||||
|
if (_sourceAddonMatched) exitWith { true };
|
||||||
|
if (_self call ["doesValueMatchAnyPrefix", [_className, _matchPrefixes]]) exitWith { true };
|
||||||
|
if (_self call ["doesValueMatchAnyPrefix", [_sourceMod, _matchPrefixes]]) 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", "", [""]]];
|
||||||
|
|
||||||
|
private _storeConfig = _self call ["getMissionStoreConfig", []];
|
||||||
|
private _categoryKey = _self call ["normalizeCategoryKey", [_category]];
|
||||||
|
private _categoryConfig = _storeConfig >> "Categories" >> _categoryKey;
|
||||||
|
private _classNames = [];
|
||||||
|
|
||||||
|
if (isArray _categoryConfig) then {
|
||||||
|
_classNames = getArray _categoryConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
_classNames apply {
|
||||||
|
private _className = "";
|
||||||
|
if (_x isEqualType "") then {
|
||||||
|
_className = _x;
|
||||||
|
} else {
|
||||||
|
_className = str _x;
|
||||||
|
};
|
||||||
|
|
||||||
|
toLowerANSI _className
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
["applyMissionStoreOverrides", compileFinal {
|
||||||
|
params [["_item", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
|
if (_item isEqualTo createHashMap) exitWith { _item };
|
||||||
|
|
||||||
|
private _className = _item getOrDefault ["className", ""];
|
||||||
|
if (_className isEqualTo "") exitWith { _item };
|
||||||
|
|
||||||
|
private _override = (_self call ["getMissionStoreConfig", []]) >> "Overrides" >> _className;
|
||||||
|
if !(isClass _override) exitWith { _item };
|
||||||
|
|
||||||
|
if (isText (_override >> "displayName")) then {
|
||||||
|
private _displayName = getText (_override >> "displayName");
|
||||||
|
if (_displayName isNotEqualTo "") then { _item set ["name", _displayName]; };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isText (_override >> "description")) then {
|
||||||
|
_item set ["description", getText (_override >> "description")];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isText (_override >> "image")) then {
|
||||||
|
_item set ["image", getText (_override >> "image")];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isText (_override >> "type")) then {
|
||||||
|
private _typeLabel = getText (_override >> "type");
|
||||||
|
if (_typeLabel isNotEqualTo "") then { _item set ["type", _typeLabel]; };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isNumber (_override >> "price")) then {
|
||||||
|
private _priceValue = floor (getNumber (_override >> "price") max 0);
|
||||||
|
_item set ["priceValue", _priceValue];
|
||||||
|
_item set ["price", _self call ["formatCurrency", [_priceValue]]];
|
||||||
|
};
|
||||||
|
|
||||||
|
_item
|
||||||
|
}],
|
||||||
|
["applyMissionStoreFilter", compileFinal {
|
||||||
|
params [["_category", "", [""]], ["_items", [], [[]]]];
|
||||||
|
|
||||||
|
private _mode = _self call ["getMissionStoreMode", []];
|
||||||
|
private _classNames = _self call ["getMissionStoreCategoryList", [_category]];
|
||||||
|
private _filteredItems = _self call ["applyMissionStoreModFilter", [_items]];
|
||||||
|
|
||||||
|
switch (_mode) do {
|
||||||
|
case "allowlist": {
|
||||||
|
_filteredItems = _filteredItems select {
|
||||||
|
(toLowerANSI (_x getOrDefault ["className", ""])) in _classNames
|
||||||
|
};
|
||||||
|
};
|
||||||
|
case "denylist": {
|
||||||
|
_filteredItems = _filteredItems select {
|
||||||
|
!((toLowerANSI (_x getOrDefault ["className", ""])) in _classNames)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
_filteredItems apply { _self call ["applyMissionStoreOverrides", [_x]] }
|
||||||
|
}],
|
||||||
["formatCurrency", compileFinal {
|
["formatCurrency", compileFinal {
|
||||||
params [["_amount", 0, [0]]];
|
params [["_amount", 0, [0]]];
|
||||||
|
|
||||||
@ -82,6 +302,8 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
|
|
||||||
private _className = configName _cfg;
|
private _className = configName _cfg;
|
||||||
private _displayName = getText (_cfg >> "displayName");
|
private _displayName = getText (_cfg >> "displayName");
|
||||||
|
private _sourceAddons = configSourceAddonList _cfg;
|
||||||
|
private _sourceMod = configSourceMod _cfg;
|
||||||
private _picture = getText (_cfg >> _imageField);
|
private _picture = getText (_cfg >> _imageField);
|
||||||
if (_picture isEqualTo "" && { _imageField isNotEqualTo "picture" }) then {
|
if (_picture isEqualTo "" && { _imageField isNotEqualTo "picture" }) then {
|
||||||
_picture = getText (_cfg >> "picture");
|
_picture = getText (_cfg >> "picture");
|
||||||
@ -97,7 +319,9 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
["price", _self call ["formatCurrency", [_priceValue]]],
|
["price", _self call ["formatCurrency", [_priceValue]]],
|
||||||
["priceValue", _priceValue],
|
["priceValue", _priceValue],
|
||||||
["image", _picture],
|
["image", _picture],
|
||||||
["type", _typeLabel]
|
["type", _typeLabel],
|
||||||
|
["sourceAddons", _sourceAddons],
|
||||||
|
["sourceMod", _sourceMod]
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
["appendCfgWeaponsByItemInfoType", compileFinal {
|
["appendCfgWeaponsByItemInfoType", compileFinal {
|
||||||
@ -196,6 +420,22 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
|
|
||||||
_items
|
_items
|
||||||
}],
|
}],
|
||||||
|
["appendCfgUnits", compileFinal {
|
||||||
|
params [["_items", [], [[]]], ["_typeLabel", "Unit", [""]], ["_fallbackDescription", "", [""]]];
|
||||||
|
|
||||||
|
{
|
||||||
|
private _cfg = _x;
|
||||||
|
private _className = configName _cfg;
|
||||||
|
if (
|
||||||
|
_self call ["isVisibleConfig", [_cfg]]
|
||||||
|
&& { _className isKindOf ["CAManBase", configFile >> "CfgVehicles"] }
|
||||||
|
) then {
|
||||||
|
_items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, _fallbackDescription, "editorPreview", true]]);
|
||||||
|
};
|
||||||
|
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
|
||||||
|
|
||||||
|
_items
|
||||||
|
}],
|
||||||
["isBackpackConfig", compileFinal {
|
["isBackpackConfig", compileFinal {
|
||||||
params [["_cfg", configNull, [configNull]]];
|
params [["_cfg", configNull, [configNull]]];
|
||||||
|
|
||||||
@ -270,6 +510,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
case "helis": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Helicopter", "Aircraft", "Live helicopter entry generated from the game inventory."]]; };
|
case "helis": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Helicopter", "Aircraft", "Live helicopter entry generated from the game inventory."]]; };
|
||||||
case "planes": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Plane", "Aircraft", "Live fixed-wing entry generated from the game inventory."]]; };
|
case "planes": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Plane", "Aircraft", "Live fixed-wing entry generated from the game inventory."]]; };
|
||||||
case "naval": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Ship", "Naval", "Live naval vehicle entry generated from the game inventory."]]; };
|
case "naval": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Ship", "Naval", "Live naval vehicle entry generated from the game inventory."]]; };
|
||||||
|
case "units": { _items = _self call ["appendCfgUnits", [_items, "Unit", "Live unit entry generated from the game inventory."]]; };
|
||||||
case "other": {
|
case "other": {
|
||||||
{
|
{
|
||||||
private _cfg = _x;
|
private _cfg = _x;
|
||||||
@ -305,10 +546,16 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
|
|
||||||
(toLowerANSI _category) in ["cars", "armor", "helis", "planes", "naval", "other"]
|
(toLowerANSI _category) in ["cars", "armor", "helis", "planes", "naval", "other"]
|
||||||
}],
|
}],
|
||||||
|
["isUnitCategory", compileFinal {
|
||||||
|
params [["_category", "", [""]]];
|
||||||
|
|
||||||
|
(toLowerANSI _category) isEqualTo "units"
|
||||||
|
}],
|
||||||
["buildPayloadCategory", compileFinal {
|
["buildPayloadCategory", compileFinal {
|
||||||
params [["_category", "", [""]]];
|
params [["_category", "", [""]]];
|
||||||
|
|
||||||
switch (toLowerANSI _category) do {
|
switch (toLowerANSI _category) do {
|
||||||
|
case "units": { "units" };
|
||||||
case "backpacks": { "backpack" };
|
case "backpacks": { "backpack" };
|
||||||
case "attachments": { "attachment" };
|
case "attachments": { "attachment" };
|
||||||
case "ammo": { "magazine" };
|
case "ammo": { "magazine" };
|
||||||
@ -327,7 +574,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
["isSupportedCategory", compileFinal {
|
["isSupportedCategory", compileFinal {
|
||||||
params [["_category", "", [""]]];
|
params [["_category", "", [""]]];
|
||||||
|
|
||||||
(_self call ["normalizeCategoryKey", [_category]]) in ["uniforms", "headgear", "vests", "backpacks", "attachments", "facewear", "ammo", "misc", "primary", "handgun", "secondary", "cars", "armor", "helis", "planes", "naval", "other"]
|
(_self call ["normalizeCategoryKey", [_category]]) in ["uniforms", "headgear", "vests", "backpacks", "attachments", "facewear", "ammo", "misc", "primary", "handgun", "secondary", "cars", "armor", "helis", "planes", "naval", "other", "units"]
|
||||||
}],
|
}],
|
||||||
["buildCategoryItems", compileFinal {
|
["buildCategoryItems", compileFinal {
|
||||||
params [["_category", "", [""]]];
|
params [["_category", "", [""]]];
|
||||||
@ -340,13 +587,17 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
|
|
||||||
private _items = _self call ["scanCategoryItems", [_categoryKey]];
|
private _items = _self call ["scanCategoryItems", [_categoryKey]];
|
||||||
private _payloadCategory = _self call ["buildPayloadCategory", [_categoryKey]];
|
private _payloadCategory = _self call ["buildPayloadCategory", [_categoryKey]];
|
||||||
private _entryKind = ["item", "vehicle"] select (_self call ["isVehicleCategory", [_categoryKey]]);
|
private _entryKind = "item";
|
||||||
|
if (_self call ["isVehicleCategory", [_categoryKey]]) then { _entryKind = "vehicle"; };
|
||||||
|
if (_self call ["isUnitCategory", [_categoryKey]]) then { _entryKind = "unit"; };
|
||||||
|
|
||||||
{
|
{
|
||||||
_x set ["category", _payloadCategory];
|
_x set ["category", _payloadCategory];
|
||||||
_x set ["entryKind", _entryKind];
|
_x set ["entryKind", _entryKind];
|
||||||
} forEach _items;
|
} forEach _items;
|
||||||
|
|
||||||
|
_items = _self call ["applyMissionStoreFilter", [_categoryKey, _items]];
|
||||||
|
|
||||||
_catalogCache set [_categoryKey, _items];
|
_catalogCache set [_categoryKey, _items];
|
||||||
_self set ["catalogCache", _catalogCache];
|
_self set ["catalogCache", _catalogCache];
|
||||||
|
|
||||||
@ -376,6 +627,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
private _category = toLowerANSI (_entry getOrDefault ["category", ""]);
|
private _category = toLowerANSI (_entry getOrDefault ["category", ""]);
|
||||||
|
|
||||||
if (_entryKind isEqualTo "vehicle") exitWith { ["cars", "armor", "helis", "planes", "naval", "other"] };
|
if (_entryKind isEqualTo "vehicle") exitWith { ["cars", "armor", "helis", "planes", "naval", "other"] };
|
||||||
|
if (_entryKind isEqualTo "unit" || { _category isEqualTo "units" }) exitWith { ["units"] };
|
||||||
if (_category isEqualTo "weapon") exitWith { ["primary", "handgun", "secondary"] };
|
if (_category isEqualTo "weapon") exitWith { ["primary", "handgun", "secondary"] };
|
||||||
if (_category isEqualTo "backpack") exitWith { ["backpacks"] };
|
if (_category isEqualTo "backpack") exitWith { ["backpacks"] };
|
||||||
if (_category isEqualTo "attachment") exitWith { ["attachments"] };
|
if (_category isEqualTo "attachment") exitWith { ["attachments"] };
|
||||||
@ -400,19 +652,21 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
_resolved
|
_resolved
|
||||||
}],
|
}],
|
||||||
["buildCheckoutRequest", compileFinal {
|
["buildCheckoutRequest", compileFinal {
|
||||||
params [["_items", [], [[]]], ["_vehicles", [], [[]]]];
|
params [["_items", [], [[]]], ["_vehicles", [], [[]]], ["_units", [], [[]]]];
|
||||||
|
|
||||||
private _result = createHashMapFromArray [
|
private _result = createHashMapFromArray [
|
||||||
["success", false],
|
["success", false],
|
||||||
["total", 0],
|
["total", 0],
|
||||||
["message", "Checkout total must be greater than zero."],
|
["message", "Checkout total must be greater than zero."],
|
||||||
["items", []],
|
["items", []],
|
||||||
["vehicles", []]
|
["vehicles", []],
|
||||||
|
["units", []]
|
||||||
];
|
];
|
||||||
private _total = 0;
|
private _total = 0;
|
||||||
private _message = "";
|
private _message = "";
|
||||||
private _resolvedItems = [];
|
private _resolvedItems = [];
|
||||||
private _resolvedVehicles = [];
|
private _resolvedVehicles = [];
|
||||||
|
private _resolvedUnits = [];
|
||||||
|
|
||||||
{
|
{
|
||||||
if (_message isEqualTo "") then {
|
if (_message isEqualTo "") then {
|
||||||
@ -463,6 +717,29 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
} forEach _vehicles;
|
} forEach _vehicles;
|
||||||
|
|
||||||
|
{
|
||||||
|
if (_message isEqualTo "") then {
|
||||||
|
private _className = _x getOrDefault ["classname", ""];
|
||||||
|
if (_className isEqualTo "") then {
|
||||||
|
_message = "Checkout contains an invalid unit entry.";
|
||||||
|
} else {
|
||||||
|
private _catalogEntry = _self call ["resolveCheckoutCatalogEntry", [createHashMapFromArray [["classname", _className], ["category", "units"], ["entryKind", "unit"]]]];
|
||||||
|
|
||||||
|
if (_catalogEntry isEqualTo createHashMap) then {
|
||||||
|
_message = format ["Unsupported store unit: %1", _className];
|
||||||
|
} else {
|
||||||
|
private _priceValue = _catalogEntry getOrDefault ["priceValue", 0];
|
||||||
|
_total = _total + _priceValue;
|
||||||
|
_resolvedUnits pushBack (createHashMapFromArray [
|
||||||
|
["classname", _className],
|
||||||
|
["category", "units"],
|
||||||
|
["priceValue", _priceValue]
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} forEach _units;
|
||||||
|
|
||||||
if (_message isNotEqualTo "") exitWith {
|
if (_message isNotEqualTo "") exitWith {
|
||||||
_result set ["message", _message];
|
_result set ["message", _message];
|
||||||
_result
|
_result
|
||||||
@ -475,12 +752,13 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
_result set ["message", ""];
|
_result set ["message", ""];
|
||||||
_result set ["items", _resolvedItems];
|
_result set ["items", _resolvedItems];
|
||||||
_result set ["vehicles", _resolvedVehicles];
|
_result set ["vehicles", _resolvedVehicles];
|
||||||
|
_result set ["units", _resolvedUnits];
|
||||||
_result
|
_result
|
||||||
}],
|
}],
|
||||||
["calculateCheckoutTotal", compileFinal {
|
["calculateCheckoutTotal", compileFinal {
|
||||||
params [["_items", [], [[]]], ["_vehicles", [], [[]]]];
|
params [["_items", [], [[]]], ["_vehicles", [], [[]]], ["_units", [], [[]]]];
|
||||||
|
|
||||||
private _checkout = _self call ["buildCheckoutRequest", [_items, _vehicles]];
|
private _checkout = _self call ["buildCheckoutRequest", [_items, _vehicles, _units]];
|
||||||
createHashMapFromArray [
|
createHashMapFromArray [
|
||||||
["success", _checkout getOrDefault ["success", false]],
|
["success", _checkout getOrDefault ["success", false]],
|
||||||
["total", _checkout getOrDefault ["total", 0]],
|
["total", _checkout getOrDefault ["total", 0]],
|
||||||
|
|||||||
@ -155,6 +155,7 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [
|
|||||||
["chargedTotal", 0],
|
["chargedTotal", 0],
|
||||||
["lockerGranted", []],
|
["lockerGranted", []],
|
||||||
["vehicleGranted", []],
|
["vehicleGranted", []],
|
||||||
|
["unitGranted", []],
|
||||||
["bankPatch", createHashMap],
|
["bankPatch", createHashMap],
|
||||||
["orgPatch", createHashMap],
|
["orgPatch", createHashMap],
|
||||||
["orgTargetUids", []],
|
["orgTargetUids", []],
|
||||||
@ -168,6 +169,71 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [
|
|||||||
|
|
||||||
format ["$%1", [_amount max 0] call EFUNC(common,formatNumber)]
|
format ["$%1", [_amount max 0] call EFUNC(common,formatNumber)]
|
||||||
}],
|
}],
|
||||||
|
["getUnitSpawnMarkers", compileFinal {
|
||||||
|
private _markers = allMapMarkers select {
|
||||||
|
private _markerName = toLowerANSI _x;
|
||||||
|
_markerName isEqualTo "unit_spawn" || { (_markerName find "unit_spawn_") == 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
_markers sort true;
|
||||||
|
_markers
|
||||||
|
}],
|
||||||
|
["getStoreObjects", compileFinal {
|
||||||
|
(allVariables missionNamespace) apply { missionNamespace getVariable [_x, objNull] } select {
|
||||||
|
_x isEqualType objNull
|
||||||
|
&& { !isNull _x }
|
||||||
|
&& { _x getVariable ["isStore", false] }
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
["getClosestStoreObject", compileFinal {
|
||||||
|
params [["_origin", objNull, [objNull]]];
|
||||||
|
|
||||||
|
if (isNull _origin) exitWith { objNull };
|
||||||
|
|
||||||
|
private _stores = _self call ["getStoreObjects", []];
|
||||||
|
if (_stores isEqualTo []) exitWith { objNull };
|
||||||
|
|
||||||
|
private _closestStore = objNull;
|
||||||
|
private _closestDistance = 1e12;
|
||||||
|
{
|
||||||
|
private _distance = _origin distance2D _x;
|
||||||
|
if (_distance < _closestDistance) then {
|
||||||
|
_closestDistance = _distance;
|
||||||
|
_closestStore = _x;
|
||||||
|
};
|
||||||
|
} forEach _stores;
|
||||||
|
|
||||||
|
_closestStore
|
||||||
|
}],
|
||||||
|
["getClosestUnitSpawnMarker", compileFinal {
|
||||||
|
params [["_origin", objNull, [objNull, []]], ["_maxDistance", -1, [0]]];
|
||||||
|
|
||||||
|
private _markers = _self call ["getUnitSpawnMarkers", []];
|
||||||
|
if (_markers isEqualTo []) exitWith { "" };
|
||||||
|
|
||||||
|
private _originPosition = if (_origin isEqualType objNull) then {
|
||||||
|
getPosATL _origin
|
||||||
|
} else {
|
||||||
|
_origin
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_maxDistance >= 0) then {
|
||||||
|
_markers = _markers select { ((getMarkerPos _x) distance2D _originPosition) <= _maxDistance };
|
||||||
|
if (_markers isEqualTo []) exitWith { "" };
|
||||||
|
};
|
||||||
|
|
||||||
|
private _closestMarker = "";
|
||||||
|
private _closestDistance = 1e12;
|
||||||
|
{
|
||||||
|
private _distance = _originPosition distance2D (getMarkerPos _x);
|
||||||
|
if (_distance < _closestDistance) then {
|
||||||
|
_closestDistance = _distance;
|
||||||
|
_closestMarker = _x;
|
||||||
|
};
|
||||||
|
} forEach _markers;
|
||||||
|
|
||||||
|
_closestMarker
|
||||||
|
}],
|
||||||
["callCheckoutBackendEnvelope", compileFinal {
|
["callCheckoutBackendEnvelope", compileFinal {
|
||||||
params [["_context", createHashMap, [createHashMap]]];
|
params [["_context", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
@ -207,7 +273,8 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [
|
|||||||
["_player", objNull, [objNull]],
|
["_player", objNull, [objNull]],
|
||||||
["_paymentMethod", "cash", [""]],
|
["_paymentMethod", "cash", [""]],
|
||||||
["_items", [], [[]]],
|
["_items", [], [[]]],
|
||||||
["_vehicles", [], [[]]]
|
["_vehicles", [], [[]]],
|
||||||
|
["_units", [], [[]]]
|
||||||
];
|
];
|
||||||
|
|
||||||
if (_uid isEqualTo "" || { isNull _player }) exitWith { createHashMap };
|
if (_uid isEqualTo "" || { isNull _player }) exitWith { createHashMap };
|
||||||
@ -225,9 +292,58 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [
|
|||||||
["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo],
|
["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo],
|
||||||
["paymentMethod", toLowerANSI _paymentMethod],
|
["paymentMethod", toLowerANSI _paymentMethod],
|
||||||
["items", _items],
|
["items", _items],
|
||||||
["vehicles", _vehicles]
|
["vehicles", _vehicles],
|
||||||
|
["units", _units]
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
|
["spawnPurchasedUnits", compileFinal {
|
||||||
|
params [["_player", objNull, [objNull]], ["_units", [], [[]]]];
|
||||||
|
|
||||||
|
private _result = createHashMapFromArray [
|
||||||
|
["spawned", []],
|
||||||
|
["failed", []]
|
||||||
|
];
|
||||||
|
if (isNull _player || { _units isEqualTo [] }) exitWith { _result };
|
||||||
|
|
||||||
|
private _group = group _player;
|
||||||
|
private _store = _self call ["getClosestStoreObject", [_player]];
|
||||||
|
private _spawnAnchor = [objNull, _store] select !(isNull _store);
|
||||||
|
if (isNull _spawnAnchor) then { _spawnAnchor = _player; };
|
||||||
|
|
||||||
|
private _spawnMarker = "";
|
||||||
|
if !(isNull _store) then {
|
||||||
|
_spawnMarker = _self call ["getClosestUnitSpawnMarker", [_store, 25]];
|
||||||
|
};
|
||||||
|
{
|
||||||
|
private _className = _x getOrDefault ["classname", ""];
|
||||||
|
if (_className isEqualTo "" || { !(isClass (configFile >> "CfgVehicles" >> _className)) }) then {
|
||||||
|
(_result get "failed") pushBack _className;
|
||||||
|
} else {
|
||||||
|
private _basePosition = getPosATL _spawnAnchor;
|
||||||
|
private _baseDirection = getDir _spawnAnchor;
|
||||||
|
if (_spawnMarker isNotEqualTo "") then {
|
||||||
|
_basePosition = getMarkerPos _spawnMarker;
|
||||||
|
_baseDirection = markerDir _spawnMarker;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _spawnPos = _basePosition findEmptyPosition [0, 18 + (_forEachIndex min 12), _className];
|
||||||
|
if (_spawnPos isEqualTo []) then {
|
||||||
|
_spawnPos = _basePosition getPos [3 + _forEachIndex, _baseDirection + 90];
|
||||||
|
};
|
||||||
|
|
||||||
|
private _unit = _group createUnit [_className, _spawnPos, [], 0, "NONE"];
|
||||||
|
if (isNull _unit) then {
|
||||||
|
(_result get "failed") pushBack _className;
|
||||||
|
} else {
|
||||||
|
_unit setDir _baseDirection;
|
||||||
|
[_unit] joinSilent _group;
|
||||||
|
(_result get "spawned") pushBack _className;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} forEach _units;
|
||||||
|
|
||||||
|
_result
|
||||||
|
}],
|
||||||
["syncCheckoutResult", compileFinal {
|
["syncCheckoutResult", compileFinal {
|
||||||
params [["_player", objNull, [objNull]], ["_result", createHashMap, [createHashMap]]];
|
params [["_player", objNull, [objNull]], ["_result", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
@ -238,6 +354,7 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [
|
|||||||
private _vgPatch = _result getOrDefault ["vgaragePatch", createHashMap];
|
private _vgPatch = _result getOrDefault ["vgaragePatch", createHashMap];
|
||||||
private _bankPatch = _result getOrDefault ["bankPatch", createHashMap];
|
private _bankPatch = _result getOrDefault ["bankPatch", createHashMap];
|
||||||
private _orgPatch = _result getOrDefault ["orgPatch", createHashMap];
|
private _orgPatch = _result getOrDefault ["orgPatch", createHashMap];
|
||||||
|
private _unitGranted = _result getOrDefault ["unitGranted", []];
|
||||||
private _uid = getPlayerUID _player;
|
private _uid = getPlayerUID _player;
|
||||||
|
|
||||||
if (keys _lockerPatch isNotEqualTo []) then {
|
if (keys _lockerPatch isNotEqualTo []) then {
|
||||||
@ -320,6 +437,14 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (_unitGranted isNotEqualTo []) then {
|
||||||
|
private _unitSpawnResult = _self call ["spawnPurchasedUnits", [_player, _unitGranted]];
|
||||||
|
private _failedUnits = _unitSpawnResult getOrDefault ["failed", []];
|
||||||
|
if (_failedUnits isNotEqualTo []) then {
|
||||||
|
["ERROR", format ["Store checkout unit spawn failed for %1: %2", _uid, _failedUnits joinString ", "]] call EFUNC(common,log);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["persistCheckoutState", compileFinal {
|
["persistCheckoutState", compileFinal {
|
||||||
@ -398,19 +523,20 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [
|
|||||||
private _paymentMethod = toLowerANSI (_payload getOrDefault ["paymentMethod", "cash"]);
|
private _paymentMethod = toLowerANSI (_payload getOrDefault ["paymentMethod", "cash"]);
|
||||||
private _items = _payload getOrDefault ["items", []];
|
private _items = _payload getOrDefault ["items", []];
|
||||||
private _vehicles = _payload getOrDefault ["vehicles", []];
|
private _vehicles = _payload getOrDefault ["vehicles", []];
|
||||||
|
private _units = _payload getOrDefault ["units", []];
|
||||||
|
|
||||||
if (isNil QGVAR(StoreCatalogService)) exitWith {
|
if (isNil QGVAR(StoreCatalogService)) exitWith {
|
||||||
_result set ["message", "Store catalog service is unavailable."];
|
_result set ["message", "Store catalog service is unavailable."];
|
||||||
_result
|
_result
|
||||||
};
|
};
|
||||||
|
|
||||||
private _checkoutRequest = GVAR(StoreCatalogService) call ["buildCheckoutRequest", [_items, _vehicles]];
|
private _checkoutRequest = GVAR(StoreCatalogService) call ["buildCheckoutRequest", [_items, _vehicles, _units]];
|
||||||
private _totalPrice = _checkoutRequest getOrDefault ["total", 0];
|
private _totalPrice = _checkoutRequest getOrDefault ["total", 0];
|
||||||
|
|
||||||
_result set ["paymentMethod", _paymentMethod];
|
_result set ["paymentMethod", _paymentMethod];
|
||||||
_result set ["chargedTotal", _totalPrice];
|
_result set ["chargedTotal", _totalPrice];
|
||||||
|
|
||||||
if (_items isEqualTo [] && { _vehicles isEqualTo [] }) exitWith {
|
if (_items isEqualTo [] && { _vehicles isEqualTo [] } && { _units isEqualTo [] }) exitWith {
|
||||||
_result set ["message", "Add at least one item before checkout."];
|
_result set ["message", "Add at least one item before checkout."];
|
||||||
_result
|
_result
|
||||||
};
|
};
|
||||||
@ -425,7 +551,8 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [
|
|||||||
_player,
|
_player,
|
||||||
_paymentMethod,
|
_paymentMethod,
|
||||||
_checkoutRequest getOrDefault ["items", []],
|
_checkoutRequest getOrDefault ["items", []],
|
||||||
_checkoutRequest getOrDefault ["vehicles", []]
|
_checkoutRequest getOrDefault ["vehicles", []],
|
||||||
|
_checkoutRequest getOrDefault ["units", []]
|
||||||
]];
|
]];
|
||||||
if (_checkoutContext isEqualTo createHashMap) exitWith {
|
if (_checkoutContext isEqualTo createHashMap) exitWith {
|
||||||
_result set ["message", "Checkout request context was invalid."];
|
_result set ["message", "Checkout request context was invalid."];
|
||||||
@ -451,13 +578,15 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [
|
|||||||
|
|
||||||
_result set ["success", true];
|
_result set ["success", true];
|
||||||
_result set ["message", _backendResult getOrDefault ["message", format [
|
_result set ["message", _backendResult getOrDefault ["message", format [
|
||||||
"Checkout completed. %1 charged, %2 locker grant(s), %3 vehicle unlock(s).",
|
"Checkout completed. %1 charged, %2 locker grant(s), %3 vehicle unlock(s), %4 unit grant(s).",
|
||||||
_self call ["formatCurrency", [_totalPrice]],
|
_self call ["formatCurrency", [_totalPrice]],
|
||||||
count (_backendResult getOrDefault ["lockerGranted", []]),
|
count (_backendResult getOrDefault ["lockerGranted", []]),
|
||||||
count (_backendResult getOrDefault ["vehicleGranted", []])
|
count (_backendResult getOrDefault ["vehicleGranted", []]),
|
||||||
|
count (_backendResult getOrDefault ["unitGranted", []])
|
||||||
]]];
|
]]];
|
||||||
_result set ["lockerGranted", _backendResult getOrDefault ["lockerGranted", []]];
|
_result set ["lockerGranted", _backendResult getOrDefault ["lockerGranted", []]];
|
||||||
_result set ["vehicleGranted", _backendResult getOrDefault ["vehicleGranted", []]];
|
_result set ["vehicleGranted", _backendResult getOrDefault ["vehicleGranted", []]];
|
||||||
|
_result set ["unitGranted", _backendResult getOrDefault ["unitGranted", []]];
|
||||||
_result set ["persistenceSucceeded", _persistenceResult getOrDefault ["success", false]];
|
_result set ["persistenceSucceeded", _persistenceResult getOrDefault ["success", false]];
|
||||||
_result set ["persistenceFailures", _persistenceResult getOrDefault ["failures", []]];
|
_result set ["persistenceFailures", _persistenceResult getOrDefault ["failures", []]];
|
||||||
_result set ["persistenceMessage", _persistenceResult getOrDefault ["message", ""]];
|
_result set ["persistenceMessage", _persistenceResult getOrDefault ["message", ""]];
|
||||||
|
|||||||
@ -226,10 +226,10 @@ If you want the accepting player's org to own the task rewards, use `fnc_handler
|
|||||||
- compiles functions
|
- compiles functions
|
||||||
- initializes `TaskStore`
|
- initializes `TaskStore`
|
||||||
- initializes task instance and entity controller classes
|
- initializes task instance and entity controller classes
|
||||||
|
- initializes generated mission provider objects and registers the built-in provider
|
||||||
|
- registers task lifecycle log and notification listeners with the event bus
|
||||||
- `XEH_postInit.sqf`
|
- `XEH_postInit.sqf`
|
||||||
- registers task lifecycle event listeners with the event bus
|
- registers CBA server events for provider registration and mission setup requests
|
||||||
- handles task reward, notification, and rating events
|
|
||||||
- syncs org and bank state through event bus listeners
|
|
||||||
- registers the ACE defuse event hook
|
- registers the ACE defuse event hook
|
||||||
|
|
||||||
## Events Emitted
|
## Events Emitted
|
||||||
@ -246,7 +246,8 @@ Task module emits the following events to the event bus:
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- the dynamic mission manager in `fnc_missionManager.sqf` is initialized during task post-init; timer-based mission generation only runs when the `forge_server_task_enableGenerator` CBA setting is enabled
|
- the dynamic mission manager in `fnc_missionManager.sqf` is initialized during task post-init; timer-based mission generation only runs when the `forge_server_task_enableGenerator` CBA setting is enabled
|
||||||
- CAD can request a specific generated mission type through `fnc_requestMissionTask.sqf`
|
- CAD hydrates generated mission types and requests generated missions through `MissionGeneratorProviderRegistry`
|
||||||
|
- custom generated mission providers register through the `forge_server_task_registerMissionGeneratorProvider` CBA server event
|
||||||
- it starts server-owned tasks through `fnc_handler.sqf` and binds them to the `default` org
|
- it starts server-owned tasks through `fnc_handler.sqf` and binds them to the `default` org
|
||||||
- task lifecycle for the mission manager is tracked through `TaskStore` status entries
|
- task lifecycle for the mission manager is tracked through `TaskStore` status entries
|
||||||
- task backend state is intentionally transient and resets with the active server/mission lifecycle
|
- task backend state is intentionally transient and resets with the active server/mission lifecycle
|
||||||
|
|||||||
@ -15,7 +15,6 @@ PREP(makeObject);
|
|||||||
PREP(makeShooter);
|
PREP(makeShooter);
|
||||||
PREP(makeTarget);
|
PREP(makeTarget);
|
||||||
PREP(missionManager);
|
PREP(missionManager);
|
||||||
PREP(requestMissionTask);
|
|
||||||
PREP(initTaskStore);
|
PREP(initTaskStore);
|
||||||
|
|
||||||
PREP_SUBDIR(generators,attackMissionGenerator);
|
PREP_SUBDIR(generators,attackMissionGenerator);
|
||||||
@ -56,6 +55,9 @@ PREP_SUBDIR(objects,TaskCatalogStore);
|
|||||||
PREP_SUBDIR(objects,TaskEntityRegistry);
|
PREP_SUBDIR(objects,TaskEntityRegistry);
|
||||||
PREP_SUBDIR(objects,TaskParticipantTracker);
|
PREP_SUBDIR(objects,TaskParticipantTracker);
|
||||||
PREP_SUBDIR(objects,TaskRewardService);
|
PREP_SUBDIR(objects,TaskRewardService);
|
||||||
|
PREP_SUBDIR(objects,TaskNotificationService);
|
||||||
|
PREP_SUBDIR(objects,MissionGeneratorProviderRegistry);
|
||||||
|
PREP_SUBDIR(objects,BuiltinMissionGeneratorProvider);
|
||||||
PREP_SUBDIR(objects,EntityControllerBaseClass);
|
PREP_SUBDIR(objects,EntityControllerBaseClass);
|
||||||
PREP_SUBDIR(objects,AttackTaskBaseClass);
|
PREP_SUBDIR(objects,AttackTaskBaseClass);
|
||||||
PREP_SUBDIR(objects,HostageTaskBaseClass);
|
PREP_SUBDIR(objects,HostageTaskBaseClass);
|
||||||
|
|||||||
@ -3,6 +3,15 @@
|
|||||||
if (isNil QEGVAR(common,EventBus)) then { call EFUNC(common,eventBus); true };
|
if (isNil QEGVAR(common,EventBus)) then { call EFUNC(common,eventBus); true };
|
||||||
if (isNil QGVAR(MissionSetupService)) then { call FUNC(initMissionSetupService); };
|
if (isNil QGVAR(MissionSetupService)) then { call FUNC(initMissionSetupService); };
|
||||||
|
|
||||||
|
[SRPC(task,registerMissionGeneratorProvider), {
|
||||||
|
params [
|
||||||
|
["_providerId", "", [""]],
|
||||||
|
["_provider", createHashMap, [createHashMap]]
|
||||||
|
];
|
||||||
|
|
||||||
|
GVAR(MissionGeneratorProviderRegistry) call ["registerProvider", [_providerId, _provider]];
|
||||||
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[SRPC(task,requestOpenMissionSetup), {
|
[SRPC(task,requestOpenMissionSetup), {
|
||||||
params [
|
params [
|
||||||
["_requester", objNull, [objNull]]
|
["_requester", objNull, [objNull]]
|
||||||
@ -100,116 +109,10 @@ if (isNil QGVAR(MissionSetupService)) then { call FUNC(initMissionSetupService);
|
|||||||
]] call EFUNC(common,log);
|
]] call EFUNC(common,log);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isNil QGVAR(MissionSetupService)) then { call FUNC(initMissionSetupService); };
|
|
||||||
["INFO", format ["Mission setup apply request accepted. Requester=%1 VarName=%2", _requester, _requesterVar]] call EFUNC(common,log);
|
["INFO", format ["Mission setup apply request accepted. Requester=%1 VarName=%2", _requester, _requesterVar]] call EFUNC(common,log);
|
||||||
GVAR(MissionSetupService) call ["apply", [_overrides]];
|
GVAR(MissionSetupService) call ["apply", [_overrides]];
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
if (isNil QGVAR(TaskLifecycleEventLogTokens)) then {
|
|
||||||
private _logTaskLifecycleEvent = {
|
|
||||||
params ["_event"];
|
|
||||||
|
|
||||||
if !(GETGVAR(enableEventLogs,false)) exitWith {};
|
|
||||||
|
|
||||||
["INFO", format [
|
|
||||||
"Task lifecycle event: %1 taskID=%2 taskType=%3 status=%4 participants=%5",
|
|
||||||
_event getOrDefault ["event", ""],
|
|
||||||
_event getOrDefault ["taskID", ""],
|
|
||||||
_event getOrDefault ["taskType", ""],
|
|
||||||
_event getOrDefault ["status", ""],
|
|
||||||
_event getOrDefault ["participants", []]
|
|
||||||
]] call EFUNC(common,log);
|
|
||||||
};
|
|
||||||
|
|
||||||
private _logTaskRewardEvent = {
|
|
||||||
params ["_event"];
|
|
||||||
|
|
||||||
if !(GETGVAR(enableEventLogs,false)) exitWith {};
|
|
||||||
|
|
||||||
["INFO", format [
|
|
||||||
"Task reward event: %1 taskID=%2 success=%3 message=%4",
|
|
||||||
_event getOrDefault ["event", ""],
|
|
||||||
_event getOrDefault ["taskID", ""],
|
|
||||||
!((_event getOrDefault ["event", ""]) in ["task.reward.failed", "task.rating.failed"]),
|
|
||||||
_event getOrDefault ["message", ""]
|
|
||||||
]] call EFUNC(common,log);
|
|
||||||
};
|
|
||||||
|
|
||||||
GVAR(TaskLifecycleEventLogTokens) = [
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.created", _logTaskLifecycleEvent, "task.lifecycle.log"]],
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.started", _logTaskLifecycleEvent, "task.lifecycle.log"]],
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.completed", _logTaskLifecycleEvent, "task.lifecycle.log"]],
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.failed", _logTaskLifecycleEvent, "task.lifecycle.log"]],
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.cleared", _logTaskLifecycleEvent, "task.lifecycle.log"]],
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.reward.requested", _logTaskRewardEvent, "task.reward.log"]],
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.reward.applied", _logTaskRewardEvent, "task.reward.log"]],
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.reward.failed", _logTaskRewardEvent, "task.reward.log"]],
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.rating.applied", _logTaskRewardEvent, "task.reward.log"]],
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.rating.failed", _logTaskRewardEvent, "task.reward.log"]]
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isNil QGVAR(TaskNotificationEventTokens)) then {
|
|
||||||
private _sendTaskNotification = {
|
|
||||||
params ["_event"];
|
|
||||||
|
|
||||||
private _type = _event getOrDefault ["notificationType", "info"];
|
|
||||||
private _title = _event getOrDefault ["title", "Tasks"];
|
|
||||||
private _message = _event getOrDefault ["message", ""];
|
|
||||||
private _participantUids = +(_event getOrDefault ["participantUids", []]);
|
|
||||||
|
|
||||||
if (_message isEqualTo "" || { _participantUids isEqualTo [] }) exitWith {};
|
|
||||||
|
|
||||||
{
|
|
||||||
private _player = [_x] call EFUNC(common,getPlayer);
|
|
||||||
if (isNull _player) then { continue; };
|
|
||||||
[CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent);
|
|
||||||
} forEach _participantUids;
|
|
||||||
|
|
||||||
if (GETGVAR(enableEventLogs,false)) then {
|
|
||||||
["INFO", format [
|
|
||||||
"Task notification event: taskID=%1 type=%2 recipients=%3 message=%4",
|
|
||||||
_event getOrDefault ["taskID", ""],
|
|
||||||
_type,
|
|
||||||
_participantUids,
|
|
||||||
_message
|
|
||||||
]] call EFUNC(common,log);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
private _sendRewardNotification = {
|
|
||||||
params ["_event"];
|
|
||||||
|
|
||||||
private _type = _event getOrDefault ["notificationType", "info"];
|
|
||||||
private _title = _event getOrDefault ["title", "Tasks"];
|
|
||||||
private _message = _event getOrDefault ["message", ""];
|
|
||||||
private _memberUids = +(_event getOrDefault ["memberUids", []]);
|
|
||||||
|
|
||||||
if (_message isEqualTo "" || { _memberUids isEqualTo [] }) exitWith {};
|
|
||||||
|
|
||||||
{
|
|
||||||
private _player = [_x] call EFUNC(common,getPlayer);
|
|
||||||
if (isNull _player) then { continue; };
|
|
||||||
[CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent);
|
|
||||||
} forEach _memberUids;
|
|
||||||
|
|
||||||
if (GETGVAR(enableEventLogs,false)) then {
|
|
||||||
["INFO", format [
|
|
||||||
"Task reward notification event: taskID=%1 type=%2 recipients=%3 message=%4",
|
|
||||||
_event getOrDefault ["taskID", ""],
|
|
||||||
_type,
|
|
||||||
_memberUids,
|
|
||||||
_message
|
|
||||||
]] call EFUNC(common,log);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
GVAR(TaskNotificationEventTokens) = [
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.notification.requested", _sendTaskNotification, "task.notification.send"]],
|
|
||||||
EGVAR(common,EventBus) call ["on", ["task.reward.notification.requested", _sendRewardNotification, "task.reward.notification.send"]]
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
["ace_explosives_defuse", {
|
["ace_explosives_defuse", {
|
||||||
private _taskID = "";
|
private _taskID = "";
|
||||||
private _explosive = objNull;
|
private _explosive = objNull;
|
||||||
|
|||||||
@ -8,30 +8,37 @@ private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
|||||||
|
|
||||||
#include "initSettings.inc.sqf"
|
#include "initSettings.inc.sqf"
|
||||||
|
|
||||||
[] call FUNC(TaskStateGateway);
|
call FUNC(TaskStateGateway);
|
||||||
[] call FUNC(TaskLifecycleReporter);
|
call FUNC(TaskLifecycleReporter);
|
||||||
[] call FUNC(TaskCatalogStore);
|
call FUNC(TaskCatalogStore);
|
||||||
[] call FUNC(TaskEntityRegistry);
|
call FUNC(TaskEntityRegistry);
|
||||||
[] call FUNC(TaskParticipantTracker);
|
call FUNC(TaskParticipantTracker);
|
||||||
[] call FUNC(TaskRewardService);
|
call FUNC(TaskRewardService);
|
||||||
[] call FUNC(TaskInstanceBaseClass);
|
call FUNC(TaskNotificationService);
|
||||||
[] call FUNC(EntityControllerBaseClass);
|
call FUNC(MissionGeneratorProviderRegistry);
|
||||||
[] call FUNC(AttackTaskBaseClass);
|
call FUNC(BuiltinMissionGeneratorProvider);
|
||||||
[] call FUNC(HostageTaskBaseClass);
|
call FUNC(TaskInstanceBaseClass);
|
||||||
[] call FUNC(HostageEntityController);
|
call FUNC(EntityControllerBaseClass);
|
||||||
[] call FUNC(TargetEntityController);
|
call FUNC(AttackTaskBaseClass);
|
||||||
[] call FUNC(ShooterEntityController);
|
call FUNC(HostageTaskBaseClass);
|
||||||
[] call FUNC(HVTEntityController);
|
call FUNC(HostageEntityController);
|
||||||
[] call FUNC(CargoEntityController);
|
call FUNC(TargetEntityController);
|
||||||
[] call FUNC(ProtectedEntityController);
|
call FUNC(ShooterEntityController);
|
||||||
[] call FUNC(IEDEntityController);
|
call FUNC(HVTEntityController);
|
||||||
[] call FUNC(DefenseEnemyController);
|
call FUNC(CargoEntityController);
|
||||||
[] call FUNC(DefuseTaskBaseClass);
|
call FUNC(ProtectedEntityController);
|
||||||
[] call FUNC(DestroyTaskBaseClass);
|
call FUNC(IEDEntityController);
|
||||||
[] call FUNC(DeliveryTaskBaseClass);
|
call FUNC(DefenseEnemyController);
|
||||||
[] call FUNC(HVTTaskBaseClass);
|
call FUNC(DefuseTaskBaseClass);
|
||||||
[] call FUNC(DefendTaskBaseClass);
|
call FUNC(DestroyTaskBaseClass);
|
||||||
|
call FUNC(DeliveryTaskBaseClass);
|
||||||
|
call FUNC(HVTTaskBaseClass);
|
||||||
|
call FUNC(DefendTaskBaseClass);
|
||||||
|
|
||||||
call FUNC(initTaskStore);
|
call FUNC(initTaskStore);
|
||||||
call FUNC(initMissionSetupService);
|
call FUNC(initMissionSetupService);
|
||||||
|
|
||||||
|
GVAR(MissionGeneratorProviderRegistry) call ["registerProvider", ["builtin", GVAR(BuiltinMissionGeneratorProvider)]];
|
||||||
|
GVAR(TaskLifecycleReporter) call ["registerEventLogListeners", []];
|
||||||
|
GVAR(TaskNotificationService) call ["registerEventListeners", []];
|
||||||
if !(isNil QGVAR(TaskStore)) then { GVAR(TaskStore) call ["resetMissionState", []]; };
|
if !(isNil QGVAR(TaskStore)) then { GVAR(TaskStore) call ["resetMissionState", []]; };
|
||||||
|
|||||||
@ -23,6 +23,13 @@ GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
_missionConfig
|
_missionConfig
|
||||||
}],
|
}],
|
||||||
|
["getServicePricingConfig", compileFinal {
|
||||||
|
private _pricingConfig = missionConfigFile >> "CfgServicePricing";
|
||||||
|
if !(isClass _pricingConfig) then {
|
||||||
|
_pricingConfig = configFile >> "CfgServicePricing";
|
||||||
|
};
|
||||||
|
_pricingConfig
|
||||||
|
}],
|
||||||
["numberOrDefault", compileFinal {
|
["numberOrDefault", compileFinal {
|
||||||
params ["_value", "_default"];
|
params ["_value", "_default"];
|
||||||
|
|
||||||
@ -80,7 +87,18 @@ GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
_overrides getOrDefault [_varName, _default]
|
_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 = _self call ["getServicePricingConfig", []];
|
||||||
|
if (isNumber (_serviceConfig >> _varName)) exitWith {
|
||||||
|
getNumber (_serviceConfig >> _varName)
|
||||||
|
};
|
||||||
|
|
||||||
|
_default
|
||||||
};
|
};
|
||||||
|
|
||||||
private _maxConcurrent = [
|
private _maxConcurrent = [
|
||||||
@ -104,6 +122,17 @@ GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
private _penMax = [["penaltyMax", -25, _overrides] call _paramOrDefault, -25] call (_self get "numberOrDefault");
|
private _penMax = [["penaltyMax", -25, _overrides] call _paramOrDefault, -25] call (_self get "numberOrDefault");
|
||||||
private _timeMin = [["timeLimitMin", 600, _overrides] call _paramOrDefault, 600] call (_self get "numberOrDefault");
|
private _timeMin = [["timeLimitMin", 600, _overrides] call _paramOrDefault, 600] call (_self get "numberOrDefault");
|
||||||
private _timeMax = [["timeLimitMax", 900, _overrides] call _paramOrDefault, 900] 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;
|
||||||
|
if !(_generatorProvider in ["builtin", "custom"]) then { _generatorProvider = "builtin"; };
|
||||||
|
|
||||||
private _enemyFaction = _overrides getOrDefault [
|
private _enemyFaction = _overrides getOrDefault [
|
||||||
"enemyFaction",
|
"enemyFaction",
|
||||||
@ -125,8 +154,15 @@ GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
_penMin = _penMin min 0;
|
_penMin = _penMin min 0;
|
||||||
_penMax = _penMax min 0;
|
_penMax = _penMax min 0;
|
||||||
|
|
||||||
_timeMin = _timeMin max 1;
|
_timeMin = _timeMin max 0;
|
||||||
_timeMax = _timeMax max _timeMin;
|
_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 [
|
private _settings = createHashMapFromArray [
|
||||||
["useMenuSettings", true],
|
["useMenuSettings", true],
|
||||||
@ -141,11 +177,31 @@ GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
["penaltyMax", _penMax],
|
["penaltyMax", _penMax],
|
||||||
["timeLimitMin", _timeMin],
|
["timeLimitMin", _timeMin],
|
||||||
["timeLimitMax", _timeMax],
|
["timeLimitMax", _timeMax],
|
||||||
["enemyFaction", _enemyFaction]
|
["medicalSpawnCost", _medicalSpawnCost],
|
||||||
|
["medicalHealCost", _medicalHealCost],
|
||||||
|
["serviceRepairCost", _serviceRepairCost],
|
||||||
|
["serviceRearmCost", _serviceRearmCost],
|
||||||
|
["fuelCost", _fuelCost],
|
||||||
|
["transportBaseFare", _transportBaseFare],
|
||||||
|
["transportPricePerKm", _transportPricePerKm],
|
||||||
|
["enemyFaction", _enemyFaction],
|
||||||
|
["generatorProvider", _generatorProvider]
|
||||||
];
|
];
|
||||||
|
|
||||||
SETMPVAR(GVAR(missionSetup_settings),_settings);
|
SETMPVAR(GVAR(missionSetup_settings),_settings);
|
||||||
SETMPVAR(GVAR(missionSetup_settingsApplied),true);
|
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]];
|
private _side = _self call ["resolveFactionSide", [_enemyFaction, east]];
|
||||||
ENEMY_SIDE = _side;
|
ENEMY_SIDE = _side;
|
||||||
@ -153,11 +209,12 @@ GVAR(MissionSetupServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
publicVariable "ENEMY_SIDE";
|
publicVariable "ENEMY_SIDE";
|
||||||
|
|
||||||
["INFO", format [
|
["INFO", format [
|
||||||
"Framework mission setup applied. Faction=%1, Side=%2, MaxConcurrent=%3, Interval=%4",
|
"Framework mission setup applied. Faction=%1, Side=%2, MaxConcurrent=%3, Interval=%4, GeneratorProvider=%5",
|
||||||
_enemyFaction,
|
_enemyFaction,
|
||||||
_side,
|
_side,
|
||||||
_maxConcurrent,
|
_maxConcurrent,
|
||||||
_interval
|
_interval,
|
||||||
|
_generatorProvider
|
||||||
]] call EFUNC(common,log);
|
]] call EFUNC(common,log);
|
||||||
|
|
||||||
if !(isNil QEGVAR(common,EventBus)) then {
|
if !(isNil QEGVAR(common,EventBus)) then {
|
||||||
|
|||||||
@ -56,6 +56,9 @@ GVAR(TaskStore) = createHashMapObject [[
|
|||||||
["isTaskCompleted", compileFinal {
|
["isTaskCompleted", compileFinal {
|
||||||
GVAR(TaskCatalogStore) call ["isTaskCompleted", _this]
|
GVAR(TaskCatalogStore) call ["isTaskCompleted", _this]
|
||||||
}],
|
}],
|
||||||
|
["isTerminalStatus", compileFinal {
|
||||||
|
GVAR(TaskCatalogStore) call ["isTerminalStatus", _this]
|
||||||
|
}],
|
||||||
["areTaskPrerequisitesSatisfied", compileFinal {
|
["areTaskPrerequisitesSatisfied", compileFinal {
|
||||||
GVAR(TaskCatalogStore) call ["areTaskPrerequisitesSatisfied", _this]
|
GVAR(TaskCatalogStore) call ["areTaskPrerequisitesSatisfied", _this]
|
||||||
}],
|
}],
|
||||||
|
|||||||
@ -1,120 +0,0 @@
|
|||||||
#include "..\script_component.hpp"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Author: IDSolutions
|
|
||||||
* Framework-owned on-demand dynamic mission request entry point for CAD and
|
|
||||||
* other server-side dispatchers.
|
|
||||||
*
|
|
||||||
* Arguments:
|
|
||||||
* 0: Generator type <STRING>
|
|
||||||
* 1: Request metadata <HASHMAP> (Default: createHashMap)
|
|
||||||
* 2: Requesting player UID <STRING> (Default: "")
|
|
||||||
*
|
|
||||||
* Return Value:
|
|
||||||
* Request result with success, message, taskID, and taskType keys <HASHMAP>
|
|
||||||
*
|
|
||||||
* Public: No
|
|
||||||
*/
|
|
||||||
|
|
||||||
if !(isServer) exitWith {
|
|
||||||
createHashMapFromArray [
|
|
||||||
["success", false],
|
|
||||||
["message", "Generated task requests must run on the server."]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
params [
|
|
||||||
["_requestedType", "", [""]],
|
|
||||||
["_metadata", createHashMap, [createHashMap]],
|
|
||||||
["_requesterUid", "", [""]]
|
|
||||||
];
|
|
||||||
|
|
||||||
private _result = createHashMapFromArray [
|
|
||||||
["success", false],
|
|
||||||
["message", "Generated task request failed."],
|
|
||||||
["taskID", ""],
|
|
||||||
["taskType", _requestedType]
|
|
||||||
];
|
|
||||||
|
|
||||||
if !(GVAR(enableGenerator)) exitWith {
|
|
||||||
_result set ["message", "Generated task requests are disabled by server settings."];
|
|
||||||
_result
|
|
||||||
};
|
|
||||||
|
|
||||||
private _typeAliases = createHashMapFromArray [
|
|
||||||
["attack", "attack"],
|
|
||||||
["defend", "defend"],
|
|
||||||
["defense", "defend"],
|
|
||||||
["delivery", "delivery"],
|
|
||||||
["deliver", "delivery"],
|
|
||||||
["destroy", "destroy"],
|
|
||||||
["defuse", "defuse"],
|
|
||||||
["hostage", "hostage"],
|
|
||||||
["hvt", "hvtkill"],
|
|
||||||
["hvtkill", "hvtkill"],
|
|
||||||
["killhvt", "hvtkill"],
|
|
||||||
["kill_hvt", "hvtkill"],
|
|
||||||
["hvtcapture", "hvtcapture"],
|
|
||||||
["capturehvt", "hvtcapture"],
|
|
||||||
["capture_hvt", "hvtcapture"]
|
|
||||||
];
|
|
||||||
|
|
||||||
private _generatorType = _typeAliases getOrDefault [toLowerANSI _requestedType, ""];
|
|
||||||
if (_generatorType isEqualTo "") exitWith {
|
|
||||||
_result set ["message", format ["Unknown generated task type: %1", _requestedType]];
|
|
||||||
_result
|
|
||||||
};
|
|
||||||
_result set ["taskType", _generatorType];
|
|
||||||
|
|
||||||
if (isNil QGVAR(TaskStore)) exitWith {
|
|
||||||
_result set ["message", "Task store is not ready yet."];
|
|
||||||
_result
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isNil QGVAR(MissionManager)) then {
|
|
||||||
call FUNC(missionManager);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isNil QGVAR(MissionManager)) exitWith {
|
|
||||||
_result set ["message", "Mission manager is not ready yet."];
|
|
||||||
_result
|
|
||||||
};
|
|
||||||
|
|
||||||
GVAR(MissionManager) call ["cleanupCompletedMissions", []];
|
|
||||||
|
|
||||||
private _activeCount = count (GVAR(MissionManager) call ["getActiveMissionIds", []]);
|
|
||||||
private _maxConcurrent = GVAR(MissionManager) call ["getMaxConcurrentMissions", []];
|
|
||||||
if (_activeCount >= _maxConcurrent) exitWith {
|
|
||||||
_result set ["message", format [
|
|
||||||
"Mission cap reached (%1/%2 active). Close or complete a task before requesting another.",
|
|
||||||
_activeCount,
|
|
||||||
_maxConcurrent
|
|
||||||
]];
|
|
||||||
_result
|
|
||||||
};
|
|
||||||
|
|
||||||
private _generator = GVAR(MissionManager) call ["getGeneratorByType", [_generatorType]];
|
|
||||||
if (_generator isEqualTo createHashMap) exitWith {
|
|
||||||
_result set ["message", format ["Generated task type is unavailable: %1", _generatorType]];
|
|
||||||
_result
|
|
||||||
};
|
|
||||||
|
|
||||||
private _taskID = _generator call ["startMission", [GVAR(MissionManager)]];
|
|
||||||
if (_taskID isEqualTo "") exitWith {
|
|
||||||
_result set ["message", format ["Mission generator failed to start task type: %1", _generatorType]];
|
|
||||||
_result
|
|
||||||
};
|
|
||||||
|
|
||||||
GVAR(MissionManager) set ["lastMissionGenerationAt", diag_tickTime];
|
|
||||||
|
|
||||||
["INFO", format [
|
|
||||||
"Dispatcher %1 requested generated %2 mission %3.",
|
|
||||||
_requesterUid,
|
|
||||||
_generatorType,
|
|
||||||
_taskID
|
|
||||||
]] call EFUNC(common,log);
|
|
||||||
|
|
||||||
_result set ["success", true];
|
|
||||||
_result set ["message", format ["Generated %1 task %2.", _generatorType, _taskID]];
|
|
||||||
_result set ["taskID", _taskID];
|
|
||||||
_result
|
|
||||||
@ -96,10 +96,10 @@ GVAR(AttackTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
|
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
|
!(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] }
|
||||||
};
|
};
|
||||||
|
|
||||||
true
|
_self call ["isTaskStoreOpen", []]
|
||||||
}],
|
}],
|
||||||
["tick", compileFinal {
|
["tick", compileFinal {
|
||||||
private _startedAt = _self getOrDefault ["startedAt", -1];
|
private _startedAt = _self getOrDefault ["startedAt", -1];
|
||||||
@ -139,7 +139,7 @@ GVAR(AttackTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
_self call ["refreshTargetsFromStore", []];
|
_self call ["refreshTargetsFromStore", []];
|
||||||
private _targets = _self getOrDefault ["targets", []];
|
private _targets = _self getOrDefault ["targets", []];
|
||||||
GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]];
|
GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]];
|
||||||
count _targets > 0
|
!(_self call ["isTaskStoreOpen", []]) || { count _targets > 0 }
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
waitUntil {
|
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", []];
|
_self call ["markActive", []];
|
||||||
|
|
||||||
while { (_self call ["getStatus", []]) isEqualTo "active" } do {
|
while { _self call ["isTaskLoopActive", []] } do {
|
||||||
private _targets = _self getOrDefault ["targets", []];
|
private _targets = _self getOrDefault ["targets", []];
|
||||||
|
|
||||||
if (_useTaskStore) then {
|
if (_useTaskStore) then {
|
||||||
@ -186,10 +196,8 @@ GVAR(AttackTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
sleep 1;
|
sleep 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((_self call ["getStatus", []]) isEqualTo "failed") then {
|
private _finalStatus = _self call ["getStatus", []];
|
||||||
private _targets = _self getOrDefault ["targets", []];
|
if (_finalStatus isEqualTo "failed") then {
|
||||||
{ deleteVehicle _x } forEach _targets;
|
|
||||||
|
|
||||||
if (_useTaskStore) then {
|
if (_useTaskStore) then {
|
||||||
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
|
||||||
@ -202,10 +210,9 @@ GVAR(AttackTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
|
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 {
|
if (_useTaskStore) then {
|
||||||
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
||||||
|
|||||||
@ -0,0 +1,151 @@
|
|||||||
|
#include "..\script_component.hpp"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Author: IDSolutions
|
||||||
|
* Built-in generated mission provider adapter around the framework mission
|
||||||
|
* manager.
|
||||||
|
*
|
||||||
|
* Arguments:
|
||||||
|
* None
|
||||||
|
*
|
||||||
|
* Return Value:
|
||||||
|
* Built-in mission generator provider object <HASHMAP OBJECT>
|
||||||
|
*
|
||||||
|
* Public: No
|
||||||
|
*/
|
||||||
|
|
||||||
|
if !(isServer) exitWith { createHashMap };
|
||||||
|
|
||||||
|
#pragma hemtt ignore_variables ["_self"]
|
||||||
|
GVAR(BuiltinMissionGeneratorProviderBaseClass) = compileFinal createHashMapFromArray [
|
||||||
|
["#type", "BuiltinMissionGeneratorProviderBaseClass"],
|
||||||
|
["emptyResult", compileFinal {
|
||||||
|
params [
|
||||||
|
["_message", "Generated task request failed.", [""]],
|
||||||
|
["_taskType", "", [""]]
|
||||||
|
];
|
||||||
|
|
||||||
|
createHashMapFromArray [
|
||||||
|
["success", false],
|
||||||
|
["message", _message],
|
||||||
|
["taskID", ""],
|
||||||
|
["taskType", _taskType]
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
["resolveGeneratorType", compileFinal {
|
||||||
|
params [["_requestedType", "", [""]]];
|
||||||
|
|
||||||
|
private _typeAliases = createHashMapFromArray [
|
||||||
|
["attack", "attack"],
|
||||||
|
["defend", "defend"],
|
||||||
|
["defense", "defend"],
|
||||||
|
["delivery", "delivery"],
|
||||||
|
["deliver", "delivery"],
|
||||||
|
["destroy", "destroy"],
|
||||||
|
["defuse", "defuse"],
|
||||||
|
["hostage", "hostage"],
|
||||||
|
["hvt", "hvtkill"],
|
||||||
|
["hvtkill", "hvtkill"],
|
||||||
|
["killhvt", "hvtkill"],
|
||||||
|
["kill_hvt", "hvtkill"],
|
||||||
|
["hvtcapture", "hvtcapture"],
|
||||||
|
["capturehvt", "hvtcapture"],
|
||||||
|
["capture_hvt", "hvtcapture"]
|
||||||
|
];
|
||||||
|
|
||||||
|
_typeAliases getOrDefault [toLowerANSI _requestedType, ""]
|
||||||
|
}],
|
||||||
|
["ensureMissionManager", compileFinal {
|
||||||
|
if (isNil QGVAR(MissionManager)) then {
|
||||||
|
call FUNC(missionManager);
|
||||||
|
};
|
||||||
|
|
||||||
|
!(isNil QGVAR(MissionManager))
|
||||||
|
}],
|
||||||
|
["getGeneratedTaskTypes", compileFinal {
|
||||||
|
if !(GVAR(enableGenerator)) exitWith {
|
||||||
|
["INFO", "Built-in generated task types disabled by forge_server_task_enableGenerator."] call EFUNC(common,log);
|
||||||
|
[]
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(_self call ["ensureMissionManager", []]) exitWith {
|
||||||
|
["INFO", "Built-in generated task types unavailable because mission manager is not ready."] call EFUNC(common,log);
|
||||||
|
[]
|
||||||
|
};
|
||||||
|
|
||||||
|
GVAR(MissionManager) call ["getGeneratedTaskTypes", []]
|
||||||
|
}],
|
||||||
|
["requestMissionTask", compileFinal {
|
||||||
|
params [
|
||||||
|
["_requestedType", "", [""]],
|
||||||
|
["_metadata", createHashMap, [createHashMap]],
|
||||||
|
["_requesterUid", "", [""]]
|
||||||
|
];
|
||||||
|
|
||||||
|
private _result = _self call ["emptyResult", ["Generated task request failed.", _requestedType]];
|
||||||
|
|
||||||
|
if !(GVAR(enableGenerator)) exitWith {
|
||||||
|
_result set ["message", "Built-in generated task requests are disabled by server settings."];
|
||||||
|
_result
|
||||||
|
};
|
||||||
|
|
||||||
|
private _generatorType = _self call ["resolveGeneratorType", [_requestedType]];
|
||||||
|
if (_generatorType isEqualTo "") exitWith {
|
||||||
|
_result set ["message", format ["Unknown built-in generated task type: %1", _requestedType]];
|
||||||
|
_result
|
||||||
|
};
|
||||||
|
_result set ["taskType", _generatorType];
|
||||||
|
|
||||||
|
if (isNil QGVAR(TaskStore)) exitWith {
|
||||||
|
_result set ["message", "Task store is not ready yet."];
|
||||||
|
_result
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(_self call ["ensureMissionManager", []]) exitWith {
|
||||||
|
_result set ["message", "Mission manager is not ready yet."];
|
||||||
|
_result
|
||||||
|
};
|
||||||
|
|
||||||
|
GVAR(MissionManager) call ["cleanupCompletedMissions", []];
|
||||||
|
|
||||||
|
private _activeCount = count (GVAR(MissionManager) call ["getActiveMissionIds", []]);
|
||||||
|
private _maxConcurrent = GVAR(MissionManager) call ["getMaxConcurrentMissions", []];
|
||||||
|
if (_activeCount >= _maxConcurrent) exitWith {
|
||||||
|
_result set ["message", format [
|
||||||
|
"Mission cap reached (%1/%2 active). Close or complete a task before requesting another.",
|
||||||
|
_activeCount,
|
||||||
|
_maxConcurrent
|
||||||
|
]];
|
||||||
|
_result
|
||||||
|
};
|
||||||
|
|
||||||
|
private _generator = GVAR(MissionManager) call ["getGeneratorByType", [_generatorType]];
|
||||||
|
if (_generator isEqualTo createHashMap) exitWith {
|
||||||
|
_result set ["message", format ["Built-in generated task type is unavailable: %1", _generatorType]];
|
||||||
|
_result
|
||||||
|
};
|
||||||
|
|
||||||
|
private _taskID = _generator call ["startMission", [GVAR(MissionManager)]];
|
||||||
|
if (_taskID isEqualTo "") exitWith {
|
||||||
|
_result set ["message", format ["Built-in mission generator failed to start task type: %1", _generatorType]];
|
||||||
|
_result
|
||||||
|
};
|
||||||
|
|
||||||
|
GVAR(MissionManager) set ["lastMissionGenerationAt", diag_tickTime];
|
||||||
|
|
||||||
|
["INFO", format [
|
||||||
|
"Dispatcher %1 requested built-in generated %2 mission %3.",
|
||||||
|
_requesterUid,
|
||||||
|
_generatorType,
|
||||||
|
_taskID
|
||||||
|
]] call EFUNC(common,log);
|
||||||
|
|
||||||
|
_result set ["success", true];
|
||||||
|
_result set ["message", format ["Generated %1 task %2.", _generatorType, _taskID]];
|
||||||
|
_result set ["taskID", _taskID];
|
||||||
|
_result
|
||||||
|
}]
|
||||||
|
];
|
||||||
|
|
||||||
|
GVAR(BuiltinMissionGeneratorProvider) = createHashMapObject [GVAR(BuiltinMissionGeneratorProviderBaseClass)];
|
||||||
|
GVAR(BuiltinMissionGeneratorProvider)
|
||||||
@ -38,6 +38,8 @@ GVAR(CargoEntityController) merge [createHashMapFromArray [
|
|||||||
|
|
||||||
private _taskID = _unit getVariable ["assignedTask", _unit getVariable [QGVAR(assignedTask), ""]];
|
private _taskID = _unit getVariable ["assignedTask", _unit getVariable [QGVAR(assignedTask), ""]];
|
||||||
if (_taskID isEqualTo "") exitWith {};
|
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 {};
|
if (_unit getVariable [QGVAR(cargoDamageWarned), false]) exitWith {};
|
||||||
|
|
||||||
_unit setVariable [QGVAR(cargoDamageWarned), true];
|
_unit setVariable [QGVAR(cargoDamageWarned), true];
|
||||||
@ -70,7 +72,13 @@ GVAR(CargoEntityController) merge [createHashMapFromArray [
|
|||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
private _entity = _self getOrDefault ["entity", objNull];
|
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", []];
|
_self call ["markFinished", []];
|
||||||
|
|||||||
@ -51,10 +51,10 @@ GVAR(DefendTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
|
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
|
!(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] }
|
||||||
};
|
};
|
||||||
|
|
||||||
true
|
_self call ["isTaskStoreOpen", []]
|
||||||
}],
|
}],
|
||||||
["countBluforInZone", compileFinal {
|
["countBluforInZone", compileFinal {
|
||||||
private _defenseZone = _self getOrDefault ["defenseZone", ""];
|
private _defenseZone = _self getOrDefault ["defenseZone", ""];
|
||||||
@ -68,6 +68,7 @@ GVAR(DefendTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
_self call ["trackParticipants", []];
|
_self call ["trackParticipants", []];
|
||||||
|
if !(_self call ["isTaskStoreOpen", []]) exitWith { true };
|
||||||
|
|
||||||
private _ready = (_self call ["countBluforInZone", []]) >= _minBlufor;
|
private _ready = (_self call ["countBluforInZone", []]) >= _minBlufor;
|
||||||
if (_ready) then {
|
if (_ready) then {
|
||||||
@ -82,7 +83,7 @@ GVAR(DefendTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
_ready
|
_ready
|
||||||
};
|
};
|
||||||
|
|
||||||
true
|
_self call ["isTaskStoreOpen", []]
|
||||||
}],
|
}],
|
||||||
["tick", compileFinal {
|
["tick", compileFinal {
|
||||||
private _taskID = _self getOrDefault ["taskID", ""];
|
private _taskID = _self getOrDefault ["taskID", ""];
|
||||||
@ -186,10 +187,18 @@ GVAR(DefendTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
_self call ["waitForAssignment", []];
|
if !(_self call ["waitForAssignment", []]) exitWith {
|
||||||
_self call ["waitForDefenseStart", []];
|
_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", []];
|
_self call ["trackParticipants", []];
|
||||||
private _snapshot = _self call ["tick", []];
|
private _snapshot = _self call ["tick", []];
|
||||||
|
|
||||||
@ -204,9 +213,12 @@ GVAR(DefendTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
sleep 1;
|
sleep 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((_self call ["getStatus", []]) isEqualTo "failed") then {
|
private _finalStatus = _self call ["getStatus", []];
|
||||||
|
if (_finalStatus isEqualTo "failed") then {
|
||||||
_self call ["handleFailureOutcome", []];
|
_self call ["handleFailureOutcome", []];
|
||||||
} else {
|
};
|
||||||
|
|
||||||
|
if (_finalStatus isEqualTo "succeeded") then {
|
||||||
_self call ["handleSuccessOutcome", []];
|
_self call ["handleSuccessOutcome", []];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,13 @@ GVAR(DefenseEnemyController) merge [createHashMapFromArray [
|
|||||||
_self call ["markActive", []];
|
_self call ["markActive", []];
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
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", []];
|
_self call ["markFinished", []];
|
||||||
|
|||||||
@ -103,7 +103,7 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
_self call ["refreshEntitiesFromStore", []];
|
_self call ["refreshEntitiesFromStore", []];
|
||||||
count (_self getOrDefault ["ieds", []]) > 0
|
!(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["ieds", []]) > 0 }
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
waitUntil {
|
waitUntil {
|
||||||
@ -112,6 +112,8 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !(_self call ["isTaskStoreOpen", []]) exitWith { false };
|
||||||
|
|
||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["waitForAssignment", compileFinal {
|
["waitForAssignment", compileFinal {
|
||||||
@ -121,10 +123,10 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
|
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
|
!(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] }
|
||||||
};
|
};
|
||||||
|
|
||||||
true
|
_self call ["isTaskStoreOpen", []]
|
||||||
}],
|
}],
|
||||||
["startIedControllers", compileFinal {
|
["startIedControllers", compileFinal {
|
||||||
if ((_self getOrDefault ["iedControllers", []]) isNotEqualTo []) exitWith { true };
|
if ((_self getOrDefault ["iedControllers", []]) isNotEqualTo []) exitWith { true };
|
||||||
@ -194,15 +196,10 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
}],
|
}],
|
||||||
["handleFailureOutcome", compileFinal {
|
["handleFailureOutcome", compileFinal {
|
||||||
private _taskID = _self getOrDefault ["taskID", ""];
|
private _taskID = _self getOrDefault ["taskID", ""];
|
||||||
private _ieds = _self getOrDefault ["ieds", []];
|
|
||||||
private _protected = _self getOrDefault ["protected", []];
|
|
||||||
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
||||||
private _ratingFail = _rewardData getOrDefault ["ratingFail", 0];
|
private _ratingFail = _rewardData getOrDefault ["ratingFail", 0];
|
||||||
private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false];
|
private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false];
|
||||||
|
|
||||||
{ deleteVehicle _x } forEach _ieds;
|
|
||||||
{ deleteVehicle _x } forEach _protected;
|
|
||||||
|
|
||||||
if (_self getOrDefault ["useTaskStore", false]) then {
|
if (_self getOrDefault ["useTaskStore", false]) then {
|
||||||
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
|
||||||
@ -219,16 +216,11 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
}],
|
}],
|
||||||
["handleSuccessOutcome", compileFinal {
|
["handleSuccessOutcome", compileFinal {
|
||||||
private _taskID = _self getOrDefault ["taskID", ""];
|
private _taskID = _self getOrDefault ["taskID", ""];
|
||||||
private _ieds = _self getOrDefault ["ieds", []];
|
|
||||||
private _protected = _self getOrDefault ["protected", []];
|
|
||||||
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
||||||
private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0];
|
private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0];
|
||||||
private _funds = _rewardData getOrDefault ["funds", 0];
|
private _funds = _rewardData getOrDefault ["funds", 0];
|
||||||
private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false];
|
private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false];
|
||||||
|
|
||||||
{ deleteVehicle _x } forEach _ieds;
|
|
||||||
{ deleteVehicle _x } forEach _protected;
|
|
||||||
|
|
||||||
if (_self getOrDefault ["useTaskStore", false]) then {
|
if (_self getOrDefault ["useTaskStore", false]) then {
|
||||||
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
||||||
@ -245,12 +237,20 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["runLoop", compileFinal {
|
["runLoop", compileFinal {
|
||||||
_self call ["waitForRequiredEntities", []];
|
if !(_self call ["waitForRequiredEntities", []]) exitWith {
|
||||||
_self call ["waitForAssignment", []];
|
_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 ["startIedControllers", []];
|
||||||
_self call ["markActive", []];
|
_self call ["markActive", []];
|
||||||
|
|
||||||
while { (_self call ["getStatus", []]) isEqualTo "active" } do {
|
while { _self call ["isTaskLoopActive", []] } do {
|
||||||
_self call ["trackParticipants", []];
|
_self call ["trackParticipants", []];
|
||||||
private _snapshot = _self call ["tick", []];
|
private _snapshot = _self call ["tick", []];
|
||||||
|
|
||||||
@ -265,9 +265,12 @@ GVAR(DefuseTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
sleep 1;
|
sleep 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((_self call ["getStatus", []]) isEqualTo "failed") then {
|
private _finalStatus = _self call ["getStatus", []];
|
||||||
|
if (_finalStatus isEqualTo "failed") then {
|
||||||
_self call ["handleFailureOutcome", []];
|
_self call ["handleFailureOutcome", []];
|
||||||
} else {
|
};
|
||||||
|
|
||||||
|
if (_finalStatus isEqualTo "succeeded") then {
|
||||||
_self call ["handleSuccessOutcome", []];
|
_self call ["handleSuccessOutcome", []];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -57,7 +57,7 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
sleep 1;
|
sleep 1;
|
||||||
_self call ["refreshEntitiesFromStore", []];
|
_self call ["refreshEntitiesFromStore", []];
|
||||||
_self call ["trackParticipants", []];
|
_self call ["trackParticipants", []];
|
||||||
count (_self getOrDefault ["cargo", []]) > 0
|
!(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["cargo", []]) > 0 }
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
waitUntil {
|
waitUntil {
|
||||||
@ -66,6 +66,8 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !(_self call ["isTaskStoreOpen", []]) exitWith { false };
|
||||||
|
|
||||||
private _cargo = _self getOrDefault ["cargo", []];
|
private _cargo = _self getOrDefault ["cargo", []];
|
||||||
private _taskParams = _self getOrDefault ["taskParams", createHashMap];
|
private _taskParams = _self getOrDefault ["taskParams", createHashMap];
|
||||||
private _requiredDelivered = _taskParams getOrDefault ["limitSuccess", -1];
|
private _requiredDelivered = _taskParams getOrDefault ["limitSuccess", -1];
|
||||||
@ -85,10 +87,10 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
|
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
|
!(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] }
|
||||||
};
|
};
|
||||||
|
|
||||||
true
|
_self call ["isTaskStoreOpen", []]
|
||||||
}],
|
}],
|
||||||
["countDeliveredCargo", compileFinal {
|
["countDeliveredCargo", compileFinal {
|
||||||
private _deliveryZone = _self getOrDefault ["deliveryZone", ""];
|
private _deliveryZone = _self getOrDefault ["deliveryZone", ""];
|
||||||
@ -126,13 +128,10 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
}],
|
}],
|
||||||
["handleFailureOutcome", compileFinal {
|
["handleFailureOutcome", compileFinal {
|
||||||
private _taskID = _self getOrDefault ["taskID", ""];
|
private _taskID = _self getOrDefault ["taskID", ""];
|
||||||
private _cargo = _self getOrDefault ["cargo", []];
|
|
||||||
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
||||||
private _ratingFail = _rewardData getOrDefault ["ratingFail", 0];
|
private _ratingFail = _rewardData getOrDefault ["ratingFail", 0];
|
||||||
private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false];
|
private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false];
|
||||||
|
|
||||||
{ deleteVehicle _x } forEach _cargo;
|
|
||||||
|
|
||||||
if (_self getOrDefault ["useTaskStore", false]) then {
|
if (_self getOrDefault ["useTaskStore", false]) then {
|
||||||
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
|
||||||
@ -149,14 +148,11 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
}],
|
}],
|
||||||
["handleSuccessOutcome", compileFinal {
|
["handleSuccessOutcome", compileFinal {
|
||||||
private _taskID = _self getOrDefault ["taskID", ""];
|
private _taskID = _self getOrDefault ["taskID", ""];
|
||||||
private _cargo = _self getOrDefault ["cargo", []];
|
|
||||||
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
||||||
private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0];
|
private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0];
|
||||||
private _funds = _rewardData getOrDefault ["funds", 0];
|
private _funds = _rewardData getOrDefault ["funds", 0];
|
||||||
private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false];
|
private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false];
|
||||||
|
|
||||||
{ deleteVehicle _x } forEach _cargo;
|
|
||||||
|
|
||||||
if (_self getOrDefault ["useTaskStore", false]) then {
|
if (_self getOrDefault ["useTaskStore", false]) then {
|
||||||
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
||||||
@ -173,11 +169,19 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["runLoop", compileFinal {
|
["runLoop", compileFinal {
|
||||||
_self call ["waitForRequiredEntities", []];
|
if !(_self call ["waitForRequiredEntities", []]) exitWith {
|
||||||
_self call ["waitForAssignment", []];
|
_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", []];
|
_self call ["markActive", []];
|
||||||
|
|
||||||
while { (_self call ["getStatus", []]) isEqualTo "active" } do {
|
while { _self call ["isTaskLoopActive", []] } do {
|
||||||
_self call ["trackParticipants", []];
|
_self call ["trackParticipants", []];
|
||||||
private _snapshot = _self call ["tick", []];
|
private _snapshot = _self call ["tick", []];
|
||||||
|
|
||||||
@ -192,9 +196,12 @@ GVAR(DeliveryTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
sleep 1;
|
sleep 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((_self call ["getStatus", []]) isEqualTo "failed") then {
|
private _finalStatus = _self call ["getStatus", []];
|
||||||
|
if (_finalStatus isEqualTo "failed") then {
|
||||||
_self call ["handleFailureOutcome", []];
|
_self call ["handleFailureOutcome", []];
|
||||||
} else {
|
};
|
||||||
|
|
||||||
|
if (_finalStatus isEqualTo "succeeded") then {
|
||||||
_self call ["handleSuccessOutcome", []];
|
_self call ["handleSuccessOutcome", []];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -52,7 +52,7 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
sleep 1;
|
sleep 1;
|
||||||
_self call ["refreshEntitiesFromStore", []];
|
_self call ["refreshEntitiesFromStore", []];
|
||||||
_self call ["trackParticipants", []];
|
_self call ["trackParticipants", []];
|
||||||
count (_self getOrDefault ["targets", []]) > 0
|
!(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["targets", []]) > 0 }
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
waitUntil {
|
waitUntil {
|
||||||
@ -61,6 +61,8 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !(_self call ["isTaskStoreOpen", []]) exitWith { false };
|
||||||
|
|
||||||
private _targets = _self getOrDefault ["targets", []];
|
private _targets = _self getOrDefault ["targets", []];
|
||||||
private _taskParams = _self getOrDefault ["taskParams", createHashMap];
|
private _taskParams = _self getOrDefault ["taskParams", createHashMap];
|
||||||
private _requiredDestroyed = _taskParams getOrDefault ["limitSuccess", -1];
|
private _requiredDestroyed = _taskParams getOrDefault ["limitSuccess", -1];
|
||||||
@ -76,10 +78,10 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
|
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
|
!(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] }
|
||||||
};
|
};
|
||||||
|
|
||||||
true
|
_self call ["isTaskStoreOpen", []]
|
||||||
}],
|
}],
|
||||||
["countDestroyedTargets", compileFinal {
|
["countDestroyedTargets", compileFinal {
|
||||||
private _targets = _self getOrDefault ["targets", []];
|
private _targets = _self getOrDefault ["targets", []];
|
||||||
@ -106,13 +108,10 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
}],
|
}],
|
||||||
["handleFailureOutcome", compileFinal {
|
["handleFailureOutcome", compileFinal {
|
||||||
private _taskID = _self getOrDefault ["taskID", ""];
|
private _taskID = _self getOrDefault ["taskID", ""];
|
||||||
private _targets = _self getOrDefault ["targets", []];
|
|
||||||
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
||||||
private _ratingFail = _rewardData getOrDefault ["ratingFail", 0];
|
private _ratingFail = _rewardData getOrDefault ["ratingFail", 0];
|
||||||
private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false];
|
private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false];
|
||||||
|
|
||||||
{ deleteVehicle _x } forEach _targets;
|
|
||||||
|
|
||||||
if (_self getOrDefault ["useTaskStore", false]) then {
|
if (_self getOrDefault ["useTaskStore", false]) then {
|
||||||
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
|
||||||
@ -129,14 +128,11 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
}],
|
}],
|
||||||
["handleSuccessOutcome", compileFinal {
|
["handleSuccessOutcome", compileFinal {
|
||||||
private _taskID = _self getOrDefault ["taskID", ""];
|
private _taskID = _self getOrDefault ["taskID", ""];
|
||||||
private _targets = _self getOrDefault ["targets", []];
|
|
||||||
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
||||||
private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0];
|
private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0];
|
||||||
private _funds = _rewardData getOrDefault ["funds", 0];
|
private _funds = _rewardData getOrDefault ["funds", 0];
|
||||||
private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false];
|
private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false];
|
||||||
|
|
||||||
{ deleteVehicle _x } forEach _targets;
|
|
||||||
|
|
||||||
if (_self getOrDefault ["useTaskStore", false]) then {
|
if (_self getOrDefault ["useTaskStore", false]) then {
|
||||||
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
||||||
@ -153,11 +149,19 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["runLoop", compileFinal {
|
["runLoop", compileFinal {
|
||||||
_self call ["waitForRequiredEntities", []];
|
if !(_self call ["waitForRequiredEntities", []]) exitWith {
|
||||||
_self call ["waitForAssignment", []];
|
_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", []];
|
_self call ["markActive", []];
|
||||||
|
|
||||||
while { (_self call ["getStatus", []]) isEqualTo "active" } do {
|
while { _self call ["isTaskLoopActive", []] } do {
|
||||||
_self call ["trackParticipants", []];
|
_self call ["trackParticipants", []];
|
||||||
private _snapshot = _self call ["tick", []];
|
private _snapshot = _self call ["tick", []];
|
||||||
|
|
||||||
@ -172,9 +176,12 @@ GVAR(DestroyTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
sleep 1;
|
sleep 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((_self call ["getStatus", []]) isEqualTo "failed") then {
|
private _finalStatus = _self call ["getStatus", []];
|
||||||
|
if (_finalStatus isEqualTo "failed") then {
|
||||||
_self call ["handleFailureOutcome", []];
|
_self call ["handleFailureOutcome", []];
|
||||||
} else {
|
};
|
||||||
|
|
||||||
|
if (_finalStatus isEqualTo "succeeded") then {
|
||||||
_self call ["handleSuccessOutcome", []];
|
_self call ["handleSuccessOutcome", []];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -76,6 +76,20 @@ GVAR(EntityControllerBaseClass) = createHashMapFromArray [
|
|||||||
private _entity = _self getOrDefault ["entity", objNull];
|
private _entity = _self getOrDefault ["entity", objNull];
|
||||||
!isNull _entity && { alive _entity }
|
!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 {
|
["assignTaskVariable", compileFinal {
|
||||||
private _entity = _self getOrDefault ["entity", objNull];
|
private _entity = _self getOrDefault ["entity", objNull];
|
||||||
private _taskID = _self getOrDefault ["taskID", ""];
|
private _taskID = _self getOrDefault ["taskID", ""];
|
||||||
@ -105,9 +119,9 @@ GVAR(EntityControllerBaseClass) = createHashMapFromArray [
|
|||||||
private _registryKey = _self call ["getRegistryKey", []];
|
private _registryKey = _self call ["getRegistryKey", []];
|
||||||
if (_registryKey isEqualTo "") exitWith { false };
|
if (_registryKey isEqualTo "") exitWith { false };
|
||||||
|
|
||||||
private _registry = missionNamespace getVariable [QGVAR(ObjectControllerInstances), createHashMap];
|
private _registry = GETMVAR(GVAR(ObjectControllerInstances),createHashMap);
|
||||||
_registry set [_registryKey, _self];
|
_registry set [_registryKey, _self];
|
||||||
missionNamespace setVariable [QGVAR(ObjectControllerInstances), _registry];
|
SETMVAR(GVAR(ObjectControllerInstances),_registry);
|
||||||
missionNamespace setVariable [_registryKey, _self];
|
missionNamespace setVariable [_registryKey, _self];
|
||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
@ -115,7 +129,7 @@ GVAR(EntityControllerBaseClass) = createHashMapFromArray [
|
|||||||
private _registryKey = _self call ["getRegistryKey", []];
|
private _registryKey = _self call ["getRegistryKey", []];
|
||||||
if (_registryKey isEqualTo "") exitWith { false };
|
if (_registryKey isEqualTo "") exitWith { false };
|
||||||
|
|
||||||
private _registry = missionNamespace getVariable [QGVAR(ObjectControllerInstances), createHashMap];
|
private _registry = GETMVAR(GVAR(ObjectControllerInstances),createHashMap);
|
||||||
_registry deleteAt _registryKey;
|
_registry deleteAt _registryKey;
|
||||||
missionNamespace setVariable [_registryKey, nil];
|
missionNamespace setVariable [_registryKey, nil];
|
||||||
true
|
true
|
||||||
|
|||||||
@ -52,12 +52,19 @@ GVAR(HVTEntityController) merge [createHashMapFromArray [
|
|||||||
private _capturer = objNull;
|
private _capturer = objNull;
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
|
if !(_self call ["isAssignedTaskOpen", []]) exitWith { true };
|
||||||
if !(_self call ["isEntityUsable", []]) exitWith { true };
|
if !(_self call ["isEntityUsable", []]) exitWith { true };
|
||||||
|
|
||||||
_capturer = _self call ["findNearbyCapturer", []];
|
_capturer = _self call ["findNearbyCapturer", []];
|
||||||
!isNull _capturer
|
!isNull _capturer
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !(_self call ["isAssignedTaskOpen", []]) exitWith {
|
||||||
|
_self call ["markAborted", []];
|
||||||
|
_self call ["cleanup", []];
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
if !(_self call ["isEntityUsable", []]) exitWith {
|
if !(_self call ["isEntityUsable", []]) exitWith {
|
||||||
_self call ["markAborted", []];
|
_self call ["markAborted", []];
|
||||||
_self call ["cleanup", []];
|
_self call ["cleanup", []];
|
||||||
|
|||||||
@ -70,7 +70,7 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
sleep 1;
|
sleep 1;
|
||||||
_self call ["refreshEntitiesFromStore", []];
|
_self call ["refreshEntitiesFromStore", []];
|
||||||
_self call ["trackParticipants", []];
|
_self call ["trackParticipants", []];
|
||||||
count (_self getOrDefault ["hvts", []]) > 0
|
!(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["hvts", []]) > 0 }
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
waitUntil {
|
waitUntil {
|
||||||
@ -79,6 +79,8 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !(_self call ["isTaskStoreOpen", []]) exitWith { false };
|
||||||
|
|
||||||
private _hvts = _self getOrDefault ["hvts", []];
|
private _hvts = _self getOrDefault ["hvts", []];
|
||||||
private _taskParams = _self getOrDefault ["taskParams", createHashMap];
|
private _taskParams = _self getOrDefault ["taskParams", createHashMap];
|
||||||
private _required = _taskParams getOrDefault ["limitSuccess", -1];
|
private _required = _taskParams getOrDefault ["limitSuccess", -1];
|
||||||
@ -122,10 +124,10 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
|
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
|
!(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] }
|
||||||
};
|
};
|
||||||
|
|
||||||
true
|
_self call ["isTaskStoreOpen", []]
|
||||||
}],
|
}],
|
||||||
["tick", compileFinal {
|
["tick", compileFinal {
|
||||||
private _startedAt = _self getOrDefault ["startedAt", -1];
|
private _startedAt = _self getOrDefault ["startedAt", -1];
|
||||||
@ -161,13 +163,10 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
}],
|
}],
|
||||||
["handleFailureOutcome", compileFinal {
|
["handleFailureOutcome", compileFinal {
|
||||||
private _taskID = _self getOrDefault ["taskID", ""];
|
private _taskID = _self getOrDefault ["taskID", ""];
|
||||||
private _hvts = _self getOrDefault ["hvts", []];
|
|
||||||
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
||||||
private _ratingFail = _rewardData getOrDefault ["ratingFail", 0];
|
private _ratingFail = _rewardData getOrDefault ["ratingFail", 0];
|
||||||
private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false];
|
private _endFail = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endFail", false];
|
||||||
|
|
||||||
{ deleteVehicle _x } forEach _hvts;
|
|
||||||
|
|
||||||
if (_self getOrDefault ["useTaskStore", false]) then {
|
if (_self getOrDefault ["useTaskStore", false]) then {
|
||||||
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
|
||||||
@ -184,14 +183,11 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
}],
|
}],
|
||||||
["handleSuccessOutcome", compileFinal {
|
["handleSuccessOutcome", compileFinal {
|
||||||
private _taskID = _self getOrDefault ["taskID", ""];
|
private _taskID = _self getOrDefault ["taskID", ""];
|
||||||
private _hvts = _self getOrDefault ["hvts", []];
|
|
||||||
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
||||||
private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0];
|
private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0];
|
||||||
private _funds = _rewardData getOrDefault ["funds", 0];
|
private _funds = _rewardData getOrDefault ["funds", 0];
|
||||||
private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false];
|
private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false];
|
||||||
|
|
||||||
{ deleteVehicle _x } forEach _hvts;
|
|
||||||
|
|
||||||
if (_self getOrDefault ["useTaskStore", false]) then {
|
if (_self getOrDefault ["useTaskStore", false]) then {
|
||||||
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
||||||
@ -208,12 +204,20 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["runLoop", compileFinal {
|
["runLoop", compileFinal {
|
||||||
_self call ["waitForRequiredEntities", []];
|
if !(_self call ["waitForRequiredEntities", []]) exitWith {
|
||||||
_self call ["waitForAssignment", []];
|
_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 ["startHvtControllers", []];
|
||||||
_self call ["markActive", []];
|
_self call ["markActive", []];
|
||||||
|
|
||||||
while { (_self call ["getStatus", []]) isEqualTo "active" } do {
|
while { _self call ["isTaskLoopActive", []] } do {
|
||||||
_self call ["trackParticipants", []];
|
_self call ["trackParticipants", []];
|
||||||
private _snapshot = _self call ["tick", []];
|
private _snapshot = _self call ["tick", []];
|
||||||
|
|
||||||
@ -228,9 +232,12 @@ GVAR(HVTTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
sleep 1;
|
sleep 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((_self call ["getStatus", []]) isEqualTo "failed") then {
|
private _finalStatus = _self call ["getStatus", []];
|
||||||
|
if (_finalStatus isEqualTo "failed") then {
|
||||||
_self call ["handleFailureOutcome", []];
|
_self call ["handleFailureOutcome", []];
|
||||||
} else {
|
};
|
||||||
|
|
||||||
|
if (_finalStatus isEqualTo "succeeded") then {
|
||||||
_self call ["handleSuccessOutcome", []];
|
_self call ["handleSuccessOutcome", []];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -103,12 +103,19 @@ GVAR(HostageEntityController) merge [createHashMapFromArray [
|
|||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
|
|
||||||
|
if !(_self call ["isAssignedTaskOpen", []]) exitWith { true };
|
||||||
if (isNull _entity || { !alive _entity }) exitWith { true };
|
if (isNull _entity || { !alive _entity }) exitWith { true };
|
||||||
|
|
||||||
_rescuer = _self call ["findNearbyRescuer", []];
|
_rescuer = _self call ["findNearbyRescuer", []];
|
||||||
!isNull _rescuer
|
!isNull _rescuer
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !(_self call ["isAssignedTaskOpen", []]) exitWith {
|
||||||
|
_self call ["markAborted", []];
|
||||||
|
_self call ["cleanup", []];
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
if (isNull _entity || { !alive _entity }) exitWith {
|
if (isNull _entity || { !alive _entity }) exitWith {
|
||||||
_self call ["markAborted", []];
|
_self call ["markAborted", []];
|
||||||
_self call ["cleanup", []];
|
_self call ["cleanup", []];
|
||||||
|
|||||||
@ -150,14 +150,15 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
_self call ["refreshEntitiesFromStore", []];
|
_self call ["refreshEntitiesFromStore", []];
|
||||||
count (_self getOrDefault ["hostages", []]) > 0
|
!(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["hostages", []]) > 0 }
|
||||||
};
|
};
|
||||||
|
if !(_self call ["isTaskStoreOpen", []]) exitWith { false };
|
||||||
|
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
_self call ["refreshEntitiesFromStore", []];
|
_self call ["refreshEntitiesFromStore", []];
|
||||||
_self call ["trackParticipants", []];
|
_self call ["trackParticipants", []];
|
||||||
count (_self getOrDefault ["shooters", []]) > 0
|
!(_self call ["isTaskStoreOpen", []]) || { count (_self getOrDefault ["shooters", []]) > 0 }
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
waitUntil {
|
waitUntil {
|
||||||
@ -171,6 +172,8 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !(_self call ["isTaskStoreOpen", []]) exitWith { false };
|
||||||
|
|
||||||
private _hostages = _self getOrDefault ["hostages", []];
|
private _hostages = _self getOrDefault ["hostages", []];
|
||||||
private _taskParams = _self getOrDefault ["taskParams", createHashMap];
|
private _taskParams = _self getOrDefault ["taskParams", createHashMap];
|
||||||
private _requiredRescues = _taskParams getOrDefault ["limitSuccess", -1];
|
private _requiredRescues = _taskParams getOrDefault ["limitSuccess", -1];
|
||||||
@ -190,10 +193,10 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
|
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
|
!(_self call ["isTaskStoreOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] }
|
||||||
};
|
};
|
||||||
|
|
||||||
true
|
_self call ["isTaskStoreOpen", []]
|
||||||
}],
|
}],
|
||||||
["countFreedHostages", compileFinal {
|
["countFreedHostages", compileFinal {
|
||||||
private _playerGroups = allPlayers apply { group _x };
|
private _playerGroups = allPlayers apply { group _x };
|
||||||
@ -290,11 +293,9 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
sleep 5;
|
sleep 5;
|
||||||
};
|
};
|
||||||
|
|
||||||
{ deleteVehicle _x } forEach _hostages;
|
|
||||||
{ deleteVehicle _x } forEach _shooters;
|
|
||||||
|
|
||||||
if (_useTaskStore) then {
|
if (_useTaskStore) then {
|
||||||
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
[_taskID, "FAILED"] call BFUNC(taskSetState);
|
||||||
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
|
||||||
|
|
||||||
sleep 1;
|
sleep 1;
|
||||||
|
|
||||||
@ -308,17 +309,12 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
}],
|
}],
|
||||||
["handleSuccessOutcome", compileFinal {
|
["handleSuccessOutcome", compileFinal {
|
||||||
private _taskID = _self getOrDefault ["taskID", ""];
|
private _taskID = _self getOrDefault ["taskID", ""];
|
||||||
private _hostages = _self getOrDefault ["hostages", []];
|
|
||||||
private _shooters = _self getOrDefault ["shooters", []];
|
|
||||||
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
private _rewardData = _self getOrDefault ["rewardData", createHashMap];
|
||||||
private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0];
|
private _ratingSuccess = _rewardData getOrDefault ["ratingSuccess", 0];
|
||||||
private _funds = _rewardData getOrDefault ["funds", 0];
|
private _funds = _rewardData getOrDefault ["funds", 0];
|
||||||
private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false];
|
private _endSuccess = (_self getOrDefault ["taskParams", createHashMap]) getOrDefault ["endSuccess", false];
|
||||||
private _useTaskStore = _self getOrDefault ["useTaskStore", false];
|
private _useTaskStore = _self getOrDefault ["useTaskStore", false];
|
||||||
|
|
||||||
{ deleteVehicle _x } forEach _hostages;
|
|
||||||
{ deleteVehicle _x } forEach _shooters;
|
|
||||||
|
|
||||||
if (_useTaskStore) then {
|
if (_useTaskStore) then {
|
||||||
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
|
||||||
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
|
||||||
@ -335,12 +331,20 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["runLoop", compileFinal {
|
["runLoop", compileFinal {
|
||||||
_self call ["waitForRequiredEntities", []];
|
if !(_self call ["waitForRequiredEntities", []]) exitWith {
|
||||||
_self call ["waitForAssignment", []];
|
_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 ["startHostageControllers", []];
|
||||||
_self call ["markActive", []];
|
_self call ["markActive", []];
|
||||||
|
|
||||||
while { (_self call ["getStatus", []]) isEqualTo "active" } do {
|
while { _self call ["isTaskLoopActive", []] } do {
|
||||||
_self call ["trackParticipants", []];
|
_self call ["trackParticipants", []];
|
||||||
private _snapshot = _self call ["tick", []];
|
private _snapshot = _self call ["tick", []];
|
||||||
|
|
||||||
@ -355,9 +359,12 @@ GVAR(HostageTaskBaseClass) merge [createHashMapFromArray [
|
|||||||
sleep 1;
|
sleep 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((_self call ["getStatus", []]) isEqualTo "failed") then {
|
private _finalStatus = _self call ["getStatus", []];
|
||||||
|
if (_finalStatus isEqualTo "failed") then {
|
||||||
_self call ["handleFailureOutcome", []];
|
_self call ["handleFailureOutcome", []];
|
||||||
} else {
|
};
|
||||||
|
|
||||||
|
if (_finalStatus isEqualTo "succeeded") then {
|
||||||
_self call ["handleSuccessOutcome", []];
|
_self call ["handleSuccessOutcome", []];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -29,10 +29,10 @@ GVAR(IEDEntityController) merge [createHashMapFromArray [
|
|||||||
|
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
sleep 1;
|
||||||
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
|
!(_self call ["isAssignedTaskOpen", []]) || { GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] }
|
||||||
};
|
};
|
||||||
|
|
||||||
true
|
_self call ["isAssignedTaskOpen", []]
|
||||||
}],
|
}],
|
||||||
["playCountdownSound", compileFinal {
|
["playCountdownSound", compileFinal {
|
||||||
params [["_timeRemaining", 0, [0]]];
|
params [["_timeRemaining", 0, [0]]];
|
||||||
@ -67,20 +67,35 @@ GVAR(IEDEntityController) merge [createHashMapFromArray [
|
|||||||
false
|
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", []];
|
_self call ["markActive", []];
|
||||||
|
|
||||||
while { (_self call ["isEntityUsable", []]) && { _countdown > 0 } } do {
|
while { (_self call ["isAssignedTaskOpen", []]) && { (_self call ["isEntityUsable", []]) && { _countdown > 0 } } } do {
|
||||||
_self call ["playCountdownSound", [_countdown]];
|
_self call ["playCountdownSound", [_countdown]];
|
||||||
_countdown = _countdown - 1;
|
_countdown = _countdown - 1;
|
||||||
_self set ["countdown", _countdown];
|
_self set ["countdown", _countdown];
|
||||||
sleep 1;
|
sleep 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((_self call ["isEntityUsable", []]) && { _countdown <= 0 }) then {
|
if ((_self call ["isAssignedTaskOpen", []]) && { (_self call ["isEntityUsable", []]) && { _countdown <= 0 } }) then {
|
||||||
_self call ["detonate", []];
|
_self call ["detonate", []];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !(_self call ["isAssignedTaskOpen", []]) exitWith {
|
||||||
|
_self call ["markAborted", []];
|
||||||
|
_self call ["cleanup", []];
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
_self call ["markFinished", []];
|
_self call ["markFinished", []];
|
||||||
_self call ["cleanup", []];
|
_self call ["cleanup", []];
|
||||||
true
|
true
|
||||||
|
|||||||
@ -0,0 +1,139 @@
|
|||||||
|
#include "..\script_component.hpp"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Author: IDSolutions
|
||||||
|
* Registry object for generated mission providers used by CAD/manual requests.
|
||||||
|
*
|
||||||
|
* Arguments:
|
||||||
|
* None
|
||||||
|
*
|
||||||
|
* Return Value:
|
||||||
|
* Mission generator provider registry object <HASHMAP OBJECT>
|
||||||
|
*
|
||||||
|
* Public: No
|
||||||
|
*/
|
||||||
|
|
||||||
|
if !(isServer) exitWith { createHashMap };
|
||||||
|
|
||||||
|
#pragma hemtt ignore_variables ["_self"]
|
||||||
|
GVAR(MissionGeneratorProviderRegistryBaseClass) = compileFinal createHashMapFromArray [
|
||||||
|
["#type", "MissionGeneratorProviderRegistryBaseClass"],
|
||||||
|
["#create", compileFinal {
|
||||||
|
_self set ["providers", createHashMap];
|
||||||
|
}],
|
||||||
|
["emptyResult", compileFinal {
|
||||||
|
params [
|
||||||
|
["_message", "Generated task request failed.", [""]],
|
||||||
|
["_taskType", "", [""]]
|
||||||
|
];
|
||||||
|
|
||||||
|
createHashMapFromArray [
|
||||||
|
["success", false],
|
||||||
|
["message", _message],
|
||||||
|
["taskID", ""],
|
||||||
|
["taskType", _taskType]
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
["normalizeProviderId", compileFinal {
|
||||||
|
params [["_providerId", "builtin", [""]]];
|
||||||
|
|
||||||
|
_providerId = toLowerANSI _providerId;
|
||||||
|
if (_providerId isEqualTo "") then { _providerId = "builtin"; };
|
||||||
|
_providerId
|
||||||
|
}],
|
||||||
|
["registerProvider", compileFinal {
|
||||||
|
params [
|
||||||
|
["_providerId", "", [""]],
|
||||||
|
["_provider", createHashMap, [createHashMap]]
|
||||||
|
];
|
||||||
|
|
||||||
|
_providerId = _self call ["normalizeProviderId", [_providerId]];
|
||||||
|
if (_provider isEqualTo createHashMap) exitWith {
|
||||||
|
["WARNING", format ["Generated mission provider registration ignored: provider '%1' was empty.", _providerId]] call EFUNC(common,log);
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if !("getGeneratedTaskTypes" in _provider) exitWith {
|
||||||
|
["WARNING", format ["Generated mission provider registration ignored: provider '%1' has no getGeneratedTaskTypes method.", _providerId]] call EFUNC(common,log);
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if !("requestMissionTask" in _provider) exitWith {
|
||||||
|
["WARNING", format ["Generated mission provider registration ignored: provider '%1' has no requestMissionTask method.", _providerId]] call EFUNC(common,log);
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
(_self get "providers") set [_providerId, _provider];
|
||||||
|
["INFO", format ["Generated mission provider registered. Provider=%1", _providerId]] call EFUNC(common,log);
|
||||||
|
true
|
||||||
|
}],
|
||||||
|
["hasProvider", compileFinal {
|
||||||
|
params [["_providerId", "builtin", [""]]];
|
||||||
|
|
||||||
|
_providerId = _self call ["normalizeProviderId", [_providerId]];
|
||||||
|
_providerId in (_self getOrDefault ["providers", createHashMap])
|
||||||
|
}],
|
||||||
|
["getProvider", compileFinal {
|
||||||
|
params [["_providerId", "builtin", [""]]];
|
||||||
|
|
||||||
|
_providerId = _self call ["normalizeProviderId", [_providerId]];
|
||||||
|
(_self getOrDefault ["providers", createHashMap]) getOrDefault [_providerId, createHashMap]
|
||||||
|
}],
|
||||||
|
["getSelectedProviderId", compileFinal {
|
||||||
|
private _providerId = _self call ["normalizeProviderId", [GETGVAR(generatorProvider,"builtin")]];
|
||||||
|
if (_self call ["hasProvider", [_providerId]]) exitWith { _providerId };
|
||||||
|
|
||||||
|
["WARNING", format [
|
||||||
|
"Generated mission provider '%1' is selected but not registered; falling back to builtin provider.",
|
||||||
|
_providerId
|
||||||
|
]] call EFUNC(common,log);
|
||||||
|
"builtin"
|
||||||
|
}],
|
||||||
|
["getActiveProvider", compileFinal {
|
||||||
|
_self call ["getProvider", [_self call ["getSelectedProviderId", []]]]
|
||||||
|
}],
|
||||||
|
["getGeneratedTaskTypes", compileFinal {
|
||||||
|
private _providerId = _self call ["getSelectedProviderId", []];
|
||||||
|
private _provider = _self call ["getProvider", [_providerId]];
|
||||||
|
if (_provider isEqualTo createHashMap) exitWith { [] };
|
||||||
|
|
||||||
|
private _types = _provider call ["getGeneratedTaskTypes", []];
|
||||||
|
if !(_types isEqualType []) exitWith {
|
||||||
|
["WARNING", format ["Generated mission provider '%1' returned invalid task types.", _providerId]] call EFUNC(common,log);
|
||||||
|
[]
|
||||||
|
};
|
||||||
|
|
||||||
|
["INFO", format [
|
||||||
|
"Generated mission provider '%1' returned task types: %2",
|
||||||
|
_providerId,
|
||||||
|
_types apply { _x getOrDefault ["value", ""] }
|
||||||
|
]] call EFUNC(common,log);
|
||||||
|
_types
|
||||||
|
}],
|
||||||
|
["requestMissionTask", compileFinal {
|
||||||
|
params [
|
||||||
|
["_requestedType", "", [""]],
|
||||||
|
["_metadata", createHashMap, [createHashMap]],
|
||||||
|
["_requesterUid", "", [""]]
|
||||||
|
];
|
||||||
|
|
||||||
|
private _providerId = _self call ["getSelectedProviderId", []];
|
||||||
|
private _provider = _self call ["getProvider", [_providerId]];
|
||||||
|
if (_provider isEqualTo createHashMap) exitWith {
|
||||||
|
_self call ["emptyResult", [format ["Generated mission provider is unavailable: %1", _providerId], _requestedType]]
|
||||||
|
};
|
||||||
|
|
||||||
|
private _result = _provider call ["requestMissionTask", [_requestedType, _metadata, _requesterUid]];
|
||||||
|
if !(_result isEqualType createHashMap) exitWith {
|
||||||
|
_self call ["emptyResult", [format ["Generated mission provider '%1' returned an invalid request result.", _providerId], _requestedType]]
|
||||||
|
};
|
||||||
|
|
||||||
|
if !("taskType" in _result) then { _result set ["taskType", _requestedType]; };
|
||||||
|
if !("taskID" in _result) then { _result set ["taskID", ""]; };
|
||||||
|
if !("success" in _result) then { _result set ["success", false]; };
|
||||||
|
if !("message" in _result) then { _result set ["message", "Generated task request completed."]; };
|
||||||
|
_result set ["provider", _providerId];
|
||||||
|
_result
|
||||||
|
}]
|
||||||
|
];
|
||||||
|
|
||||||
|
GVAR(MissionGeneratorProviderRegistry) = createHashMapObject [GVAR(MissionGeneratorProviderRegistryBaseClass)];
|
||||||
|
GVAR(MissionGeneratorProviderRegistry)
|
||||||
@ -31,7 +31,13 @@ GVAR(ProtectedEntityController) merge [createHashMapFromArray [
|
|||||||
_self call ["markActive", []];
|
_self call ["markActive", []];
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
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", []];
|
_self call ["markFinished", []];
|
||||||
|
|||||||
@ -31,7 +31,13 @@ GVAR(ShooterEntityController) merge [createHashMapFromArray [
|
|||||||
_self call ["markActive", []];
|
_self call ["markActive", []];
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
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", []];
|
_self call ["markFinished", []];
|
||||||
|
|||||||
@ -31,7 +31,13 @@ GVAR(TargetEntityController) merge [createHashMapFromArray [
|
|||||||
_self call ["markActive", []];
|
_self call ["markActive", []];
|
||||||
waitUntil {
|
waitUntil {
|
||||||
sleep 1;
|
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", []];
|
_self call ["markFinished", []];
|
||||||
|
|||||||
@ -123,6 +123,11 @@ GVAR(TaskCatalogStore) = createHashMapObject [[
|
|||||||
|
|
||||||
(_self call ["getTaskStatus", [_taskID]]) isEqualTo "succeeded"
|
(_self call ["getTaskStatus", [_taskID]]) isEqualTo "succeeded"
|
||||||
}],
|
}],
|
||||||
|
["isTerminalStatus", compileFinal {
|
||||||
|
params [["_status", "", [""]]];
|
||||||
|
|
||||||
|
(toLowerANSI _status) in ["failed", "succeeded"]
|
||||||
|
}],
|
||||||
["areTaskPrerequisitesSatisfied", compileFinal {
|
["areTaskPrerequisitesSatisfied", compileFinal {
|
||||||
params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]];
|
params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
@ -359,6 +364,28 @@ GVAR(TaskCatalogStore) = createHashMapObject [[
|
|||||||
if (_taskID isEqualTo "" || { _status isEqualTo "" }) exitWith { false };
|
if (_taskID isEqualTo "" || { _status isEqualTo "" }) exitWith { false };
|
||||||
|
|
||||||
private _normalizedStatus = toLowerANSI _status;
|
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 _runtimeCatalogRegistry = _self getOrDefault ["runtimeCatalogRegistry", createHashMap];
|
||||||
private _runtimeEntry = +(_runtimeCatalogRegistry getOrDefault [_taskID, createHashMap]);
|
private _runtimeEntry = +(_runtimeCatalogRegistry getOrDefault [_taskID, createHashMap]);
|
||||||
if (_runtimeEntry isNotEqualTo createHashMap) then {
|
if (_runtimeEntry isNotEqualTo createHashMap) then {
|
||||||
|
|||||||
@ -83,6 +83,50 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [
|
|||||||
["getStatus", compileFinal {
|
["getStatus", compileFinal {
|
||||||
_self getOrDefault ["status", "created"]
|
_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 {
|
["getRewardData", compileFinal {
|
||||||
_self getOrDefault ["rewardData", createHashMap]
|
_self getOrDefault ["rewardData", createHashMap]
|
||||||
}],
|
}],
|
||||||
@ -93,9 +137,9 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [
|
|||||||
private _registryKey = _self call ["getRegistryKey", []];
|
private _registryKey = _self call ["getRegistryKey", []];
|
||||||
if (_registryKey isEqualTo "") exitWith { false };
|
if (_registryKey isEqualTo "") exitWith { false };
|
||||||
|
|
||||||
private _registry = missionNamespace getVariable [QGVAR(ObjectTaskInstances), createHashMap];
|
private _registry = GETMVAR(GVAR(ObjectTaskInstances),createHashMap);
|
||||||
_registry set [_registryKey, _self];
|
_registry set [_registryKey, _self];
|
||||||
missionNamespace setVariable [QGVAR(ObjectTaskInstances), _registry];
|
SETMVAR(GVAR(ObjectTaskInstances),_registry);
|
||||||
missionNamespace setVariable [_registryKey, _self];
|
missionNamespace setVariable [_registryKey, _self];
|
||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
@ -103,7 +147,7 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [
|
|||||||
private _registryKey = _self call ["getRegistryKey", []];
|
private _registryKey = _self call ["getRegistryKey", []];
|
||||||
if (_registryKey isEqualTo "") exitWith { false };
|
if (_registryKey isEqualTo "") exitWith { false };
|
||||||
|
|
||||||
private _registry = missionNamespace getVariable [QGVAR(ObjectTaskInstances), createHashMap];
|
private _registry = GETMVAR(GVAR(ObjectTaskInstances),createHashMap);
|
||||||
_registry deleteAt _registryKey;
|
_registry deleteAt _registryKey;
|
||||||
missionNamespace setVariable [_registryKey, nil];
|
missionNamespace setVariable [_registryKey, nil];
|
||||||
true
|
true
|
||||||
@ -162,6 +206,8 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [
|
|||||||
["markSucceeded", compileFinal {
|
["markSucceeded", compileFinal {
|
||||||
params [["_resultSnapshot", createHashMap, [createHashMap]]];
|
params [["_resultSnapshot", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
|
if !(_self call ["canTransitionToTerminal", ["succeeded"]]) exitWith { false };
|
||||||
|
|
||||||
_self set ["status", "succeeded"];
|
_self set ["status", "succeeded"];
|
||||||
_self set ["finishedAt", serverTime];
|
_self set ["finishedAt", serverTime];
|
||||||
_self set ["resultSnapshot", _resultSnapshot];
|
_self set ["resultSnapshot", _resultSnapshot];
|
||||||
@ -173,6 +219,8 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [
|
|||||||
["markFailed", compileFinal {
|
["markFailed", compileFinal {
|
||||||
params [["_reason", "", [""]], ["_resultSnapshot", createHashMap, [createHashMap]]];
|
params [["_reason", "", [""]], ["_resultSnapshot", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
|
if !(_self call ["canTransitionToTerminal", ["failed"]]) exitWith { false };
|
||||||
|
|
||||||
_self set ["status", "failed"];
|
_self set ["status", "failed"];
|
||||||
_self set ["finishedAt", serverTime];
|
_self set ["finishedAt", serverTime];
|
||||||
_self set ["failureReason", _reason];
|
_self set ["failureReason", _reason];
|
||||||
@ -182,6 +230,14 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
|
["markAborted", compileFinal {
|
||||||
|
params [["_reason", "", [""]]];
|
||||||
|
|
||||||
|
_self set ["status", "aborted"];
|
||||||
|
_self set ["finishedAt", serverTime];
|
||||||
|
_self set ["failureReason", _reason];
|
||||||
|
true
|
||||||
|
}],
|
||||||
["cleanup", compileFinal {
|
["cleanup", compileFinal {
|
||||||
_self call ["unregisterInstance", []]
|
_self call ["unregisterInstance", []]
|
||||||
}],
|
}],
|
||||||
|
|||||||
@ -122,6 +122,53 @@ GVAR(TaskLifecycleReporter) = createHashMapObject [[
|
|||||||
_self call ["buildTaskLifecycleEventPayload", [_taskID, _status, _extra]],
|
_self call ["buildTaskLifecycleEventPayload", [_taskID, _status, _extra]],
|
||||||
createHashMapFromArray [["source", "task"]]
|
createHashMapFromArray [["source", "task"]]
|
||||||
]]
|
]]
|
||||||
|
}],
|
||||||
|
["registerEventLogListeners", compileFinal {
|
||||||
|
if !(isNil QGVAR(TaskLifecycleEventLogTokens)) exitWith { GVAR(TaskLifecycleEventLogTokens) };
|
||||||
|
|
||||||
|
private _logTaskLifecycleEvent = {
|
||||||
|
params ["_event"];
|
||||||
|
|
||||||
|
if !(GETGVAR(enableEventLogs,false)) exitWith {};
|
||||||
|
|
||||||
|
["INFO", format [
|
||||||
|
"Task lifecycle event: %1 taskID=%2 taskType=%3 status=%4 participants=%5",
|
||||||
|
_event getOrDefault ["event", ""],
|
||||||
|
_event getOrDefault ["taskID", ""],
|
||||||
|
_event getOrDefault ["taskType", ""],
|
||||||
|
_event getOrDefault ["status", ""],
|
||||||
|
_event getOrDefault ["participants", []]
|
||||||
|
]] call EFUNC(common,log);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _logTaskRewardEvent = {
|
||||||
|
params ["_event"];
|
||||||
|
|
||||||
|
if !(GETGVAR(enableEventLogs,false)) exitWith {};
|
||||||
|
|
||||||
|
["INFO", format [
|
||||||
|
"Task reward event: %1 taskID=%2 success=%3 message=%4",
|
||||||
|
_event getOrDefault ["event", ""],
|
||||||
|
_event getOrDefault ["taskID", ""],
|
||||||
|
!((_event getOrDefault ["event", ""]) in ["task.reward.failed", "task.rating.failed"]),
|
||||||
|
_event getOrDefault ["message", ""]
|
||||||
|
]] call EFUNC(common,log);
|
||||||
|
};
|
||||||
|
|
||||||
|
GVAR(TaskLifecycleEventLogTokens) = [
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.created", _logTaskLifecycleEvent, "task.lifecycle.log"]],
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.started", _logTaskLifecycleEvent, "task.lifecycle.log"]],
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.completed", _logTaskLifecycleEvent, "task.lifecycle.log"]],
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.failed", _logTaskLifecycleEvent, "task.lifecycle.log"]],
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.cleared", _logTaskLifecycleEvent, "task.lifecycle.log"]],
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.reward.requested", _logTaskRewardEvent, "task.reward.log"]],
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.reward.applied", _logTaskRewardEvent, "task.reward.log"]],
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.reward.failed", _logTaskRewardEvent, "task.reward.log"]],
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.rating.applied", _logTaskRewardEvent, "task.reward.log"]],
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.rating.failed", _logTaskRewardEvent, "task.reward.log"]]
|
||||||
|
];
|
||||||
|
|
||||||
|
GVAR(TaskLifecycleEventLogTokens)
|
||||||
}]
|
}]
|
||||||
]];
|
]];
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,103 @@
|
|||||||
|
#include "..\script_component.hpp"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Author: IDSolutions
|
||||||
|
* Dispatches task notification events to client notification UIs.
|
||||||
|
*
|
||||||
|
* Arguments:
|
||||||
|
* None
|
||||||
|
*
|
||||||
|
* Return Value:
|
||||||
|
* Task notification service object <HASHMAP OBJECT>
|
||||||
|
*
|
||||||
|
* Public: No
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma hemtt ignore_variables ["_self"]
|
||||||
|
GVAR(TaskNotificationService) = createHashMapObject [[
|
||||||
|
["#type", "TaskNotificationService"],
|
||||||
|
["sendToPlayers", compileFinal {
|
||||||
|
params [
|
||||||
|
["_uids", [], [[]]],
|
||||||
|
["_type", "info", [""]],
|
||||||
|
["_title", "Tasks", [""]],
|
||||||
|
["_message", "", [""]]
|
||||||
|
];
|
||||||
|
|
||||||
|
if (_message isEqualTo "" || { _uids isEqualTo [] }) exitWith { false };
|
||||||
|
|
||||||
|
{
|
||||||
|
private _player = [_x] call EFUNC(common,getPlayer);
|
||||||
|
if (isNull _player) then { continue; };
|
||||||
|
[CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent);
|
||||||
|
} forEach _uids;
|
||||||
|
|
||||||
|
true
|
||||||
|
}],
|
||||||
|
["handleTaskNotification", compileFinal {
|
||||||
|
params ["_event"];
|
||||||
|
|
||||||
|
private _type = _event getOrDefault ["notificationType", "info"];
|
||||||
|
private _title = _event getOrDefault ["title", "Tasks"];
|
||||||
|
private _message = _event getOrDefault ["message", ""];
|
||||||
|
private _participantUids = +(_event getOrDefault ["participantUids", []]);
|
||||||
|
|
||||||
|
if !(_self call ["sendToPlayers", [_participantUids, _type, _title, _message]]) exitWith { false };
|
||||||
|
|
||||||
|
if (GETGVAR(enableEventLogs,false)) then {
|
||||||
|
["INFO", format [
|
||||||
|
"Task notification event: taskID=%1 type=%2 recipients=%3 message=%4",
|
||||||
|
_event getOrDefault ["taskID", ""],
|
||||||
|
_type,
|
||||||
|
_participantUids,
|
||||||
|
_message
|
||||||
|
]] call EFUNC(common,log);
|
||||||
|
};
|
||||||
|
|
||||||
|
true
|
||||||
|
}],
|
||||||
|
["handleRewardNotification", compileFinal {
|
||||||
|
params ["_event"];
|
||||||
|
|
||||||
|
private _type = _event getOrDefault ["notificationType", "info"];
|
||||||
|
private _title = _event getOrDefault ["title", "Tasks"];
|
||||||
|
private _message = _event getOrDefault ["message", ""];
|
||||||
|
private _memberUids = +(_event getOrDefault ["memberUids", []]);
|
||||||
|
|
||||||
|
if !(_self call ["sendToPlayers", [_memberUids, _type, _title, _message]]) exitWith { false };
|
||||||
|
|
||||||
|
if (GETGVAR(enableEventLogs,false)) then {
|
||||||
|
["INFO", format [
|
||||||
|
"Task reward notification event: taskID=%1 type=%2 recipients=%3 message=%4",
|
||||||
|
_event getOrDefault ["taskID", ""],
|
||||||
|
_type,
|
||||||
|
_memberUids,
|
||||||
|
_message
|
||||||
|
]] call EFUNC(common,log);
|
||||||
|
};
|
||||||
|
|
||||||
|
true
|
||||||
|
}],
|
||||||
|
["registerEventListeners", compileFinal {
|
||||||
|
if !(isNil QGVAR(TaskNotificationEventTokens)) exitWith { GVAR(TaskNotificationEventTokens) };
|
||||||
|
|
||||||
|
private _sendTaskNotification = {
|
||||||
|
params ["_event"];
|
||||||
|
GVAR(TaskNotificationService) call ["handleTaskNotification", [_event]];
|
||||||
|
};
|
||||||
|
|
||||||
|
private _sendRewardNotification = {
|
||||||
|
params ["_event"];
|
||||||
|
GVAR(TaskNotificationService) call ["handleRewardNotification", [_event]];
|
||||||
|
};
|
||||||
|
|
||||||
|
GVAR(TaskNotificationEventTokens) = [
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.notification.requested", _sendTaskNotification, "task.notification.send"]],
|
||||||
|
EGVAR(common,EventBus) call ["on", ["task.reward.notification.requested", _sendRewardNotification, "task.reward.notification.send"]]
|
||||||
|
];
|
||||||
|
|
||||||
|
GVAR(TaskNotificationEventTokens)
|
||||||
|
}]
|
||||||
|
]];
|
||||||
|
|
||||||
|
GVAR(TaskNotificationService)
|
||||||
@ -39,6 +39,22 @@ GVAR(TransportServiceBase) = compileFinal createHashMapFromArray [
|
|||||||
["INFO", "Transport Service Initialized!"] call EFUNC(common,log);
|
["INFO", "Transport Service Initialized!"] call EFUNC(common,log);
|
||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
|
["numberSetting", compileFinal {
|
||||||
|
params [["_name", "", [""]], ["_default", 0, [0]]];
|
||||||
|
|
||||||
|
private _configDefault = _default;
|
||||||
|
private _serviceConfig = missionConfigFile >> "CfgServicePricing";
|
||||||
|
if !(isClass _serviceConfig) then { _serviceConfig = configFile >> "CfgServicePricing"; };
|
||||||
|
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 {
|
["notify", compileFinal {
|
||||||
params [["_unit", objNull, [objNull]], ["_type", "info", [""]], ["_title", "Transport", [""]], ["_message", "", [""]]];
|
params [["_unit", objNull, [objNull]], ["_type", "info", [""]], ["_title", "Transport", [""]], ["_message", "", [""]]];
|
||||||
|
|
||||||
@ -120,8 +136,10 @@ GVAR(TransportServiceBase) = compileFinal createHashMapFromArray [
|
|||||||
["getCost", compileFinal {
|
["getCost", compileFinal {
|
||||||
params [["_fromNode", objNull, [objNull]], ["_toNode", objNull, [objNull]], ["_options", createHashMap, [createHashMap]]];
|
params [["_fromNode", objNull, [objNull]], ["_toNode", objNull, [objNull]], ["_options", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
private _baseFare = _options getOrDefault ["baseFare", _self getOrDefault ["baseFare", 100]];
|
private _baseFareDefault = _self call ["numberSetting", ["transportBaseFare", _self getOrDefault ["baseFare", 100]]];
|
||||||
private _pricePerKm = _options getOrDefault ["pricePerKm", _self getOrDefault ["pricePerKm", 50]];
|
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;
|
private _distanceMeters = _fromNode distance2D _toNode;
|
||||||
|
|
||||||
round (_baseFare + ((_distanceMeters / 1000) * _pricePerKm))
|
round (_baseFare + ((_distanceMeters / 1000) * _pricePerKm))
|
||||||
|
|||||||
@ -19,6 +19,8 @@ pub type SurrealDb = Surreal<Client>;
|
|||||||
|
|
||||||
const CLIENT_READY_TIMEOUT: Duration = Duration::from_secs(30);
|
const CLIENT_READY_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
const CLIENT_READY_POLL_INTERVAL: Duration = Duration::from_millis(25);
|
const CLIENT_READY_POLL_INTERVAL: Duration = Duration::from_millis(25);
|
||||||
|
const INIT_MAX_ATTEMPTS: usize = 5;
|
||||||
|
const INIT_RETRY_BASE_DELAY: Duration = Duration::from_millis(150);
|
||||||
|
|
||||||
static SURREAL_DB: LazyLock<StdRwLock<Option<Arc<SurrealDb>>>> =
|
static SURREAL_DB: LazyLock<StdRwLock<Option<Arc<SurrealDb>>>> =
|
||||||
LazyLock::new(|| StdRwLock::new(None));
|
LazyLock::new(|| StdRwLock::new(None));
|
||||||
@ -27,6 +29,8 @@ static SURREAL_CONNECTION_STATE: LazyLock<StdRwLock<SurrealConnectionState>> =
|
|||||||
static SURREAL_FAILURE_REASON: LazyLock<StdRwLock<Option<String>>> =
|
static SURREAL_FAILURE_REASON: LazyLock<StdRwLock<Option<String>>> =
|
||||||
LazyLock::new(|| StdRwLock::new(None));
|
LazyLock::new(|| StdRwLock::new(None));
|
||||||
static SURREAL_INIT_GENERATION: AtomicU64 = AtomicU64::new(0);
|
static SURREAL_INIT_GENERATION: AtomicU64 = AtomicU64::new(0);
|
||||||
|
static SURREAL_INIT_LOCK: LazyLock<tokio::sync::Mutex<()>> =
|
||||||
|
LazyLock::new(|| tokio::sync::Mutex::new(()));
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
enum SurrealConnectionState {
|
enum SurrealConnectionState {
|
||||||
@ -42,6 +46,7 @@ pub fn prepare() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initialize(config: SurrealConfig) {
|
pub async fn initialize(config: SurrealConfig) {
|
||||||
|
let _init_guard = SURREAL_INIT_LOCK.lock().await;
|
||||||
let generation = SURREAL_INIT_GENERATION.fetch_add(1, Ordering::SeqCst) + 1;
|
let generation = SURREAL_INIT_GENERATION.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
prepare();
|
prepare();
|
||||||
|
|
||||||
@ -55,7 +60,7 @@ pub async fn initialize(config: SurrealConfig) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let timeout_duration = Duration::from_millis(config.connect_timeout_ms.unwrap_or(5000));
|
let timeout_duration = Duration::from_millis(config.connect_timeout_ms.unwrap_or(5000));
|
||||||
let connection = timeout(timeout_duration, connect(config)).await;
|
let connection = timeout(timeout_duration, connect_with_retries(config)).await;
|
||||||
|
|
||||||
let db = match connection {
|
let db = match connection {
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@ -98,7 +103,7 @@ pub async fn initialize(config: SurrealConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log::log("surreal", "DEBUG", "Applying SurrealDB schemas");
|
log::log("surreal", "DEBUG", "Applying SurrealDB schemas");
|
||||||
if let Err(error) = schema::apply_all(&db).await {
|
if let Err(error) = apply_schemas_with_retries(&db).await {
|
||||||
if !is_current_generation(generation) {
|
if !is_current_generation(generation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -159,6 +164,70 @@ async fn connect(config: SurrealConfig) -> Result<SurrealDb, String> {
|
|||||||
Ok(db)
|
Ok(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn connect_with_retries(config: SurrealConfig) -> Result<SurrealDb, String> {
|
||||||
|
let mut last_error = String::new();
|
||||||
|
|
||||||
|
for attempt in 1..=INIT_MAX_ATTEMPTS {
|
||||||
|
match connect(config.clone()).await {
|
||||||
|
Ok(db) => return Ok(db),
|
||||||
|
Err(error) => {
|
||||||
|
if !is_retryable_surreal_error(&error) || attempt == INIT_MAX_ATTEMPTS {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
last_error = error;
|
||||||
|
log::log(
|
||||||
|
"surreal",
|
||||||
|
"WARNING",
|
||||||
|
&format!(
|
||||||
|
"SurrealDB connection attempt {} failed with retryable error: {}",
|
||||||
|
attempt, last_error
|
||||||
|
),
|
||||||
|
);
|
||||||
|
sleep(init_retry_delay(attempt)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(last_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply_schemas_with_retries(db: &SurrealDb) -> Result<(), String> {
|
||||||
|
let mut last_error = String::new();
|
||||||
|
|
||||||
|
for attempt in 1..=INIT_MAX_ATTEMPTS {
|
||||||
|
match schema::apply_all(db).await {
|
||||||
|
Ok(()) => return Ok(()),
|
||||||
|
Err(error) => {
|
||||||
|
if !is_retryable_surreal_error(&error) || attempt == INIT_MAX_ATTEMPTS {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
last_error = error;
|
||||||
|
log::log(
|
||||||
|
"surreal",
|
||||||
|
"WARNING",
|
||||||
|
&format!(
|
||||||
|
"SurrealDB schema bootstrap attempt {} failed with retryable error: {}",
|
||||||
|
attempt, last_error
|
||||||
|
),
|
||||||
|
);
|
||||||
|
sleep(init_retry_delay(attempt)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(last_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_retryable_surreal_error(error: &str) -> bool {
|
||||||
|
error.contains("Transaction conflict") || error.contains("Resource busy")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_retry_delay(attempt: usize) -> Duration {
|
||||||
|
INIT_RETRY_BASE_DELAY * attempt as u32
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn client() -> Result<Arc<SurrealDb>, String> {
|
pub async fn client() -> Result<Arc<SurrealDb>, String> {
|
||||||
if let Some(db) = SURREAL_DB.read().unwrap().clone() {
|
if let Some(db) = SURREAL_DB.read().unwrap().clone() {
|
||||||
return Ok(db);
|
return Ok(db);
|
||||||
@ -203,6 +272,10 @@ pub fn status() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reconnect() -> String {
|
pub fn reconnect() -> String {
|
||||||
|
if *SURREAL_CONNECTION_STATE.read().unwrap() == SurrealConnectionState::Initializing {
|
||||||
|
return "reconnect skipped: connection already initializing".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
let surreal_config = config::load().surreal.clone();
|
let surreal_config = config::load().surreal.clone();
|
||||||
prepare();
|
prepare();
|
||||||
RUNTIME.spawn(async move {
|
RUNTIME.spawn(async move {
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
@echo off
|
@echo off
|
||||||
call "%~dp0UpdateMe.bat"
|
setlocal EnableExtensions
|
||||||
|
set "FORGE_SURREALDB_VERSION=%~1"
|
||||||
|
if not defined FORGE_SURREALDB_VERSION set "FORGE_SURREALDB_VERSION=3"
|
||||||
|
|
||||||
|
call "%~dp0UpdateMe.bat" "%FORGE_SURREALDB_VERSION%"
|
||||||
|
if errorlevel 1 exit /b %errorlevel%
|
||||||
call "%~dp0RunMe.bat"
|
call "%~dp0RunMe.bat"
|
||||||
|
|||||||
@ -10,12 +10,36 @@ firewall, TLS, backup, and upgrade policy before exposing the database.
|
|||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|
||||||
Install or update SurrealDB:
|
Install or update SurrealDB to the newest compatible SurrealDB 3.x release:
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
UpdateMe.bat
|
UpdateMe.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Install a specific SurrealDB release:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
UpdateMe.bat v3.1.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the latest stable SurrealDB release, including newer major versions:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
UpdateMe.bat latest
|
||||||
|
```
|
||||||
|
|
||||||
|
`latest` requires confirmation because a newer SurrealDB major version can
|
||||||
|
require rebuilding the Forge server extension from source with a compatible
|
||||||
|
`surrealdb` Rust crate.
|
||||||
|
|
||||||
|
The PowerShell entry point exposes the same behavior:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\UpdateSurrealDB.ps1
|
||||||
|
.\UpdateSurrealDB.ps1 -Version v3.1.2
|
||||||
|
.\UpdateSurrealDB.ps1 -Version latest
|
||||||
|
```
|
||||||
|
|
||||||
If this is the first install and the terminal cannot find `surreal` after the
|
If this is the first install and the terminal cannot find `surreal` after the
|
||||||
script finishes, open a new terminal so Windows reloads `PATH`.
|
script finishes, open a new terminal so Windows reloads `PATH`.
|
||||||
|
|
||||||
@ -25,12 +49,21 @@ Start Forge's local database:
|
|||||||
RunMe.bat
|
RunMe.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or start it directly with PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\RunSurrealDB.ps1
|
||||||
|
```
|
||||||
|
|
||||||
Install and start in one step:
|
Install and start in one step:
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
AllInOne.bat
|
AllInOne.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`AllInOne.bat` also defaults to the newest compatible SurrealDB 3.x release.
|
||||||
|
Pass the same version argument as `UpdateMe.bat` to override it.
|
||||||
|
|
||||||
## Linux or macOS
|
## Linux or macOS
|
||||||
|
|
||||||
Install SurrealDB:
|
Install SurrealDB:
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
@echo off
|
@echo off
|
||||||
cd /d "%~dp0"
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0RunSurrealDB.ps1"
|
||||||
surreal start --user root --pass root --bind 127.0.0.1:8000 rocksdb://forge.db
|
|
||||||
|
|||||||
16
arma/server/surrealdb/RunSurrealDB.ps1
Normal file
16
arma/server/surrealdb/RunSurrealDB.ps1
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
param(
|
||||||
|
[string]$User = "root",
|
||||||
|
[string]$Pass = "root",
|
||||||
|
[string]$Bind = "127.0.0.1:8000",
|
||||||
|
[string]$DatabasePath = "forge.db"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
Set-Location $PSScriptRoot
|
||||||
|
|
||||||
|
if (-not (Get-Command surreal -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "The 'surreal' command was not found. Run UpdateSurrealDB.ps1 first, then open a new terminal if PATH was updated."
|
||||||
|
}
|
||||||
|
|
||||||
|
surreal start --user $User --pass $Pass --bind $Bind "rocksdb://$DatabasePath"
|
||||||
@ -1,14 +1,10 @@
|
|||||||
@echo off
|
@echo off
|
||||||
where surreal >nul 2>nul
|
setlocal EnableExtensions
|
||||||
if %errorlevel% equ 0 (
|
set "DEFAULT_SURREALDB_VERSION=3"
|
||||||
surreal upgrade
|
set "TARGET_SURREALDB_VERSION=%~1"
|
||||||
surreal version
|
|
||||||
) else (
|
if not defined TARGET_SURREALDB_VERSION set "TARGET_SURREALDB_VERSION=%FORGE_SURREALDB_VERSION%"
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr https://windows.surrealdb.com -useb | iex"
|
if not defined TARGET_SURREALDB_VERSION set "TARGET_SURREALDB_VERSION=%DEFAULT_SURREALDB_VERSION%"
|
||||||
where surreal >nul 2>nul
|
|
||||||
if %errorlevel% equ 0 (
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0UpdateSurrealDB.ps1" -Version "%TARGET_SURREALDB_VERSION%"
|
||||||
surreal version
|
exit /b %errorlevel%
|
||||||
) else (
|
|
||||||
echo SurrealDB install finished. Open a new terminal if the surreal command is not available yet.
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|||||||
120
arma/server/surrealdb/UpdateSurrealDB.ps1
Normal file
120
arma/server/surrealdb/UpdateSurrealDB.ps1
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
param(
|
||||||
|
[string]$Version = "3",
|
||||||
|
[switch]$Force
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProgressPreference = "SilentlyContinue"
|
||||||
|
|
||||||
|
$VersionUrl = "https://version.surrealdb.com"
|
||||||
|
$DownloadBaseUrl = "https://download.surrealdb.com"
|
||||||
|
$Architecture = "windows-amd64"
|
||||||
|
|
||||||
|
function Normalize-Version {
|
||||||
|
param([string]$Value)
|
||||||
|
|
||||||
|
$trimmed = $Value.Trim()
|
||||||
|
if ($trimmed -match "(?i)^latest$") {
|
||||||
|
return "latest"
|
||||||
|
}
|
||||||
|
if ($trimmed -match "^v?\d+$") {
|
||||||
|
return $trimmed.TrimStart("v")
|
||||||
|
}
|
||||||
|
if ($trimmed -match "^v?\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$") {
|
||||||
|
return "v$($trimmed.TrimStart("v"))"
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Unsupported SurrealDB version '$Value'. Use a major version like '3', an exact version like 'v3.1.2', or 'latest'."
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-Latest-Version {
|
||||||
|
return (Invoke-WebRequest $VersionUrl -UseBasicParsing).Content.Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-Version {
|
||||||
|
param([string]$Target)
|
||||||
|
|
||||||
|
$normalized = Normalize-Version $Target
|
||||||
|
if ($normalized -eq "latest") {
|
||||||
|
return Get-Latest-Version
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalized -match "^\d+$") {
|
||||||
|
$latest = Get-Latest-Version
|
||||||
|
if ($latest -notmatch "^v?$normalized\.") {
|
||||||
|
throw "Latest SurrealDB is $latest, not $normalized.x. Pass an exact $normalized.x version or use 'latest' after confirming Forge compatibility."
|
||||||
|
}
|
||||||
|
|
||||||
|
return $latest
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
function Confirm-Latest {
|
||||||
|
if ($Force) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "WARNING: This will install the latest stable SurrealDB release, even if it is newer"
|
||||||
|
Write-Host "than the Forge server extension was compiled and tested against."
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "The Forge server extension currently targets SurrealDB 3.x. A newer major"
|
||||||
|
Write-Host "SurrealDB release can require rebuilding Forge from source with a compatible"
|
||||||
|
Write-Host "surrealdb Rust crate before the extension works correctly."
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$answer = Read-Host "Install latest SurrealDB anyway? [Y/N]"
|
||||||
|
if ($answer -notmatch "^(?i)y(es)?$") {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-Install-Path {
|
||||||
|
$existing = Get-Command surreal -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if ($null -ne $existing -and $existing.Source -and (Split-Path -Leaf $existing.Source) -ieq "surreal.exe") {
|
||||||
|
return $existing.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
$installDirectory = Join-Path $env:LOCALAPPDATA "SurrealDB"
|
||||||
|
New-Item -ItemType Directory -Force -Path $installDirectory | Out-Null
|
||||||
|
return Join-Path $installDirectory "surreal.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-User-Path {
|
||||||
|
param([string]$Directory)
|
||||||
|
|
||||||
|
$pathParts = $env:Path -split ";" | Where-Object { $_ }
|
||||||
|
if ($pathParts -notcontains $Directory) {
|
||||||
|
$env:Path = "$Directory;$env:Path"
|
||||||
|
}
|
||||||
|
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||||
|
$userPathParts = $userPath -split ";" | Where-Object { $_ }
|
||||||
|
if ($userPathParts -notcontains $Directory) {
|
||||||
|
$newUserPath = if ([string]::IsNullOrWhiteSpace($userPath)) { $Directory } else { "$Directory;$userPath" }
|
||||||
|
[Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
|
||||||
|
Write-Host "Added $Directory to the user PATH. Open a new terminal if 'surreal' is not found later."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedTarget = Normalize-Version $Version
|
||||||
|
if ($normalizedTarget -eq "latest") {
|
||||||
|
Confirm-Latest
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedVersion = Resolve-Version $Version
|
||||||
|
$installPath = Get-Install-Path
|
||||||
|
$installDirectory = Split-Path -Parent $installPath
|
||||||
|
New-Item -ItemType Directory -Force -Path $installDirectory | Out-Null
|
||||||
|
|
||||||
|
$downloadUrl = "$DownloadBaseUrl/$resolvedVersion/surreal-$resolvedVersion.$Architecture.exe"
|
||||||
|
$tempPath = Join-Path ([System.IO.Path]::GetTempPath()) "surreal-$resolvedVersion.$Architecture.exe"
|
||||||
|
|
||||||
|
Write-Host "Installing SurrealDB $resolvedVersion from $downloadUrl"
|
||||||
|
Invoke-WebRequest $downloadUrl -OutFile $tempPath -UseBasicParsing
|
||||||
|
Move-Item -Force -Path $tempPath -Destination $installPath
|
||||||
|
Ensure-User-Path $installDirectory
|
||||||
|
|
||||||
|
& $installPath version
|
||||||
@ -71,27 +71,27 @@ Common generated IDs:
|
|||||||
|
|
||||||
## Generated Mission Requests
|
## Generated Mission Requests
|
||||||
|
|
||||||
Dispatchers can request framework-generated mission tasks from the CAD
|
Dispatchers can request generated mission tasks from the CAD dispatcher board.
|
||||||
dispatcher board. The server hydrates the available generated task types from
|
The server hydrates the available generated task types from the selected task
|
||||||
the task mission manager as `generatedTaskTypes`; the client uses that hydrated
|
provider as `generatedTaskTypes`; the client uses that hydrated list for the
|
||||||
list for the dropdown.
|
dropdown.
|
||||||
|
|
||||||
Generated mission requests are controlled by the server CBA setting
|
Built-in generated mission requests are controlled by the server CBA setting
|
||||||
`forge_server_task_enableGenerator`:
|
`forge_server_task_enableGenerator`:
|
||||||
|
|
||||||
- Enabled: CAD receives the generated task type list and dispatchers can request
|
- Enabled: CAD can receive the built-in generated task type list and dispatchers
|
||||||
a specific generator type.
|
can request a specific built-in generator type.
|
||||||
- Disabled: CAD receives an empty generated task type list, the task request UI
|
- Disabled: the built-in provider returns no task types and rejects built-in
|
||||||
is disabled, and server-side request handling rejects any manual request.
|
manual requests.
|
||||||
|
|
||||||
The framework-owned request entry point is
|
Server CAD routes generated mission requests through the task provider registry.
|
||||||
`forge_server_task_fnc_requestMissionTask`. Server CAD uses this framework
|
The selected provider handles the request and returns the CAD response payload.
|
||||||
handler directly; it does not call mission-local generator functions.
|
|
||||||
|
|
||||||
Custom mission generators can still create CAD-visible tasks directly by
|
Custom mission generators can register a provider with the
|
||||||
registering task catalog entries and task statuses. See
|
`forge_server_task_registerMissionGeneratorProvider` CBA server event or create
|
||||||
[Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md) for the supported
|
CAD-visible tasks directly by registering task catalog entries and task
|
||||||
integration path and the current generated-task provider limitation.
|
statuses. See [Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md) for
|
||||||
|
the supported integration path.
|
||||||
|
|
||||||
## Submit a Support Request
|
## Submit a Support Request
|
||||||
|
|
||||||
|
|||||||
@ -104,13 +104,13 @@ The dispatcher-generated task dropdown is hydrated from the server
|
|||||||
`generatedTaskTypes` payload. The UI has a built-in fallback list for loading or
|
`generatedTaskTypes` payload. The UI has a built-in fallback list for loading or
|
||||||
older payload compatibility, but any hydrate payload that includes
|
older payload compatibility, but any hydrate payload that includes
|
||||||
`generatedTaskTypes` replaces that fallback. An empty hydrated list disables the
|
`generatedTaskTypes` replaces that fallback. An empty hydrated list disables the
|
||||||
request control, which is how `forge_server_task_enableGenerator = false` is surfaced
|
request control. For the built-in provider, this is how
|
||||||
client-side.
|
`forge_server_task_enableGenerator = false` is surfaced client-side.
|
||||||
|
|
||||||
Custom mission generators can still publish tasks into CAD by using the server
|
Custom mission generators can publish tasks into CAD by using the server task
|
||||||
task catalog. The generated-task dropdown itself currently needs a framework
|
catalog or by registering a task provider that supplies `generatedTaskTypes` and
|
||||||
provider extension point before custom providers can replace the built-in list
|
handles generated task requests. See
|
||||||
cleanly. See [Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md).
|
[Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md).
|
||||||
|
|
||||||
## Authorization Notes
|
## Authorization Notes
|
||||||
|
|
||||||
|
|||||||
@ -5,9 +5,8 @@ foundation that communities build on top of. Custom mission generators should
|
|||||||
integrate through the same task, CAD, and event surfaces that the built-in
|
integrate through the same task, CAD, and event surfaces that the built-in
|
||||||
mission manager uses.
|
mission manager uses.
|
||||||
|
|
||||||
This guide documents the supported integration path today and calls out the
|
This guide documents the supported integration path for custom generators,
|
||||||
current CAD generated-task provider limitation that should be addressed by a
|
including the provider registry used by CAD/manual generated task requests.
|
||||||
small framework extension point.
|
|
||||||
|
|
||||||
## Recommended Architecture
|
## Recommended Architecture
|
||||||
|
|
||||||
@ -35,13 +34,24 @@ forge_server_task_enableGenerator = false;
|
|||||||
When disabled, Forge does not run timer-based generated missions and CAD
|
When disabled, Forge does not run timer-based generated missions and CAD
|
||||||
hydrates no built-in generated task types.
|
hydrates no built-in generated task types.
|
||||||
|
|
||||||
This does not prevent custom code from creating CAD-visible tasks directly.
|
This does not prevent custom code from creating CAD-visible tasks directly or
|
||||||
It only disables the built-in generator request list and the framework-owned
|
from serving CAD/manual generated task requests through a registered custom
|
||||||
manual request entry point.
|
provider.
|
||||||
|
|
||||||
The mission setup UI does not override this setting. Generated mission
|
The mission setup UI does not override this setting. Generated mission
|
||||||
enablement is mission/server policy and should stay in CBA settings until a
|
enablement for the built-in provider is mission/server policy and stays in CBA
|
||||||
provider selection extension point exists.
|
settings.
|
||||||
|
|
||||||
|
The mission setup UI can capture a generator provider preference:
|
||||||
|
|
||||||
|
- `builtin` for Forge's built-in generated mission provider
|
||||||
|
- `custom` for mission/community-owned generated mission providers
|
||||||
|
|
||||||
|
That preference is stored in `forge_server_task_generatorProvider` and mirrored
|
||||||
|
inside `forge_server_task_missionSetup_settings`. It is intentionally separate
|
||||||
|
from `forge_server_task_enableGenerator`; the CBA setting only gates Forge's
|
||||||
|
built-in provider. A registered custom provider can still publish generated task
|
||||||
|
types and handle CAD/manual requests when selected.
|
||||||
|
|
||||||
## Framework Mission Setup UI
|
## Framework Mission Setup UI
|
||||||
|
|
||||||
@ -68,6 +78,7 @@ missionNamespace setVariable [
|
|||||||
The UI configures:
|
The UI configures:
|
||||||
|
|
||||||
- opposing faction
|
- opposing faction
|
||||||
|
- generator provider preference
|
||||||
- max concurrent generated missions
|
- max concurrent generated missions
|
||||||
- mission interval
|
- mission interval
|
||||||
- location reuse cooldown
|
- location reuse cooldown
|
||||||
@ -100,6 +111,75 @@ actor interaction entry is hidden once clients receive the public applied flag,
|
|||||||
and direct or stale open requests receive a notification explaining that setup
|
and direct or stale open requests receive a notification explaining that setup
|
||||||
has already been applied.
|
has already been applied.
|
||||||
|
|
||||||
|
## Provider Registry
|
||||||
|
|
||||||
|
Custom providers register on the server through the server-side CBA event:
|
||||||
|
|
||||||
|
```sqf
|
||||||
|
[
|
||||||
|
"forge_server_task_registerMissionGeneratorProvider",
|
||||||
|
["custom", _provider]
|
||||||
|
] call CBA_fnc_serverEvent;
|
||||||
|
```
|
||||||
|
|
||||||
|
This event is intentionally fire-and-forget. The task module validates provider
|
||||||
|
shape server-side and logs registration failures.
|
||||||
|
|
||||||
|
The provider is a hashMap/hashMapObject with two required methods:
|
||||||
|
|
||||||
|
| Method | Arguments | Return |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `getGeneratedTaskTypes` | none | Array of hashMaps with `value` and `label` |
|
||||||
|
| `requestMissionTask` | `_taskType`, `_metadata`, `_requesterUid` | Result hashMap |
|
||||||
|
|
||||||
|
The request result should include:
|
||||||
|
|
||||||
|
| Key | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `success` | Boolean | `true` when a task was generated |
|
||||||
|
| `message` | String | User-facing CAD response |
|
||||||
|
| `taskID` | String | Created task ID, or empty on failure |
|
||||||
|
| `taskType` | String | Resolved generated task type |
|
||||||
|
|
||||||
|
Example provider:
|
||||||
|
|
||||||
|
```sqf
|
||||||
|
private _provider = createHashMapObject [[
|
||||||
|
["#type", "CommunityMissionGeneratorProvider"],
|
||||||
|
["getGeneratedTaskTypes", {
|
||||||
|
[
|
||||||
|
createHashMapFromArray [["value", "pvp_hold"], ["label", "PvP Hold Area"]],
|
||||||
|
createHashMapFromArray [["value", "supply_drop"], ["label", "Supply Drop"]]
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
["requestMissionTask", {
|
||||||
|
params ["_taskType", "_metadata", "_requesterUid"];
|
||||||
|
|
||||||
|
private _taskID = format ["custom_%1_%2", _taskType, floor random 100000];
|
||||||
|
|
||||||
|
// Create/spawn the mission here, then publish it through Forge's task
|
||||||
|
// catalog/status contract so CAD can assign and track it.
|
||||||
|
|
||||||
|
createHashMapFromArray [
|
||||||
|
["success", true],
|
||||||
|
["message", format ["Generated custom %1 task %2.", _taskType, _taskID]],
|
||||||
|
["taskID", _taskID],
|
||||||
|
["taskType", _taskType]
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
]];
|
||||||
|
|
||||||
|
[
|
||||||
|
"forge_server_task_registerMissionGeneratorProvider",
|
||||||
|
["custom", _provider]
|
||||||
|
] call CBA_fnc_serverEvent;
|
||||||
|
```
|
||||||
|
|
||||||
|
When the setup UI provider toggle is set to `custom`, CAD hydrates task types
|
||||||
|
from the registered `custom` provider and CAD/manual requests call that
|
||||||
|
provider's `requestMissionTask` method. If no custom provider is registered,
|
||||||
|
Forge logs a warning and falls back to the built-in provider.
|
||||||
|
|
||||||
## CAD-Visible Task Contract
|
## CAD-Visible Task Contract
|
||||||
|
|
||||||
CAD reads assignable contracts from `TaskStore.getActiveTaskCatalog`. A custom
|
CAD reads assignable contracts from `TaskStore.getActiveTaskCatalog`. A custom
|
||||||
@ -290,78 +370,33 @@ when relevant changes occur. Custom generators usually only need to emit task
|
|||||||
status changes through TaskStore or extension commands; CAD refresh follows
|
status changes through TaskStore or extension commands; CAD refresh follows
|
||||||
from the existing listeners.
|
from the existing listeners.
|
||||||
|
|
||||||
## Generated Task Dropdown Limitation
|
## Generated Task Provider Behavior
|
||||||
|
|
||||||
The current CAD generated-task dropdown is owned by the framework task mission
|
CAD hydrates generated task types and requests generated tasks through the task
|
||||||
manager. CAD hydrates `generatedTaskTypes` from the built-in manager when
|
provider registry. The selected provider comes from
|
||||||
`forge_server_task_enableGenerator` is enabled. When that setting is disabled,
|
`forge_server_task_generatorProvider`, defaulting to `builtin`.
|
||||||
the generated-task request control is disabled.
|
|
||||||
|
|
||||||
The current CAD request handler calls `forge_server_task_fnc_requestMissionTask`
|
Use one of these supported patterns:
|
||||||
directly. It no longer falls back to mission-local generator request functions,
|
|
||||||
so third-party generated-task providers should create CAD-visible tasks directly
|
|
||||||
until a framework provider extension point is added.
|
|
||||||
|
|
||||||
Until a provider extension point is added, use one of these supported patterns:
|
1. Register a custom provider so CAD/manual generated task requests route to
|
||||||
|
community code.
|
||||||
1. Run custom generators from mission/server code and create CAD-visible tasks
|
2. Run custom generators from mission/server code and create CAD-visible tasks
|
||||||
directly.
|
directly.
|
||||||
2. Use CAD support requests or dispatch orders to let players request custom
|
3. Use CAD support requests or dispatch orders to let players request custom
|
||||||
work, then have mission code convert approved requests into tasks.
|
work, then have mission code convert approved requests into tasks.
|
||||||
3. Keep the built-in generator enabled only if the community intentionally
|
4. Keep the built-in generator enabled only if the community intentionally
|
||||||
wants the framework dropdown and request handler.
|
wants the framework dropdown and request handler.
|
||||||
|
|
||||||
## Planned Provider Extension Point
|
## Provider Extension Details
|
||||||
|
|
||||||
A future code change should make CAD generator providers explicit. The desired
|
The implemented provider shape is intentionally small:
|
||||||
shape is:
|
|
||||||
|
|
||||||
- built-in Forge provider remains the default out-of-box behavior
|
- built-in Forge provider remains the default out-of-box behavior
|
||||||
- mission/community providers can supply their own `generatedTaskTypes`
|
- mission/community providers can supply their own `generatedTaskTypes`
|
||||||
- mission/community providers can handle generated-task requests
|
- mission/community providers can handle generated-task requests
|
||||||
- disabling the built-in provider does not disable custom providers
|
- disabling the built-in provider does not disable custom providers
|
||||||
- mission designers or developers can select or toggle the active generator
|
- mission designers or developers can select or toggle the active provider from
|
||||||
provider when a mission includes custom generators
|
the framework mission setup UI when a mission includes custom generators
|
||||||
- a framework-hosted mission setup UI can display the active provider and, when
|
|
||||||
supported by the mission, allow choosing between built-in and custom
|
|
||||||
providers
|
|
||||||
|
|
||||||
Candidate SQF hooks:
|
|
||||||
|
|
||||||
```sqf
|
|
||||||
forge_custom_fnc_getGeneratedTaskTypes
|
|
||||||
forge_custom_fnc_requestMissionTask
|
|
||||||
```
|
|
||||||
|
|
||||||
or mission namespace variables:
|
|
||||||
|
|
||||||
```sqf
|
|
||||||
missionNamespace setVariable ["forge_generatorProvider_getTypes", {
|
|
||||||
[
|
|
||||||
createHashMapFromArray [["value", "supply_drop"], ["label", "Supply Drop"]],
|
|
||||||
createHashMapFromArray [["value", "pvp_hold"], ["label", "PvP Hold Area"]]
|
|
||||||
]
|
|
||||||
}];
|
|
||||||
|
|
||||||
missionNamespace setVariable ["forge_generatorProvider_requestTask", {
|
|
||||||
params ["_taskType", "_metadata", "_requesterUid"];
|
|
||||||
createHashMapFromArray [
|
|
||||||
["success", true],
|
|
||||||
["message", "Generated custom task."],
|
|
||||||
["taskID", "custom_task_01"],
|
|
||||||
["taskType", _taskType]
|
|
||||||
]
|
|
||||||
}];
|
|
||||||
```
|
|
||||||
|
|
||||||
The exact API should be implemented in the framework code before communities
|
|
||||||
depend on it.
|
|
||||||
|
|
||||||
Implementation note: the provider selection should be separate from
|
|
||||||
`forge_server_task_enableGenerator`. That CBA setting should continue to gate
|
|
||||||
the built-in Forge generator, while a new provider option can decide whether
|
|
||||||
CAD/manual requests use the built-in provider, a custom provider, both, or no
|
|
||||||
provider at all.
|
|
||||||
|
|
||||||
## Validation Checklist
|
## Validation Checklist
|
||||||
|
|
||||||
|
|||||||
@ -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
|
player's organization, and fills the vehicle only after the organization charge
|
||||||
succeeds.
|
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 `CfgServicePricing >> fuelCost` is used.
|
||||||
|
|
||||||
## Repair
|
## Repair
|
||||||
|
|
||||||
Repair is organization-funded.
|
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 client garage UI forwards selected nearby vehicle repair requests through
|
||||||
the same event.
|
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
|
||||||
|
|
||||||
Rearm is organization-funded.
|
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 client garage UI forwards selected nearby vehicle rearm requests through
|
||||||
the same event.
|
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
|
||||||
|
|
||||||
Medical is player-funded first.
|
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
|
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.
|
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
|
||||||
|
`CfgServicePricing`.
|
||||||
|
|
||||||
## Medical Debt Repayment
|
## Medical Debt Repayment
|
||||||
|
|
||||||
Medical fallback debt uses the existing organization credit-line repayment
|
Medical fallback debt uses the existing organization credit-line repayment
|
||||||
|
|||||||
@ -164,9 +164,13 @@ airports, bus stops, teleport terminals, or any other mission transport system.
|
|||||||
The framework owns the menu, billing, cargo scan, and movement logic. The
|
The framework owns the menu, billing, cargo scan, and movement logic. The
|
||||||
mission only needs placed objects and optional arrival markers.
|
mission only needs placed objects and optional arrival markers.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Place transport node objects with these variable names:
|
Place transport node objects with these variable names:
|
||||||
|
|
||||||
@ -188,7 +192,9 @@ transport_arrival_2
|
|||||||
transport_arrival_10
|
transport_arrival_10
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Objects that should be excluded from the nearby cargo scan, such as the actual
|
Objects that should be excluded from the nearby cargo scan, such as the actual
|
||||||
boat or transport vehicle used as set dressing, should use:
|
boat or transport vehicle used as set dressing, should use:
|
||||||
@ -201,7 +207,9 @@ transport_vehicle_2
|
|||||||
transport_vehicle_10
|
transport_vehicle_10
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Minimum Eden setup:
|
Minimum Eden setup:
|
||||||
|
|
||||||
@ -754,19 +762,31 @@ CAD dispatcher-requested generation.
|
|||||||
|
|
||||||
The optional framework mission setup UI lets the setup operator choose runtime
|
The optional framework mission setup UI lets the setup operator choose runtime
|
||||||
tuning such as opposing faction, mission cap, interval, location cooldown,
|
tuning such as opposing faction, mission cap, interval, location cooldown,
|
||||||
reward ranges, reputation ranges, penalty ranges, and time limits. It does not
|
reward ranges, reputation ranges, penalty ranges, time limits, and a generator
|
||||||
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.
|
||||||
|
|
||||||
|
Task time limits can be disabled from the setup UI by turning off the task
|
||||||
|
timer. That stores `timeLimitMin = 0` and `timeLimitMax = 0`, which generated
|
||||||
|
tasks treat as no timer. Positive min/max values enable task timers and are
|
||||||
|
rolled in seconds.
|
||||||
|
|
||||||
If mission setup is enabled, the mission manager waits until the setup operator
|
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
|
applies settings. Cancel, X, and Escape apply default values from CBA, mission
|
||||||
parameters, and `CfgMissions`. There is no timeout that auto-applies defaults.
|
parameters, and `CfgMissions`. There is no timeout that auto-applies defaults.
|
||||||
After settings are applied, the setup UI cannot be reopened.
|
After settings are applied, the setup UI cannot be reopened.
|
||||||
|
|
||||||
Future custom-generator support should add an explicit provider option so
|
Service pricing fallback values live in mission-local `CfgServicePricing.hpp`.
|
||||||
mission designers or developers can select or toggle a mission/community-owned
|
Mission `Params` with matching names, such as `medicalHealCost`,
|
||||||
generator without relying on mission-local fallback functions. Until then,
|
`serviceRepairCost`, `serviceRearmCost`, `fuelCost`, `transportBaseFare`, and
|
||||||
custom generators should create CAD-visible tasks directly through the task
|
`transportPricePerKm`, are read before the setup UI hydrates so mission makers
|
||||||
catalog/status contract described in
|
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
|
||||||
|
tasks directly through the task catalog/status contract described in
|
||||||
[Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md).
|
[Custom Mission Generators](./CUSTOM_MISSION_GENERATORS.md).
|
||||||
|
|
||||||
The dynamic mission generator avoids rectangle and ellipse area markers whose
|
The dynamic mission generator avoids rectangle and ellipse area markers whose
|
||||||
|
|||||||
@ -80,6 +80,25 @@ New owned garages are created with default unlocks from the Rust model.
|
|||||||
| `owned:garage:delete` | `uid` | `OK`. |
|
| `owned:garage:delete` | `uid` | `OK`. |
|
||||||
| `owned:garage:exists` | `uid` | `true` or `false`. |
|
| `owned:garage:exists` | `uid` | `true` or `false`. |
|
||||||
|
|
||||||
|
## Starting Equipment And Unlocks
|
||||||
|
|
||||||
|
Missions can include `CfgStartingEquipment.hpp` from `description.ext` to set
|
||||||
|
new-player loadout and initial virtual arsenal or garage unlocks without
|
||||||
|
recompiling the framework.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "CfgStartingEquipment.hpp"
|
||||||
|
```
|
||||||
|
|
||||||
|
`loadout[]` uses the standard Arma loadout array shape. `Unlocks.Locker`
|
||||||
|
supports `items[]`, `weapons[]`, `magazines[]`, and `backpacks[]`.
|
||||||
|
`Unlocks.Garage` supports `cars[]`, `armor[]`, `helis[]`, `planes[]`,
|
||||||
|
`naval[]`, and `other[]`.
|
||||||
|
|
||||||
|
The extension defaults are intentionally empty. The server seeds mission
|
||||||
|
starting unlocks only when a player does not already have persistent owned
|
||||||
|
locker or garage records.
|
||||||
|
|
||||||
## Add Virtual Arsenal Unlocks
|
## Add Virtual Arsenal Unlocks
|
||||||
|
|
||||||
```sqf
|
```sqf
|
||||||
|
|||||||
@ -227,7 +227,7 @@ Player workflow:
|
|||||||
1. Stand near a transport point.
|
1. Stand near a transport point.
|
||||||
2. Open the actor interaction menu.
|
2. Open the actor interaction menu.
|
||||||
3. Select Transport.
|
3. Select Transport.
|
||||||
4. Select a destination from the transport submenu, or select Back to return
|
4. Select a destination from the transport submenu, or select Close to return
|
||||||
to the default interaction menu.
|
to the default interaction menu.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
The store module processes checkout requests. It charges a payment source and
|
The store module processes checkout requests. It charges a payment source and
|
||||||
grants purchased items to the player locker, virtual arsenal locker, and
|
grants purchased items to the player locker, virtual arsenal locker, and
|
||||||
virtual garage unlocks.
|
virtual garage unlocks. Unit purchases are fulfilled as immediate server-side
|
||||||
|
spawn grants at discovered `unit_spawn` markers.
|
||||||
|
|
||||||
## Server SQF Module
|
## Server SQF Module
|
||||||
|
|
||||||
@ -20,6 +21,90 @@ post-init. The initializer matches non-null mission namespace objects whose
|
|||||||
variable names contain `store` and sets `isStore = true`, following the same
|
variable names contain `store` and sets `isStore = true`, following the same
|
||||||
pattern used by garage entities.
|
pattern used by garage entities.
|
||||||
|
|
||||||
|
## Mission Catalog Filter
|
||||||
|
|
||||||
|
The store catalog is generated from loaded Arma config classes, then an
|
||||||
|
optional mission `CfgStore` filter can allow or deny classnames per category.
|
||||||
|
Include `CfgStore.hpp` from `description.ext`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "CfgStore.hpp"
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class CfgStore {
|
||||||
|
mode = "allowlist"; // dynamic, allowlist, or denylist
|
||||||
|
modMode = "dynamic"; // dynamic, allowlist, or denylist
|
||||||
|
mods[] = {}; // ModSources child 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"};
|
||||||
|
cars[] = {"B_MRAP_01_F"};
|
||||||
|
units[] = {"B_Soldier_F"};
|
||||||
|
};
|
||||||
|
|
||||||
|
class Overrides {
|
||||||
|
class arifle_MX_F {
|
||||||
|
price = 2500;
|
||||||
|
displayName = "MX Rifle";
|
||||||
|
description = "Approved PMC service rifle.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`dynamic` keeps the full generated catalog. `allowlist` only shows classnames
|
||||||
|
listed for each category. `denylist` removes listed classnames. Overrides are
|
||||||
|
server-side and are used by both the UI payload and checkout validation.
|
||||||
|
`units[]` uses the same filter behavior as every other category.
|
||||||
|
|
||||||
|
`modMode` applies before category filtering. `dynamic` means no mod-source
|
||||||
|
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 exact config source addon/source mod names, and `prefixes[]` for classname,
|
||||||
|
source addon, or source mod prefixes. If a mod source defines no patches, it is
|
||||||
|
treated as available and only the source/prefix checks are used.
|
||||||
|
|
||||||
|
For example, to show only RHS-sourced generated inventory:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
modMode = "allowlist";
|
||||||
|
mods[] = {"rhs"};
|
||||||
|
```
|
||||||
|
|
||||||
|
The matching `class rhs` must exist under `ModSources`. Category `mode` is still
|
||||||
|
applied afterward, so leave `mode = "dynamic"` if the mod filter should be the
|
||||||
|
only inventory filter.
|
||||||
|
|
||||||
|
The current filter is global for the mission. Revisit per-store profile support
|
||||||
|
if individual vendors need different inventories.
|
||||||
|
|
||||||
|
## Unit Spawn Markers
|
||||||
|
|
||||||
|
Purchased units spawn at mission markers named `unit_spawn`, `unit_spawn_1`,
|
||||||
|
`unit_spawn_2`, and so on. The store resolves the closest initialized store
|
||||||
|
object to the requesting player, scans `allMapMarkers` when checkout fulfillment
|
||||||
|
runs, and uses the closest matching marker within 25 meters of that store.
|
||||||
|
|
||||||
|
If no matching marker exists within 25 meters, the store falls back to spawning
|
||||||
|
units around the store object. If no store object can be resolved, it falls back
|
||||||
|
to the requesting player.
|
||||||
|
|
||||||
## Checkout Model
|
## Checkout Model
|
||||||
|
|
||||||
`store:checkout` accepts one JSON context.
|
`store:checkout` accepts one JSON context.
|
||||||
@ -45,6 +130,13 @@ pattern used by garage entities.
|
|||||||
"category": "cars",
|
"category": "cars",
|
||||||
"priceValue": 1500
|
"priceValue": 1500
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"classname": "B_Soldier_F",
|
||||||
|
"category": "units",
|
||||||
|
"priceValue": 2500
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -52,12 +144,13 @@ pattern used by garage entities.
|
|||||||
Rules validated by the Rust service:
|
Rules validated by the Rust service:
|
||||||
|
|
||||||
- `requesterUid` is required.
|
- `requesterUid` is required.
|
||||||
- At least one item or vehicle is required.
|
- At least one item, vehicle, or unit is required.
|
||||||
- The checkout total must be greater than zero.
|
- The checkout total must be greater than zero.
|
||||||
- Item categories must be `item`, `attachment`, `weapon`, `magazine`, or
|
- Item categories must be `item`, `attachment`, `weapon`, `magazine`, or
|
||||||
`backpack`.
|
`backpack`.
|
||||||
- Vehicle categories must be `cars`, `armor`, `helis`, `planes`, `naval`, or
|
- Vehicle categories must be `cars`, `armor`, `helis`, `planes`, `naval`, or
|
||||||
`other`.
|
`other`.
|
||||||
|
- Unit categories must be `units` or `unit`.
|
||||||
- Payment method must be `cash`, `bank`, `org_funds`, or `credit_line`.
|
- Payment method must be `cash`, `bank`, `org_funds`, or `credit_line`.
|
||||||
- Player locker capacity cannot exceed 25 unique items after checkout.
|
- Player locker capacity cannot exceed 25 unique items after checkout.
|
||||||
- Organization funds can only be charged by the org owner or the default org
|
- Organization funds can only be charged by the org owner or the default org
|
||||||
@ -73,11 +166,12 @@ Rules validated by the Rust service:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"chargedTotal": 2000.0,
|
"chargedTotal": 4500.0,
|
||||||
"paymentMethod": "bank",
|
"paymentMethod": "bank",
|
||||||
"message": "Checkout completed. $2,000 charged, 1 locker grant(s), 1 vehicle unlock(s).",
|
"message": "Checkout completed. $4,500 charged, 1 locker grant(s), 1 vehicle unlock(s), 1 unit grant(s).",
|
||||||
"lockerGranted": [],
|
"lockerGranted": [],
|
||||||
"vehicleGranted": [],
|
"vehicleGranted": [],
|
||||||
|
"unitGranted": [],
|
||||||
"lockerPatch": {},
|
"lockerPatch": {},
|
||||||
"vaPatch": {},
|
"vaPatch": {},
|
||||||
"vgaragePatch": {},
|
"vgaragePatch": {},
|
||||||
@ -108,7 +202,8 @@ private _checkout = createHashMapFromArray [
|
|||||||
["requesterIsDefaultOrgCeo", false],
|
["requesterIsDefaultOrgCeo", false],
|
||||||
["paymentMethod", "bank"],
|
["paymentMethod", "bank"],
|
||||||
["items", [_item]],
|
["items", [_item]],
|
||||||
["vehicles", []]
|
["vehicles", []],
|
||||||
|
["units", []]
|
||||||
];
|
];
|
||||||
|
|
||||||
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
|
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
|
||||||
@ -133,7 +228,8 @@ private _checkout = createHashMapFromArray [
|
|||||||
["requesterIsDefaultOrgCeo", false],
|
["requesterIsDefaultOrgCeo", false],
|
||||||
["paymentMethod", "org_funds"],
|
["paymentMethod", "org_funds"],
|
||||||
["items", []],
|
["items", []],
|
||||||
["vehicles", [_vehicle]]
|
["vehicles", [_vehicle]],
|
||||||
|
["units", []]
|
||||||
];
|
];
|
||||||
|
|
||||||
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
|
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
|
||||||
|
|||||||
@ -44,6 +44,30 @@ cd arma/server/surrealdb
|
|||||||
.\RunMe.bat
|
.\RunMe.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On Windows, `UpdateMe.bat` is a wrapper around `UpdateSurrealDB.ps1`. By
|
||||||
|
default it installs or updates to the newest compatible SurrealDB 3.x release
|
||||||
|
reported by SurrealDB's official version endpoint. You can also pin an exact
|
||||||
|
release:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\UpdateMe.bat v3.1.2
|
||||||
|
.\UpdateSurrealDB.ps1 -Version v3.1.2
|
||||||
|
```
|
||||||
|
|
||||||
|
To intentionally install the latest stable SurrealDB release regardless of
|
||||||
|
major version, run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\UpdateMe.bat latest
|
||||||
|
```
|
||||||
|
|
||||||
|
The `latest` option prompts for confirmation because a newer SurrealDB major
|
||||||
|
version can require rebuilding the Forge server extension from source with a
|
||||||
|
compatible `surrealdb` Rust crate.
|
||||||
|
|
||||||
|
`RunMe.bat` is a wrapper around `RunSurrealDB.ps1`, which starts the local
|
||||||
|
Forge database with the same defaults shown below.
|
||||||
|
|
||||||
On Linux or macOS:
|
On Linux or macOS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -188,19 +188,27 @@ server-side.
|
|||||||
|
|
||||||
The mission setup UI does not enable or disable generated missions. It applies
|
The mission setup UI does not enable or disable generated missions. It applies
|
||||||
runtime tuning such as faction, caps, intervals, reward ranges, rating ranges,
|
runtime tuning such as faction, caps, intervals, reward ranges, rating ranges,
|
||||||
penalties, and time limits. Generator enablement remains controlled by the CBA
|
penalties, time limits, service pricing, and a generator provider preference.
|
||||||
setting above.
|
Generator enablement remains controlled by the CBA setting above.
|
||||||
|
|
||||||
When `forge_server_task_enableMissionSetup` is enabled, the mission manager
|
When `forge_server_task_enableMissionSetup` is enabled, the mission manager
|
||||||
waits for setup settings before starting. There is no timeout auto-apply.
|
waits for setup settings before starting. There is no timeout auto-apply.
|
||||||
Pressing Cancel, X, or Escape applies default values from CBA, mission
|
Pressing Cancel, X, or Escape applies default values from CBA, mission
|
||||||
parameters, and `CfgMissions`.
|
parameters, and `CfgMissions`.
|
||||||
|
|
||||||
Planned custom-generator work should add an explicit provider option for
|
Service price defaults are stored in `CfgServicePricing`. Mission
|
||||||
mission designers or developers who want to select or toggle a custom mission
|
`Params` with matching names override those defaults before the UI opens, and
|
||||||
generator. That provider option should be separate from the built-in generator
|
submitted UI values override both. The supported names are
|
||||||
CBA gate so disabling Forge's built-in generator does not prevent custom
|
`medicalSpawnCost`, `medicalHealCost`, `serviceRepairCost`,
|
||||||
providers from publishing CAD-visible work.
|
`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
|
||||||
|
provider. That provider option stays separate from the built-in generator CBA
|
||||||
|
gate so disabling Forge's built-in generator does not prevent custom providers
|
||||||
|
from publishing CAD-visible work.
|
||||||
|
|
||||||
## CAD Compatibility
|
## CAD Compatibility
|
||||||
|
|
||||||
@ -623,6 +631,10 @@ Task time limits use `0` for no limit:
|
|||||||
Positive values are measured in seconds. Do not pass `-1` as a no-limit value;
|
Positive values are measured in seconds. Do not pass `-1` as a no-limit value;
|
||||||
the task runtime treats any non-zero task time limit as active.
|
the task runtime treats any non-zero task time limit as active.
|
||||||
|
|
||||||
|
The mission setup UI uses the same rule. Turning off the task timer stores
|
||||||
|
`timeLimitMin = 0` and `timeLimitMax = 0`; turning it on uses the configured
|
||||||
|
positive min/max range for generated tasks.
|
||||||
|
|
||||||
Defuse IED timers are different. `iedTimer` must be greater than `0`, because
|
Defuse IED timers are different. `iedTimer` must be greater than `0`, because
|
||||||
IEDs are expected to have an active countdown. The Eden defuse module defaults
|
IEDs are expected to have an active countdown. The Eden defuse module defaults
|
||||||
to `300` seconds.
|
to `300` seconds.
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
The transport service provides paid point-to-point travel for players and
|
The transport service provides paid point-to-point travel for players and
|
||||||
nearby vehicles or passengers. It is framework-owned: missions only need placed
|
nearby vehicles or passengers. It is framework-owned: missions only need placed
|
||||||
transport objects and arrival markers with the expected variable names.
|
transport objects and optional arrival markers with the expected variable names.
|
||||||
|
|
||||||
## Mission Contract
|
## Mission Contract
|
||||||
|
|
||||||
@ -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
|
Use `transport_vehicle*` names for the actual boat, ferry, aircraft, or set
|
||||||
dressing object that should not be moved as cargo.
|
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, `CfgServicePricing` provides the fallback.
|
||||||
|
|
||||||
## Optional Per-Node Overrides
|
## Optional Per-Node Overrides
|
||||||
|
|
||||||
The default naming convention should cover normal missions. If a specific
|
The default naming convention should cover normal missions. If a specific
|
||||||
@ -108,11 +114,12 @@ this setVariable ["transportCargoRadius", 25, true];
|
|||||||
this setVariable ["transportIncludeCargo", true, 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
|
## Image Checklist
|
||||||
|
|
||||||
These screenshots show the default transport setup and player workflow:
|
Replace these placeholder image references after screenshots are captured:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -135,3 +142,9 @@ These screenshots show the default transport setup and player workflow:
|
|||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Mission-Side Code Requirement
|
||||||
|
|
||||||
|
No mission-side transport service, addAction script, or server event bridge is
|
||||||
|
required. The framework handles menu discovery, destination selection, pricing,
|
||||||
|
billing, cargo movement, and EventBus notifications.
|
||||||
|
|||||||
@ -44,6 +44,30 @@ cd arma/server/surrealdb
|
|||||||
.\RunMe.bat
|
.\RunMe.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On Windows, `UpdateMe.bat` is a wrapper around `UpdateSurrealDB.ps1`. By
|
||||||
|
default it installs or updates to the newest compatible SurrealDB 3.x release
|
||||||
|
reported by SurrealDB's official version endpoint. You can also pin an exact
|
||||||
|
release:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\UpdateMe.bat v3.1.2
|
||||||
|
.\UpdateSurrealDB.ps1 -Version v3.1.2
|
||||||
|
```
|
||||||
|
|
||||||
|
To intentionally install the latest stable SurrealDB release regardless of
|
||||||
|
major version, run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\UpdateMe.bat latest
|
||||||
|
```
|
||||||
|
|
||||||
|
The `latest` option prompts for confirmation because a newer SurrealDB major
|
||||||
|
version can require rebuilding the Forge server extension from source with a
|
||||||
|
compatible `surrealdb` Rust crate.
|
||||||
|
|
||||||
|
`RunMe.bat` is a wrapper around `RunSurrealDB.ps1`, which starts the local
|
||||||
|
Forge database with the same defaults shown below.
|
||||||
|
|
||||||
On Linux or macOS:
|
On Linux or macOS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -79,8 +79,8 @@ npm run build:webui
|
|||||||
title: Custom Mission Generators
|
title: Custom Mission Generators
|
||||||
to: /getting-started/custom-mission-generators
|
to: /getting-started/custom-mission-generators
|
||||||
---
|
---
|
||||||
Create CAD-visible custom generated missions and understand the current
|
Create CAD-visible custom generated missions and register custom generator
|
||||||
provider extension point.
|
providers.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::u-page-card
|
:::u-page-card
|
||||||
|
|||||||
@ -164,9 +164,13 @@ airports, bus stops, teleport terminals, or any other mission transport system.
|
|||||||
The framework owns the menu, billing, cargo scan, and movement logic. The
|
The framework owns the menu, billing, cargo scan, and movement logic. The
|
||||||
mission only needs placed objects and optional arrival markers.
|
mission only needs placed objects and optional arrival markers.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Place transport node objects with these variable names:
|
Place transport node objects with these variable names:
|
||||||
|
|
||||||
@ -188,7 +192,9 @@ transport_arrival_2
|
|||||||
transport_arrival_10
|
transport_arrival_10
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Objects that should be excluded from the nearby cargo scan, such as the actual
|
Objects that should be excluded from the nearby cargo scan, such as the actual
|
||||||
boat or transport vehicle used as set dressing, should use:
|
boat or transport vehicle used as set dressing, should use:
|
||||||
@ -201,7 +207,9 @@ transport_vehicle_2
|
|||||||
transport_vehicle_10
|
transport_vehicle_10
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Minimum Eden setup:
|
Minimum Eden setup:
|
||||||
|
|
||||||
@ -754,19 +762,19 @@ CAD dispatcher-requested generation.
|
|||||||
|
|
||||||
The optional framework mission setup UI lets the setup operator choose runtime
|
The optional framework mission setup UI lets the setup operator choose runtime
|
||||||
tuning such as opposing faction, mission cap, interval, location cooldown,
|
tuning such as opposing faction, mission cap, interval, location cooldown,
|
||||||
reward ranges, reputation ranges, penalty ranges, and time limits. It does not
|
reward ranges, reputation ranges, penalty ranges, time limits, and a generator
|
||||||
enable or disable generated missions; use the CBA setting for that policy.
|
provider preference. 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
|
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
|
applies settings. Cancel, X, and Escape apply default values from CBA, mission
|
||||||
parameters, and `CfgMissions`. There is no timeout that auto-applies defaults.
|
parameters, and `CfgMissions`. There is no timeout that auto-applies defaults.
|
||||||
After settings are applied, the setup UI cannot be reopened.
|
After settings are applied, the setup UI cannot be reopened.
|
||||||
|
|
||||||
Future custom-generator support should add an explicit provider option so
|
The setup UI stores the provider preference as `builtin` or `custom`. CAD/manual
|
||||||
mission designers or developers can select or toggle a mission/community-owned
|
generated task requests use the task provider registry and route to the selected
|
||||||
generator without relying on mission-local fallback functions. Until then,
|
provider. Custom generators should register a provider or create CAD-visible
|
||||||
custom generators should create CAD-visible tasks directly through the task
|
tasks directly through the task catalog/status contract described in
|
||||||
catalog/status contract described in
|
|
||||||
[Custom Mission Generators](/getting-started/custom-mission-generators).
|
[Custom Mission Generators](/getting-started/custom-mission-generators).
|
||||||
|
|
||||||
The dynamic mission generator avoids rectangle and ellipse area markers whose
|
The dynamic mission generator avoids rectangle and ellipse area markers whose
|
||||||
|
|||||||
@ -226,7 +226,7 @@ Player workflow:
|
|||||||
1. Stand near a transport point.
|
1. Stand near a transport point.
|
||||||
2. Open the actor interaction menu.
|
2. Open the actor interaction menu.
|
||||||
3. Select Transport.
|
3. Select Transport.
|
||||||
4. Select a destination from the transport submenu, or select Back to return
|
4. Select a destination from the transport submenu, or select Close to return
|
||||||
to the default interaction menu.
|
to the default interaction menu.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@ -43,6 +43,30 @@ cd arma/server/surrealdb
|
|||||||
.\RunMe.bat
|
.\RunMe.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On Windows, `UpdateMe.bat` is a wrapper around `UpdateSurrealDB.ps1`. By
|
||||||
|
default it installs or updates to the newest compatible SurrealDB 3.x release
|
||||||
|
reported by SurrealDB's official version endpoint. You can also pin an exact
|
||||||
|
release:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\UpdateMe.bat v3.1.2
|
||||||
|
.\UpdateSurrealDB.ps1 -Version v3.1.2
|
||||||
|
```
|
||||||
|
|
||||||
|
To intentionally install the latest stable SurrealDB release regardless of
|
||||||
|
major version, run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\UpdateMe.bat latest
|
||||||
|
```
|
||||||
|
|
||||||
|
The `latest` option prompts for confirmation because a newer SurrealDB major
|
||||||
|
version can require rebuilding the Forge server extension from source with a
|
||||||
|
compatible `surrealdb` Rust crate.
|
||||||
|
|
||||||
|
`RunMe.bat` is a wrapper around `RunSurrealDB.ps1`, which starts the local
|
||||||
|
Forge database with the same defaults shown below.
|
||||||
|
|
||||||
On Linux or macOS:
|
On Linux or macOS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -3,9 +3,8 @@ title: "Custom Mission Generators"
|
|||||||
description: "Forge can be used as a complete out-of-box PMC mission framework, or as a foundation that communities build on top of. Custom mission generators should integrate through the same task, CAD, and event surfaces that the built-in mission manager uses."
|
description: "Forge can be used as a complete out-of-box PMC mission framework, or as a foundation that communities build on top of. Custom mission generators should integrate through the same task, CAD, and event surfaces that the built-in mission manager uses."
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide documents the supported integration path today and calls out the
|
This guide documents the supported integration path for custom generators,
|
||||||
current CAD generated-task provider limitation that should be addressed by a
|
including the provider registry used by CAD/manual generated task requests.
|
||||||
small framework extension point.
|
|
||||||
|
|
||||||
## Recommended Architecture
|
## Recommended Architecture
|
||||||
|
|
||||||
@ -33,13 +32,24 @@ forge_server_task_enableGenerator = false;
|
|||||||
When disabled, Forge does not run timer-based generated missions and CAD
|
When disabled, Forge does not run timer-based generated missions and CAD
|
||||||
hydrates no built-in generated task types.
|
hydrates no built-in generated task types.
|
||||||
|
|
||||||
This does not prevent custom code from creating CAD-visible tasks directly.
|
This does not prevent custom code from creating CAD-visible tasks directly or
|
||||||
It only disables the built-in generator request list and the framework-owned
|
from serving CAD/manual generated task requests through a registered custom
|
||||||
manual request entry point.
|
provider.
|
||||||
|
|
||||||
The mission setup UI does not override this setting. Generated mission
|
The mission setup UI does not override this setting. Generated mission
|
||||||
enablement is mission/server policy and should stay in CBA settings until a
|
enablement for the built-in provider is mission/server policy and stays in CBA
|
||||||
provider selection extension point exists.
|
settings.
|
||||||
|
|
||||||
|
The mission setup UI can capture a generator provider preference:
|
||||||
|
|
||||||
|
- `builtin` for Forge's built-in generated mission provider
|
||||||
|
- `custom` for mission/community-owned generated mission providers
|
||||||
|
|
||||||
|
That preference is stored in `forge_server_task_generatorProvider` and mirrored
|
||||||
|
inside `forge_server_task_missionSetup_settings`. It is intentionally separate
|
||||||
|
from `forge_server_task_enableGenerator`; the CBA setting only gates Forge's
|
||||||
|
built-in provider. A registered custom provider can still publish generated task
|
||||||
|
types and handle CAD/manual requests when selected.
|
||||||
|
|
||||||
## Framework Mission Setup UI
|
## Framework Mission Setup UI
|
||||||
|
|
||||||
@ -66,6 +76,7 @@ missionNamespace setVariable [
|
|||||||
The UI configures:
|
The UI configures:
|
||||||
|
|
||||||
- opposing faction
|
- opposing faction
|
||||||
|
- generator provider preference
|
||||||
- max concurrent generated missions
|
- max concurrent generated missions
|
||||||
- mission interval
|
- mission interval
|
||||||
- location reuse cooldown
|
- location reuse cooldown
|
||||||
@ -98,6 +109,75 @@ actor interaction entry is hidden once clients receive the public applied flag,
|
|||||||
and direct or stale open requests receive a notification explaining that setup
|
and direct or stale open requests receive a notification explaining that setup
|
||||||
has already been applied.
|
has already been applied.
|
||||||
|
|
||||||
|
## Provider Registry
|
||||||
|
|
||||||
|
Custom providers register on the server through the server-side CBA event:
|
||||||
|
|
||||||
|
```sqf
|
||||||
|
[
|
||||||
|
"forge_server_task_registerMissionGeneratorProvider",
|
||||||
|
["custom", _provider]
|
||||||
|
] call CBA_fnc_serverEvent;
|
||||||
|
```
|
||||||
|
|
||||||
|
This event is intentionally fire-and-forget. The task module validates provider
|
||||||
|
shape server-side and logs registration failures.
|
||||||
|
|
||||||
|
The provider is a hashMap/hashMapObject with two required methods:
|
||||||
|
|
||||||
|
| Method | Arguments | Return |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `getGeneratedTaskTypes` | none | Array of hashMaps with `value` and `label` |
|
||||||
|
| `requestMissionTask` | `_taskType`, `_metadata`, `_requesterUid` | Result hashMap |
|
||||||
|
|
||||||
|
The request result should include:
|
||||||
|
|
||||||
|
| Key | Type | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `success` | Boolean | `true` when a task was generated |
|
||||||
|
| `message` | String | User-facing CAD response |
|
||||||
|
| `taskID` | String | Created task ID, or empty on failure |
|
||||||
|
| `taskType` | String | Resolved generated task type |
|
||||||
|
|
||||||
|
Example provider:
|
||||||
|
|
||||||
|
```sqf
|
||||||
|
private _provider = createHashMapObject [[
|
||||||
|
["#type", "CommunityMissionGeneratorProvider"],
|
||||||
|
["getGeneratedTaskTypes", {
|
||||||
|
[
|
||||||
|
createHashMapFromArray [["value", "pvp_hold"], ["label", "PvP Hold Area"]],
|
||||||
|
createHashMapFromArray [["value", "supply_drop"], ["label", "Supply Drop"]]
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
["requestMissionTask", {
|
||||||
|
params ["_taskType", "_metadata", "_requesterUid"];
|
||||||
|
|
||||||
|
private _taskID = format ["custom_%1_%2", _taskType, floor random 100000];
|
||||||
|
|
||||||
|
// Create/spawn the mission here, then publish it through Forge's task
|
||||||
|
// catalog/status contract so CAD can assign and track it.
|
||||||
|
|
||||||
|
createHashMapFromArray [
|
||||||
|
["success", true],
|
||||||
|
["message", format ["Generated custom %1 task %2.", _taskType, _taskID]],
|
||||||
|
["taskID", _taskID],
|
||||||
|
["taskType", _taskType]
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
]];
|
||||||
|
|
||||||
|
[
|
||||||
|
"forge_server_task_registerMissionGeneratorProvider",
|
||||||
|
["custom", _provider]
|
||||||
|
] call CBA_fnc_serverEvent;
|
||||||
|
```
|
||||||
|
|
||||||
|
When the setup UI provider toggle is set to `custom`, CAD hydrates task types
|
||||||
|
from the registered `custom` provider and CAD/manual requests call that
|
||||||
|
provider's `requestMissionTask` method. If no custom provider is registered,
|
||||||
|
Forge logs a warning and falls back to the built-in provider.
|
||||||
|
|
||||||
## CAD-Visible Task Contract
|
## CAD-Visible Task Contract
|
||||||
|
|
||||||
CAD reads assignable contracts from `TaskStore.getActiveTaskCatalog`. A custom
|
CAD reads assignable contracts from `TaskStore.getActiveTaskCatalog`. A custom
|
||||||
@ -288,78 +368,33 @@ when relevant changes occur. Custom generators usually only need to emit task
|
|||||||
status changes through TaskStore or extension commands; CAD refresh follows
|
status changes through TaskStore or extension commands; CAD refresh follows
|
||||||
from the existing listeners.
|
from the existing listeners.
|
||||||
|
|
||||||
## Generated Task Dropdown Limitation
|
## Generated Task Provider Behavior
|
||||||
|
|
||||||
The current CAD generated-task dropdown is owned by the framework task mission
|
CAD hydrates generated task types and requests generated tasks through the task
|
||||||
manager. CAD hydrates `generatedTaskTypes` from the built-in manager when
|
provider registry. The selected provider comes from
|
||||||
`forge_server_task_enableGenerator` is enabled. When that setting is disabled,
|
`forge_server_task_generatorProvider`, defaulting to `builtin`.
|
||||||
the generated-task request control is disabled.
|
|
||||||
|
|
||||||
The current CAD request handler calls `forge_server_task_fnc_requestMissionTask`
|
Use one of these supported patterns:
|
||||||
directly. It no longer falls back to mission-local generator request functions,
|
|
||||||
so third-party generated-task providers should create CAD-visible tasks directly
|
|
||||||
until a framework provider extension point is added.
|
|
||||||
|
|
||||||
Until a provider extension point is added, use one of these supported patterns:
|
1. Register a custom provider so CAD/manual generated task requests route to
|
||||||
|
community code.
|
||||||
1. Run custom generators from mission/server code and create CAD-visible tasks
|
2. Run custom generators from mission/server code and create CAD-visible tasks
|
||||||
directly.
|
directly.
|
||||||
2. Use CAD support requests or dispatch orders to let players request custom
|
3. Use CAD support requests or dispatch orders to let players request custom
|
||||||
work, then have mission code convert approved requests into tasks.
|
work, then have mission code convert approved requests into tasks.
|
||||||
3. Keep the built-in generator enabled only if the community intentionally
|
4. Keep the built-in generator enabled only if the community intentionally
|
||||||
wants the framework dropdown and request handler.
|
wants the framework dropdown and request handler.
|
||||||
|
|
||||||
## Planned Provider Extension Point
|
## Provider Extension Details
|
||||||
|
|
||||||
A future code change should make CAD generator providers explicit. The desired
|
The implemented provider shape is intentionally small:
|
||||||
shape is:
|
|
||||||
|
|
||||||
- built-in Forge provider remains the default out-of-box behavior
|
- built-in Forge provider remains the default out-of-box behavior
|
||||||
- mission/community providers can supply their own `generatedTaskTypes`
|
- mission/community providers can supply their own `generatedTaskTypes`
|
||||||
- mission/community providers can handle generated-task requests
|
- mission/community providers can handle generated-task requests
|
||||||
- disabling the built-in provider does not disable custom providers
|
- disabling the built-in provider does not disable custom providers
|
||||||
- mission designers or developers can select or toggle the active generator
|
- mission designers or developers can select or toggle the active provider from
|
||||||
provider when a mission includes custom generators
|
the framework mission setup UI when a mission includes custom generators
|
||||||
- a framework-hosted mission setup UI can display the active provider and, when
|
|
||||||
supported by the mission, allow choosing between built-in and custom
|
|
||||||
providers
|
|
||||||
|
|
||||||
Candidate SQF hooks:
|
|
||||||
|
|
||||||
```sqf
|
|
||||||
forge_custom_fnc_getGeneratedTaskTypes
|
|
||||||
forge_custom_fnc_requestMissionTask
|
|
||||||
```
|
|
||||||
|
|
||||||
or mission namespace variables:
|
|
||||||
|
|
||||||
```sqf
|
|
||||||
missionNamespace setVariable ["forge_generatorProvider_getTypes", {
|
|
||||||
[
|
|
||||||
createHashMapFromArray [["value", "supply_drop"], ["label", "Supply Drop"]],
|
|
||||||
createHashMapFromArray [["value", "pvp_hold"], ["label", "PvP Hold Area"]]
|
|
||||||
]
|
|
||||||
}];
|
|
||||||
|
|
||||||
missionNamespace setVariable ["forge_generatorProvider_requestTask", {
|
|
||||||
params ["_taskType", "_metadata", "_requesterUid"];
|
|
||||||
createHashMapFromArray [
|
|
||||||
["success", true],
|
|
||||||
["message", "Generated custom task."],
|
|
||||||
["taskID", "custom_task_01"],
|
|
||||||
["taskType", _taskType]
|
|
||||||
]
|
|
||||||
}];
|
|
||||||
```
|
|
||||||
|
|
||||||
The exact API should be implemented in the framework code before communities
|
|
||||||
depend on it.
|
|
||||||
|
|
||||||
Implementation note: the provider selection should be separate from
|
|
||||||
`forge_server_task_enableGenerator`. That CBA setting should continue to gate
|
|
||||||
the built-in Forge generator, while a new provider option can decide whether
|
|
||||||
CAD/manual requests use the built-in provider, a custom provider, both, or no
|
|
||||||
provider at all.
|
|
||||||
|
|
||||||
## Validation Checklist
|
## Validation Checklist
|
||||||
|
|
||||||
|
|||||||
158
docus/content/1.getting-started/8.git-workflow.md
Normal file
158
docus/content/1.getting-started/8.git-workflow.md
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
title: "Git Workflow"
|
||||||
|
description: "This repository uses `master` as the clean framework branch. Mission folders are kept off `master` so the framework can be versioned without bundling local test missions or playable mission copies."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Helper
|
||||||
|
|
||||||
|
The repository includes a small helper for the common branch checks and branch
|
||||||
|
switching commands:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run workflow -- status
|
||||||
|
npm run workflow -- doctor
|
||||||
|
npm run workflow -- switch dev
|
||||||
|
npm run workflow -- switch missions
|
||||||
|
npm run workflow -- start-feature cad-task-request
|
||||||
|
npm run workflow -- release-check
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper refuses branch switches and feature branch creation when the working
|
||||||
|
tree has uncommitted changes. Use the manual Git commands below when you need
|
||||||
|
more control.
|
||||||
|
|
||||||
|
## Branch Roles
|
||||||
|
|
||||||
|
- `master`: framework source, addon code, Rust extension code, docs, tooling,
|
||||||
|
and release tags.
|
||||||
|
- `missions/local-mission-copies`: local mission folders used for testing and
|
||||||
|
mission iteration. This branch is not pushed unless intentionally needed.
|
||||||
|
- `archive/pre-v0.1-history`: read-only archive of the previous full `master`
|
||||||
|
history before the `v0.1.0` baseline cleanup.
|
||||||
|
|
||||||
|
## Daily Framework Work
|
||||||
|
|
||||||
|
Start from the clean framework branch.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git switch master
|
||||||
|
git pull
|
||||||
|
git status --short --branch
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a short-lived feature branch for framework work.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git switch -c feature/garage-marker-selection
|
||||||
|
```
|
||||||
|
|
||||||
|
Make the change, validate it, then commit.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git status --short --branch
|
||||||
|
git add arma/client/addons/garage/functions/fnc_initContextService.sqf
|
||||||
|
git commit -m "Improve garage spawn marker selection"
|
||||||
|
```
|
||||||
|
|
||||||
|
Merge the work back into `master`. Squash merges keep future `master` history
|
||||||
|
compact.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git switch master
|
||||||
|
git merge --squash feature/garage-marker-selection
|
||||||
|
git commit -m "Improve garage spawn marker selection"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove the local feature branch when it is no longer needed.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git branch -D feature/garage-marker-selection
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mission Work
|
||||||
|
|
||||||
|
Switch to the local mission branch before editing mission folders.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git switch missions/local-mission-copies
|
||||||
|
git status --short --branch
|
||||||
|
```
|
||||||
|
|
||||||
|
Mission folders currently tracked on that branch:
|
||||||
|
|
||||||
|
```text
|
||||||
|
arma/forge_framework.Malden
|
||||||
|
arma/forge_pmc_simulator.Tanoa
|
||||||
|
arma/forge_pmc_simulator_v2.Tanoa
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit mission-only changes on the mission branch.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add arma/forge_pmc_simulator.Tanoa
|
||||||
|
git commit -m "Update PMC simulator mission setup"
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not merge the mission branch into `master`. If a mission change becomes
|
||||||
|
framework code, copy only the reusable files or logic onto a framework feature
|
||||||
|
branch created from `master`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git switch master
|
||||||
|
git switch -c feature/cad-on-demand-task-request
|
||||||
|
|
||||||
|
# Bring over only the framework files needed from the mission branch.
|
||||||
|
git checkout missions/local-mission-copies -- arma/client/addons/cad/functions/fnc_initUIBridge.sqf
|
||||||
|
git checkout missions/local-mission-copies -- arma/server/addons/cad/XEH_preInit.sqf
|
||||||
|
|
||||||
|
git add arma/client/addons/cad/functions/fnc_initUIBridge.sqf arma/server/addons/cad/XEH_preInit.sqf
|
||||||
|
git commit -m "Add CAD on-demand mission task request bridge"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release Versioning
|
||||||
|
|
||||||
|
Use tags to mark framework releases.
|
||||||
|
|
||||||
|
Version guideline:
|
||||||
|
|
||||||
|
- Patch, such as `v0.1.1`: fixes and small compatible changes.
|
||||||
|
- Minor, such as `v0.2.0`: new modules or features.
|
||||||
|
- Major, such as `v1.0.0`: stable release line or breaking changes.
|
||||||
|
|
||||||
|
Create a release tag from `master`.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git switch master
|
||||||
|
git pull
|
||||||
|
git status --short --branch
|
||||||
|
git tag -a v0.1.1 -m "v0.1.1"
|
||||||
|
git push origin master
|
||||||
|
git push origin v0.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety Checks
|
||||||
|
|
||||||
|
Before committing on `master`, check that no mission folders are staged.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git status --short --branch
|
||||||
|
```
|
||||||
|
|
||||||
|
On `master`, these paths should not appear:
|
||||||
|
|
||||||
|
```text
|
||||||
|
arma/forge_framework.Malden
|
||||||
|
arma/forge_pmc_simulator.Tanoa
|
||||||
|
arma/forge_pmc_simulator_v2.Tanoa
|
||||||
|
```
|
||||||
|
|
||||||
|
If mission files appear while on `master`, stop and switch to the mission
|
||||||
|
branch before continuing.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git switch missions/local-mission-copies
|
||||||
|
```
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Store Usage Guide"
|
title: "Store Usage Guide"
|
||||||
description: "The store module processes checkout requests. It charges a payment source and grants purchased items to the player locker, virtual arsenal locker, and virtual garage unlocks."
|
description: "The store module processes checkout requests. It charges a payment source and grants purchased items to the player locker, virtual arsenal locker, virtual garage unlocks, and immediate unit spawn grants."
|
||||||
---
|
---
|
||||||
|
|
||||||
## Server SQF Module
|
## Server SQF Module
|
||||||
@ -9,16 +9,70 @@ The server addon uses two long-lived module objects:
|
|||||||
|
|
||||||
- `StorefrontStore` is the storefront workflow facade. It builds hydrate
|
- `StorefrontStore` is the storefront workflow facade. It builds hydrate
|
||||||
payloads, validates checkout requests, calls the Rust `store:checkout`
|
payloads, validates checkout requests, calls the Rust `store:checkout`
|
||||||
command, syncs UI patches, and asks related module stores to save hot state.
|
command, syncs UI patches, asks related module stores to save hot state, and
|
||||||
|
spawns purchased units at discovered `unit_spawn` markers after the backend
|
||||||
|
charge succeeds.
|
||||||
- `StoreCatalogService` scans configured item and vehicle categories, builds
|
- `StoreCatalogService` scans configured item and vehicle categories, builds
|
||||||
catalog responses, resolves checkout entries, and calculates authoritative
|
catalog responses, resolves checkout entries, and calculates authoritative
|
||||||
prices.
|
prices. It also applies the optional mission `CfgStore` filter and overrides
|
||||||
|
before payloads or checkout validation use catalog entries.
|
||||||
|
|
||||||
Editor-placed store entities are initialized by `fnc_initStore` during store
|
Editor-placed store entities are initialized by `fnc_initStore` during store
|
||||||
post-init. The initializer matches non-null mission namespace objects whose
|
post-init. The initializer matches non-null mission namespace objects whose
|
||||||
variable names contain `store` and sets `isStore = true`, following the same
|
variable names contain `store` and sets `isStore = true`, following the same
|
||||||
pattern used by garage entities.
|
pattern used by garage entities.
|
||||||
|
|
||||||
|
## Mission Catalog Filter
|
||||||
|
|
||||||
|
The store catalog is generated from loaded Arma config classes, then an
|
||||||
|
optional mission `CfgStore` filter can allow or deny classnames per category.
|
||||||
|
Include `CfgStore.hpp` from `description.ext`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "CfgStore.hpp"
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class CfgStore {
|
||||||
|
mode = "allowlist"; // dynamic, allowlist, or denylist
|
||||||
|
|
||||||
|
class Categories {
|
||||||
|
primary[] = {"arifle_MX_F", "arifle_MXC_F"};
|
||||||
|
cars[] = {"B_MRAP_01_F"};
|
||||||
|
units[] = {"B_Soldier_F"};
|
||||||
|
};
|
||||||
|
|
||||||
|
class Overrides {
|
||||||
|
class arifle_MX_F {
|
||||||
|
price = 2500;
|
||||||
|
displayName = "MX Rifle";
|
||||||
|
description = "Approved PMC service rifle.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`dynamic` keeps the full generated catalog. `allowlist` only shows classnames
|
||||||
|
listed for each category. `denylist` removes listed classnames. Overrides are
|
||||||
|
server-side and are used by both the UI payload and checkout validation.
|
||||||
|
`units[]` follows the same filter behavior and is fulfilled as an immediate
|
||||||
|
server-side unit spawn at a discovered `unit_spawn` marker after checkout
|
||||||
|
succeeds.
|
||||||
|
|
||||||
|
The current filter is global for the mission. Revisit per-store profile support
|
||||||
|
if individual vendors need different inventories.
|
||||||
|
|
||||||
|
## Unit Spawn Markers
|
||||||
|
|
||||||
|
Purchased units spawn at mission markers named `unit_spawn`, `unit_spawn_1`,
|
||||||
|
`unit_spawn_2`, and so on. The store resolves the closest initialized store
|
||||||
|
object to the requesting player, scans `allMapMarkers` when checkout fulfillment
|
||||||
|
runs, and uses the closest matching marker within 25 meters of that store.
|
||||||
|
|
||||||
|
If no matching marker exists within 25 meters, the store falls back to spawning
|
||||||
|
units around the store object. If no store object can be resolved, it falls back
|
||||||
|
to the requesting player.
|
||||||
|
|
||||||
## Checkout Model
|
## Checkout Model
|
||||||
|
|
||||||
`store:checkout` accepts one JSON context.
|
`store:checkout` accepts one JSON context.
|
||||||
@ -44,6 +98,13 @@ pattern used by garage entities.
|
|||||||
"category": "cars",
|
"category": "cars",
|
||||||
"priceValue": 1500
|
"priceValue": 1500
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"classname": "B_Soldier_F",
|
||||||
|
"category": "units",
|
||||||
|
"priceValue": 2500
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -51,12 +112,13 @@ pattern used by garage entities.
|
|||||||
Rules validated by the Rust service:
|
Rules validated by the Rust service:
|
||||||
|
|
||||||
- `requesterUid` is required.
|
- `requesterUid` is required.
|
||||||
- At least one item or vehicle is required.
|
- At least one item, vehicle, or unit is required.
|
||||||
- The checkout total must be greater than zero.
|
- The checkout total must be greater than zero.
|
||||||
- Item categories must be `item`, `attachment`, `weapon`, `magazine`, or
|
- Item categories must be `item`, `attachment`, `weapon`, `magazine`, or
|
||||||
`backpack`.
|
`backpack`.
|
||||||
- Vehicle categories must be `cars`, `armor`, `helis`, `planes`, `naval`, or
|
- Vehicle categories must be `cars`, `armor`, `helis`, `planes`, `naval`, or
|
||||||
`other`.
|
`other`.
|
||||||
|
- Unit categories must be `units` or `unit`.
|
||||||
- Payment method must be `cash`, `bank`, `org_funds`, or `credit_line`.
|
- Payment method must be `cash`, `bank`, `org_funds`, or `credit_line`.
|
||||||
- Player locker capacity cannot exceed 25 unique items after checkout.
|
- Player locker capacity cannot exceed 25 unique items after checkout.
|
||||||
- Organization funds can only be charged by the org owner or the default org
|
- Organization funds can only be charged by the org owner or the default org
|
||||||
@ -72,11 +134,12 @@ Rules validated by the Rust service:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"chargedTotal": 2000.0,
|
"chargedTotal": 4500.0,
|
||||||
"paymentMethod": "bank",
|
"paymentMethod": "bank",
|
||||||
"message": "Checkout completed. $2,000 charged, 1 locker grant(s), 1 vehicle unlock(s).",
|
"message": "Checkout completed. $4,500 charged, 1 locker grant(s), 1 vehicle unlock(s), 1 unit grant(s).",
|
||||||
"lockerGranted": [],
|
"lockerGranted": [],
|
||||||
"vehicleGranted": [],
|
"vehicleGranted": [],
|
||||||
|
"unitGranted": [],
|
||||||
"lockerPatch": {},
|
"lockerPatch": {},
|
||||||
"vaPatch": {},
|
"vaPatch": {},
|
||||||
"vgaragePatch": {},
|
"vgaragePatch": {},
|
||||||
@ -107,7 +170,8 @@ private _checkout = createHashMapFromArray [
|
|||||||
["requesterIsDefaultOrgCeo", false],
|
["requesterIsDefaultOrgCeo", false],
|
||||||
["paymentMethod", "bank"],
|
["paymentMethod", "bank"],
|
||||||
["items", [_item]],
|
["items", [_item]],
|
||||||
["vehicles", []]
|
["vehicles", []],
|
||||||
|
["units", []]
|
||||||
];
|
];
|
||||||
|
|
||||||
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
|
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
|
||||||
@ -132,7 +196,8 @@ private _checkout = createHashMapFromArray [
|
|||||||
["requesterIsDefaultOrgCeo", false],
|
["requesterIsDefaultOrgCeo", false],
|
||||||
["paymentMethod", "org_funds"],
|
["paymentMethod", "org_funds"],
|
||||||
["items", []],
|
["items", []],
|
||||||
["vehicles", [_vehicle]]
|
["vehicles", [_vehicle]],
|
||||||
|
["units", []]
|
||||||
];
|
];
|
||||||
|
|
||||||
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
|
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
|
||||||
|
|||||||
@ -187,19 +187,20 @@ server-side.
|
|||||||
|
|
||||||
The mission setup UI does not enable or disable generated missions. It applies
|
The mission setup UI does not enable or disable generated missions. It applies
|
||||||
runtime tuning such as faction, caps, intervals, reward ranges, rating ranges,
|
runtime tuning such as faction, caps, intervals, reward ranges, rating ranges,
|
||||||
penalties, and time limits. Generator enablement remains controlled by the CBA
|
penalties, time limits, and a generator provider preference. Generator
|
||||||
setting above.
|
enablement remains controlled by the CBA setting above.
|
||||||
|
|
||||||
When `forge_server_task_enableMissionSetup` is enabled, the mission manager
|
When `forge_server_task_enableMissionSetup` is enabled, the mission manager
|
||||||
waits for setup settings before starting. There is no timeout auto-apply.
|
waits for setup settings before starting. There is no timeout auto-apply.
|
||||||
Pressing Cancel, X, or Escape applies default values from CBA, mission
|
Pressing Cancel, X, or Escape applies default values from CBA, mission
|
||||||
parameters, and `CfgMissions`.
|
parameters, and `CfgMissions`.
|
||||||
|
|
||||||
Planned custom-generator work should add an explicit provider option for
|
The setup UI stores the provider preference in
|
||||||
mission designers or developers who want to select or toggle a custom mission
|
`forge_server_task_generatorProvider` as `builtin` or `custom`. CAD/manual
|
||||||
generator. That provider option should be separate from the built-in generator
|
generated task requests use the task provider registry and route to the selected
|
||||||
CBA gate so disabling Forge's built-in generator does not prevent custom
|
provider. That provider option stays separate from the built-in generator CBA
|
||||||
providers from publishing CAD-visible work.
|
gate so disabling Forge's built-in generator does not prevent custom providers
|
||||||
|
from publishing CAD-visible work.
|
||||||
|
|
||||||
## CAD Compatibility
|
## CAD Compatibility
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Transport Service Guide"
|
title: "Transport Service Guide"
|
||||||
description: "The transport service provides paid point-to-point travel for players and nearby vehicles or passengers. It is framework-owned: missions only need placed transport objects and arrival markers with the expected variable names."
|
description: "The transport service provides paid point-to-point travel for players and nearby vehicles or passengers. It is framework-owned: missions only need placed transport objects and optional arrival markers with the expected variable names."
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mission Contract
|
## Mission Contract
|
||||||
@ -109,9 +109,9 @@ this setVariable ["transportIncludeCargo", true, true];
|
|||||||
|
|
||||||
Only use overrides when the default `transport*` convention is not appropriate.
|
Only use overrides when the default `transport*` convention is not appropriate.
|
||||||
|
|
||||||
## Reference Images
|
## Image Checklist
|
||||||
|
|
||||||
These screenshots show the default transport setup and player workflow:
|
Replace these placeholder image references after screenshots are captured:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -134,3 +134,9 @@ These screenshots show the default transport setup and player workflow:
|
|||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Mission-Side Code Requirement
|
||||||
|
|
||||||
|
No mission-side transport service, addAction script, or server event bridge is
|
||||||
|
required. The framework handles menu discovery, destination selection, pricing,
|
||||||
|
billing, cargo movement, and EventBus notifications.
|
||||||
|
|||||||
@ -69,27 +69,27 @@ Common generated IDs:
|
|||||||
|
|
||||||
## Generated Mission Requests
|
## Generated Mission Requests
|
||||||
|
|
||||||
Dispatchers can request framework-generated mission tasks from the CAD
|
Dispatchers can request generated mission tasks from the CAD dispatcher board.
|
||||||
dispatcher board. The server hydrates the available generated task types from
|
The server hydrates the available generated task types from the selected task
|
||||||
the task mission manager as `generatedTaskTypes`; the client uses that hydrated
|
provider as `generatedTaskTypes`; the client uses that hydrated list for the
|
||||||
list for the dropdown.
|
dropdown.
|
||||||
|
|
||||||
Generated mission requests are controlled by the server CBA setting
|
Built-in generated mission requests are controlled by the server CBA setting
|
||||||
`forge_server_task_enableGenerator`:
|
`forge_server_task_enableGenerator`:
|
||||||
|
|
||||||
- Enabled: CAD receives the generated task type list and dispatchers can request
|
- Enabled: CAD can receive the built-in generated task type list and dispatchers
|
||||||
a specific generator type.
|
can request a specific built-in generator type.
|
||||||
- Disabled: CAD receives an empty generated task type list, the task request UI
|
- Disabled: the built-in provider returns no task types and rejects built-in
|
||||||
is disabled, and server-side request handling rejects any manual request.
|
manual requests.
|
||||||
|
|
||||||
The framework-owned request entry point is
|
Server CAD routes generated mission requests through the task provider registry.
|
||||||
`forge_server_task_fnc_requestMissionTask`. Server CAD uses this framework
|
The selected provider handles the request and returns the CAD response payload.
|
||||||
handler directly; it does not call mission-local generator functions.
|
|
||||||
|
|
||||||
Custom mission generators can still create CAD-visible tasks directly by
|
Custom mission generators can register a provider with the
|
||||||
registering task catalog entries and task statuses. See
|
`forge_server_task_registerMissionGeneratorProvider` CBA server event or create
|
||||||
[Custom Mission Generators](/getting-started/custom-mission-generators) for the supported
|
CAD-visible tasks directly by registering task catalog entries and task
|
||||||
integration path and the current generated-task provider limitation.
|
statuses. See [Custom Mission Generators](/getting-started/custom-mission-generators) for
|
||||||
|
the supported integration path.
|
||||||
|
|
||||||
## Submit a Support Request
|
## Submit a Support Request
|
||||||
|
|
||||||
|
|||||||
@ -103,13 +103,13 @@ The dispatcher-generated task dropdown is hydrated from the server
|
|||||||
`generatedTaskTypes` payload. The UI has a built-in fallback list for loading or
|
`generatedTaskTypes` payload. The UI has a built-in fallback list for loading or
|
||||||
older payload compatibility, but any hydrate payload that includes
|
older payload compatibility, but any hydrate payload that includes
|
||||||
`generatedTaskTypes` replaces that fallback. An empty hydrated list disables the
|
`generatedTaskTypes` replaces that fallback. An empty hydrated list disables the
|
||||||
request control, which is how `forge_server_task_enableGenerator = false` is surfaced
|
request control. For the built-in provider, this is how
|
||||||
client-side.
|
`forge_server_task_enableGenerator = false` is surfaced client-side.
|
||||||
|
|
||||||
Custom mission generators can still publish tasks into CAD by using the server
|
Custom mission generators can publish tasks into CAD by using the server task
|
||||||
task catalog. The generated-task dropdown itself currently needs a framework
|
catalog or by registering a task provider that supplies `generatedTaskTypes` and
|
||||||
provider extension point before custom providers can replace the built-in list
|
handles generated task requests. See
|
||||||
cleanly. See [Custom Mission Generators](/getting-started/custom-mission-generators).
|
[Custom Mission Generators](/getting-started/custom-mission-generators).
|
||||||
|
|
||||||
## Authorization Notes
|
## Authorization Notes
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
use arma_rs::{
|
use arma_rs::{FromArma, IntoArma, loadout::Loadout as ArmaLoadout};
|
||||||
FromArma, IntoArma,
|
|
||||||
loadout::{AssignedItems, InventoryItem, Loadout as ArmaLoadout},
|
|
||||||
};
|
|
||||||
use forge_shared::{
|
use forge_shared::{
|
||||||
ActorValidationError, arma_value_to_json, generate_email, generate_phone_number,
|
ActorValidationError, arma_value_to_json, generate_email, generate_phone_number,
|
||||||
};
|
};
|
||||||
@ -128,26 +125,7 @@ impl Actor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_loadout_json() -> serde_json::Value {
|
fn default_loadout_json() -> serde_json::Value {
|
||||||
let mut loadout = ArmaLoadout::default();
|
serde_json::Value::Array(Vec::new())
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_loadout(&self) -> Result<ArmaLoadout, String> {
|
pub fn get_loadout(&self) -> Result<ArmaLoadout, String> {
|
||||||
|
|||||||
@ -34,8 +34,8 @@ pub use org::{
|
|||||||
};
|
};
|
||||||
pub use phone::{PhoneEmail, PhoneMessage, PhonePayload};
|
pub use phone::{PhoneEmail, PhoneMessage, PhonePayload};
|
||||||
pub use store::{
|
pub use store::{
|
||||||
StoreCheckoutContext, StoreCheckoutItemSeed, StoreCheckoutResult, StoreCheckoutVehicleSeed,
|
StoreCheckoutContext, StoreCheckoutItemSeed, StoreCheckoutResult, StoreCheckoutUnitSeed,
|
||||||
StoreGrantedItem, StoreGrantedVehicle,
|
StoreCheckoutVehicleSeed, StoreGrantedItem, StoreGrantedUnit, StoreGrantedVehicle,
|
||||||
};
|
};
|
||||||
pub use task::{
|
pub use task::{
|
||||||
TaskJsonMap, TaskOwnershipContext, TaskOwnershipMutationResult, TaskRecord, TaskRewardContext,
|
TaskJsonMap, TaskOwnershipContext, TaskOwnershipMutationResult, TaskRecord, TaskRewardContext,
|
||||||
|
|||||||
@ -18,6 +18,14 @@ pub struct StoreCheckoutVehicleSeed {
|
|||||||
pub price_value: f64,
|
pub price_value: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct StoreCheckoutUnitSeed {
|
||||||
|
pub classname: String,
|
||||||
|
pub category: String,
|
||||||
|
pub price_value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct StoreCheckoutContext {
|
pub struct StoreCheckoutContext {
|
||||||
@ -30,6 +38,8 @@ pub struct StoreCheckoutContext {
|
|||||||
pub items: Vec<StoreCheckoutItemSeed>,
|
pub items: Vec<StoreCheckoutItemSeed>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub vehicles: Vec<StoreCheckoutVehicleSeed>,
|
pub vehicles: Vec<StoreCheckoutVehicleSeed>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub units: Vec<StoreCheckoutUnitSeed>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@ -47,6 +57,13 @@ pub struct StoreGrantedVehicle {
|
|||||||
pub category: String,
|
pub category: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct StoreGrantedUnit {
|
||||||
|
pub classname: String,
|
||||||
|
pub category: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct StoreCheckoutResult {
|
pub struct StoreCheckoutResult {
|
||||||
@ -58,6 +75,8 @@ pub struct StoreCheckoutResult {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub vehicle_granted: Vec<StoreGrantedVehicle>,
|
pub vehicle_granted: Vec<StoreGrantedVehicle>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub unit_granted: Vec<StoreGrantedUnit>,
|
||||||
|
#[serde(default)]
|
||||||
pub locker_patch: HashMap<String, serde_json::Value>,
|
pub locker_patch: HashMap<String, serde_json::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub va_patch: HashMap<String, serde_json::Value>,
|
pub va_patch: HashMap<String, serde_json::Value>,
|
||||||
|
|||||||
@ -23,12 +23,8 @@ pub struct VGarage {
|
|||||||
|
|
||||||
impl VGarage {
|
impl VGarage {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default_unlocks()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_unlocks() -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
cars: vec!["B_Quadbike_01_F".to_string()],
|
cars: Vec::new(),
|
||||||
armor: Vec::new(),
|
armor: Vec::new(),
|
||||||
helis: Vec::new(),
|
helis: Vec::new(),
|
||||||
planes: Vec::new(),
|
planes: Vec::new(),
|
||||||
|
|||||||
@ -19,43 +19,11 @@ pub struct VLocker {
|
|||||||
|
|
||||||
impl VLocker {
|
impl VLocker {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default_unlocks()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_unlocks() -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
items: vec![
|
items: Vec::new(),
|
||||||
"FirstAidKit".to_string(),
|
weapons: Vec::new(),
|
||||||
"G_Combat".to_string(),
|
magazines: Vec::new(),
|
||||||
"H_Cap_blk_ION".to_string(),
|
backpacks: Vec::new(),
|
||||||
"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()],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use forge_models::{
|
use forge_models::{
|
||||||
Bank, BankCheckoutContext, BankMutationResult, EquipmentCategory, HotOrgRecord, Item, Locker,
|
Bank, BankCheckoutContext, BankMutationResult, EquipmentCategory, HotOrgRecord, Item, Locker,
|
||||||
OrgFleetEntry, StoreCheckoutContext, StoreCheckoutResult, StoreGrantedItem,
|
OrgFleetEntry, StoreCheckoutContext, StoreCheckoutResult, StoreGrantedItem, StoreGrantedUnit,
|
||||||
StoreGrantedVehicle, VGarage, VLocker, VehicleCategory,
|
StoreGrantedVehicle, VGarage, VLocker, VehicleCategory,
|
||||||
};
|
};
|
||||||
use forge_repositories::{
|
use forge_repositories::{
|
||||||
@ -229,7 +229,7 @@ where
|
|||||||
if context.requester_uid.trim().is_empty() {
|
if context.requester_uid.trim().is_empty() {
|
||||||
return Err("A valid requester UID is required.".to_string());
|
return Err("A valid requester UID is required.".to_string());
|
||||||
}
|
}
|
||||||
if context.items.is_empty() && context.vehicles.is_empty() {
|
if context.items.is_empty() && context.vehicles.is_empty() && context.units.is_empty() {
|
||||||
return Err("Add at least one item before checkout.".to_string());
|
return Err("Add at least one item before checkout.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,6 +254,7 @@ where
|
|||||||
let mut vgarage_patch = HashMap::new();
|
let mut vgarage_patch = HashMap::new();
|
||||||
let mut locker_granted = Vec::new();
|
let mut locker_granted = Vec::new();
|
||||||
let mut vehicle_granted = Vec::new();
|
let mut vehicle_granted = Vec::new();
|
||||||
|
let mut unit_granted = Vec::new();
|
||||||
let mut va_categories_changed: Vec<&str> = Vec::new();
|
let mut va_categories_changed: Vec<&str> = Vec::new();
|
||||||
let mut vgarage_categories_changed: Vec<&str> = Vec::new();
|
let mut vgarage_categories_changed: Vec<&str> = Vec::new();
|
||||||
|
|
||||||
@ -374,6 +375,22 @@ where
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for unit_seed in &context.units {
|
||||||
|
if unit_seed.classname.trim().is_empty() {
|
||||||
|
return Err("Unit checkout entry was missing a classname.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let unit_category = unit_seed.category.trim().to_ascii_lowercase();
|
||||||
|
if unit_category != "units" && unit_category != "unit" {
|
||||||
|
return Err(format!("Unit category '{}' is unsupported.", unit_category));
|
||||||
|
}
|
||||||
|
|
||||||
|
unit_granted.push(StoreGrantedUnit {
|
||||||
|
classname: unit_seed.classname.clone(),
|
||||||
|
category: "units".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for category in vgarage_categories_changed {
|
for category in vgarage_categories_changed {
|
||||||
match category {
|
match category {
|
||||||
"cars" => {
|
"cars" => {
|
||||||
@ -550,13 +567,15 @@ where
|
|||||||
charged_total,
|
charged_total,
|
||||||
payment_method,
|
payment_method,
|
||||||
message: format!(
|
message: format!(
|
||||||
"Checkout completed. {} charged, {} locker grant(s), {} vehicle unlock(s).",
|
"Checkout completed. {} charged, {} locker grant(s), {} vehicle unlock(s), {} unit grant(s).",
|
||||||
format_currency(charged_total),
|
format_currency(charged_total),
|
||||||
locker_granted.len(),
|
locker_granted.len(),
|
||||||
vehicle_granted.len()
|
vehicle_granted.len(),
|
||||||
|
unit_granted.len()
|
||||||
),
|
),
|
||||||
locker_granted,
|
locker_granted,
|
||||||
vehicle_granted,
|
vehicle_granted,
|
||||||
|
unit_granted,
|
||||||
locker_patch,
|
locker_patch,
|
||||||
va_patch,
|
va_patch,
|
||||||
vgarage_patch,
|
vgarage_patch,
|
||||||
@ -578,8 +597,13 @@ fn checkout_total(context: &StoreCheckoutContext) -> f64 {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|entry| entry.price_value.max(0.0))
|
.map(|entry| entry.price_value.max(0.0))
|
||||||
.sum::<f64>();
|
.sum::<f64>();
|
||||||
|
let unit_total = context
|
||||||
|
.units
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.price_value.max(0.0))
|
||||||
|
.sum::<f64>();
|
||||||
|
|
||||||
(item_total + vehicle_total).floor()
|
(item_total + vehicle_total + unit_total).floor()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_locker_category(category: &str) -> Result<&'static str, String> {
|
fn resolve_locker_category(category: &str) -> Result<&'static str, String> {
|
||||||
|
|||||||
@ -460,8 +460,8 @@ npm run build:webui
|
|||||||
title: Custom Mission Generators
|
title: Custom Mission Generators
|
||||||
to: /getting-started/custom-mission-generators
|
to: /getting-started/custom-mission-generators
|
||||||
---
|
---
|
||||||
Create CAD-visible custom generated missions and understand the current
|
Create CAD-visible custom generated missions and register custom generator
|
||||||
provider extension point.
|
providers.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::u-page-card
|
:::u-page-card
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user