mirror of https://github.com/OpenRCT2/OpenRCT2.git
Merge pull request #12712 from IntelOrca/plugin/tcp
Plugin: Add API for listening and communicating over TCP
This commit is contained in:
commit
f1fb86e7f6
|
@ -1,6 +1,7 @@
|
|||
0.3.0+ (in development)
|
||||
------------------------------------------------------------------------
|
||||
- Feature: [#10807] Add 2x and 4x zoom levels (currently limited to OpenGL).
|
||||
- Feature: [#12712] Add TCP / socket plugin APIs.
|
||||
- Feature: [#12840] Add Park.entranceFee to the plugin API.
|
||||
- Fix: [#400] Unable to place some saved tracks flush to the ground (original bug).
|
||||
- Fix: [#7037] Unable to save tracks starting with a sloped turn or helix.
|
||||
|
|
|
@ -1221,6 +1221,9 @@ declare global {
|
|||
kickPlayer(index: number): void;
|
||||
sendMessage(message: string): void;
|
||||
sendMessage(message: string, players: number[]): void;
|
||||
|
||||
createListener(): Listener;
|
||||
createSocket(): Socket;
|
||||
}
|
||||
|
||||
type NetworkMode = "none" | "server" | "client";
|
||||
|
@ -1677,4 +1680,39 @@ declare global {
|
|||
moveTo(position: CoordsXY | CoordsXYZ): void;
|
||||
scrollTo(position: CoordsXY | CoordsXYZ): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for incomming connections.
|
||||
* Based on node.js net.Server, see https://nodejs.org/api/net.html for more information.
|
||||
*/
|
||||
interface Listener {
|
||||
readonly listening: boolean;
|
||||
|
||||
listen(port: number): Listener;
|
||||
close(): Listener;
|
||||
|
||||
on(event: 'connection', callback: (socket: Socket) => void): Listener;
|
||||
|
||||
off(event: 'connection', callback: (socket: Socket) => void): Listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a socket such as a TCP connection.
|
||||
* Based on node.js net.Socket, see https://nodejs.org/api/net.html for more information.
|
||||
*/
|
||||
interface Socket {
|
||||
connect(port: number, host: string, callback: Function): Socket;
|
||||
destroy(error: object): Socket;
|
||||
setNoDelay(noDelay: boolean): Socket;
|
||||
end(data?: string): Socket;
|
||||
write(data: string): boolean;
|
||||
|
||||
on(event: 'close', callback: (hadError: boolean) => void): Socket;
|
||||
on(event: 'error', callback: (hadError: boolean) => void): Socket;
|
||||
on(event: 'data', callback: (data: string) => void): Socket;
|
||||
|
||||
off(event: 'close', callback: (hadError: boolean) => void): Socket;
|
||||
off(event: 'error', callback: (hadError: boolean) => void): Socket;
|
||||
off(event: 'data', callback: (data: string) => void): Socket;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,6 +159,21 @@ if (!h) {
|
|||
|
||||
All plugins have access to the same shared storage.
|
||||
|
||||
> Can plugins communicate with other processes, or the internet?
|
||||
|
||||
There is a socket API (based on net.Server and net.Socket from node.js) available for listening and communicating across TCP streams. For security purposes, plugins can only listen and connect to localhost. If you want to extend the communication further, you will need to provide your own separate reverse proxy. What port you can listen on is subject to your operating system, and how elevated the OpenRCT2 process is.
|
||||
|
||||
```js
|
||||
var server = network.createServer();
|
||||
server.on('connection', function (conn) {
|
||||
conn.on('data', function(data) {
|
||||
console.log("Received data: ", data);
|
||||
conn.write("Reply data");
|
||||
});
|
||||
});
|
||||
server.listen(8080);
|
||||
```
|
||||
|
||||
> Can I use third party JavaScript libraries?
|
||||
|
||||
Absolutely, just embed the library in your JavaScript file. There are a number of tools to help you do this.
|
||||
|
@ -199,4 +214,4 @@ This is up to you. The OpenRCT2 licence does not enforce any licence requirement
|
|||
|
||||
> Is there a good place to distribute my script to other players?
|
||||
|
||||
There is currently no official database for this. For now the recommendation is to upload releases of your script on GitHub alongside your source code (if public). Some people like to make a GitHub repository that just consists of a list of content (scripts in this case) which anyone can add to via pull requests.
|
||||
The recommendation is to upload releases of your script on GitHub alongside your source code (if public). There is a community driven repository for sharing plugins available at https://openrct2plugins.org/.
|
||||
|
|
|
@ -410,6 +410,7 @@
|
|||
<ClInclude Include="scripting\ScPark.hpp" />
|
||||
<ClInclude Include="scripting\ScRide.hpp" />
|
||||
<ClInclude Include="scripting\ScriptEngine.h" />
|
||||
<ClInclude Include="scripting\ScSocket.hpp" />
|
||||
<ClInclude Include="scripting\ScTile.hpp" />
|
||||
<ClInclude Include="sprites.h" />
|
||||
<ClInclude Include="title\TitleScreen.h" />
|
||||
|
|
|
@ -148,11 +148,6 @@ void NetworkBase::SetEnvironment(const std::shared_ptr<IPlatformEnvironment>& en
|
|||
|
||||
bool NetworkBase::Init()
|
||||
{
|
||||
if (!InitialiseWSA())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
status = NETWORK_STATUS_READY;
|
||||
|
||||
ServerName = std::string();
|
||||
|
@ -240,8 +235,6 @@ void NetworkBase::CloseConnection()
|
|||
mode = NETWORK_MODE_NONE;
|
||||
status = NETWORK_STATUS_NONE;
|
||||
_lastConnectStatus = SOCKET_STATUS_CLOSED;
|
||||
|
||||
DisposeWSA();
|
||||
}
|
||||
|
||||
bool NetworkBase::BeginClient(const std::string& host, uint16_t port)
|
||||
|
|
|
@ -34,6 +34,9 @@
|
|||
#ifndef SHUT_RD
|
||||
#define SHUT_RD SD_RECEIVE
|
||||
#endif
|
||||
#ifndef SHUT_WR
|
||||
#define SHUT_WR SD_SEND
|
||||
#endif
|
||||
#ifndef SHUT_RDWR
|
||||
#define SHUT_RDWR SD_BOTH
|
||||
#endif
|
||||
|
@ -67,8 +70,56 @@
|
|||
|
||||
constexpr auto CONNECT_TIMEOUT = std::chrono::milliseconds(3000);
|
||||
|
||||
// RAII WSA initialisation needed for Windows
|
||||
# ifdef _WIN32
|
||||
static bool _wsaInitialised = false;
|
||||
class WSA
|
||||
{
|
||||
private:
|
||||
bool _isInitialised{};
|
||||
|
||||
public:
|
||||
bool IsInitialised() const
|
||||
{
|
||||
return _isInitialised;
|
||||
}
|
||||
|
||||
bool Initialise()
|
||||
{
|
||||
if (!_isInitialised)
|
||||
{
|
||||
log_verbose("WSAStartup()");
|
||||
WSADATA wsa_data;
|
||||
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
|
||||
{
|
||||
log_error("Unable to initialise winsock.");
|
||||
return false;
|
||||
}
|
||||
_isInitialised = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
~WSA()
|
||||
{
|
||||
if (_isInitialised)
|
||||
{
|
||||
log_verbose("WSACleanup()");
|
||||
WSACleanup();
|
||||
_isInitialised = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static bool InitialiseWSA()
|
||||
{
|
||||
static WSA wsa;
|
||||
return wsa.Initialise();
|
||||
}
|
||||
# else
|
||||
static bool InitialiseWSA()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
# endif
|
||||
|
||||
class SocketException : public std::runtime_error
|
||||
|
@ -230,6 +281,14 @@ public:
|
|||
return _error.empty() ? nullptr : _error.c_str();
|
||||
}
|
||||
|
||||
void SetNoDelay(bool noDelay) override
|
||||
{
|
||||
if (_socket != INVALID_SOCKET)
|
||||
{
|
||||
SetOption(_socket, IPPROTO_TCP, TCP_NODELAY, noDelay);
|
||||
}
|
||||
}
|
||||
|
||||
void Listen(uint16_t port) override
|
||||
{
|
||||
Listen("", port);
|
||||
|
@ -331,7 +390,7 @@ public:
|
|||
int32_t rc = getnameinfo(
|
||||
reinterpret_cast<struct sockaddr*>(&client_addr), client_len, hostName, sizeof(hostName), nullptr, 0,
|
||||
NI_NUMERICHOST | NI_NUMERICSERV);
|
||||
SetOption(socket, IPPROTO_TCP, TCP_NODELAY, true);
|
||||
SetNoDelay(true);
|
||||
|
||||
if (rc == 0)
|
||||
{
|
||||
|
@ -348,7 +407,7 @@ public:
|
|||
|
||||
void Connect(const std::string& address, uint16_t port) override
|
||||
{
|
||||
if (_status != SOCKET_STATUS_CLOSED)
|
||||
if (_status != SOCKET_STATUS_CLOSED && _status != SOCKET_STATUS_WAITING)
|
||||
{
|
||||
throw std::runtime_error("Socket not closed.");
|
||||
}
|
||||
|
@ -372,7 +431,7 @@ public:
|
|||
throw SocketException("Unable to create socket.");
|
||||
}
|
||||
|
||||
SetOption(_socket, IPPROTO_TCP, TCP_NODELAY, true);
|
||||
SetNoDelay(true);
|
||||
if (!SetNonBlocking(_socket, true))
|
||||
{
|
||||
throw SocketException("Failed to set non-blocking mode.");
|
||||
|
@ -445,6 +504,11 @@ public:
|
|||
throw std::runtime_error("Socket not closed.");
|
||||
}
|
||||
|
||||
// When connect is called, the status is set to resolving, but we want to make sure
|
||||
// the status is changed before this async method exits. Otherwise, the consumer
|
||||
// might think the status has closed before it started to connect.
|
||||
_status = SOCKET_STATUS_WAITING;
|
||||
|
||||
auto saddress = std::string(address);
|
||||
std::promise<void> barrier;
|
||||
_connectFuture = barrier.get_future();
|
||||
|
@ -464,6 +528,14 @@ public:
|
|||
thread.detach();
|
||||
}
|
||||
|
||||
void Finish() override
|
||||
{
|
||||
if (_status == SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
shutdown(_socket, SHUT_WR);
|
||||
}
|
||||
}
|
||||
|
||||
void Disconnect() override
|
||||
{
|
||||
if (_status == SOCKET_STATUS_CONNECTED)
|
||||
|
@ -811,50 +883,23 @@ private:
|
|||
}
|
||||
};
|
||||
|
||||
bool InitialiseWSA()
|
||||
{
|
||||
# ifdef _WIN32
|
||||
if (!_wsaInitialised)
|
||||
{
|
||||
log_verbose("Initialising WSA");
|
||||
WSADATA wsa_data;
|
||||
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
|
||||
{
|
||||
log_error("Unable to initialise winsock.");
|
||||
return false;
|
||||
}
|
||||
_wsaInitialised = true;
|
||||
}
|
||||
return _wsaInitialised;
|
||||
# else
|
||||
return true;
|
||||
# endif
|
||||
}
|
||||
|
||||
void DisposeWSA()
|
||||
{
|
||||
# ifdef _WIN32
|
||||
if (_wsaInitialised)
|
||||
{
|
||||
WSACleanup();
|
||||
_wsaInitialised = false;
|
||||
}
|
||||
# endif
|
||||
}
|
||||
|
||||
std::unique_ptr<ITcpSocket> CreateTcpSocket()
|
||||
{
|
||||
InitialiseWSA();
|
||||
return std::make_unique<TcpSocket>();
|
||||
}
|
||||
|
||||
std::unique_ptr<IUdpSocket> CreateUdpSocket()
|
||||
{
|
||||
InitialiseWSA();
|
||||
return std::make_unique<UdpSocket>();
|
||||
}
|
||||
|
||||
# ifdef _WIN32
|
||||
static std::vector<INTERFACE_INFO> GetNetworkInterfaces()
|
||||
{
|
||||
InitialiseWSA();
|
||||
|
||||
int sock = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (sock == -1)
|
||||
{
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
enum SOCKET_STATUS
|
||||
{
|
||||
SOCKET_STATUS_CLOSED,
|
||||
SOCKET_STATUS_WAITING,
|
||||
SOCKET_STATUS_RESOLVING,
|
||||
SOCKET_STATUS_CONNECTING,
|
||||
SOCKET_STATUS_CONNECTED,
|
||||
|
@ -67,6 +68,9 @@ public:
|
|||
virtual size_t SendData(const void* buffer, size_t size) abstract;
|
||||
virtual NetworkReadPacket ReceiveData(void* buffer, size_t size, size_t* sizeReceived) abstract;
|
||||
|
||||
virtual void SetNoDelay(bool noDelay) abstract;
|
||||
|
||||
virtual void Finish() abstract;
|
||||
virtual void Disconnect() abstract;
|
||||
virtual void Close() abstract;
|
||||
};
|
||||
|
@ -94,8 +98,6 @@ public:
|
|||
virtual void Close() abstract;
|
||||
};
|
||||
|
||||
bool InitialiseWSA();
|
||||
void DisposeWSA();
|
||||
std::unique_ptr<ITcpSocket> CreateTcpSocket();
|
||||
std::unique_ptr<IUdpSocket> CreateUdpSocket();
|
||||
std::vector<std::unique_ptr<INetworkEndpoint>> GetBroadcastAddresses();
|
||||
|
|
|
@ -257,6 +257,12 @@ namespace OpenRCT2::Scripting
|
|||
return DukValue::take_from_stack(ctx);
|
||||
}
|
||||
|
||||
template<> inline DukValue ToDuk(duk_context* ctx, const bool& value)
|
||||
{
|
||||
duk_push_boolean(ctx, value);
|
||||
return DukValue::take_from_stack(ctx);
|
||||
}
|
||||
|
||||
template<> inline DukValue ToDuk(duk_context* ctx, const int32_t& value)
|
||||
{
|
||||
duk_push_int(ctx, value);
|
||||
|
@ -269,6 +275,11 @@ namespace OpenRCT2::Scripting
|
|||
return DukValue::take_from_stack(ctx);
|
||||
}
|
||||
|
||||
template<> inline DukValue ToDuk(duk_context* ctx, const std::string& value)
|
||||
{
|
||||
return ToDuk(ctx, std::string_view(value));
|
||||
}
|
||||
|
||||
template<size_t TLen> inline DukValue ToDuk(duk_context* ctx, const char (&value)[TLen])
|
||||
{
|
||||
duk_push_string(ctx, value);
|
||||
|
|
|
@ -11,12 +11,14 @@
|
|||
|
||||
#ifdef ENABLE_SCRIPTING
|
||||
|
||||
# include "../Context.h"
|
||||
# include "../actions/NetworkModifyGroupAction.hpp"
|
||||
# include "../actions/PlayerKickAction.hpp"
|
||||
# include "../actions/PlayerSetGroupAction.hpp"
|
||||
# include "../network/NetworkAction.h"
|
||||
# include "../network/network.h"
|
||||
# include "Duktape.hpp"
|
||||
# include "ScSocket.hpp"
|
||||
|
||||
namespace OpenRCT2::Scripting
|
||||
{
|
||||
|
@ -447,6 +449,38 @@ namespace OpenRCT2::Scripting
|
|||
# endif
|
||||
}
|
||||
|
||||
# ifndef DISABLE_NETWORK
|
||||
std::shared_ptr<ScListener> createListener()
|
||||
{
|
||||
auto& scriptEngine = GetContext()->GetScriptEngine();
|
||||
auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin();
|
||||
auto socket = std::make_shared<ScListener>(plugin);
|
||||
scriptEngine.AddSocket(socket);
|
||||
return socket;
|
||||
}
|
||||
# else
|
||||
void createListener()
|
||||
{
|
||||
duk_error(_context, DUK_ERR_ERROR, "Networking has been disabled.");
|
||||
}
|
||||
# endif
|
||||
|
||||
# ifndef DISABLE_NETWORK
|
||||
std::shared_ptr<ScSocket> createSocket()
|
||||
{
|
||||
auto& scriptEngine = GetContext()->GetScriptEngine();
|
||||
auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin();
|
||||
auto socket = std::make_shared<ScSocket>(plugin);
|
||||
scriptEngine.AddSocket(socket);
|
||||
return socket;
|
||||
}
|
||||
# else
|
||||
void createSocket()
|
||||
{
|
||||
duk_error(_context, DUK_ERR_ERROR, "Networking has been disabled.");
|
||||
}
|
||||
# endif
|
||||
|
||||
static void Register(duk_context* ctx)
|
||||
{
|
||||
dukglue_register_property(ctx, &ScNetwork::mode_get, nullptr, "mode");
|
||||
|
@ -462,6 +496,9 @@ namespace OpenRCT2::Scripting
|
|||
dukglue_register_method(ctx, &ScNetwork::getPlayer, "getPlayer");
|
||||
dukglue_register_method(ctx, &ScNetwork::kickPlayer, "kickPlayer");
|
||||
dukglue_register_method(ctx, &ScNetwork::sendMessage, "sendMessage");
|
||||
|
||||
dukglue_register_method(ctx, &ScNetwork::createListener, "createListener");
|
||||
dukglue_register_method(ctx, &ScNetwork::createSocket, "createSocket");
|
||||
}
|
||||
};
|
||||
} // namespace OpenRCT2::Scripting
|
||||
|
|
|
@ -0,0 +1,542 @@
|
|||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2020 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef ENABLE_SCRIPTING
|
||||
# ifndef DISABLE_NETWORK
|
||||
|
||||
# include "../Context.h"
|
||||
# include "../network/Socket.h"
|
||||
# include "Duktape.hpp"
|
||||
# include "ScriptEngine.h"
|
||||
|
||||
# include <algorithm>
|
||||
# include <vector>
|
||||
|
||||
namespace OpenRCT2::Scripting
|
||||
{
|
||||
class EventList
|
||||
{
|
||||
private:
|
||||
std::vector<std::vector<DukValue>> _listeners;
|
||||
|
||||
std::vector<DukValue>& GetListenerList(uint32_t id)
|
||||
{
|
||||
if (_listeners.size() <= id)
|
||||
{
|
||||
_listeners.resize(static_cast<size_t>(id) + 1);
|
||||
}
|
||||
return _listeners[id];
|
||||
}
|
||||
|
||||
public:
|
||||
void Raise(
|
||||
uint32_t id, const std::shared_ptr<Plugin>& plugin, const std::vector<DukValue>& args, bool isGameStateMutable)
|
||||
{
|
||||
auto& scriptEngine = GetContext()->GetScriptEngine();
|
||||
|
||||
// Use simple for i loop in case listeners is modified during the loop
|
||||
auto listeners = GetListenerList(id);
|
||||
for (size_t i = 0; i < listeners.size(); i++)
|
||||
{
|
||||
scriptEngine.ExecutePluginCall(plugin, listeners[i], args, isGameStateMutable);
|
||||
|
||||
// Safety, listeners might get reallocated
|
||||
listeners = GetListenerList(id);
|
||||
}
|
||||
}
|
||||
|
||||
void AddListener(uint32_t id, const DukValue& listener)
|
||||
{
|
||||
auto& listeners = GetListenerList(id);
|
||||
listeners.push_back(listener);
|
||||
}
|
||||
|
||||
void RemoveListener(uint32_t id, const DukValue& value)
|
||||
{
|
||||
auto& listeners = GetListenerList(id);
|
||||
listeners.erase(std::remove(listeners.begin(), listeners.end(), value), listeners.end());
|
||||
}
|
||||
|
||||
void RemoveAllListeners(uint32_t id)
|
||||
{
|
||||
auto& listeners = GetListenerList(id);
|
||||
listeners.clear();
|
||||
}
|
||||
};
|
||||
|
||||
class ScSocketBase
|
||||
{
|
||||
private:
|
||||
std::shared_ptr<Plugin> _plugin;
|
||||
|
||||
protected:
|
||||
static bool IsLocalhostAddress(std::string_view s)
|
||||
{
|
||||
return s == "localhost" || s == "127.0.0.1" || s == "::";
|
||||
}
|
||||
|
||||
public:
|
||||
ScSocketBase(const std::shared_ptr<Plugin>& plugin)
|
||||
: _plugin(plugin)
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~ScSocketBase()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
const std::shared_ptr<Plugin>& GetPlugin() const
|
||||
{
|
||||
return _plugin;
|
||||
}
|
||||
|
||||
virtual void Update() = 0;
|
||||
|
||||
virtual void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool IsDisposed() const = 0;
|
||||
};
|
||||
|
||||
class ScSocket final : public ScSocketBase
|
||||
{
|
||||
private:
|
||||
static constexpr uint32_t EVENT_NONE = std::numeric_limits<uint32_t>::max();
|
||||
static constexpr uint32_t EVENT_CLOSE = 0;
|
||||
static constexpr uint32_t EVENT_DATA = 1;
|
||||
static constexpr uint32_t EVENT_CONNECT_ONCE = 2;
|
||||
static constexpr uint32_t EVENT_ERROR = 3;
|
||||
|
||||
EventList _eventList;
|
||||
std::unique_ptr<ITcpSocket> _socket;
|
||||
bool _disposed{};
|
||||
bool _connecting{};
|
||||
bool _wasConnected{};
|
||||
|
||||
public:
|
||||
ScSocket(const std::shared_ptr<Plugin>& plugin)
|
||||
: ScSocketBase(plugin)
|
||||
{
|
||||
}
|
||||
|
||||
ScSocket(const std::shared_ptr<Plugin>& plugin, std::unique_ptr<ITcpSocket>&& socket)
|
||||
: ScSocketBase(plugin)
|
||||
, _socket(std::move(socket))
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
ScSocket* destroy(const DukValue& error)
|
||||
{
|
||||
CloseSocket();
|
||||
return this;
|
||||
}
|
||||
|
||||
ScSocket* setNoDelay(bool noDelay)
|
||||
{
|
||||
if (_socket != nullptr)
|
||||
{
|
||||
_socket->SetNoDelay(noDelay);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
ScSocket* connect(uint16_t port, const std::string& host, const DukValue& callback)
|
||||
{
|
||||
auto ctx = GetContext()->GetScriptEngine().GetContext();
|
||||
if (_socket != nullptr)
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Socket has already been created.");
|
||||
}
|
||||
else if (_disposed)
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Socket is disposed.");
|
||||
}
|
||||
else if (_connecting)
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Socket is already connecting.");
|
||||
}
|
||||
else if (!IsLocalhostAddress(host))
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "For security reasons, only connecting to localhost is allowed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_socket = CreateTcpSocket();
|
||||
try
|
||||
{
|
||||
_socket->ConnectAsync(host, port);
|
||||
_eventList.AddListener(EVENT_CONNECT_ONCE, callback);
|
||||
_connecting = true;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, e.what());
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
ScSocket* end(const DukValue& data)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
auto ctx = GetContext()->GetScriptEngine().GetContext();
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Socket is disposed.");
|
||||
}
|
||||
else if (_socket != nullptr)
|
||||
{
|
||||
if (data.type() == DukValue::Type::STRING)
|
||||
{
|
||||
write(data.as_string());
|
||||
_socket->Finish();
|
||||
}
|
||||
else
|
||||
{
|
||||
_socket->Finish();
|
||||
auto ctx = GetContext()->GetScriptEngine().GetContext();
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Only sending strings is currently supported.");
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
bool write(const std::string& data)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
auto ctx = GetContext()->GetScriptEngine().GetContext();
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Socket is disposed.");
|
||||
}
|
||||
else if (_socket != nullptr)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto sentBytes = _socket->SendData(data.c_str(), data.size());
|
||||
return sentBytes != data.size();
|
||||
}
|
||||
catch (const std::exception&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ScSocket* on(const std::string& eventType, const DukValue& callback)
|
||||
{
|
||||
auto eventId = GetEventType(eventType);
|
||||
if (eventId != EVENT_NONE)
|
||||
{
|
||||
_eventList.AddListener(eventId, callback);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
ScSocket* off(const std::string& eventType, const DukValue& callback)
|
||||
{
|
||||
auto eventId = GetEventType(eventType);
|
||||
if (eventId != EVENT_NONE)
|
||||
{
|
||||
_eventList.RemoveListener(eventId, callback);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
void CloseSocket()
|
||||
{
|
||||
if (_socket != nullptr)
|
||||
{
|
||||
_socket->Close();
|
||||
_socket = nullptr;
|
||||
if (_wasConnected)
|
||||
{
|
||||
_wasConnected = false;
|
||||
RaiseOnClose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RaiseOnClose(bool hadError)
|
||||
{
|
||||
auto ctx = GetContext()->GetScriptEngine().GetContext();
|
||||
_eventList.Raise(EVENT_CLOSE, GetPlugin(), { ToDuk(ctx, hadError) }, false);
|
||||
}
|
||||
|
||||
void RaiseOnData(const std::string& data)
|
||||
{
|
||||
auto& scriptEngine = GetContext()->GetScriptEngine();
|
||||
auto ctx = scriptEngine.GetContext();
|
||||
_eventList.Raise(EVENT_DATA, GetPlugin(), { ToDuk(ctx, data) }, false);
|
||||
}
|
||||
|
||||
uint32_t GetEventType(std::string_view name)
|
||||
{
|
||||
if (name == "close")
|
||||
return EVENT_CLOSE;
|
||||
if (name == "data")
|
||||
return EVENT_DATA;
|
||||
if (name == "error")
|
||||
return EVENT_ERROR;
|
||||
return EVENT_NONE;
|
||||
}
|
||||
|
||||
public:
|
||||
void Update() override
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (_socket != nullptr)
|
||||
{
|
||||
auto status = _socket->GetStatus();
|
||||
if (_connecting)
|
||||
{
|
||||
if (status == SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
_connecting = false;
|
||||
_wasConnected = true;
|
||||
_eventList.Raise(EVENT_CONNECT_ONCE, GetPlugin(), {}, false);
|
||||
_eventList.RemoveAllListeners(EVENT_CONNECT_ONCE);
|
||||
}
|
||||
else if (status == SOCKET_STATUS_CLOSED)
|
||||
{
|
||||
_connecting = false;
|
||||
|
||||
auto& scriptEngine = GetContext()->GetScriptEngine();
|
||||
auto ctx = scriptEngine.GetContext();
|
||||
auto err = _socket->GetError();
|
||||
if (err == nullptr)
|
||||
{
|
||||
err = "";
|
||||
}
|
||||
auto dukErr = ToDuk(ctx, std::string_view(err));
|
||||
_eventList.Raise(EVENT_ERROR, GetPlugin(), { dukErr }, true);
|
||||
}
|
||||
}
|
||||
else if (status == SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
char buffer[2048];
|
||||
size_t bytesRead{};
|
||||
auto result = _socket->ReceiveData(buffer, sizeof(buffer), &bytesRead);
|
||||
switch (result)
|
||||
{
|
||||
case NetworkReadPacket::Success:
|
||||
RaiseOnData(std::string(buffer, bytesRead));
|
||||
break;
|
||||
case NetworkReadPacket::NoData:
|
||||
break;
|
||||
case NetworkReadPacket::MoreData:
|
||||
break;
|
||||
case NetworkReadPacket::Disconnected:
|
||||
CloseSocket();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseSocket();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dispose() override
|
||||
{
|
||||
CloseSocket();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
bool IsDisposed() const override
|
||||
{
|
||||
return _disposed;
|
||||
}
|
||||
|
||||
static void Register(duk_context* ctx)
|
||||
{
|
||||
dukglue_register_method(ctx, &ScSocket::destroy, "destroy");
|
||||
dukglue_register_method(ctx, &ScSocket::setNoDelay, "setNoDelay");
|
||||
dukglue_register_method(ctx, &ScSocket::connect, "connect");
|
||||
dukglue_register_method(ctx, &ScSocket::end, "end");
|
||||
dukglue_register_method(ctx, &ScSocket::write, "write");
|
||||
dukglue_register_method(ctx, &ScSocket::on, "on");
|
||||
dukglue_register_method(ctx, &ScSocket::off, "off");
|
||||
}
|
||||
};
|
||||
|
||||
class ScListener final : public ScSocketBase
|
||||
{
|
||||
private:
|
||||
static constexpr uint32_t EVENT_NONE = std::numeric_limits<uint32_t>::max();
|
||||
static constexpr uint32_t EVENT_CONNECTION = 0;
|
||||
|
||||
EventList _eventList;
|
||||
std::unique_ptr<ITcpSocket> _socket;
|
||||
std::vector<std::shared_ptr<ScSocket>> _scClientSockets;
|
||||
bool _disposed{};
|
||||
|
||||
bool listening_get()
|
||||
{
|
||||
if (_socket != nullptr)
|
||||
{
|
||||
return _socket->GetStatus() == SOCKET_STATUS_LISTENING;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ScListener* close()
|
||||
{
|
||||
CloseSocket();
|
||||
return this;
|
||||
}
|
||||
|
||||
ScListener* listen(int32_t port, const DukValue& dukHost)
|
||||
{
|
||||
auto ctx = GetContext()->GetScriptEngine().GetContext();
|
||||
if (_disposed)
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Socket is disposed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_socket == nullptr)
|
||||
{
|
||||
_socket = CreateTcpSocket();
|
||||
}
|
||||
|
||||
if (_socket->GetStatus() == SOCKET_STATUS_LISTENING)
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Server is already listening.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dukHost.type() == DukValue::Type::STRING)
|
||||
{
|
||||
auto host = dukHost.as_string();
|
||||
if (IsLocalhostAddress(host))
|
||||
{
|
||||
try
|
||||
{
|
||||
_socket->Listen(host, port);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, e.what());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "For security reasons, only binding to localhost is allowed.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_socket->Listen("127.0.0.1", port);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
ScListener* on(const std::string& eventType, const DukValue& callback)
|
||||
{
|
||||
auto eventId = GetEventType(eventType);
|
||||
if (eventId != EVENT_NONE)
|
||||
{
|
||||
_eventList.AddListener(eventId, callback);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
ScListener* off(const std::string& eventType, const DukValue& callback)
|
||||
{
|
||||
auto eventId = GetEventType(eventType);
|
||||
if (eventId != EVENT_NONE)
|
||||
{
|
||||
_eventList.RemoveListener(eventId, callback);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
uint32_t GetEventType(const std::string_view& name)
|
||||
{
|
||||
if (name == "connection")
|
||||
return EVENT_CONNECTION;
|
||||
return EVENT_NONE;
|
||||
}
|
||||
|
||||
public:
|
||||
ScListener(const std::shared_ptr<Plugin>& plugin)
|
||||
: ScSocketBase(plugin)
|
||||
{
|
||||
}
|
||||
|
||||
void Update() override
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (_socket == nullptr)
|
||||
return;
|
||||
|
||||
if (_socket->GetStatus() == SOCKET_STATUS_LISTENING)
|
||||
{
|
||||
auto client = _socket->Accept();
|
||||
if (client != nullptr)
|
||||
{
|
||||
// Default to using Nagle's algorithm like node.js does
|
||||
client->SetNoDelay(false);
|
||||
|
||||
auto& scriptEngine = GetContext()->GetScriptEngine();
|
||||
auto clientSocket = std::make_shared<ScSocket>(GetPlugin(), std::move(client));
|
||||
scriptEngine.AddSocket(clientSocket);
|
||||
|
||||
auto ctx = scriptEngine.GetContext();
|
||||
auto dukClientSocket = GetObjectAsDukValue(ctx, clientSocket);
|
||||
_eventList.Raise(EVENT_CONNECTION, GetPlugin(), { dukClientSocket }, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CloseSocket()
|
||||
{
|
||||
if (_socket != nullptr)
|
||||
{
|
||||
_socket->Close();
|
||||
_socket = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Dispose() override
|
||||
{
|
||||
CloseSocket();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
bool IsDisposed() const override
|
||||
{
|
||||
return _disposed;
|
||||
}
|
||||
|
||||
static void Register(duk_context* ctx)
|
||||
{
|
||||
dukglue_register_property(ctx, &ScListener::listening_get, nullptr, "listening");
|
||||
dukglue_register_method(ctx, &ScListener::close, "close");
|
||||
dukglue_register_method(ctx, &ScListener::listen, "listen");
|
||||
dukglue_register_method(ctx, &ScListener::on, "on");
|
||||
dukglue_register_method(ctx, &ScListener::off, "off");
|
||||
}
|
||||
};
|
||||
} // namespace OpenRCT2::Scripting
|
||||
|
||||
# endif
|
||||
#endif
|
|
@ -33,6 +33,7 @@
|
|||
# include "ScObject.hpp"
|
||||
# include "ScPark.hpp"
|
||||
# include "ScRide.hpp"
|
||||
# include "ScSocket.hpp"
|
||||
# include "ScTile.hpp"
|
||||
|
||||
# include <iostream>
|
||||
|
@ -41,7 +42,7 @@
|
|||
using namespace OpenRCT2;
|
||||
using namespace OpenRCT2::Scripting;
|
||||
|
||||
static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 3;
|
||||
static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 4;
|
||||
|
||||
struct ExpressionStringifier final
|
||||
{
|
||||
|
@ -393,6 +394,10 @@ void ScriptEngine::Initialise()
|
|||
ScVehicle::Register(ctx);
|
||||
ScPeep::Register(ctx);
|
||||
ScGuest::Register(ctx);
|
||||
# ifndef DISABLE_NETWORK
|
||||
ScSocket::Register(ctx);
|
||||
ScListener::Register(ctx);
|
||||
# endif
|
||||
ScStaff::Register(ctx);
|
||||
|
||||
dukglue_register_global(ctx, std::make_shared<ScCheats>(), "cheats");
|
||||
|
@ -479,6 +484,7 @@ void ScriptEngine::StopPlugin(std::shared_ptr<Plugin> plugin)
|
|||
if (plugin->HasStarted())
|
||||
{
|
||||
RemoveCustomGameActions(plugin);
|
||||
RemoveSockets(plugin);
|
||||
_hookEngine.UnsubscribeAll(plugin);
|
||||
for (auto callback : _pluginStoppedSubscriptions)
|
||||
{
|
||||
|
@ -640,6 +646,7 @@ void ScriptEngine::Update()
|
|||
}
|
||||
}
|
||||
|
||||
UpdateSockets();
|
||||
ProcessREPL();
|
||||
}
|
||||
|
||||
|
@ -1127,6 +1134,54 @@ void ScriptEngine::SaveSharedStorage()
|
|||
}
|
||||
}
|
||||
|
||||
# ifndef DISABLE_NETWORK
|
||||
void ScriptEngine::AddSocket(const std::shared_ptr<ScSocketBase>& socket)
|
||||
{
|
||||
_sockets.push_back(socket);
|
||||
}
|
||||
# endif
|
||||
|
||||
void ScriptEngine::UpdateSockets()
|
||||
{
|
||||
# ifndef DISABLE_NETWORK
|
||||
// Use simple for i loop as Update calls can modify the list
|
||||
auto it = _sockets.begin();
|
||||
while (it != _sockets.end())
|
||||
{
|
||||
auto& socket = *it;
|
||||
socket->Update();
|
||||
if (socket->IsDisposed())
|
||||
{
|
||||
it = _sockets.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
it++;
|
||||
}
|
||||
}
|
||||
# endif
|
||||
}
|
||||
|
||||
void ScriptEngine::RemoveSockets(const std::shared_ptr<Plugin>& plugin)
|
||||
{
|
||||
# ifndef DISABLE_NETWORK
|
||||
auto it = _sockets.begin();
|
||||
while (it != _sockets.end())
|
||||
{
|
||||
auto socket = it->get();
|
||||
if (socket->GetPlugin() == plugin)
|
||||
{
|
||||
socket->Dispose();
|
||||
it = _sockets.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
it++;
|
||||
}
|
||||
}
|
||||
# endif
|
||||
}
|
||||
|
||||
std::string OpenRCT2::Scripting::Stringify(const DukValue& val)
|
||||
{
|
||||
return ExpressionStringifier::StringifyExpression(val);
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
# include "Plugin.h"
|
||||
|
||||
# include <future>
|
||||
# include <list>
|
||||
# include <memory>
|
||||
# include <mutex>
|
||||
# include <queue>
|
||||
|
@ -42,6 +43,10 @@ namespace OpenRCT2
|
|||
|
||||
namespace OpenRCT2::Scripting
|
||||
{
|
||||
# ifndef DISABLE_NETWORK
|
||||
class ScSocketBase;
|
||||
# endif
|
||||
|
||||
class ScriptExecutionInfo
|
||||
{
|
||||
private:
|
||||
|
@ -133,6 +138,9 @@ namespace OpenRCT2::Scripting
|
|||
};
|
||||
|
||||
std::unordered_map<std::string, CustomActionInfo> _customActions;
|
||||
# ifndef DISABLE_NETWORK
|
||||
std::list<std::shared_ptr<ScSocketBase>> _sockets;
|
||||
# endif
|
||||
|
||||
public:
|
||||
ScriptEngine(InteractiveConsole& console, IPlatformEnvironment& env);
|
||||
|
@ -186,6 +194,10 @@ namespace OpenRCT2::Scripting
|
|||
|
||||
void SaveSharedStorage();
|
||||
|
||||
# ifndef DISABLE_NETWORK
|
||||
void AddSocket(const std::shared_ptr<ScSocketBase>& socket);
|
||||
# endif
|
||||
|
||||
private:
|
||||
void Initialise();
|
||||
void StartPlugins();
|
||||
|
@ -206,6 +218,9 @@ namespace OpenRCT2::Scripting
|
|||
|
||||
void InitSharedStorage();
|
||||
void LoadSharedStorage();
|
||||
|
||||
void UpdateSockets();
|
||||
void RemoveSockets(const std::shared_ptr<Plugin>& plugin);
|
||||
};
|
||||
|
||||
bool IsGameStateMutable();
|
||||
|
|
Loading…
Reference in New Issue