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