Merge pull request #11511 from IntelOrca/plugin/improve-network-apis

Improve network plugin APIs
This commit is contained in:
Michael Steenbeek 2020-04-30 18:50:05 +02:00 committed by GitHub
commit 76f41285b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 357 additions and 34 deletions

View File

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

View File

@ -136,6 +136,7 @@ public:
void ProcessDisconnectedClients();
std::vector<std::unique_ptr<NetworkPlayer>>::iterator GetPlayerIteratorByID(uint8_t id);
NetworkPlayer* GetPlayerByID(uint8_t id);
NetworkConnection* GetPlayerConnection(uint8_t id);
std::vector<std::unique_ptr<NetworkGroup>>::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<uint8_t>& 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<uint8_t>& playerIds)
{
std::unique_ptr<NetworkPacket> packet(NetworkPacket::Allocate());
*packet << static_cast<uint32_t>(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<uint8_t> activePlayerIds;
std::vector<uint8_t> newPlayers;
std::vector<uint8_t> 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<NetworkPlayer>& 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<NetworkConnection>& conne
// Log player disconnected event
AppendServerLog(text);
ProcessPlayerLeftPluginHooks(connection_player->Id);
}
void Network::RemovePlayer(std::unique_ptr<NetworkConnection>& 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<const char*>(player->Name.c_str());
@ -2387,6 +2513,8 @@ void Network::Server_Client_Joined(const char* name, const std::string& keyhash,
player_name = static_cast<const char*>(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<int32_t>(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<uint8_t>& 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<uint8_t>& playerIds)
{
}
void network_send_password(const std::string& password)

View File

@ -201,6 +201,7 @@ private:
uint16_t _listeningPort = 0;
SOCKET _socket = INVALID_SOCKET;
std::string _ipAddress;
std::string _hostName;
std::future<void> _connectFuture;
std::string _error;
@ -322,18 +323,21 @@ public:
}
else
{
auto ipAddress = GetIpAddressFromSocket(reinterpret_cast<sockaddr_in*>(&client_addr));
char hostName[NI_MAXHOST];
int32_t rc = getnameinfo(
reinterpret_cast<struct sockaddr*>(&client_addr), client_len, hostName, sizeof(hostName), nullptr, 0,
NI_NUMERICHOST | NI_NUMERICSERV);
SetOption(socket, IPPROTO_TCP, TCP_NODELAY, true);
if (rc == 0)
{
tcpSocket = std::unique_ptr<ITcpSocket>(new TcpSocket(socket, hostName));
tcpSocket = std::unique_ptr<ITcpSocket>(new TcpSocket(socket, hostName, ipAddress));
}
else
{
tcpSocket = std::unique_ptr<ITcpSocket>(new TcpSocket(socket, ""));
tcpSocket = std::unique_ptr<ITcpSocket>(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<const sockaddr_in6*>(&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

View File

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

View File

@ -20,6 +20,7 @@
#include <memory>
#include <string>
#include <vector>
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<uint8_t>& 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);

View File

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

View File

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

View File

@ -33,6 +33,9 @@ namespace OpenRCT2::Scripting
INTERVAL_TICK,
INTERVAL_DAY,
NETWORK_CHAT,
NETWORK_AUTHENTICATE,
NETWORK_JOIN,
NETWORK_LEAVE,
COUNT,
UNDEFINED = -1,
};

View File

@ -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<std::shared_ptr<ScPlayerGroup>> groups_get() const
{
std::vector<std::shared_ptr<ScPlayerGroup>> 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<ScPlayerGroup>(groupId));
}
# endif
return groups;
}
std::vector<std::shared_ptr<ScPlayer>> players_get() const
{
std::vector<std::shared_ptr<ScPlayer>> 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<ScPlayer>(playerId));
}
# endif
return players;
}
std::shared_ptr<ScPlayer> 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<uint8_t> playerIds;
auto playerArray = players.as_array();
for (const auto& item : playerArray)
{
if (item.type() == DukValue::Type::NUMBER)
{
playerIds.push_back(static_cast<uint8_t>(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");