From dce547af997046bccca79196e4748fa95a1c466e Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 03:53:37 +0100 Subject: [PATCH 01/17] Start implementing TCP API --- distribution/openrct2.d.ts | 29 ++ src/openrct2/libopenrct2.vcxproj | 1 + src/openrct2/network/Socket.cpp | 12 + src/openrct2/network/Socket.h | 1 + src/openrct2/scripting/Duktape.hpp | 11 + src/openrct2/scripting/ScNetwork.hpp | 31 +++ src/openrct2/scripting/ScSocketServer.hpp | 313 ++++++++++++++++++++++ src/openrct2/scripting/ScriptEngine.cpp | 42 ++- src/openrct2/scripting/ScriptEngine.h | 10 + 9 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 src/openrct2/scripting/ScSocketServer.hpp diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index a1e46d79c1..293cbe419c 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -1221,6 +1221,9 @@ declare global { kickPlayer(index: number): void; sendMessage(message: string): void; sendMessage(message: string, players: number[]): void; + + createServer(): SocketServer; + createSocket(): Socket; } type NetworkMode = "none" | "server" | "client"; @@ -1677,4 +1680,30 @@ declare global { moveTo(position: CoordsXY | CoordsXYZ): void; scrollTo(position: CoordsXY | CoordsXYZ): void; } + + /** + * Represents a server that can listen for incomming connections. + * Based on node.js net.Server, see https://nodejs.org/api/net.html for more information. + */ + interface SocketServer { + listen(port: number): SocketServer; + close(): SocketServer; + + on(event: 'connection', callback: (socket: Socket) => void): SocketServer; + } + + /** + * 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: 'data', callback: (data: string) => void): Socket; + on(event: 'close', callback: (hadError: boolean) => void): Socket; + } } diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index 52faf9288d..e536ae58ad 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -410,6 +410,7 @@ + diff --git a/src/openrct2/network/Socket.cpp b/src/openrct2/network/Socket.cpp index cdc8535fb4..ba9d56fda9 100644 --- a/src/openrct2/network/Socket.cpp +++ b/src/openrct2/network/Socket.cpp @@ -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 @@ -464,6 +467,14 @@ public: thread.detach(); } + void Finish() override + { + if (_status == SOCKET_STATUS_CONNECTED) + { + shutdown(_socket, SHUT_WR); + } + } + void Disconnect() override { if (_status == SOCKET_STATUS_CONNECTED) @@ -844,6 +855,7 @@ void DisposeWSA() std::unique_ptr CreateTcpSocket() { + InitialiseWSA(); return std::make_unique(); } diff --git a/src/openrct2/network/Socket.h b/src/openrct2/network/Socket.h index e2fd2842b3..9a652264ca 100644 --- a/src/openrct2/network/Socket.h +++ b/src/openrct2/network/Socket.h @@ -67,6 +67,7 @@ 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 Finish() abstract; virtual void Disconnect() abstract; virtual void Close() abstract; }; diff --git a/src/openrct2/scripting/Duktape.hpp b/src/openrct2/scripting/Duktape.hpp index 4460451509..ea73e0b52f 100644 --- a/src/openrct2/scripting/Duktape.hpp +++ b/src/openrct2/scripting/Duktape.hpp @@ -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 inline DukValue ToDuk(duk_context* ctx, const char (&value)[TLen]) { duk_push_string(ctx, value); diff --git a/src/openrct2/scripting/ScNetwork.hpp b/src/openrct2/scripting/ScNetwork.hpp index 84bf5a1cc1..fda8e32f96 100644 --- a/src/openrct2/scripting/ScNetwork.hpp +++ b/src/openrct2/scripting/ScNetwork.hpp @@ -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 "ScSocketServer.hpp" namespace OpenRCT2::Scripting { @@ -447,6 +449,32 @@ namespace OpenRCT2::Scripting # endif } + std::shared_ptr createServer() + { +# ifndef DISABLE_NETWORK + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); + auto socket = std::make_shared(plugin); + scriptEngine.AddSocket(socket); + return socket; +# else + duk_error(_context, DUK_ERR_ERROR, "Networking has been disabled."); +# endif + } + + std::shared_ptr createSocket() + { +# ifndef DISABLE_NETWORK + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); + auto socket = std::make_shared(plugin); + scriptEngine.AddSocket(socket); + return socket; +# else + 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 +490,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::createServer, "createServer"); + dukglue_register_method(ctx, &ScNetwork::createSocket, "createSocket"); } }; } // namespace OpenRCT2::Scripting diff --git a/src/openrct2/scripting/ScSocketServer.hpp b/src/openrct2/scripting/ScSocketServer.hpp new file mode 100644 index 0000000000..eb787100f0 --- /dev/null +++ b/src/openrct2/scripting/ScSocketServer.hpp @@ -0,0 +1,313 @@ +/***************************************************************************** + * 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 + +# include "../Context.h" +# include "../network/Socket.h" +# include "Duktape.hpp" +# include "ScriptEngine.h" + +namespace OpenRCT2::Scripting +{ + class ScSocketBase + { + private: + std::shared_ptr _plugin; + + public: + ScSocketBase(const std::shared_ptr& plugin) + : _plugin(plugin) + { + } + + virtual ~ScSocketBase() + { + Dispose(); + } + + const std::shared_ptr& GetPlugin() const + { + return _plugin; + } + + virtual void Update() = 0; + + virtual void Dispose() + { + } + + virtual bool IsDisposed() const = 0; + }; + + class ScSocket : public ScSocketBase + { + private: + std::unique_ptr _socket; + bool _disposed{}; + + DukValue _onClose; + DukValue _onData; + + public: + ScSocket(const std::shared_ptr& plugin) + : ScSocketBase(plugin) + { + } + + ScSocket(const std::shared_ptr& plugin, std::unique_ptr&& socket) + : ScSocketBase(plugin) + , _socket(std::move(socket)) + { + } + + private: + ScSocket* destroy(const DukValue& error) + { + if (_socket != nullptr) + { + _socket->Close(); + _socket = nullptr; + } + 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(); + } + 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) + { + _socket->SendData(data.c_str(), data.size()); + return true; + } + return false; + } + + ScSocket* on(const std::string& eventType, const DukValue& callback) + { + if (eventType == "close") + { + _onClose = callback; + } + else if (eventType == "data") + { + _onData = callback; + } + return this; + } + + void CloseSocket() + { + if (_socket != nullptr) + { + _socket->Close(); + _socket = nullptr; + RaiseOnClose(false); + } + } + + void RaiseOnClose(bool hadError) + { + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto ctx = scriptEngine.GetContext(); + scriptEngine.ExecutePluginCall(GetPlugin(), _onClose, { ToDuk(ctx, hadError) }, false); + } + + void RaiseOnData(const std::string& data) + { + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto ctx = scriptEngine.GetContext(); + scriptEngine.ExecutePluginCall(GetPlugin(), _onData, { ToDuk(ctx, data) }, false); + } + + public: + void Update() override + { + if (_disposed) + return; + + if (_socket != nullptr) + { + if (_socket->GetStatus() == SOCKET_STATUS_CONNECTED) + { + char buffer[128]; + size_t bytesRead{}; + auto result = _socket->ReceiveData(buffer, sizeof(buffer), &bytesRead); + switch (result) + { + case NETWORK_READPACKET_SUCCESS: + RaiseOnData(std::string(buffer, bytesRead)); + break; + case NETWORK_READPACKET_NO_DATA: + break; + case NETWORK_READPACKET_MORE_DATA: + break; + case NETWORK_READPACKET_DISCONNECTED: + CloseSocket(); + _disposed = true; + break; + } + } + else + { + CloseSocket(); + _disposed = true; + } + } + } + + 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::end, "end"); + dukglue_register_method(ctx, &ScSocket::write, "write"); + dukglue_register_method(ctx, &ScSocket::on, "on"); + } + }; + + class ScSocketServer : public ScSocketBase + { + private: + std::unique_ptr _socket; + DukValue _onConnection; + std::vector> _scClientSockets; + bool _disposed{}; + + ScSocketServer* close() + { + Dispose(); + return this; + } + + ScSocketServer* listen(int32_t port, const DukValue& callback) + { + if (_disposed) + { + auto ctx = GetContext()->GetScriptEngine().GetContext(); + duk_error(ctx, DUK_ERR_ERROR, "Socket is disposed."); + } + else + { + if (_socket == nullptr) + { + _socket = CreateTcpSocket(); + } + + if (_socket->GetStatus() == SOCKET_STATUS_LISTENING) + { + auto ctx = GetContext()->GetScriptEngine().GetContext(); + duk_error(ctx, DUK_ERR_ERROR, "Server is already listening."); + } + else + { + _socket->Listen(port); + } + } + return this; + } + + ScSocketServer* on(const std::string& eventType, const DukValue& callback) + { + if (eventType == "connection") + { + _onConnection = callback; + } + return this; + } + + public: + ScSocketServer(const std::shared_ptr& 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) + { + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto clientSocket = std::make_shared(GetPlugin(), std::move(client)); + scriptEngine.AddSocket(clientSocket); + + auto ctx = scriptEngine.GetContext(); + auto dukClientSocket = GetObjectAsDukValue(ctx, clientSocket); + scriptEngine.ExecutePluginCall(GetPlugin(), _onConnection, { dukClientSocket }, false); + } + } + } + + void Dispose() override + { + if (_socket != nullptr) + { + _socket->Close(); + _socket = nullptr; + } + _disposed = true; + } + + bool IsDisposed() const override + { + return _disposed; + } + + static void Register(duk_context* ctx) + { + dukglue_register_method(ctx, &ScSocketServer::close, "close"); + dukglue_register_method(ctx, &ScSocketServer::listen, "listen"); + dukglue_register_method(ctx, &ScSocketServer::on, "on"); + } + }; +} // namespace OpenRCT2::Scripting + +#endif diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 657b3d2faf..fe59b3a431 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -33,6 +33,7 @@ # include "ScObject.hpp" # include "ScPark.hpp" # include "ScRide.hpp" +# include "ScSocketServer.hpp" # include "ScTile.hpp" # include @@ -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,8 @@ void ScriptEngine::Initialise() ScVehicle::Register(ctx); ScPeep::Register(ctx); ScGuest::Register(ctx); + ScSocket::Register(ctx); + ScSocketServer::Register(ctx); ScStaff::Register(ctx); dukglue_register_global(ctx, std::make_shared(), "cheats"); @@ -479,6 +482,7 @@ void ScriptEngine::StopPlugin(std::shared_ptr plugin) if (plugin->HasStarted()) { RemoveCustomGameActions(plugin); + RemoveSockets(plugin); _hookEngine.UnsubscribeAll(plugin); for (auto callback : _pluginStoppedSubscriptions) { @@ -640,6 +644,7 @@ void ScriptEngine::Update() } } + UpdateSockets(); ProcessREPL(); } @@ -1127,6 +1132,41 @@ void ScriptEngine::SaveSharedStorage() } } +void ScriptEngine::AddSocket(const std::shared_ptr& socket) +{ + _sockets.push_back(socket); +} + +void ScriptEngine::UpdateSockets() +{ + // Use simple for i loop as Update calls can modify the list + for (size_t i = 0; i < _sockets.size(); i++) + { + _sockets[i]->Update(); + if (_sockets[i]->IsDisposed()) + { + _sockets.erase(_sockets.begin() + i); + i--; + } + } +} + +void ScriptEngine::RemoveSockets(const std::shared_ptr& plugin) +{ + for (auto it = _sockets.begin(); it != _sockets.end();) + { + if ((*it)->GetPlugin() == plugin) + { + (*it)->Dispose(); + it = _sockets.erase(it); + } + else + { + it++; + } + } +} + std::string OpenRCT2::Scripting::Stringify(const DukValue& val) { return ExpressionStringifier::StringifyExpression(val); diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index ba2fc9666f..9947473551 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -42,6 +42,8 @@ namespace OpenRCT2 namespace OpenRCT2::Scripting { + class ScSocketBase; + class ScriptExecutionInfo { private: @@ -133,6 +135,9 @@ namespace OpenRCT2::Scripting }; std::unordered_map _customActions; +# ifndef DISABLE_NETWORK + std::vector> _sockets; +# endif public: ScriptEngine(InteractiveConsole& console, IPlatformEnvironment& env); @@ -186,6 +191,8 @@ namespace OpenRCT2::Scripting void SaveSharedStorage(); + void AddSocket(const std::shared_ptr& socket); + private: void Initialise(); void StartPlugins(); @@ -206,6 +213,9 @@ namespace OpenRCT2::Scripting void InitSharedStorage(); void LoadSharedStorage(); + + void UpdateSockets(); + void RemoveSockets(const std::shared_ptr& plugin); }; bool IsGameStateMutable(); From 7a5cb8a5b61df800e8e68e8d9fae6bc6ca7bf299 Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 12:19:19 +0100 Subject: [PATCH 02/17] Refactor events into EventList class --- src/openrct2/scripting/ScSocketServer.hpp | 121 +++++++++++++++++++--- 1 file changed, 106 insertions(+), 15 deletions(-) diff --git a/src/openrct2/scripting/ScSocketServer.hpp b/src/openrct2/scripting/ScSocketServer.hpp index eb787100f0..537a95e8c7 100644 --- a/src/openrct2/scripting/ScSocketServer.hpp +++ b/src/openrct2/scripting/ScSocketServer.hpp @@ -16,8 +16,59 @@ # include "Duktape.hpp" # include "ScriptEngine.h" +# include +# include + namespace OpenRCT2::Scripting { + class EventList + { + private: + std::vector> _listeners; + + std::vector& GetListenerList(uint32_t id) + { + if (_listeners.size() <= id) + { + _listeners.resize(static_cast(id) + 1); + } + return _listeners[id]; + } + + public: + void Raise( + uint32_t id, const std::shared_ptr& plugin, const std::vector& 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); + auto it = std::find(listeners.begin(), listeners.end(), value); + if (it != listeners.end()) + { + listeners.erase(it); + } + } + }; + class ScSocketBase { private: @@ -51,12 +102,14 @@ namespace OpenRCT2::Scripting class ScSocket : public ScSocketBase { private: + static constexpr uint32_t EVENT_NONE = std::numeric_limits::max(); + static constexpr uint32_t EVENT_CLOSE = 0; + static constexpr uint32_t EVENT_DATA = 1; + + EventList _eventList; std::unique_ptr _socket; bool _disposed{}; - DukValue _onClose; - DukValue _onData; - public: ScSocket(const std::shared_ptr& plugin) : ScSocketBase(plugin) @@ -115,13 +168,20 @@ namespace OpenRCT2::Scripting ScSocket* on(const std::string& eventType, const DukValue& callback) { - if (eventType == "close") + auto eventId = GetEventType(eventType); + if (eventId != EVENT_NONE) { - _onClose = callback; + _eventList.AddListener(eventId, callback); } - else if (eventType == "data") + return this; + } + + ScSocket* off(const std::string& eventType, const DukValue& callback) + { + auto eventId = GetEventType(eventType); + if (eventId != EVENT_NONE) { - _onData = callback; + _eventList.RemoveListener(eventId, callback); } return this; } @@ -138,16 +198,24 @@ namespace OpenRCT2::Scripting void RaiseOnClose(bool hadError) { - auto& scriptEngine = GetContext()->GetScriptEngine(); - auto ctx = scriptEngine.GetContext(); - scriptEngine.ExecutePluginCall(GetPlugin(), _onClose, { ToDuk(ctx, hadError) }, false); + 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(); - scriptEngine.ExecutePluginCall(GetPlugin(), _onData, { ToDuk(ctx, data) }, false); + _eventList.Raise(EVENT_DATA, GetPlugin(), { ToDuk(ctx, data) }, false); + } + + uint32_t GetEventType(const std::string_view& name) + { + if (name == "close") + return EVENT_CLOSE; + if (name == "data") + return EVENT_DATA; + return EVENT_NONE; } public: @@ -203,14 +271,18 @@ namespace OpenRCT2::Scripting 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 ScSocketServer : public ScSocketBase { private: + static constexpr uint32_t EVENT_NONE = std::numeric_limits::max(); + static constexpr uint32_t EVENT_CONNECTION = 0; + + EventList _eventList; std::unique_ptr _socket; - DukValue _onConnection; std::vector> _scClientSockets; bool _disposed{}; @@ -249,13 +321,31 @@ namespace OpenRCT2::Scripting ScSocketServer* on(const std::string& eventType, const DukValue& callback) { - if (eventType == "connection") + auto eventId = GetEventType(eventType); + if (eventId != EVENT_NONE) { - _onConnection = callback; + _eventList.AddListener(eventId, callback); } return this; } + ScSocketServer* 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: ScSocketServer(const std::shared_ptr& plugin) : ScSocketBase(plugin) @@ -281,7 +371,7 @@ namespace OpenRCT2::Scripting auto ctx = scriptEngine.GetContext(); auto dukClientSocket = GetObjectAsDukValue(ctx, clientSocket); - scriptEngine.ExecutePluginCall(GetPlugin(), _onConnection, { dukClientSocket }, false); + _eventList.Raise(EVENT_CONNECTION, GetPlugin(), { dukClientSocket }, false); } } } @@ -306,6 +396,7 @@ namespace OpenRCT2::Scripting dukglue_register_method(ctx, &ScSocketServer::close, "close"); dukglue_register_method(ctx, &ScSocketServer::listen, "listen"); dukglue_register_method(ctx, &ScSocketServer::on, "on"); + dukglue_register_method(ctx, &ScSocketServer::off, "off"); } }; } // namespace OpenRCT2::Scripting From 182bcaf21a4f3f9317f4fbc697fcd7de0f48eae5 Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 13:10:36 +0100 Subject: [PATCH 03/17] Implement setNoDelay --- src/openrct2/network/Socket.cpp | 12 ++++++++++-- src/openrct2/network/Socket.h | 2 ++ src/openrct2/scripting/ScSocketServer.hpp | 13 +++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/openrct2/network/Socket.cpp b/src/openrct2/network/Socket.cpp index ba9d56fda9..cbbcfdf96c 100644 --- a/src/openrct2/network/Socket.cpp +++ b/src/openrct2/network/Socket.cpp @@ -233,6 +233,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); @@ -334,7 +342,7 @@ public: int32_t rc = getnameinfo( reinterpret_cast(&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) { @@ -375,7 +383,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."); diff --git a/src/openrct2/network/Socket.h b/src/openrct2/network/Socket.h index 9a652264ca..5d9b9a89ad 100644 --- a/src/openrct2/network/Socket.h +++ b/src/openrct2/network/Socket.h @@ -67,6 +67,8 @@ 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; diff --git a/src/openrct2/scripting/ScSocketServer.hpp b/src/openrct2/scripting/ScSocketServer.hpp index 537a95e8c7..6bff92ebdb 100644 --- a/src/openrct2/scripting/ScSocketServer.hpp +++ b/src/openrct2/scripting/ScSocketServer.hpp @@ -133,6 +133,15 @@ namespace OpenRCT2::Scripting return this; } + ScSocket* setNoDelay(bool noDelay) + { + if (_socket != nullptr) + { + _socket->SetNoDelay(noDelay); + } + return this; + } + ScSocket* end(const DukValue& data) { if (_disposed) @@ -268,6 +277,7 @@ namespace OpenRCT2::Scripting 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::end, "end"); dukglue_register_method(ctx, &ScSocket::write, "write"); dukglue_register_method(ctx, &ScSocket::on, "on"); @@ -365,6 +375,9 @@ namespace OpenRCT2::Scripting 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(GetPlugin(), std::move(client)); scriptEngine.AddSocket(clientSocket); From 703dc1efa7ceeee96e4d768394620f42340bbf71 Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 14:15:35 +0100 Subject: [PATCH 04/17] Add ability to connect --- src/openrct2/scripting/ScSocketServer.hpp | 56 ++++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/openrct2/scripting/ScSocketServer.hpp b/src/openrct2/scripting/ScSocketServer.hpp index 6bff92ebdb..1c540c3d13 100644 --- a/src/openrct2/scripting/ScSocketServer.hpp +++ b/src/openrct2/scripting/ScSocketServer.hpp @@ -67,6 +67,12 @@ namespace OpenRCT2::Scripting listeners.erase(it); } } + + void RemoveAllListeners(uint32_t id) + { + auto& listeners = GetListenerList(id); + listeners.clear(); + } }; class ScSocketBase @@ -105,10 +111,13 @@ namespace OpenRCT2::Scripting static constexpr uint32_t EVENT_NONE = std::numeric_limits::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 _socket; bool _disposed{}; + bool _connecting{}; public: ScSocket(const std::shared_ptr& plugin) @@ -142,6 +151,31 @@ namespace OpenRCT2::Scripting 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 + { + _socket = CreateTcpSocket(); + _socket->Connect(host, port); + _eventList.AddListener(EVENT_CONNECT_ONCE, callback); + _connecting = true; + } + return this; + } + ScSocket* end(const DukValue& data) { if (_disposed) @@ -224,6 +258,8 @@ namespace OpenRCT2::Scripting return EVENT_CLOSE; if (name == "data") return EVENT_DATA; + if (name == "error") + return EVENT_ERROR; return EVENT_NONE; } @@ -235,9 +271,24 @@ namespace OpenRCT2::Scripting if (_socket != nullptr) { - if (_socket->GetStatus() == SOCKET_STATUS_CONNECTED) + auto status = _socket->GetStatus(); + if (_connecting) { - char buffer[128]; + if (status == SOCKET_STATUS_CONNECTED) + { + _connecting = false; + _eventList.Raise(EVENT_CONNECT_ONCE, GetPlugin(), {}, false); + _eventList.RemoveAllListeners(EVENT_CONNECT_ONCE); + } + else if (status == SOCKET_STATUS_CLOSED) + { + _connecting = false; + _eventList.Raise(EVENT_ERROR, GetPlugin(), {}, false); + } + } + else if (status == SOCKET_STATUS_CONNECTED) + { + char buffer[2048]; size_t bytesRead{}; auto result = _socket->ReceiveData(buffer, sizeof(buffer), &bytesRead); switch (result) @@ -278,6 +329,7 @@ namespace OpenRCT2::Scripting { 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"); From 5da5804f8443224a42e9e412618dafeed4bee944 Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 14:26:19 +0100 Subject: [PATCH 05/17] Add localhost limitation --- src/openrct2/scripting/ScSocketServer.hpp | 32 ++++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/openrct2/scripting/ScSocketServer.hpp b/src/openrct2/scripting/ScSocketServer.hpp index 1c540c3d13..8533fe562e 100644 --- a/src/openrct2/scripting/ScSocketServer.hpp +++ b/src/openrct2/scripting/ScSocketServer.hpp @@ -80,6 +80,12 @@ namespace OpenRCT2::Scripting private: std::shared_ptr _plugin; + protected: + static bool IsLocalhostAddress(const std::string_view& s) + { + return s == "localhost" || s == "127.0.0.1" || s == "::"; + } + public: ScSocketBase(const std::shared_ptr& plugin) : _plugin(plugin) @@ -166,6 +172,10 @@ namespace OpenRCT2::Scripting { 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(); @@ -354,11 +364,11 @@ namespace OpenRCT2::Scripting return this; } - ScSocketServer* listen(int32_t port, const DukValue& callback) + ScSocketServer* listen(int32_t port, const DukValue& dukHost) { + auto ctx = GetContext()->GetScriptEngine().GetContext(); if (_disposed) { - auto ctx = GetContext()->GetScriptEngine().GetContext(); duk_error(ctx, DUK_ERR_ERROR, "Socket is disposed."); } else @@ -370,12 +380,26 @@ namespace OpenRCT2::Scripting if (_socket->GetStatus() == SOCKET_STATUS_LISTENING) { - auto ctx = GetContext()->GetScriptEngine().GetContext(); duk_error(ctx, DUK_ERR_ERROR, "Server is already listening."); } else { - _socket->Listen(port); + if (dukHost.type() == DukValue::Type::STRING) + { + auto host = dukHost.as_string(); + if (IsLocalhostAddress(host)) + { + _socket->Listen(host, port); + } + 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; From 7dfb748500f8131bf34152a144d67853cdbee36b Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 16:54:13 +0100 Subject: [PATCH 06/17] Add listening property --- distribution/openrct2.d.ts | 11 ++++++++++- src/openrct2/scripting/ScSocketServer.hpp | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 293cbe419c..35d5873f92 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -1686,10 +1686,14 @@ declare global { * Based on node.js net.Server, see https://nodejs.org/api/net.html for more information. */ interface SocketServer { + readonly listening: boolean; + listen(port: number): SocketServer; close(): SocketServer; on(event: 'connection', callback: (socket: Socket) => void): SocketServer; + + off(event: 'connection', callback: (socket: Socket) => void): SocketServer; } /** @@ -1703,7 +1707,12 @@ declare global { end(data?: string): Socket; write(data: string): boolean; - on(event: 'data', callback: (data: string) => void): Socket; 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; } } diff --git a/src/openrct2/scripting/ScSocketServer.hpp b/src/openrct2/scripting/ScSocketServer.hpp index 8533fe562e..12c23775e2 100644 --- a/src/openrct2/scripting/ScSocketServer.hpp +++ b/src/openrct2/scripting/ScSocketServer.hpp @@ -358,6 +358,15 @@ namespace OpenRCT2::Scripting std::vector> _scClientSockets; bool _disposed{}; + bool listening_get() + { + if (_socket != nullptr) + { + return _socket->GetStatus() == SOCKET_STATUS_LISTENING; + } + return false; + } + ScSocketServer* close() { Dispose(); @@ -482,6 +491,7 @@ namespace OpenRCT2::Scripting static void Register(duk_context* ctx) { + dukglue_register_property(ctx, &ScSocketServer::listening_get, nullptr, "listening"); dukglue_register_method(ctx, &ScSocketServer::close, "close"); dukglue_register_method(ctx, &ScSocketServer::listen, "listen"); dukglue_register_method(ctx, &ScSocketServer::on, "on"); From a259b6e36370b5d13673f66e06456bad024c4c35 Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 16:58:15 +0100 Subject: [PATCH 07/17] Update distribute text in scripting.md --- distribution/scripting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/scripting.md b/distribution/scripting.md index f7a7af1259..9f520ccda7 100644 --- a/distribution/scripting.md +++ b/distribution/scripting.md @@ -199,4 +199,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 http://openrct2plugins.org/. From 670ec32de8c7c00f3a59207636dbe05877a351ce Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 17:16:20 +0100 Subject: [PATCH 08/17] Add some information about TCP streams to scripting.md --- distribution/scripting.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/distribution/scripting.md b/distribution/scripting.md index 9f520ccda7..9eca678168 100644 --- a/distribution/scripting.md +++ b/distribution/scripting.md @@ -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. From 7cf06a6d0b729debf9a415facebc12e1e52282cd Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 17:23:39 +0100 Subject: [PATCH 09/17] Update changelog --- distribution/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/distribution/changelog.txt b/distribution/changelog.txt index bc568367ef..ceaa528002 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -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. From fd6fddb61aec4f231d669e3f3bfc391da2ad75b8 Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 17:44:53 +0100 Subject: [PATCH 10/17] Improve WSA handling --- src/openrct2/network/NetworkBase.cpp | 7 --- src/openrct2/network/Socket.cpp | 84 +++++++++++++++++----------- src/openrct2/network/Socket.h | 2 - 3 files changed, 52 insertions(+), 41 deletions(-) diff --git a/src/openrct2/network/NetworkBase.cpp b/src/openrct2/network/NetworkBase.cpp index 18e8a477ac..d52209c464 100644 --- a/src/openrct2/network/NetworkBase.cpp +++ b/src/openrct2/network/NetworkBase.cpp @@ -148,11 +148,6 @@ void NetworkBase::SetEnvironment(const std::shared_ptr& 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) diff --git a/src/openrct2/network/Socket.cpp b/src/openrct2/network/Socket.cpp index cbbcfdf96c..2cf213ab2c 100644 --- a/src/openrct2/network/Socket.cpp +++ b/src/openrct2/network/Socket.cpp @@ -70,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 @@ -830,37 +878,6 @@ 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 CreateTcpSocket() { InitialiseWSA(); @@ -869,12 +886,15 @@ std::unique_ptr CreateTcpSocket() std::unique_ptr CreateUdpSocket() { + InitialiseWSA(); return std::make_unique(); } # ifdef _WIN32 static std::vector GetNetworkInterfaces() { + InitialiseWSA(); + int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock == -1) { diff --git a/src/openrct2/network/Socket.h b/src/openrct2/network/Socket.h index 5d9b9a89ad..87ba24ee65 100644 --- a/src/openrct2/network/Socket.h +++ b/src/openrct2/network/Socket.h @@ -97,8 +97,6 @@ public: virtual void Close() abstract; }; -bool InitialiseWSA(); -void DisposeWSA(); std::unique_ptr CreateTcpSocket(); std::unique_ptr CreateUdpSocket(); std::vector> GetBroadcastAddresses(); From 1c91404707b602ea0bc02c450092c9af3778d5aa Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 17:46:00 +0100 Subject: [PATCH 11/17] Add more network guards --- src/openrct2/scripting/ScNetwork.hpp | 22 ++++++++++++++-------- src/openrct2/scripting/ScSocketServer.hpp | 14 ++++++++------ src/openrct2/scripting/ScriptEngine.cpp | 8 ++++++++ src/openrct2/scripting/ScriptEngine.h | 4 ++++ 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/openrct2/scripting/ScNetwork.hpp b/src/openrct2/scripting/ScNetwork.hpp index fda8e32f96..4fd8e19b81 100644 --- a/src/openrct2/scripting/ScNetwork.hpp +++ b/src/openrct2/scripting/ScNetwork.hpp @@ -449,31 +449,37 @@ namespace OpenRCT2::Scripting # endif } +# ifndef DISABLE_NETWORK std::shared_ptr createServer() { -# ifndef DISABLE_NETWORK auto& scriptEngine = GetContext()->GetScriptEngine(); auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); auto socket = std::make_shared(plugin); scriptEngine.AddSocket(socket); return socket; -# else - duk_error(_context, DUK_ERR_ERROR, "Networking has been disabled."); -# endif } +# else + void createServer() + { + duk_error(_context, DUK_ERR_ERROR, "Networking has been disabled."); + } +# endif +# ifndef DISABLE_NETWORK std::shared_ptr createSocket() { -# ifndef DISABLE_NETWORK auto& scriptEngine = GetContext()->GetScriptEngine(); auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); auto socket = std::make_shared(plugin); scriptEngine.AddSocket(socket); return socket; -# else - duk_error(_context, DUK_ERR_ERROR, "Networking has been disabled."); -# endif } +# else + void createSocket() + { + duk_error(_context, DUK_ERR_ERROR, "Networking has been disabled."); + } +# endif static void Register(duk_context* ctx) { diff --git a/src/openrct2/scripting/ScSocketServer.hpp b/src/openrct2/scripting/ScSocketServer.hpp index 12c23775e2..98691b601a 100644 --- a/src/openrct2/scripting/ScSocketServer.hpp +++ b/src/openrct2/scripting/ScSocketServer.hpp @@ -10,14 +10,15 @@ #pragma once #ifdef ENABLE_SCRIPTING +# ifndef DISABLE_NETWORK -# include "../Context.h" -# include "../network/Socket.h" -# include "Duktape.hpp" -# include "ScriptEngine.h" +# include "../Context.h" +# include "../network/Socket.h" +# include "Duktape.hpp" +# include "ScriptEngine.h" -# include -# include +# include +# include namespace OpenRCT2::Scripting { @@ -500,4 +501,5 @@ namespace OpenRCT2::Scripting }; } // namespace OpenRCT2::Scripting +# endif #endif diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index fe59b3a431..3ba2bdcabc 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -394,8 +394,10 @@ void ScriptEngine::Initialise() ScVehicle::Register(ctx); ScPeep::Register(ctx); ScGuest::Register(ctx); +# ifndef DISABLE_NETWORK ScSocket::Register(ctx); ScSocketServer::Register(ctx); +# endif ScStaff::Register(ctx); dukglue_register_global(ctx, std::make_shared(), "cheats"); @@ -1132,13 +1134,16 @@ void ScriptEngine::SaveSharedStorage() } } +# ifndef DISABLE_NETWORK void ScriptEngine::AddSocket(const std::shared_ptr& socket) { _sockets.push_back(socket); } +# endif void ScriptEngine::UpdateSockets() { +# ifndef DISABLE_NETWORK // Use simple for i loop as Update calls can modify the list for (size_t i = 0; i < _sockets.size(); i++) { @@ -1149,10 +1154,12 @@ void ScriptEngine::UpdateSockets() i--; } } +# endif } void ScriptEngine::RemoveSockets(const std::shared_ptr& plugin) { +# ifndef DISABLE_NETWORK for (auto it = _sockets.begin(); it != _sockets.end();) { if ((*it)->GetPlugin() == plugin) @@ -1165,6 +1172,7 @@ void ScriptEngine::RemoveSockets(const std::shared_ptr& plugin) it++; } } +# endif } std::string OpenRCT2::Scripting::Stringify(const DukValue& val) diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 9947473551..6f08d2b4a0 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -42,7 +42,9 @@ namespace OpenRCT2 namespace OpenRCT2::Scripting { +# ifndef DISABLE_NETWORK class ScSocketBase; +# endif class ScriptExecutionInfo { @@ -191,7 +193,9 @@ namespace OpenRCT2::Scripting void SaveSharedStorage(); +# ifndef DISABLE_NETWORK void AddSocket(const std::shared_ptr& socket); +# endif private: void Initialise(); From 76dded4e1ebfacf1f161dd1726456685a10773c1 Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 22:04:14 +0100 Subject: [PATCH 12/17] Update distribution/scripting.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: MichaƂ Janiszewski --- distribution/scripting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/scripting.md b/distribution/scripting.md index 9eca678168..e820e7e248 100644 --- a/distribution/scripting.md +++ b/distribution/scripting.md @@ -214,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? -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 http://openrct2plugins.org/. +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/. From 173a42f656e0943387c2b42310f0f82cb2a44e65 Mon Sep 17 00:00:00 2001 From: Ted John Date: Wed, 26 Aug 2020 01:56:50 +0100 Subject: [PATCH 13/17] Apply code review suggestions --- src/openrct2/scripting/ScSocketServer.hpp | 57 +++++++++++++++-------- src/openrct2/scripting/ScriptEngine.cpp | 31 ++++++------ src/openrct2/scripting/ScriptEngine.h | 3 +- 3 files changed, 54 insertions(+), 37 deletions(-) diff --git a/src/openrct2/scripting/ScSocketServer.hpp b/src/openrct2/scripting/ScSocketServer.hpp index 98691b601a..b9c87c39aa 100644 --- a/src/openrct2/scripting/ScSocketServer.hpp +++ b/src/openrct2/scripting/ScSocketServer.hpp @@ -62,11 +62,7 @@ namespace OpenRCT2::Scripting void RemoveListener(uint32_t id, const DukValue& value) { auto& listeners = GetListenerList(id); - auto it = std::find(listeners.begin(), listeners.end(), value); - if (it != listeners.end()) - { - listeners.erase(it); - } + listeners.erase(std::remove(listeners.begin(), listeners.end(), value), listeners.end()); } void RemoveAllListeners(uint32_t id) @@ -82,7 +78,7 @@ namespace OpenRCT2::Scripting std::shared_ptr _plugin; protected: - static bool IsLocalhostAddress(const std::string_view& s) + static bool IsLocalhostAddress(std::string_view s) { return s == "localhost" || s == "127.0.0.1" || s == "::"; } @@ -112,7 +108,7 @@ namespace OpenRCT2::Scripting virtual bool IsDisposed() const = 0; }; - class ScSocket : public ScSocketBase + class ScSocket final : public ScSocketBase { private: static constexpr uint32_t EVENT_NONE = std::numeric_limits::max(); @@ -199,8 +195,14 @@ namespace OpenRCT2::Scripting 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."); } - _socket->Finish(); } return this; } @@ -214,8 +216,15 @@ namespace OpenRCT2::Scripting } else if (_socket != nullptr) { - _socket->SendData(data.c_str(), data.size()); - return true; + try + { + auto sentBytes = _socket->SendData(data.c_str(), data.size()); + return sentBytes != data.size(); + } + catch (const std::exception&) + { + return false; + } } return false; } @@ -263,7 +272,7 @@ namespace OpenRCT2::Scripting _eventList.Raise(EVENT_DATA, GetPlugin(), { ToDuk(ctx, data) }, false); } - uint32_t GetEventType(const std::string_view& name) + uint32_t GetEventType(std::string_view name) { if (name == "close") return EVENT_CLOSE; @@ -312,15 +321,13 @@ namespace OpenRCT2::Scripting case NETWORK_READPACKET_MORE_DATA: break; case NETWORK_READPACKET_DISCONNECTED: - CloseSocket(); - _disposed = true; + Dispose(); break; } } else { - CloseSocket(); - _disposed = true; + Dispose(); } } } @@ -348,7 +355,7 @@ namespace OpenRCT2::Scripting } }; - class ScSocketServer : public ScSocketBase + class ScSocketServer final : public ScSocketBase { private: static constexpr uint32_t EVENT_NONE = std::numeric_limits::max(); @@ -370,7 +377,7 @@ namespace OpenRCT2::Scripting ScSocketServer* close() { - Dispose(); + CloseSocket(); return this; } @@ -399,7 +406,14 @@ namespace OpenRCT2::Scripting auto host = dukHost.as_string(); if (IsLocalhostAddress(host)) { - _socket->Listen(host, port); + try + { + _socket->Listen(host, port); + } + catch (const std::exception& e) + { + duk_error(ctx, DUK_ERR_ERROR, e.what()); + } } else { @@ -475,13 +489,18 @@ namespace OpenRCT2::Scripting } } - void Dispose() override + void CloseSocket() { if (_socket != nullptr) { _socket->Close(); _socket = nullptr; } + } + + void Dispose() override + { + CloseSocket(); _disposed = true; } diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 3ba2bdcabc..f8b5639232 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -1145,13 +1145,18 @@ void ScriptEngine::UpdateSockets() { # ifndef DISABLE_NETWORK // Use simple for i loop as Update calls can modify the list - for (size_t i = 0; i < _sockets.size(); i++) + auto it = _sockets.begin(); + while (it != _sockets.end()) { - _sockets[i]->Update(); - if (_sockets[i]->IsDisposed()) + auto& socket = *it; + socket->Update(); + if (socket->IsDisposed()) { - _sockets.erase(_sockets.begin() + i); - i--; + it = _sockets.erase(it); + } + else + { + it++; } } # endif @@ -1160,18 +1165,10 @@ void ScriptEngine::UpdateSockets() void ScriptEngine::RemoveSockets(const std::shared_ptr& plugin) { # ifndef DISABLE_NETWORK - for (auto it = _sockets.begin(); it != _sockets.end();) - { - if ((*it)->GetPlugin() == plugin) - { - (*it)->Dispose(); - it = _sockets.erase(it); - } - else - { - it++; - } - } + _sockets.erase( + std::remove_if( + _sockets.begin(), _sockets.end(), [&plugin](const auto& socket) { return socket->GetPlugin() == plugin; }), + _sockets.end()); # endif } diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 6f08d2b4a0..c4de63bc56 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -19,6 +19,7 @@ # include "Plugin.h" # include +# include # include # include # include @@ -138,7 +139,7 @@ namespace OpenRCT2::Scripting std::unordered_map _customActions; # ifndef DISABLE_NETWORK - std::vector> _sockets; + std::list> _sockets; # endif public: From 0bddf5a5db8c21f01bc7ca7c73c7ed40f2293423 Mon Sep 17 00:00:00 2001 From: Ted John Date: Wed, 26 Aug 2020 02:17:27 +0100 Subject: [PATCH 14/17] Rename SocketServer to Listener --- distribution/openrct2.d.ts | 14 ++++++------ src/openrct2/libopenrct2.vcxproj | 2 +- src/openrct2/scripting/ScNetwork.hpp | 10 ++++----- .../{ScSocketServer.hpp => ScSocket.hpp} | 22 +++++++++---------- src/openrct2/scripting/ScriptEngine.cpp | 4 ++-- 5 files changed, 26 insertions(+), 26 deletions(-) rename src/openrct2/scripting/{ScSocketServer.hpp => ScSocket.hpp} (95%) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 35d5873f92..073fce48e2 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -1222,7 +1222,7 @@ declare global { sendMessage(message: string): void; sendMessage(message: string, players: number[]): void; - createServer(): SocketServer; + createListener(): Listener; createSocket(): Socket; } @@ -1682,18 +1682,18 @@ declare global { } /** - * Represents a server that can listen for incomming connections. + * Listens for incomming connections. * Based on node.js net.Server, see https://nodejs.org/api/net.html for more information. */ - interface SocketServer { + interface Listener { readonly listening: boolean; - listen(port: number): SocketServer; - close(): SocketServer; + listen(port: number): Listener; + close(): Listener; - on(event: 'connection', callback: (socket: Socket) => void): SocketServer; + on(event: 'connection', callback: (socket: Socket) => void): Listener; - off(event: 'connection', callback: (socket: Socket) => void): SocketServer; + off(event: 'connection', callback: (socket: Socket) => void): Listener; } /** diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index e536ae58ad..0fac581d2b 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -410,7 +410,7 @@ - + diff --git a/src/openrct2/scripting/ScNetwork.hpp b/src/openrct2/scripting/ScNetwork.hpp index 4fd8e19b81..44e6c6977b 100644 --- a/src/openrct2/scripting/ScNetwork.hpp +++ b/src/openrct2/scripting/ScNetwork.hpp @@ -18,7 +18,7 @@ # include "../network/NetworkAction.h" # include "../network/network.h" # include "Duktape.hpp" -# include "ScSocketServer.hpp" +# include "ScSocket.hpp" namespace OpenRCT2::Scripting { @@ -450,16 +450,16 @@ namespace OpenRCT2::Scripting } # ifndef DISABLE_NETWORK - std::shared_ptr createServer() + std::shared_ptr createListener() { auto& scriptEngine = GetContext()->GetScriptEngine(); auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); - auto socket = std::make_shared(plugin); + auto socket = std::make_shared(plugin); scriptEngine.AddSocket(socket); return socket; } # else - void createServer() + void createListener() { duk_error(_context, DUK_ERR_ERROR, "Networking has been disabled."); } @@ -497,7 +497,7 @@ namespace OpenRCT2::Scripting dukglue_register_method(ctx, &ScNetwork::kickPlayer, "kickPlayer"); dukglue_register_method(ctx, &ScNetwork::sendMessage, "sendMessage"); - dukglue_register_method(ctx, &ScNetwork::createServer, "createServer"); + dukglue_register_method(ctx, &ScNetwork::createListener, "createListener"); dukglue_register_method(ctx, &ScNetwork::createSocket, "createSocket"); } }; diff --git a/src/openrct2/scripting/ScSocketServer.hpp b/src/openrct2/scripting/ScSocket.hpp similarity index 95% rename from src/openrct2/scripting/ScSocketServer.hpp rename to src/openrct2/scripting/ScSocket.hpp index b9c87c39aa..684dbb4cdb 100644 --- a/src/openrct2/scripting/ScSocketServer.hpp +++ b/src/openrct2/scripting/ScSocket.hpp @@ -355,7 +355,7 @@ namespace OpenRCT2::Scripting } }; - class ScSocketServer final : public ScSocketBase + class ScListener final : public ScSocketBase { private: static constexpr uint32_t EVENT_NONE = std::numeric_limits::max(); @@ -375,13 +375,13 @@ namespace OpenRCT2::Scripting return false; } - ScSocketServer* close() + ScListener* close() { CloseSocket(); return this; } - ScSocketServer* listen(int32_t port, const DukValue& dukHost) + ScListener* listen(int32_t port, const DukValue& dukHost) { auto ctx = GetContext()->GetScriptEngine().GetContext(); if (_disposed) @@ -429,7 +429,7 @@ namespace OpenRCT2::Scripting return this; } - ScSocketServer* on(const std::string& eventType, const DukValue& callback) + ScListener* on(const std::string& eventType, const DukValue& callback) { auto eventId = GetEventType(eventType); if (eventId != EVENT_NONE) @@ -439,7 +439,7 @@ namespace OpenRCT2::Scripting return this; } - ScSocketServer* off(const std::string& eventType, const DukValue& callback) + ScListener* off(const std::string& eventType, const DukValue& callback) { auto eventId = GetEventType(eventType); if (eventId != EVENT_NONE) @@ -457,7 +457,7 @@ namespace OpenRCT2::Scripting } public: - ScSocketServer(const std::shared_ptr& plugin) + ScListener(const std::shared_ptr& plugin) : ScSocketBase(plugin) { } @@ -511,11 +511,11 @@ namespace OpenRCT2::Scripting static void Register(duk_context* ctx) { - dukglue_register_property(ctx, &ScSocketServer::listening_get, nullptr, "listening"); - dukglue_register_method(ctx, &ScSocketServer::close, "close"); - dukglue_register_method(ctx, &ScSocketServer::listen, "listen"); - dukglue_register_method(ctx, &ScSocketServer::on, "on"); - dukglue_register_method(ctx, &ScSocketServer::off, "off"); + 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 diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index f8b5639232..28c1cd323d 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -33,7 +33,7 @@ # include "ScObject.hpp" # include "ScPark.hpp" # include "ScRide.hpp" -# include "ScSocketServer.hpp" +# include "ScSocket.hpp" # include "ScTile.hpp" # include @@ -396,7 +396,7 @@ void ScriptEngine::Initialise() ScGuest::Register(ctx); # ifndef DISABLE_NETWORK ScSocket::Register(ctx); - ScSocketServer::Register(ctx); + ScListener::Register(ctx); # endif ScStaff::Register(ctx); From 33ba51b763b805e0e821e744525926553d398516 Mon Sep 17 00:00:00 2001 From: Ted John Date: Sun, 30 Aug 2020 19:39:59 +0100 Subject: [PATCH 15/17] Fix crashes and improve socket lifecycle --- src/openrct2/network/Socket.cpp | 7 ++++- src/openrct2/network/Socket.h | 1 + src/openrct2/scripting/ScSocket.hpp | 42 ++++++++++++++++++++--------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/openrct2/network/Socket.cpp b/src/openrct2/network/Socket.cpp index 2cf213ab2c..37a39c1d7e 100644 --- a/src/openrct2/network/Socket.cpp +++ b/src/openrct2/network/Socket.cpp @@ -407,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."); } @@ -504,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 barrier; _connectFuture = barrier.get_future(); diff --git a/src/openrct2/network/Socket.h b/src/openrct2/network/Socket.h index 87ba24ee65..01c5f37587 100644 --- a/src/openrct2/network/Socket.h +++ b/src/openrct2/network/Socket.h @@ -18,6 +18,7 @@ enum SOCKET_STATUS { SOCKET_STATUS_CLOSED, + SOCKET_STATUS_WAITING, SOCKET_STATUS_RESOLVING, SOCKET_STATUS_CONNECTING, SOCKET_STATUS_CONNECTED, diff --git a/src/openrct2/scripting/ScSocket.hpp b/src/openrct2/scripting/ScSocket.hpp index 684dbb4cdb..38ff998156 100644 --- a/src/openrct2/scripting/ScSocket.hpp +++ b/src/openrct2/scripting/ScSocket.hpp @@ -121,6 +121,7 @@ namespace OpenRCT2::Scripting std::unique_ptr _socket; bool _disposed{}; bool _connecting{}; + bool _wasConnected{}; public: ScSocket(const std::shared_ptr& plugin) @@ -137,11 +138,7 @@ namespace OpenRCT2::Scripting private: ScSocket* destroy(const DukValue& error) { - if (_socket != nullptr) - { - _socket->Close(); - _socket = nullptr; - } + CloseSocket(); return this; } @@ -176,9 +173,16 @@ namespace OpenRCT2::Scripting else { _socket = CreateTcpSocket(); - _socket->Connect(host, port); - _eventList.AddListener(EVENT_CONNECT_ONCE, callback); - _connecting = true; + 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; } @@ -255,7 +259,11 @@ namespace OpenRCT2::Scripting { _socket->Close(); _socket = nullptr; - RaiseOnClose(false); + if (_wasConnected) + { + _wasConnected = false; + RaiseOnClose(false); + } } } @@ -297,13 +305,23 @@ namespace OpenRCT2::Scripting 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; - _eventList.Raise(EVENT_ERROR, GetPlugin(), {}, 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) @@ -321,13 +339,13 @@ namespace OpenRCT2::Scripting case NETWORK_READPACKET_MORE_DATA: break; case NETWORK_READPACKET_DISCONNECTED: - Dispose(); + CloseSocket(); break; } } else { - Dispose(); + CloseSocket(); } } } From 858bb4045f4aef71e496b361eff756756f37a5b8 Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 31 Aug 2020 20:23:11 +0100 Subject: [PATCH 16/17] Explicitly close sockets when plugin is stopped --- src/openrct2/scripting/ScriptEngine.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 28c1cd323d..102454b362 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -1165,10 +1165,20 @@ void ScriptEngine::UpdateSockets() void ScriptEngine::RemoveSockets(const std::shared_ptr& plugin) { # ifndef DISABLE_NETWORK - _sockets.erase( - std::remove_if( - _sockets.begin(), _sockets.end(), [&plugin](const auto& socket) { return socket->GetPlugin() == plugin; }), - _sockets.end()); + 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 } From c7b8a63fa9477efa0fbfad638cd11ff589b3617b Mon Sep 17 00:00:00 2001 From: Ted John Date: Wed, 2 Sep 2020 23:15:13 +0100 Subject: [PATCH 17/17] Fix network enum --- src/openrct2/scripting/ScSocket.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/openrct2/scripting/ScSocket.hpp b/src/openrct2/scripting/ScSocket.hpp index 38ff998156..c02f011487 100644 --- a/src/openrct2/scripting/ScSocket.hpp +++ b/src/openrct2/scripting/ScSocket.hpp @@ -331,14 +331,14 @@ namespace OpenRCT2::Scripting auto result = _socket->ReceiveData(buffer, sizeof(buffer), &bytesRead); switch (result) { - case NETWORK_READPACKET_SUCCESS: + case NetworkReadPacket::Success: RaiseOnData(std::string(buffer, bytesRead)); break; - case NETWORK_READPACKET_NO_DATA: + case NetworkReadPacket::NoData: break; - case NETWORK_READPACKET_MORE_DATA: + case NetworkReadPacket::MoreData: break; - case NETWORK_READPACKET_DISCONNECTED: + case NetworkReadPacket::Disconnected: CloseSocket(); break; }