summaryrefslogtreecommitdiff
path: root/src/common
diff options
context:
space:
mode:
authorvampiric_x <vampiric_x@yuzu-emu.org>2025-03-10 22:37:56 +0100
committerMike Lothian <mike@fireburn.co.uk>2025-05-12 12:46:19 +0100
commit17c2000510c51d7b568f439240c86ee47a2667d5 (patch)
treecfdd35e1dfa32bf1d131be419aaca92f42b1be41 /src/common
parent481bf648aa34bb83d267e838471d44ab8b10618e (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.txt2
-rw-r--r--src/common/android/android_common.cpp9
-rw-r--r--src/common/android/android_common.h2
-rw-r--r--src/common/android/id_cache.cpp20
-rw-r--r--src/common/android/id_cache.h6
-rw-r--r--src/common/android/multiplayer/multiplayer.cpp350
-rw-r--r--src/common/android/multiplayer/multiplayer.h65
-rw-r--r--src/common/announce_multiplayer_room.h1
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 {