first commit
This commit is contained in:
20
src/MultiPlayer/CMakeLists.txt
Normal file
20
src/MultiPlayer/CMakeLists.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
include(FlightGearComponent)
|
||||
|
||||
set(SOURCES
|
||||
multiplaymgr.cxx
|
||||
tiny_xdr.cxx
|
||||
MPServerResolver.cxx
|
||||
mpirc.cxx
|
||||
cpdlc.cxx
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
multiplaymgr.hxx
|
||||
tiny_xdr.hxx
|
||||
MPServerResolver.hxx
|
||||
mpirc.hxx
|
||||
cpdlc.hxx
|
||||
mpmessages.hxx
|
||||
)
|
||||
|
||||
flightgear_component(MultiPlayer "${SOURCES}" "${HEADERS}")
|
||||
203
src/MultiPlayer/MPServerResolver.cxx
Normal file
203
src/MultiPlayer/MPServerResolver.cxx
Normal file
@@ -0,0 +1,203 @@
|
||||
|
||||
/*
|
||||
MPServerResolver.cxx - mpserver names lookup via DNS
|
||||
Written and copyright by Torsten Dreyer - November 2016
|
||||
|
||||
This file is part of FlightGear.
|
||||
|
||||
FlightGear is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FlightGear is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FlightGear. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <Network/DNSClient.hxx>
|
||||
#include <Main/fg_props.hxx>
|
||||
#include <cJSON.h>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "MPServerResolver.hxx"
|
||||
|
||||
using namespace simgear;
|
||||
|
||||
/**
|
||||
* Build a name=value map from base64 encoded JSON string
|
||||
*/
|
||||
class MPServerProperties : public std::map<string, string> {
|
||||
public:
|
||||
MPServerProperties (string b64)
|
||||
{
|
||||
std::vector<unsigned char> b64dec;
|
||||
simgear::strutils::decodeBase64 (b64, b64dec);
|
||||
auto jsonString = string ((char*) b64dec.data (), b64dec.size ());
|
||||
cJSON * json = ::cJSON_Parse (jsonString.c_str ());
|
||||
if (json) {
|
||||
for (int i = 0; i < ::cJSON_GetArraySize (json); i++) {
|
||||
cJSON * cj = ::cJSON_GetArrayItem (json, i);
|
||||
if (cj->string && cj->valuestring)
|
||||
emplace (cj->string, cj->valuestring);
|
||||
}
|
||||
::cJSON_Delete (json);
|
||||
} else {
|
||||
SG_LOG(SG_NETWORK,SG_WARN, "MPServerResolver: Can't parse JSON string '" << jsonString << "'" );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class MPServerResolver::MPServerResolver_priv {
|
||||
public:
|
||||
enum {
|
||||
INIT, LOADING_SRV_RECORDS, LOAD_NEXT_TXT_RECORD, LOADING_TXT_RECORDS, DONE,
|
||||
} _state = INIT;
|
||||
|
||||
FGDNSClient * _dnsClient = globals->get_subsystem<FGDNSClient> ();
|
||||
DNS::Request_ptr _dnsRequest;
|
||||
PropertyList _serverNodes;
|
||||
PropertyList::const_iterator _serverNodes_it;
|
||||
};
|
||||
|
||||
MPServerResolver::~MPServerResolver ()
|
||||
{
|
||||
if (_priv->_dnsRequest) {
|
||||
_priv->_dnsRequest->cancel();
|
||||
}
|
||||
|
||||
delete _priv;
|
||||
}
|
||||
|
||||
MPServerResolver::MPServerResolver () :
|
||||
_priv (new MPServerResolver_priv ())
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
MPServerResolver::run ()
|
||||
{
|
||||
//SG_LOG(SG_NETWORK, SG_DEBUG, "MPServerResolver::run() with state=" << _priv->_state );
|
||||
switch (_priv->_state) {
|
||||
// First call - fire DNS lookup for SRV records
|
||||
case MPServerResolver_priv::INIT:
|
||||
if (!_priv->_dnsClient) {
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "MPServerResolver: DNS subsystem not available.");
|
||||
onFailure ();
|
||||
return;
|
||||
}
|
||||
|
||||
_priv->_dnsRequest = new DNS::SRVRequest (_dnsName, _service, _protocol);
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: sending DNS request for " << _priv->_dnsRequest->getDn());
|
||||
_priv->_dnsClient->makeRequest (_priv->_dnsRequest);
|
||||
_priv->_state = MPServerResolver_priv::LOADING_SRV_RECORDS;
|
||||
break;
|
||||
|
||||
// Check if response from SRV Query
|
||||
case MPServerResolver_priv::LOADING_SRV_RECORDS:
|
||||
if (_priv->_dnsRequest->isTimeout ()) {
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "Timeout waiting for DNS response. Query was: " << _priv->_dnsRequest->getDn());
|
||||
onFailure ();
|
||||
return;
|
||||
}
|
||||
if (_priv->_dnsRequest->isComplete ()) {
|
||||
// Create a child node under _targetNode for each SRV entry of the response
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: got DNS response for " << _priv->_dnsRequest->getDn());
|
||||
int idx = 0;
|
||||
for (DNS::SRVRequest::SRV_ptr entry : dynamic_cast<DNS::SRVRequest*> (_priv->_dnsRequest.get ())->entries) {
|
||||
SG_LOG(SG_NETWORK, SG_DEBUG,
|
||||
"MPServerResolver: SRV " << entry->priority << " " << entry->weight << " " << entry->port << " " << entry->target);
|
||||
if( 0 == entry->port ) {
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: Skipping offline host " << entry->target );
|
||||
continue;
|
||||
}
|
||||
SGPropertyNode * serverNode = _targetNode->getNode ("server", idx++, true);
|
||||
serverNode->getNode ("hostname", true)->setStringValue (entry->target);
|
||||
serverNode->getNode ("priority", true)->setIntValue (entry->priority);
|
||||
serverNode->getNode ("weight", true)->setIntValue (entry->weight);
|
||||
serverNode->getNode ("port", true)->setIntValue (entry->port);
|
||||
}
|
||||
|
||||
// prepare an iterator over the server-nodes to be used later when loading the TXT records
|
||||
_priv->_serverNodes = _targetNode->getChildren ("server");
|
||||
_priv->_serverNodes_it = _priv->_serverNodes.begin ();
|
||||
if (_priv->_serverNodes_it == _priv->_serverNodes.end ()) {
|
||||
// No SRV records found - flag failure
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "MPServerResolver: no multiplayer servers defined via DNS");
|
||||
onFailure ();
|
||||
return;
|
||||
}
|
||||
_priv->_state = MPServerResolver_priv::LOAD_NEXT_TXT_RECORD;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
// get the next TXT record
|
||||
case MPServerResolver_priv::LOAD_NEXT_TXT_RECORD:
|
||||
if (_priv->_serverNodes_it == _priv->_serverNodes.end ()) {
|
||||
// we are done with all servers
|
||||
_priv->_state = MPServerResolver_priv::DONE;
|
||||
break;
|
||||
}
|
||||
|
||||
// send the DNS query for the hostnames TXT record
|
||||
_priv->_dnsRequest = new DNS::TXTRequest ((*_priv->_serverNodes_it)->getStringValue ("hostname"));
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: sending DNS request for " << _priv->_dnsRequest->getDn());
|
||||
_priv->_dnsClient->makeRequest (_priv->_dnsRequest);
|
||||
_priv->_state = MPServerResolver_priv::LOADING_TXT_RECORDS;
|
||||
break;
|
||||
|
||||
// check if response for TXT query
|
||||
case MPServerResolver_priv::LOADING_TXT_RECORDS:
|
||||
if (_priv->_dnsRequest->isTimeout ()) {
|
||||
// on timeout, try proceeding with next server
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "Timeout waiting for DNS response. Query was: " << _priv->_dnsRequest->getDn());
|
||||
_priv->_state = MPServerResolver_priv::LOAD_NEXT_TXT_RECORD;
|
||||
++_priv->_serverNodes_it;
|
||||
break;
|
||||
}
|
||||
if (_priv->_dnsRequest->isComplete ()) {
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: got DNS response for " << _priv->_dnsRequest->getDn());
|
||||
// DNS::TXTRequest automatically extracts name=value entries for us, lets retrieve them
|
||||
auto attributes = dynamic_cast<DNS::TXTRequest*> (_priv->_dnsRequest.get ())->attributes;
|
||||
auto mpserverAttribute = attributes["flightgear-mpserver"];
|
||||
if (!mpserverAttribute.empty ()) {
|
||||
// we are only interested in the 'flightgear-mpserver=something' entry, this is a base64 encoded
|
||||
// JSON string, convert this into a map<string,string>
|
||||
MPServerProperties mpserverProperties (mpserverAttribute);
|
||||
for (auto prop : mpserverProperties) {
|
||||
// and store each as a node under our servers node.
|
||||
SG_LOG(SG_NETWORK, SG_DEBUG, "MPServerResolver: TXT record attribute " << prop.first << "=" << prop.second);
|
||||
// sanitize property name, don't allow dots or forward slash
|
||||
auto propertyName = prop.first;
|
||||
std::replace( propertyName.begin(), propertyName.end(), '.', '_');
|
||||
std::replace( propertyName.begin(), propertyName.end(), '/', '_');
|
||||
(*_priv->_serverNodes_it)->setStringValue (propertyName, prop.second);
|
||||
}
|
||||
} else {
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: TXT record attributes empty");
|
||||
}
|
||||
|
||||
// procede with the net node
|
||||
++_priv->_serverNodes_it;
|
||||
_priv->_state = MPServerResolver_priv::LOAD_NEXT_TXT_RECORD;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case MPServerResolver_priv::DONE:
|
||||
_priv->_dnsRequest.clear();
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
// Relinguish control, call me back on the next frame
|
||||
globals->get_event_mgr ()->addEvent ("MPServerResolver_update", [this](){ this->run(); }, .0);
|
||||
}
|
||||
|
||||
83
src/MultiPlayer/MPServerResolver.hxx
Normal file
83
src/MultiPlayer/MPServerResolver.hxx
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
MPServerResolver.hxx - mpserver names lookup via DNS
|
||||
Written and copyright by Torsten Dreyer - November 2016
|
||||
|
||||
This file is part of FlightGear.
|
||||
|
||||
FlightGear is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FlightGear is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FlightGear. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef __FG_MPSERVERRESOLVER_HXX
|
||||
#define __FG_MPSERVERRESOLVER_HXX
|
||||
|
||||
#include <string>
|
||||
#include <simgear/props/props.hxx>
|
||||
|
||||
class MPServerResolver {
|
||||
public:
|
||||
MPServerResolver();
|
||||
virtual ~MPServerResolver();
|
||||
void run();
|
||||
|
||||
/**
|
||||
* Set the target property where the server-list gets stored
|
||||
*
|
||||
* \param value the property node to use as a target
|
||||
*/
|
||||
void setTarget( SGPropertyNode_ptr value ) { _targetNode = value; }
|
||||
|
||||
/**
|
||||
* Set the dns domain name to query. This could be either a full qualified name including the
|
||||
* service and the protocol like _fgms._udp.flightgear.org or just the domain name like
|
||||
* flightgear.org. Use setService() and setProtocol() in the latter case.
|
||||
*
|
||||
* \param value the dnsname to use for the query.
|
||||
*/
|
||||
void setDnsName( const std::string & value ) { _dnsName = value; }
|
||||
|
||||
/** Set the service name to use for the query. Don't add the underscore, this gets added
|
||||
* automatically. This builds the fully qualified DNS name to query, together with
|
||||
* setProtocol() and setDnsName().
|
||||
*
|
||||
* \param value the service name to use for the query sans the leading underscore
|
||||
*/
|
||||
void setService( const std::string & value ) { _service = value; }
|
||||
|
||||
/** Set the protocol name to use for the query. Don't add the underscore, this gets added
|
||||
* automatically. This builds the fully qualified DNS name to query, together with
|
||||
* setService() and setDnsName().
|
||||
*
|
||||
* \param value the protocol name to use for the query sans the leading underscore
|
||||
*/
|
||||
void setProtocol( const std::string & value ) { _protocol = value; }
|
||||
|
||||
/** Handler to be called if the resolver process finishes with success. Does nothing by
|
||||
* default and should be overridden be the user.
|
||||
*/
|
||||
virtual void onSuccess() {};
|
||||
|
||||
/** Handler to be called if the resolver process terminates with an error. Does nothing by
|
||||
* default and should be overridden be the user.
|
||||
*/
|
||||
virtual void onFailure() {};
|
||||
|
||||
private:
|
||||
class MPServerResolver_priv;
|
||||
std::string _dnsName;
|
||||
std::string _service;
|
||||
std::string _protocol;
|
||||
SGPropertyNode_ptr _targetNode;
|
||||
MPServerResolver_priv * _priv;
|
||||
};
|
||||
|
||||
#endif // __FG_MPSERVERRESOLVER_HXX
|
||||
190
src/MultiPlayer/cpdlc.cxx
Normal file
190
src/MultiPlayer/cpdlc.cxx
Normal file
@@ -0,0 +1,190 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// cpdlc.cxx
|
||||
//
|
||||
// started November 2020
|
||||
// Authors: Michael Filhol, Henning Stahlke
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation; either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "cpdlc.hxx"
|
||||
#include <Main/fg_props.hxx>
|
||||
|
||||
const std::string CPDLC_IRC_SERVER {"mpirc.flightgear.org"};
|
||||
const std::string CPDLC_MSGPREFIX_CONNECT {"___CPDLC_CONNECT___"};
|
||||
const std::string CPDLC_MSGPREFIX_MSG {"___CPDLC_MSG___"};
|
||||
const std::string CPDLC_MSGPREFIX_DISCONNECT {"___CPDLC_DISCONNECT___"};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// CPDLCManager
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
CPDLCManager::CPDLCManager(IRCConnection* irc) :
|
||||
_irc(irc)
|
||||
{
|
||||
// CPDLC link status props
|
||||
_pStatus = fgGetNode("/network/cpdlc/link/status", true);
|
||||
_pStatus->setIntValue(_status);
|
||||
_pDataAuthority = fgGetNode("/network/cpdlc/link/data-authority", true);
|
||||
_pDataAuthority->setStringValue(_data_authority);
|
||||
// CPDLC message output
|
||||
_pMessage = fgGetNode("/network/cpdlc/rx/message", true);
|
||||
_pMessage->setStringValue("");
|
||||
_pNewMessage = fgGetNode("/network/cpdlc/rx/new-message", true);
|
||||
_pNewMessage->setBoolValue(0);
|
||||
}
|
||||
|
||||
CPDLCManager::~CPDLCManager()
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
connect will be called by
|
||||
1) user via a fgcommand defined in multiplaymgr
|
||||
2) CPDLC::update() while waiting for IRC to become ready
|
||||
|
||||
authority parameter is accepted only when coming from disconnected state,
|
||||
disconnect must be called first if user wants to change authority (however, ATC
|
||||
handover is not blocked by this)
|
||||
|
||||
IRC connection will be initiated if necessary, it takes a while to connect to IRC
|
||||
which is why connect is called from update() when in state CPDLC_WAIT_IRC_READY
|
||||
*/
|
||||
bool CPDLCManager::connect(const std::string authority = "")
|
||||
{
|
||||
// ensure we get an authority on first call but do not accept a change before
|
||||
// resetting _data_authority in disconnect()
|
||||
if (_data_authority.empty()) {
|
||||
if (authority.empty()) {
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "cpdlcConnect not possible: empty argument!");
|
||||
return false;
|
||||
} else {
|
||||
_data_authority = authority;
|
||||
}
|
||||
}
|
||||
if (!authority.empty() && authority != _data_authority) {
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "cpdlcConnect: cannot change authority now, use disconnect first!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// launch IRC connection as needed
|
||||
if (!_irc->isConnected()) {
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "Connecting to IRC server...");
|
||||
if (!_irc->login()) {
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "IRC login failed.");
|
||||
return false;
|
||||
}
|
||||
_status = CPDLC_WAIT_IRC_READY;
|
||||
return true;
|
||||
} else if (_irc->isReady() && _status != CPDLC_ONLINE) {
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "CPDLC sending 'connect'");
|
||||
_status = CPDLC_CONNECTING;
|
||||
_pStatus->setIntValue(_status);
|
||||
return _irc->sendPrivmsg(_data_authority, CPDLC_MSGPREFIX_CONNECT);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CPDLCManager::disconnect()
|
||||
{
|
||||
if (_irc && _irc->isConnected() && !_data_authority.empty()) {
|
||||
_irc->sendPrivmsg(_data_authority, CPDLC_MSGPREFIX_DISCONNECT);
|
||||
}
|
||||
_data_authority = "";
|
||||
_pDataAuthority->setStringValue(_data_authority);
|
||||
_status = CPDLC_OFFLINE;
|
||||
_pStatus->setIntValue(_status);
|
||||
}
|
||||
|
||||
bool CPDLCManager::send(const std::string message)
|
||||
{
|
||||
std::string textline(CPDLC_MSGPREFIX_MSG); // TODO surely a way to format std string in line
|
||||
textline += ' ';
|
||||
textline += message;
|
||||
return _irc->sendPrivmsg(_data_authority, textline);
|
||||
}
|
||||
|
||||
// move next message from input queue to property tree
|
||||
void CPDLCManager::getMessage()
|
||||
{
|
||||
if (!_incoming_messages.empty()) {
|
||||
struct IRCMessage entry = _incoming_messages.front();
|
||||
_incoming_messages.pop_front();
|
||||
_pMessage->setStringValue(entry.textline.substr(CPDLC_MSGPREFIX_MSG.length() + 1));
|
||||
if (_incoming_messages.empty()) {
|
||||
_pNewMessage->setBoolValue(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
update() call this regularly to complete connect and check for new messages
|
||||
update frequency requirement should be low (e.g. once per second) but frame rate
|
||||
will not hurt as the code is slim
|
||||
*/
|
||||
void CPDLCManager::update()
|
||||
{
|
||||
if (_irc) {
|
||||
if (_irc->isConnected()) {
|
||||
if (_status == CPDLC_WAIT_IRC_READY && _irc->isReady()) {
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "CPDLC IRC ready, connecting...");
|
||||
connect();
|
||||
}
|
||||
if (_irc->hasMessage()) {
|
||||
processMessage(_irc->getMessage());
|
||||
}
|
||||
}
|
||||
// IRC disconnected (unexpectedly)
|
||||
else if (_status != CPDLC_OFFLINE) {
|
||||
_status = CPDLC_OFFLINE;
|
||||
_pStatus->setIntValue(_status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process incoming message
|
||||
void CPDLCManager::processMessage(struct IRCMessage message)
|
||||
{
|
||||
// connection accepted by ATC, or new data authority (been transferred)
|
||||
if (message.textline.find(CPDLC_MSGPREFIX_CONNECT) == 0) {
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "CPDLC got connected.");
|
||||
_data_authority = message.sender;
|
||||
_pDataAuthority->setStringValue(_data_authority); // make this known to ACFT
|
||||
_status = CPDLC_ONLINE;
|
||||
_pStatus->setIntValue(_status);
|
||||
}
|
||||
// do not process message if sender does not match our current data authority
|
||||
if (message.sender != _data_authority) {
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "Received CPDLC message from foreign authority.");
|
||||
return;
|
||||
}
|
||||
// connection rejected, or terminated by ATC
|
||||
if (message.textline.find(CPDLC_MSGPREFIX_DISCONNECT) == 0) {
|
||||
if (message.sender == _data_authority) {
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "CPDLC got disconnect.");
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
// store valid message in queue for later retrieval by aircraft
|
||||
else if (message.textline.find(CPDLC_MSGPREFIX_MSG) == 0) {
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "CPDLC message");
|
||||
_incoming_messages.push_back(message);
|
||||
_pNewMessage->setBoolValue(1);
|
||||
}
|
||||
}
|
||||
76
src/MultiPlayer/cpdlc.hxx
Normal file
76
src/MultiPlayer/cpdlc.hxx
Normal file
@@ -0,0 +1,76 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// cpdlc.hxx
|
||||
//
|
||||
// started November 2020
|
||||
// Authors: Henning Stahlke, Michael Filhol
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation; either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef CPDLC_H
|
||||
#define CPDLC_H
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
#include <simgear/compiler.h>
|
||||
#include <simgear/props/props.hxx>
|
||||
#include <simgear/io/raw_socket.hxx>
|
||||
#include <simgear/io/sg_socket.hxx>
|
||||
#include "mpirc.hxx"
|
||||
|
||||
enum CPDLCStatus {
|
||||
CPDLC_OFFLINE,
|
||||
CPDLC_CONNECTING,
|
||||
CPDLC_ONLINE,
|
||||
|
||||
// private (not exposed to property tree) below this line:
|
||||
CPDLC_WAIT_IRC_READY,
|
||||
};
|
||||
|
||||
//
|
||||
// CPDLCManager implements a ControllerPilotDataLinkConnection via an IRC connection
|
||||
//
|
||||
class CPDLCManager
|
||||
{
|
||||
public:
|
||||
CPDLCManager(IRCConnection* irc);
|
||||
~CPDLCManager();
|
||||
|
||||
bool connect(const std::string authority);
|
||||
void disconnect();
|
||||
bool send(const std::string message);
|
||||
void getMessage();
|
||||
void update();
|
||||
|
||||
private:
|
||||
IRCConnection *_irc;
|
||||
std::string _data_authority {""};
|
||||
std::deque<struct IRCMessage> _incoming_messages;
|
||||
CPDLCStatus _status {CPDLC_OFFLINE};
|
||||
|
||||
SGPropertyNode *_pStatus {nullptr};
|
||||
SGPropertyNode *_pDataAuthority {nullptr};
|
||||
SGPropertyNode *_pMessage {nullptr};
|
||||
SGPropertyNode *_pNewMessage {nullptr};
|
||||
|
||||
void processMessage(struct IRCMessage entry);
|
||||
};
|
||||
|
||||
#endif
|
||||
327
src/MultiPlayer/mpirc.cxx
Normal file
327
src/MultiPlayer/mpirc.cxx
Normal file
@@ -0,0 +1,327 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// mpirc.cxx
|
||||
//
|
||||
// started November 2020
|
||||
// Authors: Michael Filhol, Henning Stahlke
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation; either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "mpirc.hxx"
|
||||
#include <Main/fg_props.hxx>
|
||||
|
||||
const std::string IRC_TEST_CHANNEL{"#mptest"}; // for development
|
||||
const std::string IRC_MSG_TERMINATOR {"\r\n"};
|
||||
// https://www.alien.net.au/irc/irc2numerics.html
|
||||
const std::string IRC_RPL_WELCOME {"001"};
|
||||
const std::string IRC_RPL_YOURID {"042"};
|
||||
const std::string IRC_RPL_MOTD {"372"};
|
||||
const std::string IRC_RPL_MOTDSTART {"375"};
|
||||
const std::string IRC_RPL_ENDOFMOTD {"376"};
|
||||
const std::string IRC_ERR_NOSUCHNICK {"401"};
|
||||
|
||||
IRCConnection::IRCConnection(const std::string &nickname, const std::string &servername, const std::string &port) : SGSocket(servername, port, "tcp"),
|
||||
_nickname(nickname)
|
||||
{
|
||||
}
|
||||
|
||||
IRCConnection::~IRCConnection()
|
||||
{
|
||||
}
|
||||
|
||||
// setup properties to reflect the status of this IRC connection in the prop tree
|
||||
void IRCConnection::setupProperties(std::string path)
|
||||
{
|
||||
if (path.back() != '/') path.push_back('/');
|
||||
if (!_pReadyFlag) _pReadyFlag = fgGetNode(path + "irc-ready", true);
|
||||
_pReadyFlag->setBoolValue(_logged_in);
|
||||
|
||||
if (!_pMessageCountIn) _pMessageCountIn = fgGetNode(path + "msg-count-in", true);
|
||||
if (!_pMessageCountOut) _pMessageCountOut = fgGetNode(path + "msg-count-out", true);
|
||||
if (!_pIRCReturnCode) _pIRCReturnCode = fgGetNode(path + "last-return-code", true);
|
||||
}
|
||||
|
||||
|
||||
bool IRCConnection::login(const std::string &nickname)
|
||||
{
|
||||
if (!_connected && !connect()) {
|
||||
return false;
|
||||
}
|
||||
if (!nickname.empty()) {
|
||||
_nickname = nickname;
|
||||
} else {
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "IRC login requires nickname argument.");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string lines("NICK ");
|
||||
lines += _nickname;
|
||||
lines += IRC_MSG_TERMINATOR;
|
||||
lines += "USER ";
|
||||
lines += _nickname; //IRC <user>
|
||||
lines += " 0 * :"; //IRC <mode> <unused>
|
||||
lines += _nickname; //IRC <realname>
|
||||
lines += IRC_MSG_TERMINATOR;
|
||||
return writestring(lines.c_str());
|
||||
}
|
||||
|
||||
// login with nickname given to constructor
|
||||
bool IRCConnection::login()
|
||||
{
|
||||
return login(_nickname);
|
||||
}
|
||||
|
||||
// the polite way to leave
|
||||
void IRCConnection::quit()
|
||||
{
|
||||
if (!_connected) return;
|
||||
writestring("QUIT goodbye\r\n");
|
||||
disconnect();
|
||||
}
|
||||
|
||||
bool IRCConnection::sendPrivmsg(const std::string &recipient, const std::string &textline)
|
||||
{
|
||||
if (!_logged_in) {
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "IRC 'privmsg' command unvailable. Login first!");
|
||||
return false;
|
||||
}
|
||||
std::string line("PRIVMSG ");
|
||||
line += recipient;
|
||||
line += " :";
|
||||
line += textline;
|
||||
line += IRC_MSG_TERMINATOR;
|
||||
if (writestring(line.c_str())) {
|
||||
if (_pMessageCountOut) _pMessageCountOut->setIntValue(_pMessageCountOut->getIntValue() + 1);
|
||||
if (_pIRCReturnCode) _pIRCReturnCode->setStringValue("");
|
||||
return true;
|
||||
} else {
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "IRC send privmsg failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// join an IRC channel
|
||||
bool IRCConnection::join(const std::string &channel)
|
||||
{
|
||||
if (!_logged_in) {
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "IRC 'join' command unvailable. Login first!");
|
||||
return false;
|
||||
}
|
||||
std::string lines("JOIN ");
|
||||
lines += channel;
|
||||
lines += IRC_MSG_TERMINATOR;
|
||||
return writestring(lines.c_str());
|
||||
}
|
||||
|
||||
// leave an IRC channel
|
||||
bool IRCConnection::part(const std::string &channel)
|
||||
{
|
||||
if (!_logged_in) {
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "IRC 'part' command unvailable. Login first!");
|
||||
return false;
|
||||
}
|
||||
std::string lines("PART ");
|
||||
lines += channel;
|
||||
lines += IRC_MSG_TERMINATOR;
|
||||
return writestring(lines.c_str());
|
||||
}
|
||||
|
||||
/*
|
||||
Call update() regularly to maintain connection (ping/pong) and process messages.
|
||||
For information only:
|
||||
The ping timeout appears to depend on the server settings and can be in the order
|
||||
of minutes. However, for smooth message processing the update frequency should be
|
||||
at least a few times per second and calling this at frame rate should not hurt.
|
||||
*/
|
||||
void IRCConnection::update()
|
||||
{
|
||||
if (_connected && readline(_read_buffer, sizeof(_read_buffer) - 1) > 0) {
|
||||
std::string line(_read_buffer); // TODO: buffer size check required?
|
||||
parseReceivedLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// private methods
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// open a connection to IRC server
|
||||
bool IRCConnection::connect()
|
||||
{
|
||||
if (_connected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
_connected = open(SG_IO_OUT);
|
||||
if (_connected) {
|
||||
nonblock();
|
||||
} else {
|
||||
disconnect();
|
||||
SG_LOG(SG_NETWORK, SG_WARN, "IRCConnection::connect error");
|
||||
}
|
||||
return _connected;
|
||||
}
|
||||
|
||||
void IRCConnection::disconnect()
|
||||
{
|
||||
_logged_in = false;
|
||||
if (_pReadyFlag) _pReadyFlag->setBoolValue(_logged_in);
|
||||
|
||||
if (_connected) {
|
||||
_connected = false;
|
||||
close();
|
||||
SG_LOG(SG_NETWORK, SG_INFO, "IRCConnection::disconnect");
|
||||
}
|
||||
}
|
||||
|
||||
void IRCConnection::pong(const std::string &recipient)
|
||||
{
|
||||
if (!_connected) return;
|
||||
std::string line("PONG ");
|
||||
line += recipient;
|
||||
line += IRC_MSG_TERMINATOR;
|
||||
writestring(line.c_str());
|
||||
}
|
||||
|
||||
bool IRCConnection::parseReceivedLine(std::string line)
|
||||
{
|
||||
/*
|
||||
https://tools.ietf.org/html/rfc2812#section-3.7.2
|
||||
2.3.1 Message format in Augmented BNF
|
||||
|
||||
The protocol messages must be extracted from the contiguous stream of
|
||||
octets. The current solution is to designate two characters, CR and
|
||||
LF, as message separators. Empty messages are silently ignored,
|
||||
which permits use of the sequence CR-LF between messages without
|
||||
extra problems.
|
||||
|
||||
The extracted message is parsed into the components <prefix>,
|
||||
<command> and list of parameters (<params>).
|
||||
|
||||
The Augmented BNF representation for this is:
|
||||
|
||||
message = [ ":" prefix SPACE ] command [ params ] crlf
|
||||
prefix = servername / ( nickname [ [ "!" user ] "@" host ] )
|
||||
command = 1*letter / 3digit
|
||||
params = *14( SPACE middle ) [ SPACE ":" trailing ]
|
||||
=/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]
|
||||
|
||||
nospcrlfcl = %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
|
||||
; any octet except NUL, CR, LF, " " and ":"
|
||||
middle = nospcrlfcl *( ":" / nospcrlfcl )
|
||||
trailing = *( ":" / " " / nospcrlfcl )
|
||||
|
||||
SPACE = %x20 ; space character
|
||||
crlf = %x0D %x0A ; "carriage return" "linefeed"
|
||||
|
||||
*/
|
||||
|
||||
// removes trailing '\r\n'
|
||||
// TODO: for length(IRC_MSG_TERMINATOR) do line.pop_back();
|
||||
line.pop_back();
|
||||
line.pop_back();
|
||||
|
||||
std::string prefix;
|
||||
std::string command;
|
||||
std::string params;
|
||||
|
||||
std::size_t pos = line.find(" ", 1);
|
||||
//prefix
|
||||
if (line.at(0) == ':') {
|
||||
prefix = line.substr(1, pos - 1); // remove leading ":"
|
||||
std::size_t end = line.find(" ", pos + 1);
|
||||
command = line.substr(pos + 1, end - pos - 1);
|
||||
pos = end;
|
||||
} else {
|
||||
command = line.substr(0, pos);
|
||||
}
|
||||
params = line.substr(pos + 1);
|
||||
|
||||
// uncomment next line for debug output
|
||||
//cout << "[prefix]" << prefix << "[cmd]" << command << "[params]" << params << "[end]" << endl;
|
||||
|
||||
// receiving a message
|
||||
if (command == "PRIVMSG") {
|
||||
if (_pMessageCountIn) _pMessageCountIn->setIntValue(_pMessageCountIn->getIntValue() + 1);
|
||||
std::string recipient = params.substr(0, params.find(" :"));
|
||||
// direct private message
|
||||
if (recipient == _nickname) {
|
||||
struct IRCMessage rcv;
|
||||
rcv.sender = prefix.substr(0, prefix.find("!"));
|
||||
rcv.textline = params.substr(params.find(":") + 1);
|
||||
_incoming_private_messages.push_back(rcv);
|
||||
} else {
|
||||
// Most likely from an IRC channel if we joined any. In this case
|
||||
// recipient equals channel name (e.g. "#mptest"). IRC channel
|
||||
// support could be implemented here in future.
|
||||
SG_LOG(SG_NETWORK, SG_DEV_WARN, "Ignoring PRIVMSG to '" + recipient + "' (should be '" + _nickname + "')");
|
||||
}
|
||||
} else if (command == "PING") {
|
||||
// server pings us
|
||||
std::string server = params.substr(0, params.find(" "));
|
||||
pong(server);
|
||||
} else if (command == "JOIN") {
|
||||
// server acks our join request
|
||||
std::string channel = params.substr(0, params.find(" "));
|
||||
SG_LOG(SG_NETWORK, SG_DEV_WARN, "Joined IRC channel " + channel); //DEBUG
|
||||
} else if (command == IRC_RPL_WELCOME) {
|
||||
// after welcome we are logged in and allowed to send commands/messages to the IRC
|
||||
_logged_in = true;
|
||||
if (_pReadyFlag) _pReadyFlag->setBoolValue(1);
|
||||
|
||||
//joining channel might help while development, maybe removed later
|
||||
//join(IRC_TEST_CHANNEL);
|
||||
}
|
||||
else if (command == IRC_RPL_MOTD) {
|
||||
}
|
||||
else if (command == IRC_RPL_MOTDSTART) {
|
||||
}
|
||||
else if (command == IRC_RPL_ENDOFMOTD) {
|
||||
}
|
||||
else if (command == IRC_ERR_NOSUCHNICK) {
|
||||
// server return code if we send to invalid nickname
|
||||
if (_pIRCReturnCode) _pIRCReturnCode->setStringValue(IRC_ERR_NOSUCHNICK);
|
||||
}
|
||||
else if (command == "ERROR") {
|
||||
if (_pIRCReturnCode) _pIRCReturnCode->setStringValue(params);
|
||||
disconnect();
|
||||
}
|
||||
// unexpected IRC message
|
||||
else {
|
||||
//SG_LOG(SG_NETWORK, SG_MANDATORY_INFO, "Unhandled IRC message "); //DEBUG
|
||||
//cout << "[prefix]" << prefix << "[cmd]" << command << "[params]" << params << "[end]" << endl;
|
||||
|
||||
// TODO: anything sensitive here that we should handle?
|
||||
// e.g. IRC user has disconnected and username == {current-cpdlc-authority}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
IRCMessage IRCConnection::getMessage()
|
||||
{
|
||||
struct IRCMessage entry {
|
||||
"", ""
|
||||
};
|
||||
if (!_incoming_private_messages.empty()) {
|
||||
entry = _incoming_private_messages.front();
|
||||
_incoming_private_messages.pop_front();
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
96
src/MultiPlayer/mpirc.hxx
Normal file
96
src/MultiPlayer/mpirc.hxx
Normal file
@@ -0,0 +1,96 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// mpirc.hxx
|
||||
//
|
||||
// started November 2020
|
||||
// Authors: Henning Stahlke, Michael Filhol
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation; either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef MPIRC_H
|
||||
#define MPIRC_H
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
#include <simgear/compiler.h>
|
||||
#include <simgear/props/props.hxx>
|
||||
#include <simgear/io/raw_socket.hxx>
|
||||
#include <simgear/io/sg_socket.hxx>
|
||||
|
||||
const std::string IRC_DEFAULT_PORT {"6667"};
|
||||
const int IRC_BUFFER_SIZE = 1024;
|
||||
|
||||
struct IRCMessage {
|
||||
std::string sender;
|
||||
std::string textline;
|
||||
};
|
||||
|
||||
/*
|
||||
IRCConnection implements a basic IRC client for transmitting and receiving
|
||||
private messages via an IRC server
|
||||
|
||||
In general it is possible to have multiple instances of this class but you
|
||||
have to consider the following points:
|
||||
- you cannot connect to a server with the same nickname more than once at a time
|
||||
- if you want to expose status info to the property tree, you have to pass
|
||||
an unique prefix to setupProperties() per instance
|
||||
*/
|
||||
class IRCConnection : SGSocket
|
||||
{
|
||||
public:
|
||||
IRCConnection(const std::string &nickname, const std::string &servername, const std::string &port = IRC_DEFAULT_PORT);
|
||||
~IRCConnection();
|
||||
|
||||
void setupProperties(std::string path);
|
||||
void update();
|
||||
|
||||
bool login(const std::string &nickname);
|
||||
bool login();
|
||||
void quit();
|
||||
|
||||
bool sendPrivmsg(const std::string &recipient, const std::string &textline);
|
||||
bool join(const std::string &channel);
|
||||
bool part(const std::string &channel);
|
||||
|
||||
|
||||
bool isConnected() const { return _connected; }
|
||||
bool isReady() const { return _logged_in; }
|
||||
bool hasMessage() const { return !_incoming_private_messages.empty(); }
|
||||
IRCMessage getMessage();
|
||||
|
||||
private:
|
||||
bool connect();
|
||||
void disconnect();
|
||||
void pong(const std::string &recipient);
|
||||
bool parseReceivedLine(std::string irc_line);
|
||||
|
||||
bool _connected {false}; // TCP session ok
|
||||
bool _logged_in {false}; // IRC login completed
|
||||
std::string _nickname {""};
|
||||
char _read_buffer[IRC_BUFFER_SIZE];
|
||||
std::deque<IRCMessage> _incoming_private_messages;
|
||||
|
||||
SGPropertyNode *_pReadyFlag {nullptr};
|
||||
SGPropertyNode *_pMessageCountIn {nullptr};
|
||||
SGPropertyNode *_pMessageCountOut {nullptr};
|
||||
SGPropertyNode *_pIRCReturnCode {nullptr};
|
||||
};
|
||||
|
||||
#endif
|
||||
186
src/MultiPlayer/mpmessages.hxx
Normal file
186
src/MultiPlayer/mpmessages.hxx
Normal file
@@ -0,0 +1,186 @@
|
||||
// mpmessages.hxx -- Message definitions for multiplayer communications
|
||||
// within a multiplayer Flightgear
|
||||
//
|
||||
// Written by Duncan McCreanor, started February 2003.
|
||||
// duncan.mccreanor@airservicesaustralia.com
|
||||
//
|
||||
// Copyright (C) 2003 Airservices Australia
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation; either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
|
||||
#ifndef MPMESSAGES_H
|
||||
#define MPMESSAGES_H
|
||||
|
||||
#define MPMESSAGES_HID "$Id$"
|
||||
|
||||
/****************************************************************
|
||||
* @version $Id$
|
||||
*
|
||||
* Description: Each message used for multiplayer communications
|
||||
* consists of a header and optionally a block of data. The combined
|
||||
* header and data is sent as one IP packet.
|
||||
*
|
||||
******************************************************************/
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <simgear/compiler.h>
|
||||
#include <simgear/props/props.hxx>
|
||||
#include <simgear/math/SGMath.hxx>
|
||||
#include "tiny_xdr.hxx"
|
||||
|
||||
// magic value for messages
|
||||
const uint32_t MSG_MAGIC = 0x46474653; // "FGFS"
|
||||
// protocoll version
|
||||
const uint32_t PROTO_VER = 0x00010001; // 1.1
|
||||
|
||||
// Message identifiers
|
||||
#define CHAT_MSG_ID 1
|
||||
#define UNUSABLE_POS_DATA_ID 2
|
||||
#define OLD_OLD_POS_DATA_ID 3
|
||||
#define OLD_POS_DATA_ID 4
|
||||
#define OLD_PROP_MSG_ID 5
|
||||
#define RESET_DATA_ID 6
|
||||
#define POS_DATA_ID 7
|
||||
#define MP_2017_DATA_ID 8
|
||||
|
||||
// XDR demands 4 byte alignment, but some compilers use8 byte alignment
|
||||
// so it's safe to let the overall size of a network message be a
|
||||
// multiple of 8!
|
||||
#define MAX_CALLSIGN_LEN 8
|
||||
#define MAX_CHAT_MSG_LEN 256
|
||||
#define MAX_MODEL_NAME_LEN 96
|
||||
#define MAX_PROPERTY_LEN 52
|
||||
|
||||
// Header for use with all messages sent
|
||||
struct T_MsgHdr {
|
||||
xdr_data_t Magic; // Magic Value
|
||||
xdr_data_t Version; // Protocoll version
|
||||
xdr_data_t MsgId; // Message identifier
|
||||
xdr_data_t MsgLen; // absolute length of message
|
||||
xdr_data_t RequestedRangeNm; // obsolete field (ReplyAddress) reused to request a range to fgms
|
||||
xdr_data_t ReplyPort; // player's receiver port
|
||||
char Callsign[MAX_CALLSIGN_LEN]; // Callsign used by the player
|
||||
};
|
||||
|
||||
// Chat message
|
||||
struct T_ChatMsg {
|
||||
char Text[MAX_CHAT_MSG_LEN]; // Text of chat message
|
||||
};
|
||||
|
||||
// Position message
|
||||
struct T_PositionMsg {
|
||||
char Model[MAX_MODEL_NAME_LEN]; // Name of the aircraft model
|
||||
|
||||
// Time when this packet was generated
|
||||
xdr_data2_t time;
|
||||
xdr_data2_t lag;
|
||||
|
||||
// position wrt the earth centered frame
|
||||
xdr_data2_t position[3];
|
||||
// orientation wrt the earth centered frame, stored in the angle axis
|
||||
// representation where the angle is coded into the axis length
|
||||
xdr_data_t orientation[3];
|
||||
|
||||
// linear velocity wrt the earth centered frame measured in
|
||||
// the earth centered frame
|
||||
xdr_data_t linearVel[3];
|
||||
// angular velocity wrt the earth centered frame measured in
|
||||
// the earth centered frame
|
||||
xdr_data_t angularVel[3];
|
||||
|
||||
// linear acceleration wrt the earth centered frame measured in
|
||||
// the earth centered frame
|
||||
xdr_data_t linearAccel[3];
|
||||
// angular acceleration wrt the earth centered frame measured in
|
||||
// the earth centered frame
|
||||
xdr_data_t angularAccel[3];
|
||||
// Padding. The alignment is 8 bytes on x86_64 because there are
|
||||
// 8-byte types in the message, so the size should be explicitly
|
||||
// rounded out to a multiple of 8. Of course, it's a bad idea to
|
||||
// put a C struct directly on the wire, but that's a fight for
|
||||
// another day...
|
||||
xdr_data_t pad;
|
||||
};
|
||||
|
||||
struct FGPropertyData {
|
||||
unsigned id;
|
||||
|
||||
// While the type isn't transmitted, it is needed for the destructor
|
||||
simgear::props::Type type;
|
||||
union {
|
||||
int int_value;
|
||||
float float_value;
|
||||
char* string_value;
|
||||
};
|
||||
FGPropertyData() : string_value(nullptr) {}
|
||||
~FGPropertyData() {
|
||||
if ((type == simgear::props::STRING) || (type == simgear::props::UNSPECIFIED))
|
||||
{
|
||||
delete [] string_value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Position message
|
||||
struct FGExternalMotionData {
|
||||
// simulation time when this packet was generated
|
||||
double time;
|
||||
// the artificial lag the client should stay behind the average
|
||||
// simulation time to arrival time difference
|
||||
// FIXME: should be some 'per model' instead of 'per packet' property
|
||||
double lag;
|
||||
|
||||
// position wrt the earth centered frame
|
||||
SGVec3d position;
|
||||
// orientation wrt the earth centered frame
|
||||
SGQuatf orientation;
|
||||
|
||||
// linear velocity wrt the earth centered frame measured in
|
||||
// the earth centered frame
|
||||
SGVec3f linearVel;
|
||||
// angular velocity wrt the earth centered frame measured in
|
||||
// the earth centered frame
|
||||
SGVec3f angularVel;
|
||||
|
||||
// linear acceleration wrt the earth centered frame measured in
|
||||
// the earth centered frame
|
||||
SGVec3f linearAccel;
|
||||
// angular acceleration wrt the earth centered frame measured in
|
||||
// the earth centered frame
|
||||
SGVec3f angularAccel;
|
||||
|
||||
// The set of properties received for this timeslot
|
||||
std::vector<FGPropertyData*> properties;
|
||||
|
||||
~FGExternalMotionData()
|
||||
{
|
||||
std::vector<FGPropertyData*>::const_iterator propIt;
|
||||
std::vector<FGPropertyData*>::const_iterator propItEnd;
|
||||
propIt = properties.begin();
|
||||
propItEnd = properties.end();
|
||||
|
||||
while (propIt != propItEnd)
|
||||
{
|
||||
delete *propIt;
|
||||
propIt++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
2876
src/MultiPlayer/multiplaymgr.cxx
Normal file
2876
src/MultiPlayer/multiplaymgr.cxx
Normal file
File diff suppressed because it is too large
Load Diff
163
src/MultiPlayer/multiplaymgr.hxx
Normal file
163
src/MultiPlayer/multiplaymgr.hxx
Normal file
@@ -0,0 +1,163 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// multiplaymgr.hxx
|
||||
//
|
||||
// Written by Duncan McCreanor, started February 2003.
|
||||
// duncan.mccreanor@airservicesaustralia.com
|
||||
//
|
||||
// Copyright (C) 2003 Airservices Australia
|
||||
// Copyright (C) 2005 Oliver Schroeder
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation; either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef MULTIPLAYMGR_H
|
||||
#define MULTIPLAYMGR_H
|
||||
|
||||
#define MULTIPLAYTXMGR_HID "$Id$"
|
||||
|
||||
const int MIN_MP_PROTOCOL_VERSION = 1;
|
||||
const int MAX_MP_PROTOCOL_VERSION = 2;
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
#include <simgear/compiler.h>
|
||||
#include <simgear/props/props.hxx>
|
||||
#include <simgear/io/raw_socket.hxx>
|
||||
#include <simgear/structure/subsystem_mgr.hxx>
|
||||
|
||||
class IRCConnection;
|
||||
class CPDLCManager;
|
||||
|
||||
const std::string MPIRC_SERVER_HOST_DEFAULT {"mpirc.flightgear.org"};
|
||||
const std::string MPIRC_SERVER_HOST_PROPERTY {"/network/mpirc/server-host"};
|
||||
const std::string MPIRC_SERVER_PORT_PROPERTY {"/network/mpirc/server-port"};
|
||||
const std::string MPIRC_NICK_PREFIX {"MP_IRC_"};
|
||||
|
||||
struct FGExternalMotionData;
|
||||
class MPPropertyListener;
|
||||
struct T_MsgHdr;
|
||||
class FGAIMultiplayer;
|
||||
|
||||
|
||||
class FGMultiplayMgr : public SGSubsystem
|
||||
{
|
||||
public:
|
||||
FGMultiplayMgr();
|
||||
~FGMultiplayMgr();
|
||||
|
||||
// Subsystem API.
|
||||
void init() override;
|
||||
void reinit() override;
|
||||
void shutdown() override;
|
||||
void update(double dt) override;
|
||||
|
||||
// Subsystem identification.
|
||||
static const char* staticSubsystemClassId() { return "mp"; }
|
||||
|
||||
// transmitter
|
||||
|
||||
void SendTextMessage(const std::string &sMsgText);
|
||||
// receiver
|
||||
|
||||
FGAIMultiplayer* getMultiplayer(const std::string& callsign);
|
||||
|
||||
std::shared_ptr<vector<char>> popMessageHistory();
|
||||
void pushMessageHistory(std::shared_ptr<vector<char>> message);
|
||||
|
||||
// Remove motion information for all multiplayer aircraft, e.g. when
|
||||
// scrubbing during replay.
|
||||
void ClearMotion();
|
||||
CPDLCManager *getCPDLC() { return _cpdlc.get(); };
|
||||
|
||||
private:
|
||||
std::unique_ptr<IRCConnection> _mpirc;
|
||||
std::unique_ptr<CPDLCManager> _cpdlc;
|
||||
friend class MPPropertyListener;
|
||||
|
||||
void setPropertiesChanged()
|
||||
{
|
||||
mPropertiesChanged = true;
|
||||
}
|
||||
int getProtocolToUse()
|
||||
{
|
||||
int protocolVersion = pProtocolVersion->getIntValue();
|
||||
if (protocolVersion >= MIN_MP_PROTOCOL_VERSION && protocolVersion <= MAX_MP_PROTOCOL_VERSION)
|
||||
return protocolVersion;
|
||||
else
|
||||
return MIN_MP_PROTOCOL_VERSION;
|
||||
}
|
||||
|
||||
void findProperties();
|
||||
|
||||
void Send(double currentMPTime);
|
||||
void SendMyPosition(const FGExternalMotionData& motionInfo);
|
||||
short get_scaled_short(double v, double scale);
|
||||
|
||||
union MsgBuf;
|
||||
FGAIMultiplayer* addMultiplayer(const std::string& callsign,
|
||||
const std::string& modelName,
|
||||
const int fallback_model_index);
|
||||
void FillMsgHdr(T_MsgHdr *MsgHdr, int iMsgId, unsigned _len = 0u);
|
||||
void ProcessPosMsg(const MsgBuf& Msg, const simgear::IPAddress& SenderAddress,
|
||||
long stamp);
|
||||
void ProcessChatMsg(const MsgBuf& Msg, const simgear::IPAddress& SenderAddress);
|
||||
bool isSane(const FGExternalMotionData& motionInfo);
|
||||
int GetMsgNetwork(MsgBuf& msgBuf, simgear::IPAddress& SenderAddress);
|
||||
int GetMsg(MsgBuf& msgBuf, simgear::IPAddress& SenderAddress);
|
||||
|
||||
/// maps from the callsign string to the FGAIMultiplayer
|
||||
typedef std::map<std::string, SGSharedPtr<FGAIMultiplayer> > MultiPlayerMap;
|
||||
MultiPlayerMap mMultiPlayerMap;
|
||||
|
||||
std::unique_ptr<simgear::Socket> mSocket;
|
||||
simgear::IPAddress mServer;
|
||||
bool mHaveServer;
|
||||
bool mInitialised;
|
||||
std::string mCallsign;
|
||||
|
||||
// Map between the property id's from the multiplayers network packets
|
||||
// and the property nodes
|
||||
typedef std::map<unsigned int, SGSharedPtr<SGPropertyNode> > PropertyMap;
|
||||
PropertyMap mPropertyMap;
|
||||
SGPropertyNode *pProtocolVersion;
|
||||
SGPropertyNode *pXmitLen;
|
||||
SGPropertyNode *pMultiPlayDebugLevel;
|
||||
SGPropertyNode *pMultiPlayRange;
|
||||
SGPropertyNode *pMultiPlayTransmitPropertyBase;
|
||||
SGPropertyNode *pReplayState;
|
||||
SGPropertyNode *pLogRawSpeedMultiplayer;
|
||||
|
||||
typedef std::map<unsigned int, const struct IdPropertyList*> PropertyDefinitionMap;
|
||||
PropertyDefinitionMap mPropertyDefinition;
|
||||
|
||||
bool mPropertiesChanged;
|
||||
|
||||
MPPropertyListener* mListener;
|
||||
|
||||
double mDt; // reciprocal of /sim/multiplay/tx-rate-hz
|
||||
double mNextTransmitTime = 0.0;
|
||||
|
||||
std::deque<std::shared_ptr<std::vector<char>>> mRecordMessageQueue;
|
||||
std::deque<std::shared_ptr<std::vector<char>>> mReplayMessageQueue;
|
||||
};
|
||||
|
||||
#endif
|
||||
202
src/MultiPlayer/tiny_xdr.cxx
Normal file
202
src/MultiPlayer/tiny_xdr.cxx
Normal file
@@ -0,0 +1,202 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Tiny XDR implementation for flightgear
|
||||
// written by Oliver Schroeder
|
||||
// released to the public domain
|
||||
//
|
||||
// This implementation is not complete, but implements
|
||||
// everything we need.
|
||||
//
|
||||
// For further reading on XDR read RFC 1832.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "tiny_xdr.hxx"
|
||||
|
||||
/* XDR 8bit integers */
|
||||
xdr_data_t
|
||||
XDR_encode_int8 ( const int8_t & n_Val )
|
||||
{
|
||||
return (SWAP32(static_cast<xdr_data_t> (n_Val)));
|
||||
}
|
||||
|
||||
xdr_data_t
|
||||
XDR_encode_uint8 ( const uint8_t & n_Val )
|
||||
{
|
||||
return (SWAP32(static_cast<xdr_data_t> (n_Val)));
|
||||
}
|
||||
|
||||
int8_t
|
||||
XDR_decode_int8 ( const xdr_data_t & n_Val )
|
||||
{
|
||||
return (static_cast<int8_t> (SWAP32(n_Val)));
|
||||
}
|
||||
|
||||
uint8_t
|
||||
XDR_decode_uint8 ( const xdr_data_t & n_Val )
|
||||
{
|
||||
return (static_cast<uint8_t> (SWAP32(n_Val)));
|
||||
}
|
||||
|
||||
/* XDR 16bit integers */
|
||||
xdr_data_t
|
||||
XDR_encode_int16 ( const int16_t & n_Val )
|
||||
{
|
||||
return (SWAP32(static_cast<xdr_data_t> (n_Val)));
|
||||
}
|
||||
|
||||
xdr_data_t
|
||||
XDR_encode_uint16 ( const uint16_t & n_Val )
|
||||
{
|
||||
return (SWAP32(static_cast<xdr_data_t> (n_Val)));
|
||||
}
|
||||
|
||||
int16_t
|
||||
XDR_decode_int16 ( const xdr_data_t & n_Val )
|
||||
{
|
||||
return (static_cast<int16_t> (SWAP32(n_Val)));
|
||||
}
|
||||
|
||||
uint16_t
|
||||
XDR_decode_uint16 ( const xdr_data_t & n_Val )
|
||||
{
|
||||
return (static_cast<uint16_t> (SWAP32(n_Val)));
|
||||
}
|
||||
|
||||
|
||||
/* XDR 32bit integers */
|
||||
xdr_data_t
|
||||
XDR_encode_int32 ( const int32_t & n_Val )
|
||||
{
|
||||
return (SWAP32(static_cast<xdr_data_t> (n_Val)));
|
||||
}
|
||||
|
||||
/*
|
||||
* Safely convert from an int into a short. Anything outside the bounds of a short will
|
||||
* simply be the max/min value of short (+/- 32767)
|
||||
*/
|
||||
static short XDR_convert_int_to_short(int v1)
|
||||
{
|
||||
if (v1 < -32767)
|
||||
v1 = -32767;
|
||||
|
||||
if (v1 > 32767)
|
||||
v1 = 32767;
|
||||
|
||||
return (short)v1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Pack two 16bit shorts into a 32 bit int. By convention v1 is packed in the highword
|
||||
*/
|
||||
xdr_data_t XDR_encode_shortints32(const int v1, const int v2)
|
||||
{
|
||||
return XDR_encode_uint32(((XDR_convert_int_to_short(v1) << 16) & 0xffff0000) | ((XDR_convert_int_to_short(v2)) & 0xffff));
|
||||
}
|
||||
/* Decode packed shorts into two ints. V1 in the highword ($V1..V2..)*/
|
||||
void XDR_decode_shortints32(const xdr_data_t & n_Val, int &v1, int &v2)
|
||||
{
|
||||
int _v1 = XDR_decode_int32(n_Val);
|
||||
short s2 = (short)(_v1 & 0xffff);
|
||||
short s1 = (short)(_v1 >> 16);
|
||||
v1 = s1;
|
||||
v2 = s2;
|
||||
}
|
||||
|
||||
xdr_data_t
|
||||
XDR_encode_uint32 ( const uint32_t & n_Val )
|
||||
{
|
||||
return (SWAP32(static_cast<xdr_data_t> (n_Val)));
|
||||
}
|
||||
|
||||
int32_t
|
||||
XDR_decode_int32 ( const xdr_data_t & n_Val )
|
||||
{
|
||||
return (static_cast<int32_t> (SWAP32(n_Val)));
|
||||
}
|
||||
|
||||
uint32_t
|
||||
XDR_decode_uint32 ( const xdr_data_t & n_Val )
|
||||
{
|
||||
return (static_cast<uint32_t> (SWAP32(n_Val)));
|
||||
}
|
||||
|
||||
|
||||
/* XDR 64bit integers */
|
||||
xdr_data2_t
|
||||
XDR_encode_int64 ( const int64_t & n_Val )
|
||||
{
|
||||
return (SWAP64(static_cast<xdr_data2_t> (n_Val)));
|
||||
}
|
||||
|
||||
xdr_data2_t
|
||||
XDR_encode_uint64 ( const uint64_t & n_Val )
|
||||
{
|
||||
return (SWAP64(static_cast<xdr_data2_t> (n_Val)));
|
||||
}
|
||||
|
||||
int64_t
|
||||
XDR_decode_int64 ( const xdr_data2_t & n_Val )
|
||||
{
|
||||
return (static_cast<int64_t> (SWAP64(n_Val)));
|
||||
}
|
||||
|
||||
uint64_t
|
||||
XDR_decode_uint64 ( const xdr_data2_t & n_Val )
|
||||
{
|
||||
return (static_cast<uint64_t> (SWAP64(n_Val)));
|
||||
}
|
||||
|
||||
|
||||
/* float */
|
||||
xdr_data_t
|
||||
XDR_encode_float ( const float & f_Val )
|
||||
{
|
||||
union {
|
||||
xdr_data_t x;
|
||||
float f;
|
||||
} tmp;
|
||||
|
||||
tmp.f = f_Val;
|
||||
return (XDR_encode_int32 (tmp.x));
|
||||
}
|
||||
|
||||
float
|
||||
XDR_decode_float ( const xdr_data_t & f_Val )
|
||||
{
|
||||
union {
|
||||
xdr_data_t x;
|
||||
float f;
|
||||
} tmp;
|
||||
|
||||
tmp.x = XDR_decode_int32 (f_Val);
|
||||
return tmp.f;
|
||||
}
|
||||
|
||||
/* double */
|
||||
xdr_data2_t
|
||||
XDR_encode_double ( const double & d_Val )
|
||||
{
|
||||
union {
|
||||
xdr_data2_t x;
|
||||
double d;
|
||||
} tmp;
|
||||
|
||||
tmp.d = d_Val;
|
||||
return (XDR_encode_int64 (tmp.x));
|
||||
}
|
||||
|
||||
double
|
||||
XDR_decode_double ( const xdr_data2_t & d_Val )
|
||||
{
|
||||
union {
|
||||
xdr_data2_t x;
|
||||
double d;
|
||||
} tmp;
|
||||
|
||||
tmp.x = XDR_decode_int64 (d_Val);
|
||||
return tmp.d;
|
||||
}
|
||||
|
||||
80
src/MultiPlayer/tiny_xdr.hxx
Normal file
80
src/MultiPlayer/tiny_xdr.hxx
Normal file
@@ -0,0 +1,80 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Tiny XDR implementation for flightgear
|
||||
// written by Oliver Schroeder
|
||||
// released to the public domain
|
||||
//
|
||||
// This implementation is not complete, but implements
|
||||
// everything we need.
|
||||
//
|
||||
// For further reading on XDR read RFC 1832.
|
||||
//
|
||||
// NEW
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef TINY_XDR_HEADER
|
||||
#define TINY_XDR_HEADER
|
||||
|
||||
#if defined HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <simgear/misc/stdint.hxx>
|
||||
|
||||
#define SWAP32(arg) sgIsLittleEndian() ? sg_bswap_32(arg) : arg
|
||||
#define SWAP64(arg) sgIsLittleEndian() ? sg_bswap_64(arg) : arg
|
||||
|
||||
#define XDR_BYTES_PER_UNIT 4
|
||||
|
||||
typedef uint32_t xdr_data_t; /* 4 Bytes */
|
||||
typedef uint64_t xdr_data2_t; /* 8 Bytes */
|
||||
|
||||
/* XDR 8bit integers */
|
||||
xdr_data_t XDR_encode_int8 ( const int8_t & n_Val );
|
||||
xdr_data_t XDR_encode_uint8 ( const uint8_t & n_Val );
|
||||
int8_t XDR_decode_int8 ( const xdr_data_t & n_Val );
|
||||
uint8_t XDR_decode_uint8 ( const xdr_data_t & n_Val );
|
||||
|
||||
/* XDR 16bit integers */
|
||||
xdr_data_t XDR_encode_int16 ( const int16_t & n_Val );
|
||||
xdr_data_t XDR_encode_uint16 ( const uint16_t & n_Val );
|
||||
int16_t XDR_decode_int16 ( const xdr_data_t & n_Val );
|
||||
uint16_t XDR_decode_uint16 ( const xdr_data_t & n_Val );
|
||||
|
||||
/* XDR 32bit integers */
|
||||
xdr_data_t XDR_encode_int32 ( const int32_t & n_Val );
|
||||
xdr_data_t XDR_encode_uint32 ( const uint32_t & n_Val );
|
||||
int32_t XDR_decode_int32 ( const xdr_data_t & n_Val );
|
||||
uint32_t XDR_decode_uint32 ( const xdr_data_t & n_Val );
|
||||
|
||||
/* XDR 64bit integers */
|
||||
xdr_data2_t XDR_encode_int64 ( const int64_t & n_Val );
|
||||
xdr_data2_t XDR_encode_uint64 ( const uint64_t & n_Val );
|
||||
int64_t XDR_decode_int64 ( const xdr_data2_t & n_Val );
|
||||
uint64_t XDR_decode_uint64 ( const xdr_data2_t & n_Val );
|
||||
|
||||
xdr_data_t XDR_encode_shortints32(const int v1, const int v2);
|
||||
void XDR_decode_shortints32(const xdr_data_t & n_Val, int &v1, int &v2);
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
//
|
||||
// FIXME: #1 these funtions must be fixed for
|
||||
// none IEEE-encoding architecturs
|
||||
// (eg. vax, big suns etc)
|
||||
// FIXME: #2 some compilers return 'double'
|
||||
// regardless of return-type 'float'
|
||||
// this must be fixed, too
|
||||
// FIXME: #3 some machines may need to use a
|
||||
// different endianess for floats!
|
||||
//
|
||||
//////////////////////////////////////////////////
|
||||
/* float */
|
||||
xdr_data_t XDR_encode_float ( const float & f_Val );
|
||||
float XDR_decode_float ( const xdr_data_t & f_Val );
|
||||
|
||||
/* double */
|
||||
xdr_data2_t XDR_encode_double ( const double & d_Val );
|
||||
double XDR_decode_double ( const xdr_data2_t & d_Val );
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user