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 | |
| parent | 481bf648aa34bb83d267e838471d44ab8b10618e (diff) | |
android: Initial multiplayer support
And give room owners mod access on both Android and QT
Diffstat (limited to 'src/common')
| -rw-r--r-- | src/common/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/common/android/android_common.cpp | 9 | ||||
| -rw-r--r-- | src/common/android/android_common.h | 2 | ||||
| -rw-r--r-- | src/common/android/id_cache.cpp | 20 | ||||
| -rw-r--r-- | src/common/android/id_cache.h | 6 | ||||
| -rw-r--r-- | src/common/android/multiplayer/multiplayer.cpp | 350 | ||||
| -rw-r--r-- | src/common/android/multiplayer/multiplayer.h | 65 | ||||
| -rw-r--r-- | src/common/announce_multiplayer_room.h | 1 |
8 files changed, 452 insertions, 3 deletions
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index ddd402d4d..5848026b2 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -187,6 +187,8 @@ if(ANDROID) android/android_common.h android/id_cache.cpp android/id_cache.h + android/multiplayer/multiplayer.cpp + android/multiplayer/multiplayer.h android/applets/software_keyboard.cpp android/applets/software_keyboard.h ) diff --git a/src/common/android/android_common.cpp b/src/common/android/android_common.cpp index 8913413c0..8c4090928 100644 --- a/src/common/android/android_common.cpp +++ b/src/common/android/android_common.cpp @@ -34,6 +34,15 @@ jstring ToJString(JNIEnv* env, std::string_view str) { static_cast<jint>(converted_string.size())); } +jobjectArray ToJStringArray(JNIEnv* env, const std::vector<std::string>& strs) { + jobjectArray array = + env->NewObjectArray(static_cast<jsize>(strs.size()), env->FindClass("java/lang/String"), env->NewStringUTF("")); + for (std::size_t i = 0; i < strs.size(); ++i) { + env->SetObjectArrayElement(array, static_cast<jsize>(i), ToJString(env, strs[i])); + } + return array; +} + jstring ToJString(JNIEnv* env, std::u16string_view str) { return ToJString(env, Common::UTF16ToUTF8(str)); } diff --git a/src/common/android/android_common.h b/src/common/android/android_common.h index 67e9b9e13..3e58550c6 100644 --- a/src/common/android/android_common.h +++ b/src/common/android/android_common.h @@ -26,4 +26,6 @@ jobject ToJLong(JNIEnv* env, s64 value); bool GetJBoolean(JNIEnv* env, jobject jboolean); jobject ToJBoolean(JNIEnv* env, bool value); +jobjectArray ToJStringArray(JNIEnv* env, const std::vector<std::string>& strs); + } // namespace Common::Android diff --git a/src/common/android/id_cache.cpp b/src/common/android/id_cache.cpp index 94bb5fab6..93968a05e 100644 --- a/src/common/android/id_cache.cpp +++ b/src/common/android/id_cache.cpp @@ -8,6 +8,9 @@ #include "common/assert.h" #include "common/fs/fs_android.h" #include "video_core/rasterizer_interface.h" +#include "common/android/multiplayer/multiplayer.h" +#include <network/network.h> + static JavaVM* s_java_vm; static jclass s_native_library_class; @@ -92,6 +95,8 @@ static jmethodID s_yuzu_input_device_get_supports_vibration; static jmethodID s_yuzu_input_device_vibrate; static jmethodID s_yuzu_input_device_get_axes; static jmethodID s_yuzu_input_device_has_keys; +static jmethodID s_add_netplay_message; +static jmethodID s_clear_chat; static constexpr jint JNI_VERSION = JNI_VERSION_1_6; @@ -404,6 +409,15 @@ jmethodID GetYuzuDeviceHasKeys() { return s_yuzu_input_device_has_keys; } +jmethodID GetAddNetPlayMessage() { + return s_add_netplay_message; +} + +jmethodID ClearChat() { + return s_clear_chat; +} + + #ifdef __cplusplus extern "C" { #endif @@ -569,6 +583,10 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { s_yuzu_input_device_has_keys = env->GetMethodID(yuzu_input_device_interface, "hasKeys", "([I)[Z"); env->DeleteLocalRef(yuzu_input_device_interface); + s_add_netplay_message = env->GetStaticMethodID(s_native_library_class, "addNetPlayMessage", + "(ILjava/lang/String;)V"); + s_clear_chat = env->GetStaticMethodID(s_native_library_class, "clearChat", "()V"); + // Initialize Android Storage Common::FS::Android::RegisterCallbacks(env, s_native_library_class); @@ -604,6 +622,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { // UnInitialize applets SoftwareKeyboard::CleanupJNI(env); + + NetworkShutdown(); } #ifdef __cplusplus diff --git a/src/common/android/id_cache.h b/src/common/android/id_cache.h index b4da9b77a..8f685b830 100644 --- a/src/common/android/id_cache.h +++ b/src/common/android/id_cache.h @@ -5,6 +5,7 @@ #include <future> #include <jni.h> +#include <network/network.h> #include "video_core/rasterizer_interface.h" @@ -112,5 +113,6 @@ jmethodID GetYuzuDeviceGetSupportsVibration(); jmethodID GetYuzuDeviceVibrate(); jmethodID GetYuzuDeviceGetAxes(); jmethodID GetYuzuDeviceHasKeys(); - -} // namespace Common::Android +jmethodID GetAddNetPlayMessage(); +jmethodID ClearChat(); +} // namespace Android 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; +} diff --git a/src/common/android/multiplayer/multiplayer.h b/src/common/android/multiplayer/multiplayer.h new file mode 100644 index 000000000..657624e45 --- /dev/null +++ b/src/common/android/multiplayer/multiplayer.h @@ -0,0 +1,65 @@ +// Copyright 2024 Mandarine Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <string> +#include <vector> + +#include <common/common_types.h> +#include <network/network.h> + +enum class NetPlayStatus : s32 { + NO_ERROR, + + NETWORK_ERROR, + LOST_CONNECTION, + NAME_COLLISION, + MAC_COLLISION, + CONSOLE_ID_COLLISION, + WRONG_VERSION, + WRONG_PASSWORD, + COULD_NOT_CONNECT, + ROOM_IS_FULL, + HOST_BANNED, + PERMISSION_DENIED, + NO_SUCH_USER, + ALREADY_IN_ROOM, + CREATE_ROOM_ERROR, + HOST_KICKED, + UNKNOWN_ERROR, + + ROOM_UNINITIALIZED, + ROOM_IDLE, + ROOM_JOINING, + ROOM_JOINED, + ROOM_MODERATOR, + + MEMBER_JOIN, + MEMBER_LEAVE, + MEMBER_KICKED, + MEMBER_BANNED, + ADDRESS_UNBANNED, + + CHAT_MESSAGE, +}; + +bool NetworkInit(Network::RoomNetwork* room_network); +NetPlayStatus NetPlayCreateRoom(const std::string& ipaddress, int port, + const std::string& username, const std::string& password, + const std::string& room_name, int max_players); +NetPlayStatus NetPlayJoinRoom(const std::string& ipaddress, int port, + const std::string& username, const std::string& password); +std::vector<std::string> NetPlayRoomInfo(); +bool NetPlayIsJoined(); +bool NetPlayIsHostedRoom(); +bool NetPlayIsModerator(); +void NetPlaySendMessage(const std::string& msg); +void NetPlayKickUser(const std::string& username); +void NetPlayBanUser(const std::string& username); +void NetPlayLeaveRoom(); +std::string NetPlayGetConsoleId(); +void NetworkShutdown(); +std::vector<std::string> NetPlayGetBanList(); +void NetPlayUnbanUser(const std::string& username); diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index f32060196..0a4719427 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -35,7 +35,6 @@ struct RoomInformation { u16 port; ///< The port of this room GameInfo preferred_game; ///< Game to advertise that you want to play std::string host_username; ///< Forum username of the host - bool enable_yuzu_mods; ///< Allow yuzu Moderators to moderate on this room }; struct Room { |
