From dce547af997046bccca79196e4748fa95a1c466e Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 17 Aug 2020 03:53:37 +0100 Subject: [PATCH] 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();