Added support for heartbeat global option.
This commit is contained in:
parent
8685afd9f6
commit
366537dc22
|
@ -34,7 +34,8 @@ CServerProxy::CServerProxy(IClient* client,
|
|||
m_client(client),
|
||||
m_input(adoptedInput),
|
||||
m_output(adoptedOutput),
|
||||
m_seqNum(0)
|
||||
m_seqNum(0),
|
||||
m_heartRate(kHeartRate)
|
||||
{
|
||||
assert(m_client != NULL);
|
||||
assert(m_input != NULL);
|
||||
|
@ -76,7 +77,7 @@ CServerProxy::mainLoop()
|
|||
// wait for a message
|
||||
LOG((CLOG_DEBUG2 "waiting for message"));
|
||||
UInt8 code[4];
|
||||
UInt32 n = getInputStream()->read(code, 4, kHeartRate);
|
||||
UInt32 n = getInputStream()->read(code, 4, m_heartRate);
|
||||
|
||||
// check if server hungup
|
||||
if (n == 0) {
|
||||
|
@ -86,7 +87,7 @@ CServerProxy::mainLoop()
|
|||
|
||||
// check for time out
|
||||
if (n == (UInt32)-1 ||
|
||||
(kHeartRate >= 0.0 && heartbeat.getTime() > kHeartRate)) {
|
||||
(m_heartRate >= 0.0 && heartbeat.getTime() > m_heartRate)) {
|
||||
// send heartbeat
|
||||
CLock lock(&m_mutex);
|
||||
CProtocolUtil::writef(getOutputStream(), kMsgCNoop);
|
||||
|
@ -663,9 +664,20 @@ CServerProxy::resetOptions()
|
|||
// forward
|
||||
getClient()->resetOptions();
|
||||
|
||||
CLock lock(&m_mutex);
|
||||
|
||||
// reset heart rate
|
||||
m_heartRate = kHeartRate;
|
||||
|
||||
// reset modifier translation table
|
||||
for (KeyModifierID id = 0; id < kKeyModifierIDLast; ++id)
|
||||
for (KeyModifierID id = 0; id < kKeyModifierIDLast; ++id) {
|
||||
m_modifierTranslationTable[id] = id;
|
||||
}
|
||||
|
||||
// send heartbeat if necessary
|
||||
if (m_heartRate >= 0.0) {
|
||||
CProtocolUtil::writef(getOutputStream(), kMsgCNoop);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -679,6 +691,8 @@ CServerProxy::setOptions()
|
|||
// forward
|
||||
getClient()->setOptions(options);
|
||||
|
||||
CLock lock(&m_mutex);
|
||||
|
||||
// update modifier table
|
||||
for (UInt32 i = 0, n = options.size(); i < n; i += 2) {
|
||||
KeyModifierID id = kKeyModifierIDNull;
|
||||
|
@ -697,6 +711,15 @@ CServerProxy::setOptions()
|
|||
else if (options[i] == kOptionModifierMapForSuper) {
|
||||
id = kKeyModifierIDSuper;
|
||||
}
|
||||
else if (options[i] == kOptionHeartbeat) {
|
||||
// update heart rate
|
||||
m_heartRate = 1.0e-3 * static_cast<double>(options[i + 1]);
|
||||
|
||||
// send heartbeat if necessary
|
||||
if (m_heartRate >= 0.0) {
|
||||
CProtocolUtil::writef(getOutputStream(), kMsgCNoop);
|
||||
}
|
||||
}
|
||||
if (id != kKeyModifierIDNull) {
|
||||
m_modifierTranslationTable[id] =
|
||||
static_cast<KeyModifierID>(options[i + 1]);
|
||||
|
|
|
@ -130,6 +130,7 @@ private:
|
|||
bool m_ignoreMouse;
|
||||
|
||||
KeyModifierID m_modifierTranslationTable[kKeyModifierIDLast];
|
||||
double m_heartRate;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -59,3 +59,9 @@ CClientProxy::getName() const
|
|||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
const CMutex*
|
||||
CClientProxy::getMutex() const
|
||||
{
|
||||
return &m_mutex;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#define CCLIENTPROXY_H
|
||||
|
||||
#include "IClient.h"
|
||||
#include "CMutex.h"
|
||||
#include "CString.h"
|
||||
|
||||
class IInputStream;
|
||||
|
@ -84,7 +85,16 @@ public:
|
|||
virtual void getCursorPos(SInt32& x, SInt32& y) const = 0;
|
||||
virtual void getCursorCenter(SInt32& x, SInt32& y) const = 0;
|
||||
|
||||
protected:
|
||||
//! Get mutex
|
||||
/*!
|
||||
Returns the mutex for this object. Subclasses should use this
|
||||
mutex to protect their data.
|
||||
*/
|
||||
const CMutex* getMutex() const;
|
||||
|
||||
private:
|
||||
CMutex m_mutex;
|
||||
IServer* m_server;
|
||||
CString m_name;
|
||||
IInputStream* m_input;
|
||||
|
|
|
@ -31,7 +31,9 @@
|
|||
|
||||
CClientProxy1_0::CClientProxy1_0(IServer* server, const CString& name,
|
||||
IInputStream* input, IOutputStream* output) :
|
||||
CClientProxy(server, name, input, output)
|
||||
CClientProxy(server, name, input, output),
|
||||
m_heartRate(kHeartRate),
|
||||
m_heartDeath(kHeartRate * kHeartBeatsUntilDeath)
|
||||
{
|
||||
for (UInt32 i = 0; i < kClipboardEnd; ++i) {
|
||||
m_clipboardDirty[i] = true;
|
||||
|
@ -81,7 +83,7 @@ CClientProxy1_0::mainLoop()
|
|||
|
||||
// wait for a message
|
||||
UInt8 code[4];
|
||||
UInt32 n = getInputStream()->read(code, 4, kHeartRate);
|
||||
UInt32 n = getInputStream()->read(code, 4, m_heartRate);
|
||||
CThread::testCancel();
|
||||
|
||||
// check if client hungup
|
||||
|
@ -92,7 +94,7 @@ CClientProxy1_0::mainLoop()
|
|||
|
||||
// check if client has stopped sending heartbeats
|
||||
if (n == (UInt32)-1) {
|
||||
if (kHeartDeath >= 0.0 && heartTimer.getTime() > kHeartDeath) {
|
||||
if (m_heartDeath >= 0.0 && heartTimer.getTime() > m_heartDeath) {
|
||||
LOG((CLOG_NOTE "client \"%s\" is dead", getName().c_str()));
|
||||
return;
|
||||
}
|
||||
|
@ -117,6 +119,7 @@ CClientProxy1_0::mainLoop()
|
|||
}
|
||||
else if (memcmp(code, kMsgCNoop, 4) == 0) {
|
||||
// discard no-ops
|
||||
LOG((CLOG_DEBUG2 "no-op from", getName().c_str()));
|
||||
continue;
|
||||
}
|
||||
else if (memcmp(code, kMsgCClipboard, 4) == 0) {
|
||||
|
@ -168,7 +171,7 @@ void
|
|||
CClientProxy1_0::setClipboard(ClipboardID id, const CString& data)
|
||||
{
|
||||
// ignore if this clipboard is already clean
|
||||
CLock lock(&m_mutex);
|
||||
CLock lock(getMutex());
|
||||
if (m_clipboardDirty[id]) {
|
||||
// this clipboard is now clean
|
||||
m_clipboardDirty[id] = false;
|
||||
|
@ -185,14 +188,14 @@ CClientProxy1_0::grabClipboard(ClipboardID id)
|
|||
CProtocolUtil::writef(getOutputStream(), kMsgCClipboard, id, 0);
|
||||
|
||||
// this clipboard is now dirty
|
||||
CLock lock(&m_mutex);
|
||||
CLock lock(getMutex());
|
||||
m_clipboardDirty[id] = true;
|
||||
}
|
||||
|
||||
void
|
||||
CClientProxy1_0::setClipboardDirty(ClipboardID id, bool dirty)
|
||||
{
|
||||
CLock lock(&m_mutex);
|
||||
CLock lock(getMutex());
|
||||
m_clipboardDirty[id] = dirty;
|
||||
}
|
||||
|
||||
|
@ -257,6 +260,11 @@ CClientProxy1_0::resetOptions()
|
|||
{
|
||||
LOG((CLOG_DEBUG1 "send reset options to \"%s\"", getName().c_str()));
|
||||
CProtocolUtil::writef(getOutputStream(), kMsgCResetOptions);
|
||||
|
||||
// reset heart rate and death
|
||||
CLock lock(getMutex());
|
||||
m_heartRate = kHeartRate;
|
||||
m_heartDeath = kHeartRate * kHeartBeatsUntilDeath;
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -264,19 +272,31 @@ CClientProxy1_0::setOptions(const COptionsList& options)
|
|||
{
|
||||
LOG((CLOG_DEBUG1 "send set options to \"%s\" size=%d", getName().c_str(), options.size()));
|
||||
CProtocolUtil::writef(getOutputStream(), kMsgDSetOptions, &options);
|
||||
|
||||
// check options
|
||||
CLock lock(getMutex());
|
||||
for (UInt32 i = 0, n = options.size(); i < n; i += 2) {
|
||||
if (options[i] == kOptionHeartbeat) {
|
||||
m_heartRate = 1.0e-3 * static_cast<double>(options[i + 1]);
|
||||
if (m_heartRate <= 0.0) {
|
||||
m_heartRate = -1.0;
|
||||
}
|
||||
m_heartDeath = m_heartRate * kHeartBeatsUntilDeath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SInt32
|
||||
CClientProxy1_0::getJumpZoneSize() const
|
||||
{
|
||||
CLock lock(&m_mutex);
|
||||
CLock lock(getMutex());
|
||||
return m_info.m_zoneSize;
|
||||
}
|
||||
|
||||
void
|
||||
CClientProxy1_0::getShape(SInt32& x, SInt32& y, SInt32& w, SInt32& h) const
|
||||
{
|
||||
CLock lock(&m_mutex);
|
||||
CLock lock(getMutex());
|
||||
x = m_info.m_x;
|
||||
y = m_info.m_y;
|
||||
w = m_info.m_w;
|
||||
|
@ -292,7 +312,7 @@ CClientProxy1_0::getCursorPos(SInt32&, SInt32&) const
|
|||
void
|
||||
CClientProxy1_0::getCursorCenter(SInt32& x, SInt32& y) const
|
||||
{
|
||||
CLock lock(&m_mutex);
|
||||
CLock lock(getMutex());
|
||||
x = m_info.m_mx;
|
||||
y = m_info.m_my;
|
||||
}
|
||||
|
@ -301,7 +321,7 @@ void
|
|||
CClientProxy1_0::recvInfo(bool notify)
|
||||
{
|
||||
{
|
||||
CLock lock(&m_mutex);
|
||||
CLock lock(getMutex());
|
||||
|
||||
// parse the message
|
||||
SInt16 x, y, w, h, zoneSize, mx, my;
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
#include "CClientProxy.h"
|
||||
#include "ProtocolTypes.h"
|
||||
#include "CMutex.h"
|
||||
|
||||
//! Proxy for client implementing protocol version 1.0
|
||||
class CClientProxy1_0 : public CClientProxy {
|
||||
|
@ -60,9 +59,10 @@ private:
|
|||
void recvGrabClipboard();
|
||||
|
||||
private:
|
||||
CMutex m_mutex;
|
||||
CClientInfo m_info;
|
||||
bool m_clipboardDirty[kClipboardEnd];
|
||||
double m_heartRate;
|
||||
double m_heartDeath;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#include "XSocket.h"
|
||||
#include "stdistream.h"
|
||||
#include "stdostream.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
//
|
||||
// CConfig
|
||||
|
@ -504,31 +505,57 @@ CConfig::readLine(std::istream& s, CString& line)
|
|||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
OptionValue
|
||||
CConfig::parseBoolean(const CString& arg)
|
||||
{
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "true"))
|
||||
return true;
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "false"))
|
||||
return false;
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "true")) {
|
||||
return static_cast<OptionValue>(true);
|
||||
}
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "false")) {
|
||||
return static_cast<OptionValue>(false);
|
||||
}
|
||||
throw XConfigRead("invalid argument");
|
||||
}
|
||||
|
||||
OptionValue
|
||||
CConfig::parseInt(const CString& arg)
|
||||
{
|
||||
const char* s = arg.c_str();
|
||||
char* end;
|
||||
long tmp = strtol(s, &end, 10);
|
||||
if (*end != '\0') {
|
||||
// invalid characters
|
||||
throw XConfigRead("invalid argument");
|
||||
}
|
||||
OptionValue value = static_cast<OptionValue>(tmp);
|
||||
if (value != tmp) {
|
||||
// out of range
|
||||
throw XConfigRead("argument out of range");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
OptionValue
|
||||
CConfig::parseModifierKey(const CString& arg)
|
||||
{
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "shift"))
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "shift")) {
|
||||
return static_cast<OptionValue>(kKeyModifierIDShift);
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "ctrl"))
|
||||
}
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "ctrl")) {
|
||||
return static_cast<OptionValue>(kKeyModifierIDControl);
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "alt"))
|
||||
}
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "alt")) {
|
||||
return static_cast<OptionValue>(kKeyModifierIDAlt);
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "meta"))
|
||||
}
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "meta")) {
|
||||
return static_cast<OptionValue>(kKeyModifierIDMeta);
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "super"))
|
||||
}
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "super")) {
|
||||
return static_cast<OptionValue>(kKeyModifierIDSuper);
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "none"))
|
||||
}
|
||||
if (CStringUtil::CaselessCmp::equal(arg, "none")) {
|
||||
return static_cast<OptionValue>(kKeyModifierIDNull);
|
||||
}
|
||||
throw XConfigRead("invalid argument");
|
||||
}
|
||||
|
||||
|
@ -556,10 +583,13 @@ CConfig::getOptionName(OptionID id)
|
|||
if (id == kOptionModifierMapForSuper) {
|
||||
return "super";
|
||||
}
|
||||
if (id == kOptionHeartbeat) {
|
||||
return "heartbeat";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char*
|
||||
CString
|
||||
CConfig::getOptionValue(OptionID id, OptionValue value)
|
||||
{
|
||||
if (id == kOptionHalfDuplexCapsLock ||
|
||||
|
@ -591,6 +621,9 @@ CConfig::getOptionValue(OptionID id, OptionValue value)
|
|||
return "none";
|
||||
}
|
||||
}
|
||||
if (id == kOptionHeartbeat) {
|
||||
return CStringUtil::print("%d", value);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
@ -599,7 +632,7 @@ void
|
|||
CConfig::readSection(std::istream& s)
|
||||
{
|
||||
static const char s_section[] = "section:";
|
||||
static const char s_network[] = "network";
|
||||
static const char s_options[] = "options";
|
||||
static const char s_screens[] = "screens";
|
||||
static const char s_links[] = "links";
|
||||
static const char s_aliases[] = "aliases";
|
||||
|
@ -627,8 +660,8 @@ CConfig::readSection(std::istream& s)
|
|||
}
|
||||
|
||||
// read section
|
||||
if (name == s_network) {
|
||||
readSectionNetwork(s);
|
||||
if (name == s_options) {
|
||||
readSectionOptions(s);
|
||||
}
|
||||
else if (name == s_screens) {
|
||||
readSectionScreens(s);
|
||||
|
@ -645,7 +678,7 @@ CConfig::readSection(std::istream& s)
|
|||
}
|
||||
|
||||
void
|
||||
CConfig::readSectionNetwork(std::istream& s)
|
||||
CConfig::readSectionOptions(std::istream& s)
|
||||
{
|
||||
CString line;
|
||||
CString name;
|
||||
|
@ -693,6 +726,9 @@ CConfig::readSectionNetwork(std::istream& s)
|
|||
throw XConfigRead("invalid http argument");
|
||||
}
|
||||
}
|
||||
else if (name == "heartbeat") {
|
||||
addOption("", kOptionHeartbeat, parseInt(value));
|
||||
}
|
||||
else {
|
||||
throw XConfigRead("unknown argument");
|
||||
}
|
||||
|
@ -931,15 +967,28 @@ operator>>(std::istream& s, CConfig& config)
|
|||
std::ostream&
|
||||
operator<<(std::ostream& s, const CConfig& config)
|
||||
{
|
||||
// network section
|
||||
s << "section: network" << std::endl;
|
||||
// options section
|
||||
s << "section: options" << std::endl;
|
||||
const CConfig::CScreenOptions* options = config.getOptions("");
|
||||
if (options != NULL && options->size() > 0) {
|
||||
for (CConfig::CScreenOptions::const_iterator
|
||||
option = options->begin();
|
||||
option != options->end(); ++option) {
|
||||
const char* name = CConfig::getOptionName(option->first);
|
||||
CString value = CConfig::getOptionValue(option->first,
|
||||
option->second);
|
||||
if (name != NULL && !value.empty()) {
|
||||
s << "\t" << name << " = " << value << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.m_synergyAddress.isValid()) {
|
||||
s << "\taddress=" << config.m_synergyAddress.getHostname().c_str() <<
|
||||
std::endl;
|
||||
s << "\taddress = " <<
|
||||
config.m_synergyAddress.getHostname().c_str() << std::endl;
|
||||
}
|
||||
if (config.m_httpAddress.isValid()) {
|
||||
s << "\thttp=" << config.m_httpAddress.getHostname().c_str() <<
|
||||
std::endl;
|
||||
s << "\thttp = " <<
|
||||
config.m_httpAddress.getHostname().c_str() << std::endl;
|
||||
}
|
||||
s << "end" << std::endl;
|
||||
|
||||
|
@ -954,9 +1003,9 @@ operator<<(std::ostream& s, const CConfig& config)
|
|||
option = options->begin();
|
||||
option != options->end(); ++option) {
|
||||
const char* name = CConfig::getOptionName(option->first);
|
||||
const char* value = CConfig::getOptionValue(option->first,
|
||||
CString value = CConfig::getOptionValue(option->first,
|
||||
option->second);
|
||||
if (name != NULL && value != NULL) {
|
||||
if (name != NULL && !value.empty()) {
|
||||
s << "\t\t" << name << " = " << value << std::endl;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -292,12 +292,13 @@ public:
|
|||
|
||||
private:
|
||||
static bool readLine(std::istream&, CString&);
|
||||
static bool parseBoolean(const CString&);
|
||||
static OptionValue parseBoolean(const CString&);
|
||||
static OptionValue parseInt(const CString&);
|
||||
static OptionValue parseModifierKey(const CString&);
|
||||
static const char* getOptionName(OptionID);
|
||||
static const char* getOptionValue(OptionID, OptionValue);
|
||||
static CString getOptionValue(OptionID, OptionValue);
|
||||
void readSection(std::istream&);
|
||||
void readSectionNetwork(std::istream&);
|
||||
void readSectionOptions(std::istream&);
|
||||
void readSectionScreens(std::istream&);
|
||||
void readSectionLinks(std::istream&);
|
||||
void readSectionAliases(std::istream&);
|
||||
|
|
|
@ -217,12 +217,29 @@ CServer::setConfig(const CConfig& config)
|
|||
CLock lock(&m_mutex);
|
||||
m_config = config;
|
||||
|
||||
// process global options
|
||||
const CConfig::CScreenOptions* options = m_config.getOptions("");
|
||||
if (options != NULL && options->size() > 0) {
|
||||
/*
|
||||
for (CConfig::CScreenOptions::const_iterator index = options->begin();
|
||||
index != options->end(); ++index) {
|
||||
const OptionID id = index->first;
|
||||
const OptionValue value = index->second;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// tell primary screen about reconfiguration
|
||||
if (m_primaryClient != NULL) {
|
||||
m_primaryClient->reconfigure(getActivePrimarySides());
|
||||
}
|
||||
|
||||
// FIXME -- tell all (connected) clients about current options
|
||||
// tell all (connected) clients about current options
|
||||
for (CClientList::const_iterator index = m_clients.begin();
|
||||
index != m_clients.end(); ++index) {
|
||||
IClient* client = index->second;
|
||||
sendOptions(client);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1622,21 +1639,32 @@ CServer::sendOptions(IClient* client) const
|
|||
{
|
||||
// note -- must be locked on entry
|
||||
|
||||
// look up options for client. we're done if there aren't any.
|
||||
COptionsList optionsList;
|
||||
|
||||
// look up options for client
|
||||
const CConfig::CScreenOptions* options =
|
||||
m_config.getOptions(client->getName());
|
||||
if (options == NULL || options->size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options != NULL && options->size() > 0) {
|
||||
// convert options to a more convenient form for sending
|
||||
COptionsList optionsList;
|
||||
optionsList.reserve(2 * options->size());
|
||||
for (CConfig::CScreenOptions::const_iterator index = options->begin();
|
||||
index != options->end(); ++index) {
|
||||
optionsList.push_back(index->first);
|
||||
optionsList.push_back(static_cast<UInt32>(index->second));
|
||||
}
|
||||
}
|
||||
|
||||
// look up global options
|
||||
options = m_config.getOptions("");
|
||||
if (options != NULL && options->size() > 0) {
|
||||
// convert options to a more convenient form for sending
|
||||
optionsList.reserve(optionsList.size() + 2 * options->size());
|
||||
for (CConfig::CScreenOptions::const_iterator index = options->begin();
|
||||
index != options->end(); ++index) {
|
||||
optionsList.push_back(index->first);
|
||||
optionsList.push_back(static_cast<UInt32>(index->second));
|
||||
}
|
||||
}
|
||||
|
||||
// send the options
|
||||
client->setOptions(optionsList);
|
||||
|
|
|
@ -50,6 +50,7 @@ static const OptionID kOptionModifierMapForControl = OPTION_CODE("MMFC");
|
|||
static const OptionID kOptionModifierMapForAlt = OPTION_CODE("MMFA");
|
||||
static const OptionID kOptionModifierMapForMeta = OPTION_CODE("MMFM");
|
||||
static const OptionID kOptionModifierMapForSuper = OPTION_CODE("MMFR");
|
||||
static const OptionID kOptionHeartbeat = OPTION_CODE("HART");
|
||||
//@}
|
||||
|
||||
#undef OPTION_CODE
|
||||
|
|
|
@ -31,8 +31,8 @@ static const UInt32 kMaxHelloLength = 1024;
|
|||
// heartbeat.
|
||||
static const double kHeartRate = -1.0;
|
||||
|
||||
// time without a heartbeat that constitutes death
|
||||
static const double kHeartDeath = 3.0 * kHeartRate;
|
||||
// number of skipped heartbeats that constitutes death
|
||||
static const double kHeartBeatsUntilDeath = 3.0;
|
||||
|
||||
// direction constants
|
||||
enum EDirection {
|
||||
|
|
Loading…
Reference in New Issue