Skip to content

Commit

Permalink
Medical Vitals - Add SPO2 (#9360)
Browse files Browse the repository at this point in the history
Co-authored-by: johnb432 <[email protected]>
Co-authored-by: Grim <[email protected]>
Co-authored-by: LinkIsGrim <[email protected]>
  • Loading branch information
4 people authored Feb 7, 2024
1 parent 2f9b700 commit 1649422
Show file tree
Hide file tree
Showing 20 changed files with 206 additions and 11 deletions.
2 changes: 1 addition & 1 deletion addons/advanced_fatigue/XEH_postInit.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ if (!hasInterface) exitWith {};
}, true] call CBA_fnc_addPlayerEventHandler;

// - Duty factors -------------------------------------------------------------
if (["ace_medical"] call EFUNC(common,isModLoaded)) then {
if (GVAR(medicalLoaded)) then {
[QEGVAR(medical,pain), { // 0->1.0, 0.5->1.05, 1->1.1
linearConversion [0, 1, (_this getVariable [QEGVAR(medical,pain), 0]), 1, 1.1, true];
}] call FUNC(addDutyFactor);
Expand Down
1 change: 1 addition & 0 deletions addons/advanced_fatigue/XEH_preInit.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ GVAR(dutyList) = createHashMap;
GVAR(setAnimExclusions) = [];
GVAR(inertia) = 0;
GVAR(inertiaCache) = createHashMap;
GVAR(medicalLoaded) = ["ace_medical"] call EFUNC(common,isModLoaded);

ADDON = true;
20 changes: 13 additions & 7 deletions addons/advanced_fatigue/functions/fnc_mainLoop.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ if (!alive ACE_player) exitWith {
_staminaBarContainer ctrlCommit 1;
};


private _oxygen = 0.9; // Default AF oxygen saturation
if (GVAR(medicalLoaded) && {EGVAR(medical_vitals,simulateSpo2)}) then {
_oxygen = (ACE_player getVariable [QEGVAR(medical,spo2), 97]) / 100;
};

private _currentWork = REE;
private _currentSpeed = (vectorMagnitude (velocity ACE_player)) min 6;

Expand All @@ -42,8 +48,8 @@ GVAR(muscleDamage) = (GVAR(muscleDamage) + (_currentWork / GVAR(peakPower)) ^ 3.
private _muscleIntegritySqrt = sqrt (1 - GVAR(muscleDamage));

// Calculate available power
private _ae1PathwayPowerFatigued = GVAR(ae1PathwayPower) * sqrt (GVAR(ae1Reserve) / AE1_MAXRESERVE) * OXYGEN * _muscleIntegritySqrt;
private _ae2PathwayPowerFatigued = GVAR(ae2PathwayPower) * sqrt (GVAR(ae2Reserve) / AE2_MAXRESERVE) * OXYGEN * _muscleIntegritySqrt;
private _ae1PathwayPowerFatigued = GVAR(ae1PathwayPower) * sqrt (GVAR(ae1Reserve) / AE1_MAXRESERVE) * _oxygen * _muscleIntegritySqrt;
private _ae2PathwayPowerFatigued = GVAR(ae2PathwayPower) * sqrt (GVAR(ae2Reserve) / AE2_MAXRESERVE) * _oxygen * _muscleIntegritySqrt;

// Calculate how much power is consumed from each reserve
private _ae1Power = _currentWork min _ae1PathwayPowerFatigued;
Expand All @@ -58,8 +64,8 @@ GVAR(anReserve) = GVAR(anReserve) - _anPower / WATTSPERATP;
GVAR(anFatigue) = GVAR(anFatigue) + _anPower * (0.057 / GVAR(peakPower)) * 1.1;

// Aerobic ATP reserve recovery
GVAR(ae1Reserve) = ((GVAR(ae1Reserve) + OXYGEN * 6.60 * (GVAR(ae1PathwayPower) - _ae1Power) / GVAR(ae1PathwayPower) * GVAR(recoveryFactor)) min AE1_MAXRESERVE) max 0;
GVAR(ae2Reserve) = ((GVAR(ae2Reserve) + OXYGEN * 5.83 * (GVAR(ae2PathwayPower) - _ae2Power) / GVAR(ae2PathwayPower) * GVAR(recoveryFactor)) min AE2_MAXRESERVE) max 0;
GVAR(ae1Reserve) = ((GVAR(ae1Reserve) + _oxygen * 6.60 * (GVAR(ae1PathwayPower) - _ae1Power) / GVAR(ae1PathwayPower) * GVAR(recoveryFactor)) min AE1_MAXRESERVE) max 0;
GVAR(ae2Reserve) = ((GVAR(ae2Reserve) + _oxygen * 5.83 * (GVAR(ae2PathwayPower) - _ae2Power) / GVAR(ae2PathwayPower) * GVAR(recoveryFactor)) min AE2_MAXRESERVE) max 0;

// Anaerobic ATP reserver and fatigue recovery
GVAR(anReserve) = ((GVAR(anReserve)
Expand All @@ -70,9 +76,9 @@ GVAR(anFatigue) = ((GVAR(anFatigue)
- (_ae1PathwayPowerFatigued + _ae2PathwayPowerFatigued - _ae1Power - _ae2Power) * (0.057 / GVAR(peakPower)) * GVAR(anFatigue) ^ 2 * GVAR(recoveryFactor)
) min 1) max 0;

private _aeReservePercentage = (GVAR(ae1Reserve) / AE1_MAXRESERVE + GVAR(ae2Reserve) / AE2_MAXRESERVE) / 2;
private _anReservePercentage = GVAR(anReserve) / AN_MAXRESERVE;
private _perceivedFatigue = 1 - (_anReservePercentage min _aeReservePercentage);
GVAR(aeReservePercentage) = (GVAR(ae1Reserve) / AE1_MAXRESERVE + GVAR(ae2Reserve) / AE2_MAXRESERVE) / 2;
GVAR(anReservePercentage) = GVAR(anReserve) / AN_MAXRESERVE;
private _perceivedFatigue = 1 - (GVAR(anReservePercentage) min GVAR(aeReservePercentage));

[ACE_player, _perceivedFatigue, _currentSpeed, GVAR(anReserve) == 0] call FUNC(handleEffects);

Expand Down
4 changes: 4 additions & 0 deletions addons/medical_engine/script_macros_medical.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#define GET_ARRAY(config,default) (if (isArray (config)) then {getArray (config)} else {default})

#define DEFAULT_HEART_RATE 80
#define DEFAULT_SPO2 97
#define DEFAULT_PERIPH_RES 100

// --- blood
Expand Down Expand Up @@ -153,6 +154,8 @@
#define VAR_WOUND_BLEEDING QEGVAR(medical,woundBleeding)
#define VAR_CRDC_ARRST QEGVAR(medical,inCardiacArrest)
#define VAR_HEART_RATE QEGVAR(medical,heartRate)
#define VAR_SPO2 QEGVAR(medical,spo2)
#define VAR_OXYGEN_DEMAND QEGVAR(medical,oxygenDemand)
#define VAR_PAIN QEGVAR(medical,pain)
#define VAR_PAIN_SUPP QEGVAR(medical,painSuppress)
#define VAR_PERIPH_RES QEGVAR(medical,peripheralResistance)
Expand All @@ -175,6 +178,7 @@
#define GET_BLOOD_VOLUME(unit) (unit getVariable [VAR_BLOOD_VOL, DEFAULT_BLOOD_VOLUME])
#define GET_WOUND_BLEEDING(unit) (unit getVariable [VAR_WOUND_BLEEDING, 0])
#define GET_HEART_RATE(unit) (unit getVariable [VAR_HEART_RATE, DEFAULT_HEART_RATE])
#define GET_SPO2(unit) (unit getVariable [VAR_SPO2, DEFAULT_SPO2])
#define GET_HEMORRHAGE(unit) (unit getVariable [VAR_HEMORRHAGE, 0])
#define GET_PAIN(unit) (unit getVariable [VAR_PAIN, 0])
#define GET_PAIN_SUPPRESS(unit) (unit getVariable [VAR_PAIN_SUPP, 0])
Expand Down
4 changes: 3 additions & 1 deletion addons/medical_status/functions/fnc_initUnit.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ if (damage _unit > 0) then {
if (_isRespawn) then {
TRACE_1("reseting all vars on respawn",_isRespawn); // note: state is handled by ace_medical_statemachine_fnc_resetStateDefault

// - Blood and heart ----------------------------------------------------------
// - Vitals ------------------------------------------------------------------
_unit setVariable [VAR_BLOOD_VOL, DEFAULT_BLOOD_VOLUME, true];
_unit setVariable [VAR_HEART_RATE, DEFAULT_HEART_RATE, true];
_unit setVariable [VAR_BLOOD_PRESS, [80, 120], true];
_unit setVariable [VAR_PERIPH_RES, DEFAULT_PERIPH_RES, true];
_unit setVariable [VAR_CRDC_ARRST, false, true];
_unit setVariable [VAR_HEMORRHAGE, 0, true];
_unit setVariable [VAR_SPO2, DEFAULT_SPO2, true];
_unit setVariable [VAR_OXYGEN_DEMAND, 0, true];

// - Pain ---------------------------------------------------------------------
_unit setVariable [VAR_PAIN, 0, true];
Expand Down
2 changes: 2 additions & 0 deletions addons/medical_treatment/functions/fnc_fullHealLocal.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ _patient setVariable [VAR_FRACTURES, DEFAULT_FRACTURE_VALUES, true];
_patient setVariable [VAR_HEART_RATE, DEFAULT_HEART_RATE, true];
_patient setVariable [VAR_BLOOD_PRESS, [80, 120], true];
_patient setVariable [VAR_PERIPH_RES, DEFAULT_PERIPH_RES, true];
_patient setVariable [VAR_SPO2, DEFAULT_SPO2, true];
_patient setVariable [VAR_OXYGEN_DEMAND, 0, true];

// IVs
_patient setVariable [QEGVAR(medical,ivBags), nil, true];
Expand Down
10 changes: 10 additions & 0 deletions addons/medical_vitals/CfgWeapons.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CfgWeapons {
class H_HelmetB;
class H_PilotHelmetFighter_B: H_HelmetB {
GVAR(oxygenSupply) = QUOTE(vehicle _this isKindOf 'Plane' || vehicle _this isKindOf 'Helicopter');
};
class Vest_Camo_Base;
class V_RebreatherB: Vest_Camo_Base {
GVAR(oxygenSupply) = QUOTE(eyePos _this select 2 < 0); // will only work for sea-level water
};
};
2 changes: 2 additions & 0 deletions addons/medical_vitals/XEH_PREP.hpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
PREP(handleUnitVitals);
PREP(scanConfig);
PREP(updateHeartRate);
PREP(updateOxygen);
PREP(updatePainSuppress);
PREP(updatePeripheralResistance);
4 changes: 4 additions & 0 deletions addons/medical_vitals/XEH_preInit.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;

#include "initSettings.inc.sqf"

GVAR(oxygenSupplyConditionCache) = uiNamespace getVariable QGVAR(oxygenSupplyConditionCache);

ADDON = true;
6 changes: 6 additions & 0 deletions addons/medical_vitals/XEH_preStart.sqf
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
#include "script_component.hpp"

#include "XEH_PREP.hpp"

GVAR(oxygenSupplyConditionCache) = createHashMap;

call FUNC(scanConfig);

GVAR(oxygenSupplyConditionCache) = compileFinal GVAR(oxygenSupplyConditionCache);
1 change: 1 addition & 0 deletions addons/medical_vitals/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ class CfgPatches {
};

#include "CfgEventHandlers.hpp"
#include "CfgWeapons.hpp"

#endif
3 changes: 3 additions & 0 deletions addons/medical_vitals/functions/fnc_handleUnitVitals.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ if (_syncValues) then {
_unit setVariable [QGVAR(lastMomentValuesSynced), CBA_missionTime];
};

// Update SPO2 intake and usage since last update
[_unit, _deltaT, _syncValues] call FUNC(updateOxygen);

private _bloodVolume = GET_BLOOD_VOLUME(_unit) + ([_unit, _deltaT, _syncValues] call EFUNC(medical_status,getBloodVolumeChange));
_bloodVolume = 0 max _bloodVolume min DEFAULT_BLOOD_VOLUME;

Expand Down
23 changes: 23 additions & 0 deletions addons/medical_vitals/functions/fnc_scanConfig.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include "..\script_component.hpp"
/*
* Author: LinkIsGrim
* Cache a hashmap of all oxygen-providing items for SpO2 simulation
*
* Arguments:
* None
*
* Return Value:
* None
*
* Public: No
*/

private _filter = toString {getText (_x >> QGVAR(oxygenSupply)) != ""};

{
private _cfgRoot = configFile >> _x;
{
private _condition = compile getText (_x >> QGVAR(oxygenSupply));
GVAR(oxygenSupplyConditionCache) set [configName _x, _condition];
} forEach (_filter configClasses _cfgRoot);
} forEach ["CfgWeapons", "CfgGoggles"];
6 changes: 5 additions & 1 deletion addons/medical_vitals/functions/fnc_updateHeartRate.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ if IN_CRDC_ARRST(_unit) then {
if (_bloodVolume > BLOOD_VOLUME_CLASS_4_HEMORRHAGE) then {
GET_BLOOD_PRESSURE(_unit) params ["_bloodPressureL", "_bloodPressureH"];
private _meanBP = (2/3) * _bloodPressureH + (1/3) * _bloodPressureL;
private _spo2 = GET_SPO2(_unit);
private _painLevel = GET_PAIN_PERCEIVED(_unit);

private _targetBP = 107;
Expand All @@ -51,8 +52,11 @@ if IN_CRDC_ARRST(_unit) then {
if (_painLevel > 0.2) then {
_targetHR = _targetHR max (80 + 50 * _painLevel);
};
// Increase HR to compensate for low blood oxygen
// Increase HR to compensate for higher oxygen demand (e.g. running, recovering from sprint)
private _oxygenDemand = _unit getVariable [VAR_OXYGEN_DEMAND, 0];
_targetHR = _targetHR + ((97 - _spo2) * 2) + (_oxygenDemand * -1000);
_targetHR = (_targetHR + _hrTargetAdjustment) max 0;

_hrChange = round(_targetHR - _heartRate) / 2;
} else {
_hrChange = -round(_heartRate / 10);
Expand Down
75 changes: 75 additions & 0 deletions addons/medical_vitals/functions/fnc_updateOxygen.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#include "..\script_component.hpp"
/*
* Author: Brett Mayson
* Update the oxygen levels
*
* Arguments:
* 0: The Unit <OBJECT>
* 1: Time since last update <NUMBER>
* 2: Sync value? <BOOL>
*
* ReturnValue:
* Current SPO2 <NUMBER>
*
* Example:
* [player, 1, false] call ace_medical_vitals_fnc_updateOxygen
*
* Public: No
*/

params ["_unit", "_deltaT", "_syncValue"];

if (!GVAR(simulateSpO2)) exitWith {}; // changing back to default is handled in initSettings.inc.sqf

#define IDEAL_PPO2 0.255

private _current = GET_SPO2(_unit);
private _heartRate = GET_HEART_RATE(_unit);

private _altitude = EGVAR(common,mapAltitude) + ((getPosASL _unit) select 2);
private _po2 = if (missionNamespace getVariable [QEGVAR(weather,enabled), false]) then {
private _temperature = _altitude call EFUNC(weather,calculateTemperatureAtHeight);
private _pressure = _altitude call EFUNC(weather,calculateBarometricPressure);
[_temperature, _pressure, EGVAR(weather,currentHumidity)] call EFUNC(weather,calculateOxygenDensity)
} else {
// Rough approximation of the partial pressure of oxygen in the air
0.25725 * (_altitude / 1000 + 1)
};

private _oxygenSaturation = (IDEAL_PPO2 min _po2) / IDEAL_PPO2;

// Check gear for oxygen supply
[goggles _unit, headgear _unit, vest _unit] findIf {
_x in GVAR(oxygenSupplyConditionCache) &&
{ACE_player call (GVAR(oxygenSupplyConditionCache) get _x)} &&
{ // Will only run this if other conditions are met due to lazy eval
_oxygenSaturation = 1;
_po2 = IDEAL_PPO2;
true
}
};

// Base oxygen consumption rate
private _negativeChange = BASE_OXYGEN_USE;

// Fatigue & exercise will demand more oxygen
// Assuming a trained male in midst of peak exercise will have a peak heart rate of ~180 BPM
// Ref: https://academic.oup.com/bjaed/article-pdf/4/6/185/894114/mkh050.pdf table 2, though we don't take stroke volume change into account
if (_unit == ACE_player && {missionNamespace getVariable [QEGVAR(advanced_fatigue,enabled), false]}) then {
_negativeChange = _negativeChange - ((1 - EGVAR(advanced_fatigue,aeReservePercentage)) * 0.1) - ((1 - EGVAR(advanced_fatigue,anReservePercentage)) * 0.05);
};

// Effectiveness of capturing oxygen
// increases slightly as po2 starts lowering
// but falls off quickly as po2 drops further
private _capture = 1 max ((_po2 / IDEAL_PPO2) ^ (-_po2 * 3));
private _positiveChange = _heartRate * 0.00368 * _oxygenSaturation * _capture;

private _breathingEffectiveness = 1;

private _rateOfChange = _negativeChange + (_positiveChange * _breathingEffectiveness);

private _spo2 = (_current + (_rateOfChange * _deltaT)) max 0 min 100;

_unit setVariable [VAR_OXYGEN_DEMAND, _negativeChange - BASE_OXYGEN_USE];
_unit setVariable [VAR_SPO2, _spo2, _syncValue];
15 changes: 15 additions & 0 deletions addons/medical_vitals/initSettings.inc.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[
QGVAR(simulateSpO2),
"CHECKBOX",
[LSTRING(simulateSpO2_DisplayName), LSTRING(simulateSpO2_Description)],
[ELSTRING(medical,Category), LSTRING(SubCategory)],
true,
1,
{
if (_this) exitWith {}; // skip if true
{
_x setVariable [VAR_OXYGEN_DEMAND, 0, true];
_x setVariable [VAR_SPO2, DEFAULT_SPO2, true];
} forEach (allUnits select {local _x})
} // reset oxygen demand on setting change
] call CBA_fnc_addSetting;
2 changes: 2 additions & 0 deletions addons/medical_vitals/script_component.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@

#include "\z\ace\addons\medical_engine\script_macros_medical.hpp"
#include "\z\ace\addons\main\script_macros.hpp"

#define BASE_OXYGEN_USE -0.25
15 changes: 15 additions & 0 deletions addons/medical_vitals/stringtable.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project name="ACE">
<Package name="Medical_Vitals">
<Key ID="STR_ACE_Medical_Vitals_SubCategory">
<English>Vitals</English>
<Portuguese>Vitais</Portuguese>
</Key>
<Key ID="STR_ACE_Medical_Vitals_simulateSpO2_DisplayName">
<English>Enable SpO2 Simulation</English>
</Key>
<Key ID="STR_ACE_Medical_Vitals_simulateSpO2_Description">
<English>Enables oxygen saturation simulation, providing variable heart rate and oxygen demand based on physical activity and altitude. Required for Airway Management.</English>
</Key>
</Package>
</Project>
2 changes: 1 addition & 1 deletion addons/weather/XEH_PREP.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

PREP(calculateAirDensity);
PREP(calculateBarometricPressure);
PREP(calculateDensityAltitude);
PREP(calculateDewPoint);
PREP(calculateHeatIndex);
PREP(calculateOxygenDensity);
PREP(calculateRoughnessLength);
PREP(calculateSpeedOfSound);
PREP(calculateTemperatureAtHeight);
Expand Down
20 changes: 20 additions & 0 deletions addons/weather/functions/fnc_calculateOxygenDensity.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include "..\script_component.hpp"
/*
* Author: Brett Mayson
* Calculates the oxygen density
*
* Arguments:
* 0: Temperature - °C <NUMBER>
* 1: Pressure - hPa <NUMBER>
* 2: Relative humidity - value between 0.0 and 1.0 <NUMBER>
*
* Return Value:
* Density of oxygen - kg * m^(-3) <NUMBER>
*
* Example:
* [0, 1020] call ace_weather_fnc_calculateOxygenDensity
*
* Public: No
*/

(_this call FUNC(calculateAirDensity)) * 0.21

0 comments on commit 1649422

Please sign in to comment.