diff options
| author | vampiric_x <vampiric_x@yuzu-emu.org> | 2025-03-10 22:37:56 +0100 |
|---|---|---|
| committer | Mike Lothian <mike@fireburn.co.uk> | 2025-05-12 12:46:19 +0100 |
| commit | 17c2000510c51d7b568f439240c86ee47a2667d5 (patch) | |
| tree | cfdd35e1dfa32bf1d131be419aaca92f42b1be41 /src/common/android/multiplayer/multiplayer.cpp | |
| parent | 481bf648aa34bb83d267e838471d44ab8b10618e (diff) | |
android: Initial multiplayer support
And give room owners mod access on both Android and QT
Diffstat (limited to 'src/common/android/multiplayer/multiplayer.cpp')
| -rw-r--r-- | src/common/android/multiplayer/multiplayer.cpp | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/src/common/android/multiplayer/multiplayer.cpp b/src/common/android/multiplayer/multiplayer.cpp new file mode 100644 index 000000000..6da730ed5 --- /dev/null +++ b/src/common/android/multiplayer/multiplayer.cpp @@ -0,0 +1,350 @@ +// Copyright 2024 Mandarine Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/android/id_cache.h" +#include "multiplayer.h" + +#include "common/android/android_common.h" + +#include "core/core.h" +#include "network/network.h" +#include "android/log.h" + + +#include <thread> +#include <chrono> + +namespace IDCache = Common::Android; +Network::RoomNetwork* room_network; + +void AddNetPlayMessage(jint type, jstring msg) { + IDCache::GetEnvForThread()->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), + IDCache::GetAddNetPlayMessage(), type, msg); +} + +void AddNetPlayMessage(int type, const std::string& msg) { + JNIEnv* env = IDCache::GetEnvForThread(); + AddNetPlayMessage(type, Common::Android::ToJString(env, msg)); +} + +void ClearChat() { + IDCache::GetEnvForThread()->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), + IDCache::ClearChat()); +} + + +bool NetworkInit(Network::RoomNetwork* room_network_) { + room_network = room_network_; + bool result = room_network->Init(); + + if (!result) { + return false; + } + + if (auto member = room_network->GetRoomMember().lock()) { + // register the network structs to use in slots and signals + member->BindOnStateChanged([](const Network::RoomMember::State& state) { + if (state == Network::RoomMember::State::Joined || + state == Network::RoomMember::State::Moderator) { + NetPlayStatus status; + std::string msg; + switch (state) { + case Network::RoomMember::State::Joined: + status = NetPlayStatus::ROOM_JOINED; + break; + case Network::RoomMember::State::Moderator: + status = NetPlayStatus::ROOM_MODERATOR; + break; + default: + return; + } + AddNetPlayMessage(static_cast<int>(status), msg); + } + }); + member->BindOnError([](const Network::RoomMember::Error& error) { + NetPlayStatus status; + std::string msg; + switch (error) { + case Network::RoomMember::Error::LostConnection: + status = NetPlayStatus::LOST_CONNECTION; + break; + case Network::RoomMember::Error::HostKicked: + status = NetPlayStatus::HOST_KICKED; + break; + case Network::RoomMember::Error::UnknownError: + status = NetPlayStatus::UNKNOWN_ERROR; + break; + case Network::RoomMember::Error::NameCollision: + status = NetPlayStatus::NAME_COLLISION; + break; + case Network::RoomMember::Error::IpCollision: + status = NetPlayStatus::MAC_COLLISION; + break; + case Network::RoomMember::Error::WrongVersion: + status = NetPlayStatus::WRONG_VERSION; + break; + case Network::RoomMember::Error::WrongPassword: + status = NetPlayStatus::WRONG_PASSWORD; + break; + case Network::RoomMember::Error::CouldNotConnect: + status = NetPlayStatus::COULD_NOT_CONNECT; + break; + case Network::RoomMember::Error::RoomIsFull: + status = NetPlayStatus::ROOM_IS_FULL; + break; + case Network::RoomMember::Error::HostBanned: + status = NetPlayStatus::HOST_BANNED; + break; + case Network::RoomMember::Error::PermissionDenied: + status = NetPlayStatus::PERMISSION_DENIED; + break; + case Network::RoomMember::Error::NoSuchUser: + status = NetPlayStatus::NO_SUCH_USER; + break; + } + AddNetPlayMessage(static_cast<int>(status), msg); + }); + member->BindOnStatusMessageReceived([](const Network::StatusMessageEntry& status_message) { + NetPlayStatus status = NetPlayStatus::NO_ERROR; + std::string msg(status_message.nickname); + switch (status_message.type) { + case Network::IdMemberJoin: + status = NetPlayStatus::MEMBER_JOIN; + break; + case Network::IdMemberLeave: + status = NetPlayStatus::MEMBER_LEAVE; + break; + case Network::IdMemberKicked: + status = NetPlayStatus::MEMBER_KICKED; + break; + case Network::IdMemberBanned: + status = NetPlayStatus::MEMBER_BANNED; + break; + case Network::IdAddressUnbanned: + status = NetPlayStatus::ADDRESS_UNBANNED; + break; + } + AddNetPlayMessage(static_cast<int>(status), msg); + }); + member->BindOnChatMessageReceived([](const Network::ChatEntry& chat) { + NetPlayStatus status = NetPlayStatus::CHAT_MESSAGE; + std::string msg(chat.nickname); + msg += ": "; + msg += chat.message; + AddNetPlayMessage(static_cast<int>(status), msg); + }); + } + + return true; +} +NetPlayStatus NetPlayCreateRoom(const std::string& ipaddress, int port, + const std::string& username, const std::string& password, + const std::string& room_name, int max_players) { + + __android_log_print(ANDROID_LOG_INFO, "NetPlay", "NetPlayCreateRoom called with ipaddress: %s, port: %d, username: %s, room_name: %s, max_players: %d", ipaddress.c_str(), port, username.c_str(), room_name.c_str(), max_players); + + auto member = room_network->GetRoomMember().lock(); + if (!member) { + return NetPlayStatus::NETWORK_ERROR; + } + + if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) { + return NetPlayStatus::ALREADY_IN_ROOM; + } + + auto room = room_network->GetRoom().lock(); + if (!room) { + return NetPlayStatus::NETWORK_ERROR; + } + + if (room_name.length() < 3 || room_name.length() > 20) { + return NetPlayStatus::CREATE_ROOM_ERROR; + } + + // Placeholder game info + const AnnounceMultiplayerRoom::GameInfo game{ + .name = "Default Game", + .id = 0, // Default program ID + }; + + port = (port == 0) ? Network::DefaultRoomPort : static_cast<u16>(port); + + if (!room->Create(room_name, "", ipaddress, static_cast<u16>(port), password, + static_cast<u32>(std::min(max_players, 16)), username, game, nullptr, {})) { + return NetPlayStatus::CREATE_ROOM_ERROR; + } + + // Failsafe timer to avoid joining before creation + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + member->Join(username, ipaddress.c_str(), static_cast<u16>(port), 0, Network::NoPreferredIP, password, ""); + + // Failsafe timer to avoid joining before creation + for (int i = 0; i < 5; i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (member->GetState() == Network::RoomMember::State::Joined || + member->GetState() == Network::RoomMember::State::Moderator) { + return NetPlayStatus::NO_ERROR; + } + } + + // If join failed while room is created, clean up the room + room->Destroy(); + return NetPlayStatus::CREATE_ROOM_ERROR; +} + +NetPlayStatus NetPlayJoinRoom(const std::string& ipaddress, int port, + const std::string& username, const std::string& password) { + auto member = room_network->GetRoomMember().lock(); + if (!member) { + return NetPlayStatus::NETWORK_ERROR; + } + + port = + (port == 0) ? Network::DefaultRoomPort : static_cast<u16>(port); + + + if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) { + return NetPlayStatus::ALREADY_IN_ROOM; + } + + member->Join(username, ipaddress.c_str(), static_cast<u16>(port), 0, Network::NoPreferredIP, password, ""); + + // Wait a bit for the connection and join process to complete + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + if (member->GetState() == Network::RoomMember::State::Joined || + member->GetState() == Network::RoomMember::State::Moderator) { + return NetPlayStatus::NO_ERROR; + } + + if (!member->IsConnected()) { + return NetPlayStatus::COULD_NOT_CONNECT; + } + + return NetPlayStatus::WRONG_PASSWORD; +} + +void NetPlaySendMessage(const std::string& msg) { + if (auto room = room_network->GetRoomMember().lock()) { + if (room->GetState() != Network::RoomMember::State::Joined && + room->GetState() != Network::RoomMember::State::Moderator) { + + return; + } + room->SendChatMessage(msg); + } +} + +void NetPlayKickUser(const std::string& username) { + if (auto room = room_network->GetRoomMember().lock()) { + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&username](const Network::RoomMember::MemberInformation& member) { + return member.nickname == username; + }); + if (it != members.end()) { + room->SendModerationRequest(Network::RoomMessageTypes::IdModKick, username); + } + } +} + +void NetPlayBanUser(const std::string& username) { + if (auto room = room_network->GetRoomMember().lock()) { + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&username](const Network::RoomMember::MemberInformation& member) { + return member.nickname == username; + }); + if (it != members.end()) { + room->SendModerationRequest(Network::RoomMessageTypes::IdModBan, username); + } + } +} + +void NetPlayUnbanUser(const std::string& username) { + if (auto room = room_network->GetRoomMember().lock()) { + room->SendModerationRequest(Network::RoomMessageTypes::IdModUnban, username); + } +} + +std::vector<std::string> NetPlayRoomInfo() { + std::vector<std::string> info_list; + if (auto room = room_network->GetRoomMember().lock()) { + auto members = room->GetMemberInformation(); + if (!members.empty()) { + // name and max players + auto room_info = room->GetRoomInformation(); + info_list.push_back(room_info.name + "|" + std::to_string(room_info.member_slots)); + // all members + for (const auto& member : members) { + info_list.push_back(member.nickname); + } + } + } + return info_list; +} + +bool NetPlayIsJoined() { + auto member = room_network->GetRoomMember().lock(); + if (!member) { + return false; + } + + return (member->GetState() == Network::RoomMember::State::Joined || + member->GetState() == Network::RoomMember::State::Moderator); +} + +bool NetPlayIsHostedRoom() { + if (auto room = room_network->GetRoom().lock()) { + return room->GetState() == Network::Room::State::Open; + } + return false; +} + +void NetPlayLeaveRoom() { + if (auto room = room_network->GetRoom().lock()) { + // if you are in a room, leave it + if (auto member = room_network->GetRoomMember().lock()) { + member->Leave(); + } + + ClearChat(); + + // if you are hosting a room, also stop hosting + if (room->GetState() == Network::Room::State::Open) { + room->Destroy(); + } + } +} + +void NetworkShutdown() { + room_network->Shutdown(); +} + +bool NetPlayIsModerator() { + auto member = room_network->GetRoomMember().lock(); + if (!member) { + return false; + } + return member->GetState() == Network::RoomMember::State::Moderator; +} + +std::vector<std::string> NetPlayGetBanList() { + std::vector<std::string> ban_list; + if (auto room = room_network->GetRoom().lock()) { + auto [username_bans, ip_bans] = room->GetBanList(); + + // Add username bans + for (const auto& username : username_bans) { + ban_list.push_back(username); + } + + // Add IP bans + for (const auto& ip : ip_bans) { + ban_list.push_back(ip); + } + } + return ban_list; +} |
