Merge pull request #12712 from IntelOrca/plugin/tcp

Plugin: Add API for listening and communicating over TCP
This commit is contained in:
Ted John 2020-09-03 23:06:26 +01:00 committed by GitHub
commit f1fb86e7f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 801 additions and 46 deletions

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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/.

View File

@ -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" />

View File

@ -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)

View File

@ -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)
{

View File

@ -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();

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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();