Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 131 additions & 51 deletions src/Sources/Code/Module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,22 @@ uint16_t MOBIFLIGHT_MAX_VARS_PER_FRAME = 30;
// due to the maximum client-data-array-size (SIMCONNECT_CLIENTDATA_MAX_SIZE) of 8kB!
constexpr uint16_t MOBIFLIGHT_STRING_SIMVAR_VALUE_MAX_LEN = 128;

//declare struct Client
struct Client;

// data struct for dynamically registered SimVars
struct SimVar {
int ID;
int Offset;
std::string Name;
float Value;
struct Client * client;
};

struct StringSimVar {
int ID;
int Offset;
std::string Name;
std::string Value;
struct Client * client;
};

// data struct for client accessing SimVars
Expand All @@ -81,12 +84,23 @@ struct Client {
// This is an optimization to be able to re-use already defined data definition IDs & request IDs
// after resetting registered SimVars
uint16_t MaxClientDataDefinition = 0;
// Runtime Rolling CLient Data reading Index
//std::vector<SimVar>::iterator RollingClientDataReadIndex;
uint16_t RollingClientDataReadIndex;
};

// Runtime Rolling CLient Data reading Index
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "CLient" should be "Client".

Suggested change
// Runtime Rolling CLient Data reading Index
// Runtime Rolling Client Data reading Index

Copilot uses AI. Check for mistakes.
uint16_t RollingDataReadIndex = 0;

//RPN code execution for reading values in every frame
struct ReadRPNCode {
std::string Code;
//RetType: 0:float 1:integer 2:string
int RetType;
std::vector<SimVar> SimVars;
std::vector<StringSimVar> StringSimVars;
};

std::vector<ReadRPNCode> RPNCodelist;


// The list of currently registered clients
std::vector<Client*> RegisteredClients;

Expand Down Expand Up @@ -255,10 +269,10 @@ void WriteSimVar(StringSimVar& simVar, Client* client)
);

if (hr != S_OK) {
fprintf(stderr, "MobiFlight[%s]: Error on Setting String Client Data. %lu, SimVar: %s (String-ID: %ul)\n", client->Name.c_str(), hr, simVar.Name.c_str(), simVar.ID);
fprintf(stderr, "MobiFlight[%s]: Error on Setting String Client Data. %lu, SimVar: (String-ID: %ul)\n", client->Name.c_str(), hr, simVar.ID);
}
#if _DEBUG
std::cout << "MobiFlight[" << client->Name.c_str() << "]: Written String-SimVar " << simVar.Name.c_str();
std::cout << "MobiFlight[" << client->Name.c_str() << "]: Written String-SimVar";
std::cout << " with String-ID " << simVar.ID << " has value " << simVar.Value.c_str() << std::endl;
#endif
}
Expand All @@ -276,27 +290,49 @@ void WriteSimVar(SimVar& simVar, Client* client) {
);

if (hr != S_OK) {
fprintf(stderr, "MobiFlight[%s]: Error on Setting Client Data. %lu, SimVar: %s (ID: %u)", client->Name.c_str(), hr, simVar.Name.c_str(), simVar.ID);
fprintf(stderr, "MobiFlight[%s]: Error on Setting Client Data. %lu, SimVar: (ID: %u)", client->Name.c_str(), hr, simVar.ID);
}
#if _DEBUG
std::cout << "MobiFlight[" << client->Name.c_str() << "]: Written SimVar " << simVar.Name.c_str();
std::cout << "MobiFlight[" << client->Name.c_str() << "]: Written SimVar";
std::cout << " with ID " << simVar.ID << " has value " << simVar.Value << std::endl;
#endif
}

//check whether SimVar has already registered in SimVar list
ReadRPNCode* IsDuplicatedSimVar(const std::string code) {
for (auto &rpn : RPNCodelist) {
if (rpn.Code == code) {
return &rpn;
}
}
return nullptr;
}
Comment on lines +302 to +309
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading function name. The function is named IsDuplicatedSimVar but it checks for duplicate RPN code strings, not SimVars. Multiple SimVars can have the same code (which is the whole point of the optimization). A better name would be FindExistingRPNCode or GetRPNCodeByString to clarify that it's searching for an existing RPN code entry, not checking if a SimVar is duplicated.

Copilot uses AI. Check for mistakes.

// Register a single Float-SimVar and send the current value to SimConnect Clients
void RegisterFloatSimVar(const std::string code, Client* client) {
std::vector<SimVar>* SimVars = &(client->SimVars);
std::vector<StringSimVar>* StringSimVars = &(client->StringSimVars);
SimVar newSimVar;
ReadRPNCode* pdupRpn = IsDuplicatedSimVar(code);
HRESULT hr;

newSimVar.Name = code;
newSimVar.ID = SimVars->size() + client->DataDefinitionIdSimVarsStart;
newSimVar.Offset = SimVars->size() * (sizeof(float));
newSimVar.Value = 0.0F;
newSimVar.client = client;
SimVars->push_back(newSimVar);

//duplicated SimVar
if (pdupRpn) {
pdupRpn->SimVars.push_back(newSimVar);
} else {
ReadRPNCode rpnCode;
rpnCode.Code = code;
rpnCode.RetType = 0;//hardcoded type id
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded RetType value (0 for float) reduces maintainability. Consider defining an enum or constants like RPN_TYPE_FLOAT = 0, RPN_TYPE_INTEGER = 1, RPN_TYPE_STRING = 2 to make the code more readable and maintainable.

Copilot uses AI. Check for mistakes.
rpnCode.SimVars.push_back(newSimVar);
RPNCodelist.push_back(rpnCode);
}
Comment on lines 323 to +334
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data synchronization issue: newSimVar is pushed to both client->SimVars (line 323) and rpn.SimVars (line 327 or 332). These are separate copies of the struct. When ReadSimVarFloat updates simVar.Value in rpn.SimVars (line 484), the corresponding entry in client->SimVars becomes stale and won't reflect the current value. While SimConnect receives updates via WriteSimVar, the client->SimVars vector will contain outdated values, which could cause issues if this vector is read elsewhere in the codebase or during debugging.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no "WriteSimVar" API


if (client->MaxClientDataDefinition < (SimVars->size() + StringSimVars->size())) {
hr = SimConnect_AddToClientDataDefinition(
g_hSimConnect,
Expand All @@ -307,11 +343,11 @@ void RegisterFloatSimVar(const std::string code, Client* client) {
);

if (hr != S_OK) {
fprintf(stderr, "MobiFlight[%s]: Error on adding Client Data \"%s\" with ID: %u, Offset: %u and Size: %lu\n", client->Name.c_str(), newSimVar.Name.c_str(), newSimVar.ID, newSimVar.Offset, sizeof(float));
fprintf(stderr, "MobiFlight[%s]: Error on adding Client Data \"%s\" with ID: %u, Offset: %u and Size: %lu\n", client->Name.c_str(), code.c_str(), newSimVar.ID, newSimVar.Offset, sizeof(float));
}
#if _DEBUG
else {
std::cout << "MobiFlight[" << client->Name.c_str() << "]: Added SimVar > " << newSimVar.Name.c_str();
std::cout << "MobiFlight[" << client->Name.c_str() << "]: Added SimVar > " << code.c_str();
std::cout << " with ID: " << newSimVar.ID << ", Offset: " << newSimVar.Offset << " and Size: " << sizeof(float) << std::endl;
}
std::cout << "MobiFlight[" << client->Name.c_str() << "]: RegisterFloatSimVar SimVars Size: " << SimVars->size() << std::endl;
Expand All @@ -324,7 +360,7 @@ void RegisterFloatSimVar(const std::string code, Client* client) {
newSimVar.Value = floatVal;
WriteSimVar(newSimVar, client);
#if _DEBUG
std::cout << "MobiFlight[" << client->Name.c_str() << "]: RegisterFloatSimVar > " << newSimVar.Name.c_str();
std::cout << "MobiFlight[" << client->Name.c_str() << "]: RegisterFloatSimVar > " << code.c_str();
std::cout << " ID [" << newSimVar.ID << "] : Offset(" << newSimVar.Offset << ") : Value(" << newSimVar.Value << ")" << std::endl;
#endif
}
Expand All @@ -334,14 +370,26 @@ void RegisterStringSimVar(const std::string code, Client* client) {
std::vector<SimVar>* SimVars = &(client->SimVars);
std::vector<StringSimVar>* StringSimVars = &(client->StringSimVars);
StringSimVar newStringSimVar;
ReadRPNCode* pdupRpn = IsDuplicatedSimVar(code);
HRESULT hr;

newStringSimVar.Name = code;
newStringSimVar.ID = StringSimVars->size() + client->DataDefinitionIdStringVarsStart;
newStringSimVar.Offset = StringSimVars->size() * MOBIFLIGHT_STRING_SIMVAR_VALUE_MAX_LEN;
newStringSimVar.Value.empty();
newStringSimVar.client = client;
StringSimVars->push_back(newStringSimVar);

//duplicated SimVar
if (pdupRpn) {
pdupRpn->StringSimVars.push_back(newStringSimVar);
} else {
ReadRPNCode rpnCode;
rpnCode.Code = code;
rpnCode.RetType = 2;//hardcoded type id
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded RetType value (2 for string) reduces maintainability. Consider defining an enum or constants like RPN_TYPE_FLOAT = 0, RPN_TYPE_INTEGER = 1, RPN_TYPE_STRING = 2 to make the code more readable and maintainable.

Copilot uses AI. Check for mistakes.
rpnCode.StringSimVars.push_back(newStringSimVar);
RPNCodelist.push_back(rpnCode);
}
Comment on lines 380 to +391
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data synchronization issue: newStringSimVar is pushed to both client->StringSimVars (line 380) and rpn.StringSimVars (line 384 or 389). These are separate copies of the struct. When ReadSimVarString updates simVar.Value in rpn.StringSimVars (line 504), the corresponding entry in client->StringSimVars becomes stale and won't reflect the current value. While SimConnect receives updates via WriteSimVar, the client->StringSimVars vector will contain outdated values, which could cause issues if this vector is read elsewhere in the codebase or during debugging.

Copilot uses AI. Check for mistakes.

if (client->MaxClientDataDefinition < (SimVars->size() + StringSimVars->size())) {
hr = SimConnect_AddToClientDataDefinition(
g_hSimConnect,
Expand All @@ -352,11 +400,11 @@ void RegisterStringSimVar(const std::string code, Client* client) {
);

if (hr != S_OK) {
fprintf(stderr, "MobiFlight[%s]: Error on adding Client Data \"%s\" with String-ID: %u, String-Offset: %u and Size: %u\n", client->Name.c_str(), newStringSimVar.Name.c_str(), newStringSimVar.ID, newStringSimVar.Offset, MOBIFLIGHT_STRING_SIMVAR_VALUE_MAX_LEN);
fprintf(stderr, "MobiFlight[%s]: Error on adding Client Data \"%s\" with String-ID: %u, String-Offset: %u and Size: %u\n", client->Name.c_str(), code.c_str(), newStringSimVar.ID, newStringSimVar.Offset, MOBIFLIGHT_STRING_SIMVAR_VALUE_MAX_LEN);
}
#if _DEBUG
else {
std::cout << "MobiFlight[" << client->Name.c_str() << "]: Added String-SimVar > " << newStringSimVar.Name.c_str();
std::cout << "MobiFlight[" << client->Name.c_str() << "]: Added String-SimVar > " << code.c_str();
std::cout << " with String-ID: " << newStringSimVar.ID << ", String-Offset: " << newStringSimVar.Offset << " and Size: " << MOBIFLIGHT_STRING_SIMVAR_VALUE_MAX_LEN << std::endl;
}
std::cout << "MobiFlight[" << client->Name.c_str() << "]: RegisterStringSimVar StringSimVars Size: " << StringSimVars->size() << std::endl;
Expand All @@ -369,7 +417,7 @@ void RegisterStringSimVar(const std::string code, Client* client) {
newStringSimVar.Value = std::string(charVal, strnlen(charVal, MOBIFLIGHT_STRING_SIMVAR_VALUE_MAX_LEN));
WriteSimVar(newStringSimVar, client);
#if _DEBUG
std::cout << "MobiFlight[" << client->Name.c_str() << "]: RegisterStringSimVar > " << newStringSimVar.Name.c_str();
std::cout << "MobiFlight[" << client->Name.c_str() << "]: RegisterStringSimVar > " << code.c_str();
std::cout << " ID [" << newStringSimVar.ID << "] : Offset(" << newStringSimVar.Offset << ") : Value(" << newStringSimVar.Value << ")" << std::endl;
#endif
}
Expand All @@ -391,66 +439,99 @@ void ClearSimVars(Client* client) {
WriteSimVar(simVar, client);
}
client->StringSimVars.clear();
// clear RNP code list
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "RNP" should be "RPN" to match the actual data structure name (ReadRPNCode).

Suggested change
// clear RNP code list
// clear RPN code list

Copilot uses AI. Check for mistakes.
for (std::vector<ReadRPNCode>::iterator rit = RPNCodelist.begin(); rit != RPNCodelist.end();) {
for (std::vector<StringSimVar>::iterator it = (*rit).StringSimVars.begin(); it != (*rit).StringSimVars.end();) {
if ((*it).client == client) {
it = (*rit).StringSimVars.erase(it);
} else {
++it;
}
}
for (std::vector<SimVar>::iterator sit = (*rit).SimVars.begin(); sit != (*rit).SimVars.end();) {
if ((*sit).client == client) {
sit = (*rit).SimVars.erase(sit);
} else {
++sit;
}
}
//remove empty RNP code
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "RNP" should be "RPN" to match the actual data structure name (ReadRPNCode).

Suggested change
//remove empty RNP code
//remove empty RPN code

Copilot uses AI. Check for mistakes.
if ((*rit).StringSimVars.empty() && (*rit).SimVars.empty()) {
rit = RPNCodelist.erase(rit);
} else {
++rit;
}
}

std::cout << "MobiFlight[" << client->Name.c_str() << "]: Cleared SimVar tracking." << std::endl;
//client->RollingClientDataReadIndex = client->SimVars.begin();
client->RollingClientDataReadIndex = 0;
//client->RollingDataReadIndex = client->SimVars.begin();
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outdated comment reference. The comment refers to client->RollingDataReadIndex but the code now uses the global RollingDataReadIndex. Either update the comment to reflect the current implementation or remove it as it may cause confusion.

Suggested change
//client->RollingDataReadIndex = client->SimVars.begin();

Copilot uses AI. Check for mistakes.
RollingDataReadIndex = 0;
}


// Read a single SimVar and send the current value to SimConnect Clients (overloaded for float SimVars)
void ReadSimVar(SimVar &simVar, Client* client) {
void ReadSimVarFloat(ReadRPNCode &rpn) {
FLOAT64 floatVal = 0;

execute_calculator_code(std::string(simVar.Name).c_str(), &floatVal, nullptr, nullptr);
execute_calculator_code(std::string(rpn.Code).c_str(), &floatVal, nullptr, nullptr);

if (simVar.Value == floatVal) return;
simVar.Value = floatVal;
for (auto& simVar : rpn.SimVars) {
if ((simVar.Value > floatVal) && (simVar.Value - floatVal < 0.00001F)) {
continue;
} else if ((simVar.Value < floatVal) && (floatVal - simVar.Value < 0.00001F)) {
continue;
}
Comment on lines +479 to +483
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The floating-point comparison logic is flawed. The condition (simVar.Value > floatVal) && (simVar.Value - floatVal < 0.00001F) will skip updates when values are nearly equal AND the old value is greater. However, if simVar.Value is exactly equal to floatVal, or if they differ by more than 0.00001, the update proceeds. The logic should use std::abs(simVar.Value - floatVal) < 0.00001F to properly check if values are approximately equal regardless of which is larger. Also note the epsilon threshold may be too small for some float ranges.

Copilot uses AI. Check for mistakes.
simVar.Value = floatVal;

WriteSimVar(simVar, client);
WriteSimVar(simVar, simVar.client);

#if _DEBUG
std::cout << "MobiFlight[" << client->Name.c_str() << "]: SimVar " << simVar.Name.c_str();
std::cout << " with ID " << simVar.ID << " has value " << simVar.Value << std::endl;
std::cout << "MobiFlight[" << simVar.client->Name.c_str() << "]: SimVar " << rpn.Code.c_str();
std::cout << " with ID " << simVar.ID << " has value " << simVar.Value << std::endl;
#endif
}
}

// Read a single SimVar and send the current value to SimConnect Clients (overloaded for string SimVars)
void ReadSimVar(StringSimVar &simVar, Client* client) {
void ReadSimVarString(ReadRPNCode &rpn) {
PCSTRINGZ charVal = nullptr;

execute_calculator_code(std::string(simVar.Name).c_str(), nullptr, nullptr, &charVal);
execute_calculator_code(std::string(rpn.Code).c_str(), nullptr, nullptr, &charVal);
std::string stringVal = std::string(charVal, strnlen(charVal, MOBIFLIGHT_STRING_SIMVAR_VALUE_MAX_LEN));
if (simVar.Value == stringVal) return;
simVar.Value = stringVal;

WriteSimVar(simVar, client);
for (auto& simVar : rpn.StringSimVars) {
if (simVar.Value == stringVal) continue;
simVar.Value = stringVal;

WriteSimVar(simVar, simVar.client);

#if _DEBUG
std::cout << "MobiFlight[" << client->Name.c_str() << "]: StringSimVar " << simVar.Name.c_str();
std::cout << " with ID " << simVar.ID << " has value " << simVar.Value << std::endl;
std::cout << "MobiFlight[" << simVar.client->Name.c_str() << "]: StringSimVar " << rpn.Code.c_str();
std::cout << " with ID " << simVar.ID << " has value " << simVar.Value << std::endl;
#endif
}
}

// Read a single SimVar and send the current value to SimConnect Clients (overloaded for float SimVars)
void ReadSimVar(ReadRPNCode &rpn) {
if (rpn.RetType == 0) {
ReadSimVarFloat(rpn);
Comment on lines +515 to +518
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing case for RetType == 1 (integer). According to the comment on line 95, RetType can be 0 (float), 1 (integer), or 2 (string). However, the code only handles types 0 and 2. If a ReadRPNCode entry has RetType == 1, it will be silently ignored without executing the RPN code or updating any values. Either add handling for integer types or remove the integer type from the comment if it's not supported.

Suggested change
// Read a single SimVar and send the current value to SimConnect Clients (overloaded for float SimVars)
void ReadSimVar(ReadRPNCode &rpn) {
if (rpn.RetType == 0) {
ReadSimVarFloat(rpn);
// Read a single SimVar and send the current value to SimConnect Clients (overloaded for float SimVars)
void ReadSimVarInt(ReadRPNCode &rpn) {
int intVal = 0;
execute_calculator_code(std::string(rpn.Code).c_str(), &intVal, nullptr, nullptr);
for (auto& simVar : rpn.SimVars) {
if (simVar.Value == static_cast<double>(intVal)) continue;
simVar.Value = static_cast<double>(intVal);
WriteSimVar(simVar, simVar.client);
#if _DEBUG
std::cout << "MobiFlight[" << simVar.client->Name.c_str() << "]: IntSimVar " << rpn.Code.c_str();
std::cout << " with ID " << simVar.ID << " has value " << simVar.Value << std::endl;
#endif
}
}
void ReadSimVar(ReadRPNCode &rpn) {
if (rpn.RetType == 0) {
ReadSimVarFloat(rpn);
} else if (rpn.RetType == 1) {
ReadSimVarInt(rpn);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only float and string, there is no int

} else if (rpn.RetType == 2) {
ReadSimVarString(rpn);
}
}

// Read all dynamically registered SimVars
void ReadSimVars() {
for (auto& client : RegisteredClients) {
std::vector<SimVar>* SimVars = &(client->SimVars);
std::vector<StringSimVar>* StringSimVars = &(client->StringSimVars);
int totalSimVars = RPNCodelist.size();
int maxVarsPerFrame = (totalSimVars < MOBIFLIGHT_MAX_VARS_PER_FRAME) ? totalSimVars : MOBIFLIGHT_MAX_VARS_PER_FRAME;

int totalSimVars = SimVars->size() + StringSimVars->size();
int maxVarsPerFrame = (totalSimVars < MOBIFLIGHT_MAX_VARS_PER_FRAME) ? totalSimVars : MOBIFLIGHT_MAX_VARS_PER_FRAME;
for (int i=0; i < maxVarsPerFrame; ++i) {
ReadSimVar(RPNCodelist.at(RollingDataReadIndex));

for (int i=0; i < maxVarsPerFrame; ++i) {
if(client->RollingClientDataReadIndex < SimVars->size() ) {
ReadSimVar(SimVars->at(client->RollingClientDataReadIndex), client);
}
else {
ReadSimVar(StringSimVars->at(client->RollingClientDataReadIndex - SimVars->size()), client);
}
client->RollingClientDataReadIndex++;
if (client->RollingClientDataReadIndex >= totalSimVars)
client->RollingClientDataReadIndex = 0;
}
RollingDataReadIndex++;
if (RollingDataReadIndex >= totalSimVars)
RollingDataReadIndex = 0;
}
Comment on lines +529 to 535
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential out-of-bounds access. If RPNCodelist is empty (totalSimVars == 0), maxVarsPerFrame will be 0, and the loop won't execute. However, if the list becomes empty during execution (e.g., all clients disconnect between frames), and RollingDataReadIndex is non-zero from a previous frame, the next frame could attempt to access RPNCodelist.at(RollingDataReadIndex) with an invalid index. Add a check: if (totalSimVars == 0) { RollingDataReadIndex = 0; return; } before the loop.

Copilot uses AI. Check for mistakes.
}

Expand Down Expand Up @@ -544,8 +625,7 @@ Client* RegisterNewClient(const std::string clientName) {
newClient->DataDefinitionIDStringCommand = newClient->DataDefinitionIDStringResponse + 1;
newClient->SimVars = std::vector<SimVar>();
newClient->StringSimVars = std::vector<StringSimVar>();
//newClient->RollingClientDataReadIndex = newClient->SimVars.begin();
newClient->RollingClientDataReadIndex = 0;
RollingDataReadIndex = 0;
newClient->DataDefinitionIdSimVarsStart = SIMVAR_OFFSET + (newClient->ID * (CLIENT_DATA_DEF_ID_SIMVAR_RANGE + CLIENT_DATA_DEF_ID_STRINGVAR_RANGE));
newClient->DataDefinitionIdStringVarsStart = newClient->DataDefinitionIdSimVarsStart + CLIENT_DATA_DEF_ID_SIMVAR_RANGE;

Expand Down