diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 5857815266..9b33e9d62f 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -185,6 +185,7 @@ declare global { subscribe(hook: "interval.tick", callback: () => void): IDisposable; subscribe(hook: "interval.day", callback: () => void): IDisposable; subscribe(hook: "network.chat", callback: (e: NetworkChatEventArgs) => void): IDisposable; + subscribe(hook: "network.authenticate", callback: (e: NetworkAuthenticateEventArgs) => void): IDisposable; subscribe(hook: "network.join", callback: (e: NetworkEventArgs) => void): IDisposable; subscribe(hook: "network.leave", callback: (e: NetworkEventArgs) => void): IDisposable; } @@ -283,6 +284,13 @@ declare global { message: string; } + interface NetworkAuthenticateEventArgs { + readonly name: number; + readonly ipAddress: string; + readonly publicKeyHash: string; + cancel: boolean; + } + /** * APIs for the in-game date. */ @@ -588,13 +596,16 @@ declare global { */ interface Network { readonly mode: NetworkMode; - readonly groups: number; - readonly players: number; + readonly numGroups: number; + readonly numPlayers: number; + readonly groups: PlayerGroup[]; + readonly players: Player[]; defaultGroup: number; getServerInfo(): ServerInfo; + addGroup(): void; getGroup(index: number): PlayerGroup; - setGroups(groups: PlayerGroup[]): void; + removeGroup(index: number): void; getPlayer(index: number): Player; kickPlayer(index: number): void; sendMessage(message: string): void; @@ -613,6 +624,8 @@ declare global { readonly ping: number; readonly commandsRan: number; readonly moneySpent: number; + readonly ipAddress: string; + readonly publicKeyHash: string; } interface PlayerGroup { diff --git a/src/openrct2/network/Network.cpp b/src/openrct2/network/Network.cpp index d1c011f7f2..ed3a7d6c38 100644 --- a/src/openrct2/network/Network.cpp +++ b/src/openrct2/network/Network.cpp @@ -136,6 +136,7 @@ public: void ProcessDisconnectedClients(); std::vector>::iterator GetPlayerIteratorByID(uint8_t id); NetworkPlayer* GetPlayerByID(uint8_t id); + NetworkConnection* GetPlayerConnection(uint8_t id); std::vector>::iterator GetGroupIteratorByID(uint8_t id); NetworkGroup* GetGroupByID(uint8_t id); static const char* FormatChat(NetworkPlayer* fromplayer, const char* text); @@ -176,7 +177,7 @@ public: void Server_Send_TOKEN(NetworkConnection& connection); void Server_Send_MAP(NetworkConnection* connection = nullptr); void Client_Send_CHAT(const char* text); - void Server_Send_CHAT(const char* text); + void Server_Send_CHAT(const char* text, const std::vector& playerIds = {}); void Client_Send_GAME_ACTION(const GameAction* action); void Server_Send_GAME_ACTION(const GameAction* action); void Server_Send_TICK(); @@ -665,6 +666,19 @@ uint8_t Network::GetPlayerID() return player_id; } +NetworkConnection* Network::GetPlayerConnection(uint8_t id) +{ + auto player = GetPlayerByID(id); + if (player != nullptr) + { + auto clientIt = std::find_if( + client_connection_list.begin(), client_connection_list.end(), + [player](const auto& conn) -> bool { return conn->Player == player; }); + return clientIt != client_connection_list.end() ? clientIt->get() : nullptr; + } + return nullptr; +} + void Network::Update() { _closeLock = true; @@ -1654,12 +1668,28 @@ void Network::Client_Send_CHAT(const char* text) _serverConnection->QueuePacket(std::move(packet)); } -void Network::Server_Send_CHAT(const char* text) +void Network::Server_Send_CHAT(const char* text, const std::vector& playerIds) { std::unique_ptr packet(NetworkPacket::Allocate()); *packet << static_cast(NETWORK_COMMAND_CHAT); packet->WriteString(text); - SendPacketToClients(*packet); + + if (playerIds.empty()) + { + // Empty players / default value means send to all players + SendPacketToClients(*packet); + } + else + { + for (auto playerId : playerIds) + { + auto conn = GetPlayerConnection(playerId); + if (conn != nullptr && !conn->IsDisconnected) + { + conn->QueuePacket(NetworkPacket::Duplicate(*packet)); + } + } + } } void Network::Client_Send_GAME_ACTION(const GameAction* action) @@ -1942,6 +1972,80 @@ void Network::ProcessPending() ProcessPlayerList(); } +static bool ProcessPlayerAuthenticatePluginHooks( + const NetworkConnection& connection, const std::string_view& name, const std::string_view& publicKeyHash) +{ +# ifdef ENABLE_SCRIPTING + using namespace OpenRCT2::Scripting; + + auto& hookEngine = GetContext()->GetScriptEngine().GetHookEngine(); + if (hookEngine.HasSubscriptions(OpenRCT2::Scripting::HOOK_TYPE::NETWORK_AUTHENTICATE)) + { + auto ctx = GetContext()->GetScriptEngine().GetContext(); + + // Create event args object + DukObject eObj(ctx); + eObj.Set("name", name); + eObj.Set("publicKeyHash", publicKeyHash); + eObj.Set("ipAddress", connection.Socket->GetIpAddress()); + eObj.Set("cancel", false); + auto e = eObj.Take(); + + // Call the subscriptions + hookEngine.Call(OpenRCT2::Scripting::HOOK_TYPE::NETWORK_AUTHENTICATE, e, false); + + // Check if any hook has cancelled the join + if (AsOrDefault(e["cancel"], false)) + { + return false; + } + } +# endif + return true; +} + +static void ProcessPlayerJoinedPluginHooks(uint8_t playerId) +{ +# ifdef ENABLE_SCRIPTING + using namespace OpenRCT2::Scripting; + + auto& hookEngine = GetContext()->GetScriptEngine().GetHookEngine(); + if (hookEngine.HasSubscriptions(OpenRCT2::Scripting::HOOK_TYPE::NETWORK_JOIN)) + { + auto ctx = GetContext()->GetScriptEngine().GetContext(); + + // Create event args object + DukObject eObj(ctx); + eObj.Set("player", playerId); + auto e = eObj.Take(); + + // Call the subscriptions + hookEngine.Call(OpenRCT2::Scripting::HOOK_TYPE::NETWORK_JOIN, e, false); + } +# endif +} + +static void ProcessPlayerLeftPluginHooks(uint8_t playerId) +{ +# ifdef ENABLE_SCRIPTING + using namespace OpenRCT2::Scripting; + + auto& hookEngine = GetContext()->GetScriptEngine().GetHookEngine(); + if (hookEngine.HasSubscriptions(OpenRCT2::Scripting::HOOK_TYPE::NETWORK_LEAVE)) + { + auto ctx = GetContext()->GetScriptEngine().GetContext(); + + // Create event args object + DukObject eObj(ctx); + eObj.Set("player", playerId); + auto e = eObj.Take(); + + // Call the subscriptions + hookEngine.Call(OpenRCT2::Scripting::HOOK_TYPE::NETWORK_LEAVE, e, false); + } +# endif +} + void Network::ProcessPlayerList() { if (GetMode() == NETWORK_MODE_SERVER) @@ -1966,6 +2070,8 @@ void Network::ProcessPlayerList() // List of active players found in the list. std::vector activePlayerIds; + std::vector newPlayers; + std::vector removedPlayers; for (auto&& pendingPlayer : itPending->second.players) { @@ -1984,6 +2090,8 @@ void Network::ProcessPlayerList() _serverConnection->Player = player; } } + + newPlayers.push_back(player->Id); } else { @@ -1993,19 +2101,35 @@ void Network::ProcessPlayerList() } // Remove any players that are not in newly received list - auto it = player_list.begin(); - while (it != player_list.end()) + for (const auto& player : player_list) { - if (std::find(activePlayerIds.begin(), activePlayerIds.end(), (*it)->Id) == activePlayerIds.end()) + if (std::find(activePlayerIds.begin(), activePlayerIds.end(), player->Id) == activePlayerIds.end()) { - it = player_list.erase(it); - } - else - { - it++; + removedPlayers.push_back(player->Id); } } + // Run player removed hooks (must be before players removed from list) + for (auto playerId : removedPlayers) + { + ProcessPlayerLeftPluginHooks(playerId); + } + + // Run player joined hooks (must be after players added to list) + for (auto playerId : newPlayers) + { + ProcessPlayerJoinedPluginHooks(playerId); + } + + // Now actually remove removed players from player list + player_list.erase( + std::remove_if( + player_list.begin(), player_list.end(), + [&removedPlayers](const std::unique_ptr& player) { + return std::find(removedPlayers.begin(), removedPlayers.end(), player->Id) != removedPlayers.end(); + }), + player_list.end()); + _pendingPlayerLists.erase(itPending); itPending = _pendingPlayerLists.begin(); } @@ -2100,6 +2224,8 @@ void Network::ServerClientDisconnected(std::unique_ptr& conne // Log player disconnected event AppendServerLog(text); + + ProcessPlayerLeftPluginHooks(connection_player->Id); } void Network::RemovePlayer(std::unique_ptr& connection) @@ -2367,9 +2493,9 @@ void Network::Client_Handle_AUTH(NetworkConnection& connection, NetworkPacket& p void Network::Server_Client_Joined(const char* name, const std::string& keyhash, NetworkConnection& connection) { - NetworkPlayer* player = AddPlayer(name, keyhash); + auto player = AddPlayer(name, keyhash); connection.Player = player; - if (player) + if (player != nullptr) { char text[256]; const char* player_name = static_cast(player->Name.c_str()); @@ -2387,6 +2513,8 @@ void Network::Server_Client_Joined(const char* name, const std::string& keyhash, player_name = static_cast(playerNameHash.c_str()); format_string(text, 256, STR_MULTIPLAYER_PLAYER_HAS_JOINED_THE_GAME, &player_name); AppendServerLog(text); + + ProcessPlayerJoinedPluginHooks(player->Id); } } @@ -2668,9 +2796,16 @@ void Network::Server_Handle_AUTH(NetworkConnection& connection, NetworkPacket& p } else if (connection.AuthStatus == NETWORK_AUTH_VERIFIED) { - connection.AuthStatus = NETWORK_AUTH_OK; const std::string hash = connection.Key.PublicKeyHash(); - Server_Client_Joined(name, hash, connection); + if (ProcessPlayerAuthenticatePluginHooks(connection, name, hash)) + { + connection.AuthStatus = NETWORK_AUTH_OK; + Server_Client_Joined(name, hash, connection); + } + else + { + connection.AuthStatus = NETWORK_AUTH_VERIFICATIONFAILURE; + } } else if (connection.AuthStatus != NETWORK_AUTH_REQUIREPASSWORD) { @@ -2886,7 +3021,7 @@ void Network::Client_Handle_CHAT([[maybe_unused]] NetworkConnection& connection, } } -static bool ProcessChatMessagePluginHooks(const NetworkPlayer& player, std::string& text) +static bool ProcessChatMessagePluginHooks(uint8_t playerId, std::string& text) { # ifdef ENABLE_SCRIPTING auto& hookEngine = GetContext()->GetScriptEngine().GetHookEngine(); @@ -2896,7 +3031,7 @@ static bool ProcessChatMessagePluginHooks(const NetworkPlayer& player, std::stri // Create event args object auto objIdx = duk_push_object(ctx); - duk_push_number(ctx, static_cast(player.Id)); + duk_push_number(ctx, playerId); duk_put_prop_string(ctx, objIdx, "player"); duk_push_string(ctx, text.c_str()); duk_put_prop_string(ctx, objIdx, "message"); @@ -2940,7 +3075,7 @@ void Network::Server_Handle_CHAT(NetworkConnection& connection, NetworkPacket& p std::string text = szText; if (connection.Player != nullptr) { - if (!ProcessChatMessagePluginHooks(*connection.Player, text)) + if (!ProcessChatMessagePluginHooks(connection.Player->Id, text)) { // Message not to be relayed return; @@ -3385,6 +3520,26 @@ money32 network_get_player_money_spent(uint32_t index) return gNetwork.player_list[index]->MoneySpent; } +std::string network_get_player_ip_address(uint32_t id) +{ + auto conn = gNetwork.GetPlayerConnection(id); + if (conn != nullptr && conn->Socket != nullptr) + { + return conn->Socket->GetIpAddress(); + } + return {}; +} + +std::string network_get_player_public_key_hash(uint32_t id) +{ + auto player = gNetwork.GetPlayerByID(id); + if (player != nullptr) + { + return player->KeyHash; + } + return {}; +} + void network_add_player_money_spent(uint32_t index, money32 cost) { gNetwork.player_list[index]->AddMoneySpent(cost); @@ -3830,7 +3985,7 @@ void network_send_map() gNetwork.Server_Send_MAP(); } -void network_send_chat(const char* text) +void network_send_chat(const char* text, const std::vector& playerIds) { if (gNetwork.GetMode() == NETWORK_MODE_CLIENT) { @@ -3838,10 +3993,22 @@ void network_send_chat(const char* text) } else if (gNetwork.GetMode() == NETWORK_MODE_SERVER) { - NetworkPlayer* player = gNetwork.GetPlayerByID(gNetwork.GetPlayerID()); - const char* formatted = gNetwork.FormatChat(player, text); - chat_history_add(formatted); - gNetwork.Server_Send_CHAT(formatted); + std::string message = text; + if (ProcessChatMessagePluginHooks(gNetwork.GetPlayerID(), message)) + { + auto player = gNetwork.GetPlayerByID(gNetwork.GetPlayerID()); + if (player != nullptr) + { + auto formatted = gNetwork.FormatChat(player, message.c_str()); + if (playerIds.empty() + || std::find(playerIds.begin(), playerIds.end(), gNetwork.GetPlayerID()) != playerIds.end()) + { + // Server is one of the recipients + chat_history_add(formatted); + } + gNetwork.Server_Send_CHAT(formatted, playerIds); + } + } } } @@ -4054,6 +4221,14 @@ money32 network_get_player_money_spent(uint32_t index) { return MONEY(0, 0); } +std::string network_get_player_ip_address(uint32_t id) +{ + return {}; +} +std::string network_get_player_public_key_hash(uint32_t id) +{ + return {}; +} void network_add_player_money_spent(uint32_t index, money32 cost) { } @@ -4154,7 +4329,7 @@ int32_t network_get_pickup_peep_old_x(uint8_t playerid) { return _pickup_peep_old_x; } -void network_send_chat(const char* text) +void network_send_chat(const char* text, const std::vector& playerIds) { } void network_send_password(const std::string& password) diff --git a/src/openrct2/network/Socket.cpp b/src/openrct2/network/Socket.cpp index b4a7f0c08d..7c192084ef 100644 --- a/src/openrct2/network/Socket.cpp +++ b/src/openrct2/network/Socket.cpp @@ -201,6 +201,7 @@ private: uint16_t _listeningPort = 0; SOCKET _socket = INVALID_SOCKET; + std::string _ipAddress; std::string _hostName; std::future _connectFuture; std::string _error; @@ -322,18 +323,21 @@ public: } else { + auto ipAddress = GetIpAddressFromSocket(reinterpret_cast(&client_addr)); + char hostName[NI_MAXHOST]; 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); + if (rc == 0) { - tcpSocket = std::unique_ptr(new TcpSocket(socket, hostName)); + tcpSocket = std::unique_ptr(new TcpSocket(socket, hostName, ipAddress)); } else { - tcpSocket = std::unique_ptr(new TcpSocket(socket, "")); + tcpSocket = std::unique_ptr(new TcpSocket(socket, "", ipAddress)); } } } @@ -546,11 +550,17 @@ public: return _hostName.empty() ? nullptr : _hostName.c_str(); } + std::string GetIpAddress() const override + { + return _ipAddress; + } + private: - explicit TcpSocket(SOCKET socket, const std::string& hostName) + explicit TcpSocket(SOCKET socket, const std::string& hostName, const std::string& ipAddress) { _socket = socket; _hostName = hostName; + _ipAddress = ipAddress; _status = SOCKET_STATUS_CONNECTED; } @@ -563,6 +573,32 @@ private: } _status = SOCKET_STATUS_CLOSED; } + + std::string GetIpAddressFromSocket(const sockaddr_in* addr) + { + std::string result; +# if defined(__MINGW32__) + if (addr->sin_family == AF_INET) + { + result = inet_ntoa(addr->sin_addr); + } +# else + if (addr->sin_family == AF_INET) + { + char str[INET_ADDRSTRLEN]{}; + inet_ntop(AF_INET, &addr->sin_addr, str, sizeof(str)); + result = str; + } + else if (addr->sin_family == AF_INET6) + { + auto addrv6 = reinterpret_cast(&addr); + char str[INET6_ADDRSTRLEN]{}; + inet_ntop(AF_INET6, &addrv6->sin6_addr, str, sizeof(str)); + result = str; + } +# endif + return result; + } }; class UdpSocket final : public IUdpSocket, protected Socket diff --git a/src/openrct2/network/Socket.h b/src/openrct2/network/Socket.h index 601168b31b..562cad8d5f 100644 --- a/src/openrct2/network/Socket.h +++ b/src/openrct2/network/Socket.h @@ -55,6 +55,7 @@ public: virtual SOCKET_STATUS GetStatus() const abstract; virtual const char* GetError() const abstract; virtual const char* GetHostName() const abstract; + virtual std::string GetIpAddress() const abstract; virtual void Listen(uint16_t port) abstract; virtual void Listen(const std::string& address, uint16_t port) abstract; diff --git a/src/openrct2/network/network.h b/src/openrct2/network/network.h index 1b3d079445..380b01a2ed 100644 --- a/src/openrct2/network/network.h +++ b/src/openrct2/network/network.h @@ -20,6 +20,7 @@ #include #include +#include struct json_t; struct GameAction; @@ -61,6 +62,8 @@ uint32_t network_get_player_flags(uint32_t index); int32_t network_get_player_ping(uint32_t index); int32_t network_get_player_id(uint32_t index); money32 network_get_player_money_spent(uint32_t index); +std::string network_get_player_ip_address(uint32_t id); +std::string network_get_player_public_key_hash(uint32_t id); void network_add_player_money_spent(uint32_t index, money32 cost); int32_t network_get_player_last_action(uint32_t index, int32_t time); void network_set_player_last_action(uint32_t index, int32_t command); @@ -92,7 +95,7 @@ void network_set_pickup_peep_old_x(uint8_t playerid, int32_t x); int32_t network_get_pickup_peep_old_x(uint8_t playerid); void network_send_map(); -void network_send_chat(const char* text); +void network_send_chat(const char* text, const std::vector& playerIds = {}); void network_send_game_action(const GameAction* action); void network_enqueue_game_action(const GameAction* action); void network_send_password(const std::string& password); diff --git a/src/openrct2/scripting/Duktape.hpp b/src/openrct2/scripting/Duktape.hpp index 6091b0a2ca..61c9c4be0b 100644 --- a/src/openrct2/scripting/Duktape.hpp +++ b/src/openrct2/scripting/Duktape.hpp @@ -37,6 +37,11 @@ namespace OpenRCT2::Scripting return value.type() == DukValue::NUMBER ? value.as_int() : defaultValue; } + template<> inline bool AsOrDefault(const DukValue& value, const bool& defaultValue) + { + return value.type() == DukValue::BOOLEAN ? value.as_bool() : defaultValue; + } + /** * Allows creation of an object on the duktape stack and setting properties on it before * retrieving the DukValue instance of it. diff --git a/src/openrct2/scripting/HookEngine.cpp b/src/openrct2/scripting/HookEngine.cpp index 20010f4dc7..bfed9d0b76 100644 --- a/src/openrct2/scripting/HookEngine.cpp +++ b/src/openrct2/scripting/HookEngine.cpp @@ -25,6 +25,9 @@ HOOK_TYPE OpenRCT2::Scripting::GetHookType(const std::string& name) { "interval.tick", HOOK_TYPE::INTERVAL_TICK }, { "interval.day", HOOK_TYPE::INTERVAL_DAY }, { "network.chat", HOOK_TYPE::NETWORK_CHAT }, + { "network.authenticate", HOOK_TYPE::NETWORK_AUTHENTICATE }, + { "network.join", HOOK_TYPE::NETWORK_JOIN }, + { "network.leave", HOOK_TYPE::NETWORK_LEAVE }, }); auto result = LookupTable.find(name); return (result != LookupTable.end()) ? result->second : HOOK_TYPE::UNDEFINED; diff --git a/src/openrct2/scripting/HookEngine.h b/src/openrct2/scripting/HookEngine.h index 8971ad9d60..896cbe7292 100644 --- a/src/openrct2/scripting/HookEngine.h +++ b/src/openrct2/scripting/HookEngine.h @@ -33,6 +33,9 @@ namespace OpenRCT2::Scripting INTERVAL_TICK, INTERVAL_DAY, NETWORK_CHAT, + NETWORK_AUTHENTICATE, + NETWORK_JOIN, + NETWORK_LEAVE, COUNT, UNDEFINED = -1, }; diff --git a/src/openrct2/scripting/ScNetwork.hpp b/src/openrct2/scripting/ScNetwork.hpp index 94e5ab47a0..5cb76da3f1 100644 --- a/src/openrct2/scripting/ScNetwork.hpp +++ b/src/openrct2/scripting/ScNetwork.hpp @@ -233,6 +233,16 @@ namespace OpenRCT2::Scripting # endif } + std::string ipAddress_get() const + { + return network_get_player_ip_address(_id); + } + + std::string publicKeyHash_get() const + { + return network_get_player_public_key_hash(_id); + } + static void Register(duk_context* ctx) { dukglue_register_property(ctx, &ScPlayer::id_get, nullptr, "id"); @@ -241,6 +251,8 @@ namespace OpenRCT2::Scripting dukglue_register_property(ctx, &ScPlayer::ping_get, nullptr, "ping"); dukglue_register_property(ctx, &ScPlayer::commandsRan_get, nullptr, "commandsRan"); dukglue_register_property(ctx, &ScPlayer::moneySpent_get, nullptr, "moneySpent"); + dukglue_register_property(ctx, &ScPlayer::ipAddress_get, nullptr, "ipAddress"); + dukglue_register_property(ctx, &ScPlayer::publicKeyHash_get, nullptr, "publicKeyHash"); } }; @@ -271,7 +283,7 @@ namespace OpenRCT2::Scripting # endif return "none"; } - int32_t players_get() const + int32_t numPlayers_get() const { # ifndef DISABLE_NETWORK return network_get_num_players(); @@ -279,7 +291,7 @@ namespace OpenRCT2::Scripting return 0; # endif } - int32_t groups_get() const + int32_t numGroups_get() const { # ifndef DISABLE_NETWORK return network_get_num_groups(); @@ -303,6 +315,34 @@ namespace OpenRCT2::Scripting # endif } + std::vector> groups_get() const + { + std::vector> groups; +# ifndef DISABLE_NETWORK + auto numGroups = network_get_num_groups(); + for (int32_t i = 0; i < numGroups; i++) + { + auto groupId = network_get_group_id(i); + groups.push_back(std::make_shared(groupId)); + } +# endif + return groups; + } + + std::vector> players_get() const + { + std::vector> players; +# ifndef DISABLE_NETWORK + auto numPlayers = network_get_num_players(); + for (int32_t i = 0; i < numPlayers; i++) + { + auto playerId = network_get_player_id(i); + players.push_back(std::make_shared(playerId)); + } +# endif + return players; + } + std::shared_ptr getPlayer(int32_t index) const { # ifndef DISABLE_NETWORK @@ -329,6 +369,27 @@ namespace OpenRCT2::Scripting return nullptr; } + void addGroup() + { +# ifndef DISABLE_NETWORK + auto networkModifyGroup = NetworkModifyGroupAction(ModifyGroupType::AddGroup); + GameActions::Execute(&networkModifyGroup); +# endif + } + + void removeGroup(int32_t index) + { +# ifndef DISABLE_NETWORK + auto numGroups = network_get_num_groups(); + if (index < numGroups) + { + auto groupId = network_get_group_id(index); + auto networkAction = NetworkModifyGroupAction(ModifyGroupType::RemoveGroup, groupId); + GameActions::Execute(&networkAction); + } +# endif + } + void kickPlayer(int32_t index) { # ifndef DISABLE_NETWORK @@ -347,7 +408,26 @@ namespace OpenRCT2::Scripting # ifndef DISABLE_NETWORK if (players.is_array()) { - duk_error(players.context(), DUK_ERR_ERROR, "Not yet supported"); + if (network_get_mode() == NETWORK_MODE_SERVER) + { + std::vector playerIds; + auto playerArray = players.as_array(); + for (const auto& item : playerArray) + { + if (item.type() == DukValue::Type::NUMBER) + { + playerIds.push_back(static_cast(item.as_int())); + } + } + if (!playerArray.empty()) + { + network_send_chat(message.c_str(), playerIds); + } + } + else + { + duk_error(players.context(), DUK_ERR_ERROR, "Only servers can send private messages."); + } } else { @@ -359,10 +439,14 @@ namespace OpenRCT2::Scripting static void Register(duk_context* ctx) { dukglue_register_property(ctx, &ScNetwork::mode_get, nullptr, "mode"); + dukglue_register_property(ctx, &ScNetwork::numGroups_get, nullptr, "numGroups"); + dukglue_register_property(ctx, &ScNetwork::numPlayers_get, nullptr, "numPlayers"); dukglue_register_property(ctx, &ScNetwork::groups_get, nullptr, "groups"); dukglue_register_property(ctx, &ScNetwork::players_get, nullptr, "players"); dukglue_register_property(ctx, &ScNetwork::defaultGroup_get, &ScNetwork::defaultGroup_set, "defaultGroup"); + dukglue_register_method(ctx, &ScNetwork::addGroup, "addGroup"); dukglue_register_method(ctx, &ScNetwork::getGroup, "getGroup"); + dukglue_register_method(ctx, &ScNetwork::removeGroup, "removeGroup"); dukglue_register_method(ctx, &ScNetwork::getPlayer, "getPlayer"); dukglue_register_method(ctx, &ScNetwork::kickPlayer, "kickPlayer"); dukglue_register_method(ctx, &ScNetwork::sendMessage, "sendMessage");