diff options
| author | Briar <205427297+icy-briar@users.noreply.github.com> | 2025-05-03 17:53:09 +0000 |
|---|---|---|
| committer | icy-briar <briar@noreply.localhost> | 2025-05-03 17:53:09 +0000 |
| commit | 54c3c4503adc502d7f860350e8daa39514960b5d (patch) | |
| tree | 76df14613f5e356812912e55f084d07a920d5152 /src/common/android | |
| parent | d9eea0dc72cd73d9cbeb91e275752fc39fcb536e (diff) | |
android: Add initial frontend for LAN network rooms (#76)
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/76
Co-authored-by: Briar <205427297+icy-briar@users.noreply.github.com>
Co-committed-by: Briar <205427297+icy-briar@users.noreply.github.com>
Diffstat (limited to 'src/common/android')
| -rw-r--r-- | src/common/android/android_common.cpp | 12 | ||||
| -rw-r--r-- | src/common/android/android_common.h | 5 | ||||
| -rw-r--r-- | src/common/android/id_cache.cpp | 24 | ||||
| -rw-r--r-- | src/common/android/id_cache.h | 7 | ||||
| -rw-r--r-- | src/common/android/multiplayer/multiplayer.cpp | 353 | ||||
| -rw-r--r-- | src/common/android/multiplayer/multiplayer.h | 68 |
6 files changed, 468 insertions, 1 deletions
diff --git a/src/common/android/android_common.cpp b/src/common/android/android_common.cpp index e79005658d..0df2dd7b35 100644 --- a/src/common/android/android_common.cpp +++ b/src/common/android/android_common.cpp @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #include "android_common.h" #include <string> @@ -34,6 +37,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 d0ccb4ec2f..05e2c2515b 100644 --- a/src/common/android/android_common.h +++ b/src/common/android/android_common.h @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #pragma once #include <string> @@ -19,7 +22,7 @@ jobject ToJDouble(JNIEnv* env, double value); s32 GetJInteger(JNIEnv* env, jobject jinteger); jobject ToJInteger(JNIEnv* env, s32 value); - +jobjectArray ToJStringArray(JNIEnv* env, const std::vector<std::string>& strs); bool GetJBoolean(JNIEnv* env, jobject jboolean); jobject ToJBoolean(JNIEnv* env, bool value); diff --git a/src/common/android/id_cache.cpp b/src/common/android/id_cache.cpp index 1145cbdf28..649fafca09 100644 --- a/src/common/android/id_cache.cpp +++ b/src/common/android/id_cache.cpp @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #include <jni.h> #include "applets/software_keyboard.h" @@ -8,6 +11,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; @@ -88,6 +94,9 @@ 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; @@ -388,6 +397,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 @@ -547,6 +565,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); @@ -582,6 +604,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 cd2844dcc6..0b64bbc866 100644 --- a/src/common/android/id_cache.h +++ b/src/common/android/id_cache.h @@ -1,10 +1,14 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #pragma once #include <future> #include <jni.h> +#include <network/network.h> #include "video_core/rasterizer_interface.h" @@ -108,5 +112,8 @@ jmethodID GetYuzuDeviceGetSupportsVibration(); jmethodID GetYuzuDeviceVibrate(); jmethodID GetYuzuDeviceGetAxes(); jmethodID GetYuzuDeviceHasKeys(); +jmethodID GetAddNetPlayMessage(); +jmethodID ClearChat(); + } // namespace Common::Android diff --git a/src/common/android/multiplayer/multiplayer.cpp b/src/common/android/multiplayer/multiplayer.cpp new file mode 100644 index 0000000000..41d46d71b3 --- /dev/null +++ b/src/common/android/multiplayer/multiplayer.cpp @@ -0,0 +1,353 @@ +// Copyright 2024 Mandarine Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#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 0000000000..4f32c514e0 --- /dev/null +++ b/src/common/android/multiplayer/multiplayer.h @@ -0,0 +1,68 @@ +// Copyright 2024 Mandarine Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#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); |
